From 9a6cd33d90c74d42e63211acecc6ba9294cce92d Mon Sep 17 00:00:00 2001 From: tianhao Date: Sat, 25 Jul 2026 11:22:07 +0800 Subject: [PATCH] =?UTF-8?q?release:=20v0.2.0=20=E2=80=94=20TypeScript=20?= =?UTF-8?q?=E6=BA=90=E7=A0=81=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### 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 --- .eslintrc.json | 28 + .gitea/workflows/ci.yml | 40 + .gitignore | 3 + CHANGELOG.md | 47 + README.md | 6 +- babel.config.js | 16 - build.sh | 74 +- jest.config.cjs | 34 + jest.config.js | 17 - package-lock.json | 1987 +++++++----------------- package.json | 41 +- rollup.config.js | 168 +- serve.sh | 2 +- site/demo.html | 4 +- site/docs.html | 2 +- site/index.html | 16 +- src/animations.js | 111 -- src/animations.ts | 89 ++ src/api.ts | 617 ++++++++ src/{constants.js => constants.ts} | 132 +- src/core.js | 1243 --------------- src/{i18n.js => i18n.ts} | 369 ++--- src/{icons.js => icons.ts} | 218 +-- src/{index.js => index.ts} | 209 ++- src/{locales.js => locales.ts} | 6 +- src/plugins.js | 210 --- src/plugins.ts | 220 +++ src/{styles.js => styles.ts} | 95 +- src/templates.ts | 79 + src/{themes.js => themes.ts} | 196 +-- src/toast.ts | 551 +++++++ src/types.ts | 552 +++++++ src/{utils.js => utils.ts} | 20 +- tests/{index.test.js => index.test.ts} | 674 +++----- tsconfig.json | 24 + types/index.d.ts | 1046 ------------- 36 files changed, 3673 insertions(+), 5473 deletions(-) create mode 100644 .eslintrc.json create mode 100644 .gitea/workflows/ci.yml create mode 100644 CHANGELOG.md delete mode 100644 babel.config.js create mode 100644 jest.config.cjs delete mode 100644 jest.config.js delete mode 100644 src/animations.js create mode 100644 src/animations.ts create mode 100644 src/api.ts rename src/{constants.js => constants.ts} (82%) delete mode 100644 src/core.js rename src/{i18n.js => i18n.ts} (55%) rename src/{icons.js => icons.ts} (98%) rename src/{index.js => index.ts} (59%) rename src/{locales.js => locales.ts} (98%) delete mode 100644 src/plugins.js create mode 100644 src/plugins.ts rename src/{styles.js => styles.ts} (87%) create mode 100644 src/templates.ts rename src/{themes.js => themes.ts} (62%) create mode 100644 src/toast.ts create mode 100644 src/types.ts rename src/{utils.js => utils.ts} (55%) rename tests/{index.test.js => index.test.ts} (80%) create mode 100644 tsconfig.json delete mode 100644 types/index.d.ts diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..8ae6f3e --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,28 @@ +{ + "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": "warn", + "no-unused-vars": "off", + "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_" }], + "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/"] +} diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..26dd42c --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,40 @@ +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 + continue-on-error: true + + - name: Run tests + run: npm test + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index b96bc23..f08e9e0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,7 @@ node_modules/ dist/ coverage/ .DS_Store +.zcode/ + +# 本地 npm 发布凭据(含 _auth,勿提交) .npmrc diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..1d60028 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +All notable changes to MetonaToast will be documented in this file. + +## [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 类型声明 +- 零依赖 diff --git a/README.md b/README.md index c601a87..a43c481 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ ## 特性 -- **零依赖** — 纯原生 JavaScript,gzip 后不到 10KB +- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB - **107 种图标类型** — 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** — 完整类型定义,已对齐实际实现无 phantom 方法 +- **TypeScript** — 严格模式,源码级类型安全,类型从源码自动生成无手动维护 - **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入监控系统 ## 安装 @@ -58,7 +58,7 @@ const MeToast = require('@metona-team/metona-toast'); | Opera | 51+ | | iOS Safari | 11+ | | Android Chrome | 64+ | -| Node.js | 18+(ESM & CJS 双格式) | +| Node.js | 16+(ESM & CJS 双格式) | | TypeScript | 4.5+(完整类型声明) | 核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。 diff --git a/babel.config.js b/babel.config.js deleted file mode 100644 index 3483a00..0000000 --- a/babel.config.js +++ /dev/null @@ -1,16 +0,0 @@ -module.exports = { - presets: [ - [ - '@babel/preset-env', - { - targets: { - node: 'current', - }, - modules: 'commonjs', - }, - ], - ], - plugins: [ - '@babel/plugin-transform-modules-commonjs', - ], -}; diff --git a/build.sh b/build.sh index 273850c..9d16350 100644 --- a/build.sh +++ b/build.sh @@ -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. 浏览器: " -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 "" diff --git a/jest.config.cjs b/jest.config.cjs new file mode 100644 index 0000000..86f37ab --- /dev/null +++ b/jest.config.cjs @@ -0,0 +1,34 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'jsdom', + transform: { + '^.+\\.ts$': ['ts-jest', { + tsconfig: { + target: 'ES2020', + module: 'ESNext', + moduleResolution: 'node', + esModuleInterop: true, + strict: true, + lib: ['ES2020', 'DOM', 'DOM.Iterable'], + allowImportingTsExtensions: true, + noEmit: true, + }, + }], + }, + 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'], + verbose: true, +}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 1f60f98..0000000 --- a/jest.config.js +++ /dev/null @@ -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, -}; diff --git a/package-lock.json b/package-lock.json index 56587f7..9a83ad4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,22 +1,21 @@ { - "name": "metona-toast", - "version": "2.0.0", + "name": "@metona-team/metona-toast", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "metona-toast", - "version": "2.0.0", + "name": "@metona-team/metona-toast", + "version": "0.2.0", "license": "MIT", "devDependencies": { - "@babel/core": "^7.22.0", - "@babel/plugin-transform-modules-commonjs": "^7.22.0", - "@babel/preset-env": "^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", - "babel-jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.40.0", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", @@ -25,10 +24,12 @@ "rollup-plugin-dts": "^5.3.0", "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-serve": "^2.0.1", + "ts-jest": "^29.4.12", + "tslib": "^2.8.1", "typescript": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" } }, "node_modules/@babel/code-frame": { @@ -104,19 +105,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", - "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", @@ -134,63 +122,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", - "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-member-expression-to-functions": "^7.29.7", - "@babel/helper-optimise-call-expression": "^7.29.7", - "@babel/helper-replace-supers": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/traverse": "^7.29.7", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.29.7.tgz", - "integrity": "sha512-907Uymvqgg1dwUA+7IGwFAOSYzQOuzPXKNJ1yxzwPffzkYFg2q2eHi1fIOs6sXkG9NbIUMunnUlkYsfRFNvomg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.8", - "resolved": "https://registry.npmmirror.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.8.tgz", - "integrity": "sha512-47UwBLPpQi1NoWzLuHNjRoHlYXMwIJoBf7MFou6viC/sIHWYygpvr0B6IAyh5sBdA2nr2LPIRww8lfaUVQINBA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/helper-globals": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.29.7.tgz", @@ -201,20 +132,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", - "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", @@ -247,19 +164,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", - "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", @@ -270,56 +174,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.29.7.tgz", - "integrity": "sha512-16AMiW26DbXWBbr3B8wNozKM0ydMLB892vaOaJW/fPJdnT8vJk5sdkQcU/isqUxyCE0cEoa8wZOcbgDuC4b6Og==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-wrap-function": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", - "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.29.7", - "@babel/helper-optimise-call-expression": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", - "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-string-parser": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", @@ -350,21 +204,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/helper-wrap-function/-/helper-wrap-function-7.29.7.tgz", - "integrity": "sha512-iES0Skag9ERIF68aXadpO6dbXa03mNWK3sEqJaMnLNs/eC3l0lkImdfoy6Y09/SfkpawdAB4RjQ7PVA7TcVGdw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.29.7", - "@babel/traverse": "^7.29.7", - "@babel/types": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.7.tgz", @@ -395,120 +234,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.29.7.tgz", - "integrity": "sha512-j8SrR0zLZrRsC09DlszEx8FpMiwukKffYXMK0d5LmOglO7vGG6sz/BR/20yHqWH+Lnn31JTt2PE3hIWNgM2J6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.29.7.tgz", - "integrity": "sha512-r8j8escF+U2FUHo0KOhPUdMzUO+jp9fInva6+ACVAF3Y97Ev+5iNZwiqTghmzNeWwDkOPlYuTcfb1vDaoZKmAQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.29.7.tgz", - "integrity": "sha512-GE1TFSiuFeGsCxmYXZl8HwoPrVlwe4rHPFE8weieGKZqnDORK+Ar3vgWMgW+AOxQ6/2TgLSKx9p6W7O4rC6qgQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-safari-rest-destructuring-rhs-array/-/plugin-bugfix-safari-rest-destructuring-rhs-array-7.29.7.tgz", - "integrity": "sha512-oBNVCvnO5tND+xSopWvV8WNGfpTfgP4Zr/YXXSj8zfmcPktp5Ku/aZlsIowgSD4fjmgHn6sGmB9APVsU5zOdhA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.29.7.tgz", - "integrity": "sha512-QQt9qKHZ2sg/kivaLr7lnQr8HVrQDdBNSfCsTjiDxRuX/K5ORyKq+Bu8Xr0cDE3Dfkv0cw28Ve0EKyKMvulkOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", - "@babel/plugin-transform-optional-chaining": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.29.7.tgz", - "integrity": "sha512-pn6QacGLgvCcwc+syUhKE/qSjV2D1IHDB84RNxWYSt1mW3K/SCtjinZ2p0cETJxAWBjPy3K/1lHwG5BjjPxNlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmmirror.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -564,22 +289,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.29.7.tgz", - "integrity": "sha512-/An1OCBN93thpBAGyfsK2pcf0jvju1SAtKkL2Ny++B5Sy6sqgzXDQH1cZxWbF96Wuk+bn41MDA9bLd4VVAw6rw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.29.7.tgz", @@ -764,980 +473,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmmirror.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.29.7.tgz", - "integrity": "sha512-N7zArUXWzAMzm+/N0uPBeVB3Fam5lMxtUwMmDK5f/IBBS7a7p1qeUoxd/6CckXoxUdgsntq1Dh8xNW06maZbDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.7.tgz", - "integrity": "sha512-d98gXZkgswvkyohMBABkhm3GeXhYj8psWfwQ2C7gtfrKGTykQa/iOIi+JJhwMjPlZ6Vm2XN+DCf3Es1EoG4ZLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-remap-async-to-generator": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.29.7.tgz", - "integrity": "sha512-pcUb2SS+RMo9TWVBwKGI5ShtoG7R+zBsFmCKDa6fe8c+hPr3XJlZgoE5j6i8W7gDjhyvy+85vmYexanvXh3d1w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-remap-async-to-generator": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.29.7.tgz", - "integrity": "sha512-cUSmjh72N+rN4PrkFlN1dJwNCwjVp5d38/CQrEsFggkD10UiFlBFgdH3tv5dNsLuHY+3S8db2xCHjhZcv5WgvA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.29.7.tgz", - "integrity": "sha512-ONyr4+AZhKh8yKWInVxU9AXA9EbsyeLcL6V0dJy6M2/62vuvpGm29zzuymbTpdc451GEpDIdAyPLP3r+P61yKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.29.7.tgz", - "integrity": "sha512-GtcpjFvanPfzNQi3eTitsCqtRRmmqzpy/A+yhTR1HaZo1Ly3EA8ZXxlPyHdR8/IuRMYc3E4wdGBewB2QKQjAaA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.29.7.tgz", - "integrity": "sha512-kibJgmEdX2iMwsHY2tSZNDgj8PwIlCQz7FK9KuGKO8zsuoUwSEhoNnNVp/emKWrbY4HeO6kkXfdMqRKKKXBm2A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.29.7.tgz", - "integrity": "sha512-qV0OGGBVacduzQHE649JyCneOFI/maT+YKsO+K4Yi3xv2wTPNjM/W2o2gdzMwEAZz7fXNTHAe0NcSg30bIN69g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-globals": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-replace-supers": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.29.7.tgz", - "integrity": "sha512-RK7/IyU5phpuCdBAuig5VkzG/EnbDaui5SQGdU9BFrHdV+mV4cUjLMQ9lJDjLNtWHsqtiefpGZUXQP2BiTYMsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/template": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.29.7.tgz", - "integrity": "sha512-iPX8aD6H9zV5s7ZsqTdNocPN/MGQ5sSMnElKrktxjJRMnB2jN/1p2+R7GkfD6CAYoVFqy5A4XnSIUeGgJzIWpg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.29.7.tgz", - "integrity": "sha512-3qc18hsD2RdZiyJNDNc7HQpv6xbncwh8FYtxNFFzclSyh/trPD9KkVR9BDECUjDLvb7yJVF15GfYUuC+LMkkiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.29.7.tgz", - "integrity": "sha512-6IvRRriEMqnBwD6chtxdLpMYCHWEzN+oL5cyQtjykya19UgzbmKhxmhZgKC/LHxS2nYr9Q/qYPZ5Lr6jOL9+yQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.7.tgz", - "integrity": "sha512-2wiIyo2BjtgU7HufSeDnL9L2O7zr8jmhFKuSr65VpRkUiRKRNpb0mdlk56+XPPKoIrfHqzbMuglDvZun0RISsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.29.7.tgz", - "integrity": "sha512-giOlEm/EFjfjr+te9NsdjkUo2v4f8rS/SXPumRVHAtbNcyNlvtREkU1dZzaIDclNpnaVhlCqRdFKhJBjBikzLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.29.7.tgz", - "integrity": "sha512-Rstj7coNz8sE+7Ju7ihpHLI564lsK5pUpNNlvptCIC/16E/S5hbl6n3kESPKdNRmqEWlpn5xpS5Q2dvXBsySLw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/plugin-transform-destructuring": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.29.7.tgz", - "integrity": "sha512-zFpMOTLZBdW5LfObqcSbL6kefg4R4eLdmvS0wbN9M6D5Mym/sKm9toOoWyVOa+xDjvCnuWcHls2YonXwHvH3CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.29.7.tgz", - "integrity": "sha512-24B2nOy2TeJSMheqwPD4DDQOV/elLSIlKxjZt4i05H5AgdPdWR3n18HnNrcJ+j76WJd9gbwb9jPjNYUy6RautA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.29.7.tgz", - "integrity": "sha512-zeSIHh0+E1Um1WJRXCFlHQYu2ieJNdivLLjlBEp+dIBu3S51n+SZZmIXjxnItw6pz56Cn+KvK68BIBVsxq2JiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.29.7.tgz", - "integrity": "sha512-otRWaHXE6fbAGkePvaj/kvs3HsqXfPhlnzwSOlnFgbqCPMd975dW+4wZ00WFBt+/YlBGcJwNrARQTOJOb4ZrIg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.29.7.tgz", - "integrity": "sha512-RRnE2+eon1rJAq8MnoF1b5kTpY1vU88twHcvcKMrsqP/jxIRqDVs9iJB5fqPuqyeFAW0wJo4MlUIPpQCq/aRsg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.29.7.tgz", - "integrity": "sha512-DZ/oLP21ZuWx1vKqnoNv6/tvEK48AQOBRai40CX9dTjGluvT/YZCyY3rryDtyUqCEoyNroy5KKPwX2iQCiRvyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.29.7.tgz", - "integrity": "sha512-A0H91hh6W8MFRkp5TqJmMr39jzGD1A1E1Ysiv2O06Sfbhkapm+XyIzxWCEh5kqwOZ1/8QZ0dY3SeQ7XBqfJd5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.29.7.tgz", - "integrity": "sha512-hl1kwFZCCiDyfH25Xmco9jTrkPgnS9pmOzSG7W5I4SaGbLeqKv417hcU2RKmaxoPEgsoJh7ZPOrnPGq99bHoUg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.29.7.tgz", - "integrity": "sha512-fxtQoH3m5ywUSIfaH0FGCzWu4McsYon5bD3K4XnskC7f+OyQMj7rsOMi4NvvmJ83WwBAg4UCe+ov4VZlqEvyew==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", - "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.7.tgz", - "integrity": "sha512-TM2ZcQLoG2/y4HODiStCo10DibYhWhGWAwVv+EQKmG/7GFl0N+AAmUiXOMKM+aiJ9XBJ9AHVZBvTzMnJ2sM3cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-validator-identifier": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.29.7.tgz", - "integrity": "sha512-B4UkaTK3QpgCwJnrxKfMPKdo92CN7OKXAlpAAnM3UPu0Q0lCCk57ylA9AJbRy2v8dDKOPAAWcoR6CMyeoHwRCA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.7.tgz", - "integrity": "sha512-vuFoLwr4qnv2xbZ16SQd6uPcH5FNrLHhk/Jzo++0XJFcaDsr4gjJVg6j398oMHiC+83k/GiBzviwF5KBJkPUtQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.29.7.tgz", - "integrity": "sha512-fEo41GmsOUhOBlw8ioo6zvjX5Xc2Lqkzlyfqbpsk3eB6TReV18uhxZ0esfEokVbY2+PVJAQHNKxER6lGrzNd3A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.29.7.tgz", - "integrity": "sha512-idmp1dFaekP9GbcMvG24Kvw2BfhFZjHnNJCkV4WuIY4PskJzwI3f1N5OdgYke38T7rftO6ERulFRn2cFeZwRkg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.29.7.tgz", - "integrity": "sha512-zR7fv/z14OjgHl4AgRtkDBvBMhIzCxqV/qN/2BCRC7LjFwvuzjYe7gDWxC4Wl/SNsLM6SE1IWvRPYMgSJaUvNw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.29.7.tgz", - "integrity": "sha512-Ld98jn4c0smUywL57m7SgsHq3OpThOa6LqZJif3G6jYOovPleoFhVrBJ1WegRApSFB2wu4+RelAj9AC9G08Z4A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/plugin-transform-destructuring": "^7.29.7", - "@babel/plugin-transform-parameters": "^7.29.7", - "@babel/traverse": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.29.7.tgz", - "integrity": "sha512-Ea/diGcw0twB5IlZPO5sgET6fJsLJqPABqTuFWIR+iMPGPZJkATEIWx0wa+aEQ5UY1CBQyP/gkAiLEqn1vBiQA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-replace-supers": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.29.7.tgz", - "integrity": "sha512-sLsyndxK2VwX6yNUOakMb7Sh553ZTe/vVM1XJ+9Z5aW1ytsc8xOIwmyk05NNjN60vkc5/KqoTH6hB4V41LJhng==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.29.7.tgz", - "integrity": "sha512-6GM1dhvK3gNODkXcEcMCOLEDCLSoZ/sBbro2Ax8HURyasQ4NshagQixkRFdh5niI6E4gmA/jYI/4aT7rRos3ZQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.29.7.tgz", - "integrity": "sha512-ZDOBqV/qLYJI0YElr8DcENEyARsFQeESqWXH6gZlghYXuPPjvweuDhP4VyEi4BlUBlLRFZVjxoZDMjxhLW766g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.29.7.tgz", - "integrity": "sha512-/6Rz4DK1ETDEM/bWHsPHcaEe7ZaT1EqSXjtSP/L0DijOYuaUhiRiOKcwpZ8P7zR4xXEHc2ITdiCgBm9Tpyv9ug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.29.7.tgz", - "integrity": "sha512-+BNo06dnrzdNNqCm1X6YUaVv0DKk8Q+JYcoZfOkLhYWNCXzlwTSRq8zGWayT1csjcpNXV9CQTBRRbmTLZac5cA==", - "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" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.29.7.tgz", - "integrity": "sha512-bOMRLQuI0A5ZqHq3OWJ89/rXpJ/NJrbVhXiP4zwPGMs6kpcVsuTUNjwoE30K0Qm3mf48a/TnRYYD6vPNqcg6jA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.7.tgz", - "integrity": "sha512-rNNFV0DBAJp988xW2DOntfDoYn1eR8GGF5AT5vYc+rjyfaQkM242c9tZUHHPe7KYaiJizXPWhQTzzdbXySyhBw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.29.7.tgz", - "integrity": "sha512-mB5Fs0VWrJ42ZCmc8114v60qetdaUVNkj9PmSZRmanCZM3S9hm0CFRLjRmYIsuXav14l2jvZ+4T8iiCGnhj3nQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.29.7.tgz", - "integrity": "sha512-5+YhdpVgmfSmwZyLMftfaiffLRMHjzIRHFHHLdibcSyJm2pasMrKHrO3Ptrt2DRshjvpgjEJJ1zVW14WPq/6QA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.29.7.tgz", - "integrity": "sha512-I+WYbGBAiCn7nA6xBrlgPH+MB7HWb4u8pv5S0Pv7OtwNvIFvCCb24YlttKEeUFVurfBCEaOTnuhlqsb7f0Z5Dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.29.7.tgz", - "integrity": "sha512-/u5K1QWada7tbYNqTjMh96718g9NTwh9tfPJMsSmVsQwGT447FskV+KcfeXkXq2GWki4EM/MuTdmBec+hOuVTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.29.7.tgz", - "integrity": "sha512-BCHzNYJGe9l7EpwwDBN/ztlL2NYFFq8hp9ddjtUEM9f2O7S7kKV/lL6Fwo7IF7NSkYhPK2vO+86nIGltA90MsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.29.7.tgz", - "integrity": "sha512-NCSEJ4sLFU2gqAub45HYh4fus2yQ36rr6ei6vpU7NdoJqCpxvEG8E6eJpscGyXP3VHD2Ny+fSXr04k1hoUrFqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.29.7.tgz", - "integrity": "sha512-223mNGoTkBiTEWFoK+Q6Go3tueMRclO8vxxxxquNCYuNI4jWOofFKJRRDu6SDrB8Sgo1UEGW9T4GAQ8ZyRso1A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^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", - "integrity": "sha512-jCfXxSjf94lf4E0hKE0AByxF6F3/pVFqRdUUNkDJhsY0m1ZKjnN6ZYyMeHNpzflxb/0q5b7t3p+BE+SLF1WOtA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.29.7.tgz", - "integrity": "sha512-OgZ+zoAJgZLUCunsTRQ5LAjOywDv5zzZ2/hQ5aMw1pGXyY2rtE8/chXYUmu3AlVHKpm10KEdG9aMwbI/K76ZGw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.29.7.tgz", - "integrity": "sha512-7D/x/23/d/3VqZ0QA+LGbZMlGwZjztBygSWWWsfTPoQ1oQ6Q1P6Mr3d0kk42XabyUVw+fha3LqdRsFqeKqvCyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.29.7.tgz", - "integrity": "sha512-BLOhLht9DOJwIxlmp91wHvkXv1lguuHS3/FwUO8HL1H0u8s4hR1gASVFyilu9iGtcTRYqjTZmlsFFeQletntEg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.29.7", - "resolved": "https://registry.npmmirror.com/@babel/preset-env/-/preset-env-7.29.7.tgz", - "integrity": "sha512-GYzX36n1nsciIb0uyH0GHwxwtNwPQIcpxSeiVLDtG/B7jB5xXgchnmL1f/jCX5o+pwnaDBtO60ONSJhEBJfxYA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.29.7", - "@babel/helper-compilation-targets": "^7.29.7", - "@babel/helper-plugin-utils": "^7.29.7", - "@babel/helper-validator-option": "^7.29.7", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.29.7", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.29.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.29.7", - "@babel/plugin-bugfix-safari-rest-destructuring-rhs-array": "^7.29.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.29.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.29.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.29.7", - "@babel/plugin-syntax-import-attributes": "^7.29.7", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.29.7", - "@babel/plugin-transform-async-generator-functions": "^7.29.7", - "@babel/plugin-transform-async-to-generator": "^7.29.7", - "@babel/plugin-transform-block-scoped-functions": "^7.29.7", - "@babel/plugin-transform-block-scoping": "^7.29.7", - "@babel/plugin-transform-class-properties": "^7.29.7", - "@babel/plugin-transform-class-static-block": "^7.29.7", - "@babel/plugin-transform-classes": "^7.29.7", - "@babel/plugin-transform-computed-properties": "^7.29.7", - "@babel/plugin-transform-destructuring": "^7.29.7", - "@babel/plugin-transform-dotall-regex": "^7.29.7", - "@babel/plugin-transform-duplicate-keys": "^7.29.7", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.7", - "@babel/plugin-transform-dynamic-import": "^7.29.7", - "@babel/plugin-transform-explicit-resource-management": "^7.29.7", - "@babel/plugin-transform-exponentiation-operator": "^7.29.7", - "@babel/plugin-transform-export-namespace-from": "^7.29.7", - "@babel/plugin-transform-for-of": "^7.29.7", - "@babel/plugin-transform-function-name": "^7.29.7", - "@babel/plugin-transform-json-strings": "^7.29.7", - "@babel/plugin-transform-literals": "^7.29.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.29.7", - "@babel/plugin-transform-member-expression-literals": "^7.29.7", - "@babel/plugin-transform-modules-amd": "^7.29.7", - "@babel/plugin-transform-modules-commonjs": "^7.29.7", - "@babel/plugin-transform-modules-systemjs": "^7.29.7", - "@babel/plugin-transform-modules-umd": "^7.29.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.7", - "@babel/plugin-transform-new-target": "^7.29.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.29.7", - "@babel/plugin-transform-numeric-separator": "^7.29.7", - "@babel/plugin-transform-object-rest-spread": "^7.29.7", - "@babel/plugin-transform-object-super": "^7.29.7", - "@babel/plugin-transform-optional-catch-binding": "^7.29.7", - "@babel/plugin-transform-optional-chaining": "^7.29.7", - "@babel/plugin-transform-parameters": "^7.29.7", - "@babel/plugin-transform-private-methods": "^7.29.7", - "@babel/plugin-transform-private-property-in-object": "^7.29.7", - "@babel/plugin-transform-property-literals": "^7.29.7", - "@babel/plugin-transform-regenerator": "^7.29.7", - "@babel/plugin-transform-regexp-modifiers": "^7.29.7", - "@babel/plugin-transform-reserved-words": "^7.29.7", - "@babel/plugin-transform-shorthand-properties": "^7.29.7", - "@babel/plugin-transform-spread": "^7.29.7", - "@babel/plugin-transform-sticky-regex": "^7.29.7", - "@babel/plugin-transform-template-literals": "^7.29.7", - "@babel/plugin-transform-typeof-symbol": "^7.29.7", - "@babel/plugin-transform-unicode-escapes": "^7.29.7", - "@babel/plugin-transform-unicode-property-regex": "^7.29.7", - "@babel/plugin-transform-unicode-regex": "^7.29.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.29.7", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.15", - "babel-plugin-polyfill-corejs3": "^0.14.0", - "babel-plugin-polyfill-regenerator": "^0.6.6", - "core-js-compat": "^3.48.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmmirror.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/template": { "version": "7.29.7", "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz", @@ -2468,6 +1203,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", @@ -2688,6 +1450,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", @@ -2934,48 +1991,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.17", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.17.tgz", - "integrity": "sha512-aTyf30K/rqAsNwN76zYrdtx8obu0E4KoUME29B1xj+B3WxgvWkp943vYQ+z8Mv3lw9xHXMHpvSPOBxzAkIa94w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.8", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.14.2", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.2.tgz", - "integrity": "sha512-coWpDLJ410R781Npmn/SIBZEsAetR4xVi0SxLMXPaMO4lSf1MwnkGYMtkFxew0Dn8B3/CpbpYxN0JCgg8mn67g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8", - "core-js-compat": "^3.48.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.8", - "resolved": "https://registry.npmmirror.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.8.tgz", - "integrity": "sha512-M762rNHfSF1EV3SLtnCJXFoQbbIIz0OyRwnCmV0KPC7qosSfCO0QLTSuJX3ayAebubhE6oYBAYPrBA5ljowaZg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.8" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmmirror.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -3111,6 +2126,19 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmmirror.com/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/bser": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/bser/-/bser-2.1.1.tgz", @@ -3365,20 +2393,6 @@ "dev": true, "license": "MIT" }, - "node_modules/core-js-compat": { - "version": "3.49.0", - "resolved": "https://registry.npmmirror.com/core-js-compat/-/core-js-compat-3.49.0.tgz", - "integrity": "sha512-VQXt1jr9cBz03b331DFDCCP90b3fanciLkgiOoy8SBHy06gNf+vQ1A3WFLqG7I8TipYIKeYK9wxd0tUrvHcOZA==", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/create-jest": { "version": "29.7.0", "resolved": "https://registry.npmmirror.com/create-jest/-/create-jest-29.7.0.tgz", @@ -4073,6 +3087,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", @@ -4366,6 +3398,28 @@ "dev": true, "license": "MIT" }, + "node_modules/handlebars": { + "version": "4.7.9", + "resolved": "https://registry.npmmirror.com/handlebars/-/handlebars-4.7.9.tgz", + "integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", @@ -5704,10 +4758,10 @@ "node": ">=8" } }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", "dev": true, "license": "MIT" }, @@ -5767,6 +4821,13 @@ "node": ">=10" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmmirror.com/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmmirror.com/makeerror/-/makeerror-1.0.12.tgz", @@ -5880,6 +4941,16 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", @@ -5894,6 +4965,13 @@ "dev": true, "license": "MIT" }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmmirror.com/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmmirror.com/node-int64/-/node-int64-0.4.0.tgz", @@ -6361,64 +5439,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmmirror.com/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmmirror.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmmirror.com/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmmirror.com/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.13.2", - "resolved": "https://registry.npmmirror.com/regjsparser/-/regjsparser-0.13.2.tgz", - "integrity": "sha512-NgRBy2Nx/bE+9F27nVHnqcN5HjyLmecqsqx2PJHu3/IEtADD4WuxuXIVExD5PoSDFVrl78dOonfcOe5O+5nbzQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", @@ -6980,6 +6000,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 +6066,105 @@ "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/ts-jest": { + "version": "29.4.12", + "resolved": "https://registry.npmmirror.com/ts-jest/-/ts-jest-29.4.12.tgz", + "integrity": "sha512-Ov6ClY53Fflh6BGAnY2DlTq1hYDrTycz2PVTXBWFW2CU+9zrEqAp9fWdGXl42EXO5RLSFAcAZ2JFKbP+zBTFfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.9", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.8.5", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <7" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/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/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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", @@ -7079,6 +6215,20 @@ "node": ">=14.17" } }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmmirror.com/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/undici-types": { "version": "7.24.6", "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.24.6.tgz", @@ -7086,50 +6236,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmmirror.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmmirror.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmmirror.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmmirror.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/universalify": { "version": "0.2.0", "resolved": "https://registry.npmmirror.com/universalify/-/universalify-0.2.0.tgz", @@ -7303,6 +6409,13 @@ "node": ">=0.10.0" } }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", diff --git a/package.json b/package.json index ce16e5d..5dbd20a 100644 --- a/package.json +++ b/package.json @@ -1,38 +1,38 @@ { "name": "@metona-team/metona-toast", - "version": "0.1.2", - "description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。", - "main": "dist/metona-toast.js", - "module": "src/index.js", + "version": "0.2.0", + "description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。", + "type": "module", + "main": "dist/metona-toast.cjs.js", + "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.js", + "types": "./dist/metona-toast.d.ts" } }, "files": [ "dist/", "src/", - "types/", "README.md", "LICENSE" ], "scripts": { - "build": "rollup -c", + "build": "tsc --noEmit && rollup -c", "dev": "rollup -c -w", "test": "jest", "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", + "prepublishOnly": "npm run typecheck && npm test && npm run build", + "docs": "typedoc src/ --out docs", "example": "serve examples/" }, "repository": { @@ -57,14 +57,13 @@ }, "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", "@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", - "babel-jest": "^29.5.0", + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", "eslint": "^8.40.0", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", @@ -73,10 +72,12 @@ "rollup-plugin-dts": "^5.3.0", "rollup-plugin-livereload": "^2.0.5", "rollup-plugin-serve": "^2.0.1", + "ts-jest": "^29.4.12", + "tslib": "^2.8.1", "typescript": "^5.0.0" }, "engines": { - "node": ">=14.0.0" + "node": ">=16.0.0" }, "browserslist": [ "> 1%", diff --git a/rollup.config.js b/rollup.config.js index 294884c..c08d5fd 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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,99 @@ 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 + { + input: 'src/index.ts', + output: { + file: 'dist/metona-toast.cjs.js', + format: 'cjs', + exports: 'named', + sourcemap: true, + }, + 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, - }, - format: { - comments: false, - }, - mangle: { - toplevel: true, - }, - }), - ], - }, - - // TypeScript声明文件 - { - input: 'types/index.d.ts', - output: { - file: 'dist/metona-toast.d.ts', - format: 'es', + // TypeScript 声明文件合并 + { + input: 'src/index.ts', + output: { + file: 'dist/metona-toast.d.ts', + format: 'es', + }, + plugins: [dts()], }, - plugins: [dts()], - }, + ]), ]; diff --git a/serve.sh b/serve.sh index f0ebe92..57b3b42 100644 --- a/serve.sh +++ b/serve.sh @@ -15,7 +15,7 @@ else exit 1 fi -PORT=${1:-3000} +PORT=${1:-3001} echo "✅ Python: $($PYTHON --version 2>&1)" echo "🌐 启动服务器..." diff --git a/site/demo.html b/site/demo.html index ffbc685..428c9c9 100644 --- a/site/demo.html +++ b/site/demo.html @@ -244,7 +244,7 @@
-
🛡️ onError 错误捕获 v0.1.2
+
🛡️ onError 错误捕获 v0.2.0
@@ -282,7 +282,7 @@ diff --git a/site/docs.html b/site/docs.html index d1bfc3d..9e320b1 100644 --- a/site/docs.html +++ b/site/docs.html @@ -87,7 +87,7 @@

API 文档

-

MetonaToast v0.1.2 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 配置项 作为可选第二参数。

+

MetonaToast v0.2.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 配置项 作为可选第二参数。

show(message, opts?)

diff --git a/site/index.html b/site/index.html index 2f8bc57..8ec211b 100644 --- a/site/index.html +++ b/site/index.html @@ -80,7 +80,7 @@

MetonaToast

-

轻量 · 零依赖 · 精致美观的 Toast 通知库。纯原生 JavaScript,gzip 不到 10KB。

+

轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式源码,gzip 不到 10KB。

📦 <10KB gzip🎨 107 图标🎭 11 动画🌍 国际化🔌 插件📘 TypeScript
@@ -126,8 +126,8 @@
-

零依赖

-

纯原生 JavaScript 实现,无需任何运行时依赖。兼容所有现代浏览器。

+

零依赖

+

TypeScript 严格模式,纯原生实现,无需任何运行时依赖。兼容所有现代浏览器。

🛡️
@@ -141,8 +141,8 @@
📘
-

TypeScript

-

完整类型定义,与实现严格对齐零 phantom 方法。MeToast.success().confirm().action() 全部有类型。

+

TypeScript

+

严格模式源码,类型从源码自动生成。strictNullChecks、noImplicitAny 全开,零类型漂移。

@@ -173,8 +173,8 @@
📦
-

Node.js

-

18+

+

Node.js

+

16+

📘
@@ -247,7 +247,7 @@ orderGroup.dismiss(); // 一键关闭
-

MetonaToast v0.1.2 · MIT License · Gitea

+

MetonaToast v0.2.0 · MIT License · Gitea

diff --git a/src/animations.js b/src/animations.js deleted file mode 100644 index f70242d..0000000 --- a/src/animations.js +++ /dev/null @@ -1,111 +0,0 @@ -/** - * MetonaToast Animations - 动画管理 v0.1.2 - * @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()); - }, - - /** - * 获取已注册动画总数 - * @returns {number} 已注册动画数量 - */ - getActiveCount() { - return animationMap.size; - }, - - /** - * 取消所有动画(兼容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)', -}); diff --git a/src/animations.ts b/src/animations.ts new file mode 100644 index 0000000..cb876c7 --- /dev/null +++ b/src/animations.ts @@ -0,0 +1,89 @@ +/** + * MetonaToast Animations — 动画管理 + * @module animations + * @version 0.2.0 + */ + +import { ANIMATIONS } from './constants.js'; +import type { AnimationConfig, AnimationUtils } from './types.js'; + +// 动画缓存 +const animationMap: Map = new Map(); + +// 注册默认动画 +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): 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(); + }, +}; + +/** + * 动画预设(占位,兼容旧API) + */ +export const animationPresets: Record = {}; + +/** + * 创建自定义动画配置 + */ +export const createAnimation = (config: Partial): AnimationConfig => ({ + enter: config.enter || {}, + leave: config.leave || {}, + duration: config.duration || 300, + easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)', +}); diff --git a/src/api.ts b/src/api.ts new file mode 100644 index 0000000..8eb60c1 --- /dev/null +++ b/src/api.ts @@ -0,0 +1,617 @@ +/** + * MetonaToast API — meToast 核心 API 对象 + * @module api + * @version 0.2.0 + */ + +import { Toast, _containerCache } from './toast.js'; +import { DEFAULTS } from './constants.js'; +import { escapeHTML } from './utils.js'; +import { t } from './i18n.js'; +import { applyTheme } from './themes.js'; +import { setCurrentLocale } from './i18n.js'; +import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js'; +import type { + ToastConfig, ToastOptions, ToastInstance, + LoadingControl, ProgressControl, CountdownControl, + ActionControl, QueueControl, GroupAPI, + ActionButton, PromiseOptions, ConfirmOptions, + PromptOptions, ProgressOptions, CountdownOptions, + QueueOptions, StackOptions, MeToast, ErrorInfo, +} from './types.js'; + +/** + * 参数标准化工具 + */ +const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions => { + const [first, second] = args; + + if (typeof first === 'string') { + return { + ...((second as Record) || {}), + type: (second as Record)?.type as string || defaultType, + message: first, + }; + } + + if (first && typeof first === 'object') { + return { + ...(first as Record), + type: (first as Record).type as string || defaultType, + } as ToastOptions; + } + + return { + type: defaultType, + message: '', + }; +}; + +/** + * meToast API 对象 + */ +const meToast: MeToast = { + _toasts: new Map(), + _config: { ...DEFAULTS } as unknown as ToastConfig, + version: '0.2.0', + + configure(opts: Partial): 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); + } + + 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(); + 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) => { toast.update(p); return this; }, + dismiss: () => toast.close(), + }; + }, + + promise(promise: Promise, opts: PromiseOptions = {}): Promise { + 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; + }); + }, + + _resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null { + const id = loadingToast.id; + const old = this._toasts.get(id); + if (!old) return null; + + const position = old.config.position; + old.close(); + + return this._emit({ ...opts, type, message, position } as ToastOptions); + }, + + confirm(message: string, opts: ConfirmOptions = {}): Promise { + if (typeof message !== 'string') { + console.error('MeToast.confirm: message must be a string'); + return Promise.resolve(false); + } + + return new Promise((resolve) => { + let resolved = false; + const safeResolve = (val: 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 { + if (typeof message !== 'string') { + console.error('MeToast.prompt: message must be a string'); + return Promise.resolve(null); + } + + return new Promise((resolve) => { + let resolved = false; + const safeResolve = (val: 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 | 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', () => { + try { a.onClick(toast); } catch (e) { console.error('Action onClick error:', e); } + if (a.close !== false) toast.close(); + }); + } + }); + }, 0); + } + + return { id: toast.id, toast, dismiss: () => toast.close() }; + }, + + queue(messages: Array, 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((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, 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()); + }, + + clear(position?: string): void { + this._toasts.forEach(t => { + if (!position || t.config.position === position) t.close(); + }); + }, + + destroy(): void { + this.dismiss(); + + if (typeof document !== 'undefined') { + const containers = document.querySelectorAll('.met-container'); + containers.forEach(c => { if (c.parentNode) c.parentNode.removeChild(c); }); + + const style = document.getElementById('metona-toast-styles'); + if (style && style.parentNode) style.parentNode.removeChild(style); + } + + this._toasts.clear(); + _containerCache.clear(); + }, + + getAll(): Map { + 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 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 unknown>; + if (isObj && args.length === 1) { + return selfMethods[m]({ ...(lastArg as Record), group: name }); + } + if (isObj) { + args.pop(); + return selfMethods[m](...args, { ...(lastArg as Record), 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): 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); + }, + + // ====== 以下由 index.ts 增强 ====== + + init(_options?: Record): MeToast { return this; }, + getStatus() { return { version: '0.2.0', toasts: 0, theme: '', locale: '', plugins: [], animations: 0 }; }, + getConfig(): ToastConfig { return { ...this._config }; }, + updateConfig(_config: Partial): MeToast { return this; }, + resetConfig(): MeToast { return this; }, + use(_plugin: string | Record, _options?: Record): MeToast { return this; }, + + 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._onError = (info: ErrorInfo) => { + const onError = meToast._config.onError; + if (typeof onError === 'function') { + try { onError(info); } catch (_e) { /* noop */ } + } +}; +Toast._removeToast = (id: string) => { + meToast._toasts.delete(id); +}; + +export { meToast as default, meToast }; diff --git a/src/constants.js b/src/constants.ts similarity index 82% rename from src/constants.js rename to src/constants.ts index 57bf979..25fc3b9 100644 --- a/src/constants.js +++ b/src/constants.ts @@ -1,72 +1,51 @@ /** - * MetonaToast Constants - 常量定义 + * MetonaToast Constants — 常量定义 * @module constants - * @version 0.1.2 - * @description 默认配置、颜色、动画、主题等常量 + * @version 0.2.0 */ +import { ICONS } from './icons.js'; +import { LOCALES } from './locales.js'; + +export { ICONS, LOCALES }; + /** * 默认配置 */ export const DEFAULTS = Object.freeze({ - // 基础配置 position: 'top-right', duration: 4000, max: 6, gap: 12, offset: 24, - - // 交互配置 pauseOnHover: true, closeOnClick: true, draggable: true, - - // 显示配置 showProgress: true, - progressDirection: 'horizontal', + 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, + onShow: null as ((...args: unknown[]) => 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, - - // 国际化 + onError: null as ((...args: unknown[]) => void) | null, locale: 'zh-CN', - - // 插件 - plugins: [], + plugins: [] as Array>, }); -// Icons — 从 icons.js 导入并重新导出(保持向后兼容) -import { ICONS } from './icons.js'; -export { ICONS }; - /** * 类型颜色配置 */ -export const TYPE_COLORS = { +export const TYPE_COLORS: Record = { 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)' }, @@ -179,70 +158,75 @@ export const TYPE_COLORS = { /** * 动画配置 */ -export const ANIMATIONS = { +export const ANIMATIONS: Record; + leave: Record; + 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)', }, @@ -251,7 +235,15 @@ export const ANIMATIONS = { /** * 主题配置 */ -export const THEMES = { +export const THEMES: Record = { light: { bg: 'rgba(255, 255, 255, 0.96)', text: '#1f2937', @@ -291,7 +283,7 @@ export const THEMES = { }; /** - * 位置配置 + * 位置列表 */ export const POSITIONS = [ 'top-left', @@ -300,10 +292,10 @@ export const POSITIONS = [ 'bottom-left', 'bottom-center', 'bottom-right', -]; +] as const; /** - * 类型配置 + * 类型列表 */ export const TYPES = [ 'default', 'success', 'error', 'warning', 'info', 'loading', @@ -322,23 +314,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; diff --git a/src/core.js b/src/core.js deleted file mode 100644 index 6a645cd..0000000 --- a/src/core.js +++ /dev/null @@ -1,1243 +0,0 @@ -/** - * MetonaToast Core - 核心Toast逻辑 - * @module core - * @version 0.1.2 - * @description 重构后的核心模块,包含Toast类的优化实现 - */ - -import { generateId, escapeHTML } from './utils.js'; -import { DEFAULTS, ICONS, TYPE_COLORS, THEMES } from './constants.js'; -import { injectStyles } from './styles.js'; -import { getTheme, applyTheme } from './themes.js'; -import { t as i18nT, setCurrentLocale, getLocaleDirection, getCurrentLocale } from './i18n.js'; - -// 模块级共享容器缓存,所有 Toast 实例共用 -// 使用 Map 而非 WeakMap:document.body 在页面存活期间永不被 GC, -// 容器需要显式通过 destroy() 清理,WeakMap 在此场景无实际收益。 -const _containerCache = new Map(); - -/** - * 参数标准化工具 - * 支持多种调用方式: - * - success('message') - * - success({ message: 'text' }) - * - success({ title: 'title', message: 'text' }) - * - success('message', { duration: 5000 }) - */ -const normalizeArgs = (args, defaultType = 'default') => { - const [first, second] = args; - - // 情况1: success('message') - if (typeof first === 'string') { - return { - ...second, - type: second?.type || defaultType, - message: first, - }; - } - - // 情况2: success({ message: 'text' }) 或 success({ title: 't', message: 'm' }) - if (first && typeof first === 'object') { - return { - ...first, - type: first.type || defaultType, - }; - } - - // 情况3: success() 无参数 - return { - type: defaultType, - message: '', - }; -}; - -/** - * Toast类 - 核心通知组件 - */ -class Toast { - // 静态钩子系统 - static _hooks = new Map(); - static on(name, fn) { - if (!this._hooks.has(name)) this._hooks.set(name, []); - this._hooks.get(name).push(fn); - return () => this.off(name, fn); - } - static off(name, fn) { - const list = this._hooks.get(name); - if (list) this._hooks.set(name, list.filter(f => f !== fn)); - } - static trigger(name, toast) { - const list = this._hooks.get(name); - if (list) list.forEach(fn => { - try { fn(toast); } - catch (e) { - console.error('Hook error:', name, e); - if (meToast._config.onError) { - try { meToast._config.onError({ hook: name, error: e, toast }); } catch (_) {} - } - } - }); - } - - constructor(opts) { - this.id = opts.id || generateId(); - this.type = opts.type || 'default'; - this.title = opts.title || ''; - this.message = opts.message ?? opts.content ?? ''; - this.html = opts.html || ''; - this.iconHTML = opts.iconHTML || ''; - this.config = { ...DEFAULTS, ...opts }; - // 防止嵌套对象被多个 toast 实例共享引用 - if (opts.style && typeof opts.style === 'object') { - this.config.style = { ...opts.style }; - } - this.el = null; - this.barEl = null; - this.rafId = null; - this.remaining = this.config.duration; - this.startedAt = 0; - this.paused = false; - this.closing = false; - this._cleanups = []; - this.group = opts.group || null; - } - - _palette() { - const theme = getTheme(this.config.theme); - const c = TYPE_COLORS[this.type] || TYPE_COLORS.default; - const tc = THEMES[theme] || THEMES.light; - const t = { bg: tc.bg, border: tc.border, shadow: tc.shadow }; - return { theme, c, t }; - } - - create() { - if (typeof window === 'undefined' || typeof document === 'undefined') return this; - - Toast.trigger('beforeShow', this); - injectStyles(); - - const container = this._getContainer(); - const { theme, c, t } = this._palette(); - - this._limitToasts(container); - - const el = document.createElement('div'); - el.className = this._buildClassName(theme); - el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status'); - el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite'); - el.dataset.id = this.id; - - this._applyStyles(el, theme, t); - this._buildContent(el, c); - - this.el = el; - // Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore - const pos = this.config.position || 'top-right'; - if (pos.startsWith('top')) { - container.insertBefore(el, container.firstChild); - } else { - container.appendChild(el); - } - - this._bindEvents(el); - this._startTimer(); - - // 进入动画 - requestAnimationFrame(() => { - requestAnimationFrame(() => { - el.classList.add('met-show'); - }); - }); - - if (typeof this.config.onShow === 'function') { - try { - this.config.onShow(this); - } catch (e) { - console.error('onShow callback error:', e); - } - } - - Toast.trigger('afterShow', this); - - return this; - } - - _getContainer() { - const position = this.config.position || 'top-right'; - const zIndex = this.config.zIndex || 9999; - - if (_containerCache.has(document.body)) { - const containers = _containerCache.get(document.body); - if (containers.has(position)) { - return containers.get(position); - } - } else { - _containerCache.set(document.body, new Map()); - } - - const el = document.createElement('div'); - el.className = `met-container ${position}`; - el.setAttribute('aria-label', 'Notifications'); - el.setAttribute('role', 'region'); - - // cssText 完全接管布局,不依赖 CSS class - const posStyles = { - 'top-left': 'top:0;left:0;align-items:flex-start', - 'top-center': 'top:0;left:0;right:0;align-items:center', - 'top-right': 'top:0;right:0;align-items:flex-end', - 'bottom-left': 'bottom:0;left:0;align-items:flex-start', - 'bottom-center':'bottom:0;left:0;right:0;align-items:center', - 'bottom-right': 'bottom:0;right:0;align-items:flex-end', - }; - el.style.cssText = ` - display:flex; - flex-direction:column; - gap:${this.config.gap || 12}px; - box-sizing:border-box; - padding:${this.config.offset || 24}px; - max-width:100vw; - position:fixed; - z-index:${zIndex}; - pointer-events:none; - ${posStyles[position] || 'top:0;right:0;align-items:flex-end'} - `; - - document.body.appendChild(el); - _containerCache.get(document.body).set(position, el); - - return el; - } - - _limitToasts(container) { - const max = this.config.max || 6; - const list = Array.from(container.querySelectorAll('.met-toast')); - if (list.length >= max) { - const first = list[0]; - const id = first && first.dataset.id; - if (id) { - const old = meToast._toasts.get(id); - if (old) old.close(true); - } - } - } - - _buildClassName(theme) { - // CSS 支持的动画类型,未列出的 fallback 到 slide - const CSS_ANIMS = ['slide','fade','scale','bounce','flip','rotate','zoom', - 'slideUp','slideDown','slideLeft','slideRight']; - const anim = CSS_ANIMS.includes(this.config.animation) ? this.config.animation : 'slide'; - return [ - 'met-toast', - `met-${this.type}`, - `met-theme-${theme}`, - `met-anim-${anim}`, - (this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '', - this.config.className || '', - ].filter(Boolean).join(' '); - } - - _applyStyles(el, theme, t) { - const widthValue = typeof this.config.width === 'number' - ? `${this.config.width}px` - : (typeof this.config.width === 'string' ? this.config.width : '360px'); - - // 只强制核心布局属性,动画/滤镜/颜色由 CSS class 控制 - const set = (p, v) => el.style.setProperty(p, v, 'important'); - set('position', 'relative'); - set('flex-shrink', '0'); - set('min-width', '240px'); - set('max-width', 'calc(100vw - 48px)'); - set('width', widthValue); - // 用户自定义样式最后应用 - if (this.config.style && Object.keys(this.config.style).length > 0) { - Object.entries(this.config.style).forEach(([k, v]) => { el.style[k] = v; }); - } - } - - _buildContent(el, c) { - // 自定义渲染函数 — 完全接管 DOM 构建 - if (typeof this.config.render === 'function') { - el.innerHTML = this.config.render(this); - this.barEl = el.querySelector('.met-bar, .met-bar-v'); - return; - } - - const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]); - const showClose = this.config.closeButton !== false; - const showProgress = this.config.showProgress !== false && this.config.duration > 0; - const showSide = (!showIcon && this.type !== 'default'); - - const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; - const safeMessage = this.html - ? `
${this.html}
` - : (this.message ? `
${escapeHTML(this.message)}
` : ''); - - const iconHTML = showIcon - ? `
${this.iconHTML || ICONS[this.type]}
` : ''; - const closeHTML = showClose - ? `` - : ''; - const progressHTML = showProgress - ? (this.config.progressDirection === 'vertical' - ? `
` - : `
`) - : ''; - const sideHTML = showSide - ? `
` : ''; - - el.innerHTML = ` - ${iconHTML} - ${sideHTML} -
- ${safeTitle} - ${safeMessage} -
- ${closeHTML} - ${progressHTML} - `; - - this.barEl = el.querySelector('.met-bar, .met-bar-v'); - } - - _bindEvents(el) { - const eventHandler = (e) => { - const target = e.target; - - if (target.closest('.met-close')) { - e.stopPropagation(); - this.close(); - return; - } - - 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) { - const mouseEnter = () => this._pause(); - const mouseLeave = () => this._resume(); - - el.addEventListener('mouseenter', mouseEnter); - el.addEventListener('mouseleave', mouseLeave); - - this._cleanups.push(() => { - el.removeEventListener('mouseenter', mouseEnter); - el.removeEventListener('mouseleave', mouseLeave); - }); - } - - if (this.config.draggable) { - this._bindDrag(el); - } - } - - _bindDrag(el) { - let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false; - - const down = (e) => { - if (e.target.closest('.met-close')) return; - dragging = true; - sx = e.clientX; - sy = e.clientY; - el.setPointerCapture(e.pointerId); - el.style.transition = 'none'; - this._pause(); - }; - - const move = (e) => { - if (!dragging) return; - dx = e.clientX - sx; - dy = e.clientY - sy; - el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`; - el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200)); - }; - - const up = (e) => { - if (!dragging) return; - dragging = false; - el.releasePointerCapture(e.pointerId); - el.style.transition = ''; - - if (Math.abs(dx) > 120) { - 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; - }; - - el.addEventListener('pointerdown', down); - el.addEventListener('pointermove', move); - el.addEventListener('pointerup', up); - el.addEventListener('pointercancel', up); - - this._cleanups.push(() => { - el.removeEventListener('pointerdown', down); - el.removeEventListener('pointermove', move); - el.removeEventListener('pointerup', up); - el.removeEventListener('pointercancel', up); - }); - } - - _startTimer(resuming = false) { - if (this.config.duration <= 0) return; - - // 仅在首次启动时重置时间基准;resume 场景由 _resume() 预先调整 startedAt - if (!resuming) { - this.startedAt = Date.now(); - this.remaining = this.config.duration; - } - - const tick = () => { - if (this.paused || this.closing) return; - - try { - const elapsed = Date.now() - this.startedAt; - this.remaining = Math.max(0, this.config.duration - elapsed); - - if (this.barEl) { - const ratio = this.remaining / this.config.duration; - const t = this.config.progressDirection === 'vertical' - ? `scaleY(${ratio})` : `scaleX(${ratio})`; - this.barEl.style.transform = t; - } - - if (this.remaining <= 0) { - this.close(); - return; - } - } catch (e) { - console.error('Timer tick error:', e); - if (meToast._config.onError) { - try { meToast._config.onError({ source: 'timer', error: e, toast: this }); } catch (_) {} - } - } - - this.rafId = requestAnimationFrame(tick); - }; - - this.rafId = requestAnimationFrame(tick); - } - - _pause() { - if (this.paused || this.config.duration <= 0) return; - this.paused = true; - cancelAnimationFrame(this.rafId); - this.remaining = Math.max(0, this.config.duration - (Date.now() - this.startedAt)); - } - - _resume() { - if (!this.paused) return; - this.paused = false; - // 校准时间基准,补偿暂停期间经过的时间 - this.startedAt = Date.now() - (this.config.duration - this.remaining); - this._startTimer(true); - } - - update(partial) { - Toast.trigger('beforeUpdate', this); - const typeChanged = partial.type && partial.type !== this.type; - if (partial.type) this.type = partial.type; - if (partial.title !== undefined) this.title = partial.title; - if (partial.message !== undefined) this.message = partial.message; - if (partial.html !== undefined) this.html = partial.html; - - if (!this.el) { Toast.trigger('afterUpdate', this); return this; } - - // 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色 - if (typeChanged) { - const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default']; - typeClasses.forEach(c => this.el.classList.remove(c)); - this.el.classList.add(`met-${this.type}`); - if (this.barEl) { - const c = (TYPE_COLORS[this.type] || TYPE_COLORS.default); - this.barEl.style.background = c.fg; - } - const side = this.el.querySelector('.met-side'); - if (side) { - const c = (TYPE_COLORS[this.type] || TYPE_COLORS.default); - side.style.background = c.fg; - } - } - - const content = this.el.querySelector('.met-content'); - if (content) { - const safeTitle = this.title ? `
${escapeHTML(this.title)}
` : ''; - const safeMessage = this.html - ? `
${this.html}
` - : (this.message ? `
${escapeHTML(this.message)}
` : ''); - content.innerHTML = `${safeTitle}${safeMessage}`; - } - - // resetTimerOnUpdate: 更新内容后重置计时器 - if (this.config.resetTimerOnUpdate && this.config.duration > 0) { - cancelAnimationFrame(this.rafId); - this.startedAt = Date.now(); - this.remaining = this.config.duration; - 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; - } - - close(immediate = false) { - if (this.closing) return; - this.closing = true; - - Toast.trigger('beforeClose', this); - cancelAnimationFrame(this.rafId); - this._cleanups.forEach(fn => { try { fn(); } catch (_) {} }); - this._cleanups = []; - - const el = this.el; - if (!el) { - meToast._remove(this.id); - return; - } - - // 立即脱离文档流:记录当前位置,然后设为 absolute,让下方 toast 自然上移 - const container = el.parentNode; - if (container && !immediate) { - const elRect = el.getBoundingClientRect(); - const containerRect = container.getBoundingClientRect(); - el.style.position = 'absolute'; - el.style.top = (elRect.top - containerRect.top) + 'px'; - el.style.left = (elRect.left - containerRect.left) + 'px'; - el.style.width = elRect.width + 'px'; - el.style.margin = '0'; - } - - el.classList.add('met-leaving'); - el.classList.remove('met-show'); - - const pos = this.config.position || 'top-right'; - const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl'; - let transform = 'scale(.96)'; - - if (pos.includes('right')) transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)'; - else if (pos.includes('left')) transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)'; - else if (pos.startsWith('top')) transform = 'translateY(-20px)'; - else transform = 'translateY(20px)'; - - el.style.transform = transform; - el.style.opacity = '0'; - - setTimeout(() => this._destroy(), immediate ? 0 : 300); - } - - _destroy() { - if (this.el && this.el.parentNode) { - this.el.parentNode.removeChild(this.el); - } - this.el = null; - - if (typeof this.config.onClose === 'function') { - try { - this.config.onClose(this); - } catch (e) { - console.error('onClose callback error:', e); - } - } - - Toast.trigger('afterClose', this); - meToast._remove(this.id); - } -} - -// ========== HTML模板辅助函数 ========== - -/** - * 按钮行模板 — confirm/prompt 共用 - * @param {string} buttons - 按钮HTML - * @returns {string} 按钮行HTML - */ -const _btnRow = (buttons) => ` -
- ${buttons} -
`; - -/** - * 确认对话框模板 - * @param {Object} opts - 选项 - * @returns {string} HTML - */ -const _confirmHTML = (opts) => { - const confirmText = opts.confirmText || i18nT('confirm'); - const cancelText = opts.cancelText || i18nT('cancel'); - const confirmColor = opts.confirmColor || '#10b981'; - const cancelColor = opts.cancelColor || '#6b7280'; - return _btnRow(` - - - `); -}; - -/** - * 输入对话框模板 - * @param {Object} opts - 选项 - * @returns {string} HTML - */ -const _promptHTML = (opts) => { - const submitText = opts.submitText || i18nT('confirm'); - const cancelText = opts.cancelText || i18nT('cancel'); - const submitColor = opts.submitColor || '#3b82f6'; - const cancelColor = opts.cancelColor || '#6b7280'; - return ` - - ${_btnRow(` - - - `)} - `; -}; - -/** - * 进度条模板 - * @param {Object} opts - 选项 - * @returns {string} HTML - */ -const _progressHTML = (opts) => { - const color = opts.progressColor || '#3b82f6'; - return ` -
-
-
-
0%
- `; -}; - -/** - * Action Toast 模板 - * @param {Array} actions - 操作按钮数组 [{ text, onClick, style }] - * @returns {string} HTML - */ -const _actionHTML = (actions) => { - if (!Array.isArray(actions) || actions.length === 0) return ''; - const buttons = actions.map((a, i) => { - const bg = a.style?.background || a.color || '#6366f1'; - const cls = `met-action-btn-${i}`; - return ``; - }).join(''); - return _btnRow(buttons); -}; - -/** - * 主对象 - */ -const meToast = { - _toasts: new Map(), - _config: { ...DEFAULTS }, - version: '0.1.2', - - configure(opts) { - if (!opts || typeof opts !== 'object') return this; - // 原地更新,保持 _config 引用一致(index.js 的增强对象共享同一个 _config) - Object.assign(this._config, opts); - - if (opts.theme) { - applyTheme(opts.theme); - } - - if (opts.locale) { - setCurrentLocale(opts.locale); - } - - return this; - }, - - _emit(opts) { - const merged = { ...this._config, ...opts }; - if (merged.content && !merged.message) merged.message = merged.content; - - const t = new Toast(merged); - t.create(); - this._toasts.set(t.id, t); - - return t; - }, - - _remove(id) { - this._toasts.delete(id); - }, - - find(id) { - if (!id) return undefined; - return this._toasts.get(id); - }, - - /** - * 显示默认Toast - * @param {string|Object} messageOrOpts - 消息字符串或选项对象 - * @param {Object} [opts] - 选项对象(当第一个参数是字符串时) - */ - show(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'default'); - return this._emit(normalized); - }, - - /** - * 显示成功Toast - * @param {string|Object} messageOrOpts - 消息字符串或选项对象 - * @param {Object} [opts] - 选项对象 - */ - success(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'success'); - return this._emit(normalized); - }, - - /** - * 显示错误Toast - * @param {string|Object} messageOrOpts - 消息字符串或选项对象 - * @param {Object} [opts] - 选项对象 - */ - error(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'error'); - return this._emit(normalized); - }, - - /** - * 显示警告Toast - * @param {string|Object} messageOrOpts - 消息字符串或选项对象 - * @param {Object} [opts] - 选项对象 - */ - warning(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'warning'); - return this._emit(normalized); - }, - - /** - * 显示信息Toast - * @param {string|Object} messageOrOpts - 消息字符串或选项对象 - * @param {Object} [opts] - 选项对象 - */ - info(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'info'); - return this._emit(normalized); - }, - - /** - * 显示加载Toast - * @param {string|Object} messageOrOpts - 消息字符串或选项对象 - * @param {Object} [opts] - 选项对象 - */ - loading(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'loading'); - normalized.duration = 0; - normalized.closeButton = false; - normalized.showProgress = false; - - const t = this._emit(normalized); - - return { - id: t.id, - success: (msg, o = {}) => this._resolve(t, 'success', msg, o), - error: (msg, o = {}) => this._resolve(t, 'error', msg, o), - info: (msg, o = {}) => this._resolve(t, 'info', msg, o), - warning: (msg, o = {}) => this._resolve(t, 'warning', msg, o), - update: (p) => { t.update(p); return this; }, - dismiss: () => t.close(), - }; - }, - - promise(promise, opts = {}) { - if (!promise || typeof promise.then !== 'function') { - console.error('MeToast.promise: first argument must be a Promise'); - return Promise.reject(new Error('Invalid promise')); - } - - const loadingMsg = opts.loading || i18nT('loading'); - const successMsg = opts.success || i18nT('success'); - const errorMsg = opts.error || i18nT('error'); - - const ctrl = this.loading(loadingMsg); - - return Promise.resolve(promise) - .then((data) => { - ctrl.success(successMsg); - return data; - }) - .catch((err) => { - ctrl.error(errorMsg); - throw err; - }); - }, - - _resolve(loadingToast, type, message, opts) { - const id = loadingToast.id; - const old = this._toasts.get(id); - if (!old) return null; - - const position = old.config.position; - old.close(); - - return this._emit({ ...opts, type, message, position }); - }, - - /** - * 确认对话框 - * @param {string} message - 消息内容 - * @param {Object} [opts] - 选项 - * @returns {Promise} - */ - confirm(message, opts = {}) { - if (typeof message !== 'string') { - console.error('MeToast.confirm: message must be a string'); - return Promise.resolve(false); - } - - return new Promise((resolve) => { - let resolved = false; - const safeResolve = (val) => { if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); } }; - const safetyTimeout = setTimeout(() => safeResolve(false), 10000); - - const t = this._emit({ - ...opts, - type: opts.type || 'warning', - message, - duration: 0, - closeButton: false, - closeOnClick: false, - draggable: false, - html: _confirmHTML(opts), - }); - - setTimeout(() => { - const confirmBtn = t.el?.querySelector('.met-confirm-btn'); - const cancelBtn = t.el?.querySelector('.met-cancel-btn'); - - if (confirmBtn) { - confirmBtn.addEventListener('click', () => { - t.close(); - safeResolve(true); - }); - } - - if (cancelBtn) { - cancelBtn.addEventListener('click', () => { - t.close(); - safeResolve(false); - }); - } - }, 0); - }); - }, - - /** - * 输入对话框 - * @param {string} message - 消息内容 - * @param {Object} [opts] - 选项 - * @returns {Promise} - */ - prompt(message, opts = {}) { - if (typeof message !== 'string') { - console.error('MeToast.prompt: message must be a string'); - return Promise.resolve(null); - } - - return new Promise((resolve) => { - let resolved = false; - const safeResolve = (val) => { if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); } }; - const safetyTimeout = setTimeout(() => safeResolve(null), 10000); - - const t = this._emit({ - ...opts, - type: opts.type || 'info', - message, - duration: 0, - closeButton: false, - closeOnClick: false, - draggable: false, - html: _promptHTML(opts), - }); - - setTimeout(() => { - const input = t.el?.querySelector('.met-input'); - const submitBtn = t.el?.querySelector('.met-submit-btn'); - const cancelBtn = t.el?.querySelector('.met-cancel-btn'); - - if (input) { - input.focus(); - - input.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - t.close(); - safeResolve(input.value); - } - }); - } - - if (submitBtn) { - submitBtn.addEventListener('click', () => { - t.close(); - safeResolve(input?.value || null); - }); - } - - if (cancelBtn) { - cancelBtn.addEventListener('click', () => { - t.close(); - safeResolve(null); - }); - } - }, 0); - }); - }, - - /** - * 进度Toast - * @param {string|Object} messageOrOpts - 消息或选项 - * @param {Object} [opts] - 选项 - * @returns {Object} 控制对象 - */ - progress(messageOrOpts, opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'info'); - normalized.duration = 0; - normalized.closeButton = false; - normalized.showProgress = false; - - const t = this._emit({ - ...normalized, - html: _progressHTML(opts), - }); - - return { - id: t.id, - - setProgress(percent) { - const fill = t.el?.querySelector('.met-progress-fill'); - const text = t.el?.querySelector('.met-progress-text'); - - if (fill) { - fill.style.width = `${Math.min(100, Math.max(0, percent))}%`; - } - - if (text) { - text.textContent = `${Math.round(percent)}%`; - } - }, - - complete(message) { - this.setProgress(100); - setTimeout(() => { - t.update({ - type: 'success', - message: message || i18nT('success'), - html: '', - }); - setTimeout(() => t.close(), 1000); - }, 300); - }, - - error(message) { - t.update({ - type: 'error', - message: message || i18nT('error'), - html: '', - }); - setTimeout(() => t.close(), 2000); - }, - - dismiss() { - t.close(); - }, - }; - }, - - /** - * 倒计时Toast - * @param {string} message - 消息内容 - * @param {number} seconds - 秒数 - * @param {Object} [opts] - 选项 - * @returns {Object} 控制对象 - */ - countdown(message, seconds = 10, opts = {}) { - if (typeof message !== 'string') { - console.error('MeToast.countdown: message must be a string'); - return { cancel: () => {}, pause: () => {}, resume: () => {} }; - } - - let remaining = Math.max(1, parseInt(seconds) || 10); - let timer = null; - - const t = this._emit({ - ...opts, - type: opts.type || 'warning', - message: message.replace(/\{seconds\}/g, remaining), - duration: 0, - closeButton: true, - showProgress: false, - }); - - const tick = () => { - remaining--; - - if (remaining <= 0) { - clearInterval(timer); - t.close(); - - if (typeof opts.onComplete === 'function') { - try { - opts.onComplete(); - } catch (e) { - console.error('countdown onComplete error:', e); - } - } - return; - } - - t.update({ - message: message.replace(/\{seconds\}/g, remaining), - }); - }; - - timer = setInterval(tick, 1000); - - return { - id: t.id, - - cancel() { - clearInterval(timer); - t.close(); - }, - - pause() { - clearInterval(timer); - }, - - resume() { - clearInterval(timer); - timer = setInterval(tick, 1000); - }, - }; - }, - - /** - * Action Toast — 带操作按钮的通知 - * @param {string|Object} messageOrOpts - 消息 - * @param {Array} actions - 操作按钮 [{ text, onClick, color }] - * @param {Object} [opts] - 选项 - * @returns {Object} toast实例 + dismiss - */ - action(messageOrOpts, actions = [], opts = {}) { - const normalized = normalizeArgs([messageOrOpts, opts], 'info'); - normalized.duration = opts.duration ?? 0; - normalized.closeButton = opts.closeButton ?? true; - - const t = this._emit({ - ...normalized, - html: (normalized.html || '') + _actionHTML(actions), - }); - - if (Array.isArray(actions)) { - setTimeout(() => { - actions.forEach((a, i) => { - const btn = t.el?.querySelector(`.met-action-btn-${i}`); - if (btn && typeof a.onClick === 'function') { - btn.addEventListener('click', () => { - try { a.onClick(t); } catch (e) { console.error('Action onClick error:', e); } - if (a.close !== false) t.close(); - }); - } - }); - }, 0); - } - - return { id: t.id, toast: t, dismiss: () => t.close() }; - }, - - /** - * 队列Toast - * @param {Array} messages - 消息数组 - * @param {Object} [opts] - 选项 - * @returns {Promise} - */ - queue(messages, opts = {}) { - if (!Array.isArray(messages)) { - console.error('MeToast.queue: messages must be an array'); - return { then: (fn) => Promise.resolve().then(fn), cancel: () => {} }; - } - - let cancelled = false; - const promise = new Promise((resolve) => { - let index = 0; - const delay = opts.delay || 1000; - const userOnClose = opts.onClose; - - const showNext = () => { - if (cancelled || index >= messages.length) { - resolve(); - return; - } - - const message = messages[index]; - index++; - - const msg = typeof message === 'string' ? message : (message?.message || ''); - const msgObj = (typeof message === 'object' && message !== null) ? message : {}; - const msgOnClose = msgObj.onClose; - - this._emit({ - ...opts, - ...msgObj, - message: msg, - duration: msgObj.duration || opts.duration || 3000, - onClose: (toast) => { - if (typeof msgOnClose === 'function') { - try { msgOnClose(toast); } catch (_) {} - } - if (typeof userOnClose === 'function') { - try { userOnClose(toast); } catch (_) {} - } - setTimeout(showNext, delay); - }, - }); - }; - - showNext(); - }); - return { - then: (fn, rj) => promise.then(fn, rj), - catch: (rj) => promise.catch(rj), - cancel: () => { cancelled = true; }, - }; - }, - - /** - * 堆叠Toast - * @param {Array} messages - 消息数组 - * @param {Object} [opts] - 选项 - */ - stack(messages, opts = {}) { - if (!Array.isArray(messages)) { - console.error('MeToast.stack: messages must be an array'); - return; - } - - messages.forEach((message, index) => { - setTimeout(() => { - const msg = typeof message === 'string' ? message : (message?.message || ''); - - this._emit({ - ...opts, - ...((typeof message === 'object' && message !== null) ? message : {}), - message: msg, - }); - }, index * (opts.stagger || 100)); - }); - }, - - dismiss(id) { - if (id) { - const t = this._toasts.get(id); - if (t) t.close(); - return; - } - this._toasts.forEach(t => t.close()); - }, - - clear(position) { - this._toasts.forEach(t => { - if (!position || t.config.position === position) t.close(); - }); - }, - - destroy() { - this.dismiss(); - - 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(); - _containerCache.clear(); - }, - - getAll() { - return new Map(this._toasts); - }, - - count() { - return this._toasts.size; - }, - - /** - * 创建Toast分组 — 返回一个自动添加 group 参数的子对象 - * @param {string} name - 分组名称 - * @returns {Object} - */ - group(name) { - const self = this; - const methods = ['show','success','error','warning','info','loading','action']; - const g = { _group: name }; - methods.forEach(m => { - g[m] = (...args) => { - const lastArg = args[args.length - 1]; - const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg); - // 单对象参数:直接合并group - if (isObj && args.length === 1) { - return self[m]({ ...lastArg, group: name }); - } - // 末尾是配置对象:pop后合并 - if (isObj) { - args.pop(); - return self[m](...args, { ...lastArg, group: name }); - } - // 无配置对象 - return self[m](...args, { group: name }); - }; - }); - g.dismiss = () => self.dismissGroup(name); - g.count = () => self._groupCount(name); - return g; - }, - - /** - * 按组关闭Toast - * @param {string} name - 分组名称 - */ - dismissGroup(name) { - this._toasts.forEach(t => { if (t.group === name) t.close(); }); - }, - - _groupCount(name) { - let c = 0; - this._toasts.forEach(t => { if (t.group === name) c++; }); - return c; - }, -}; - -export { meToast as default, meToast, Toast }; diff --git a/src/i18n.js b/src/i18n.ts similarity index 55% rename from src/i18n.js rename to src/i18n.ts index fe7f8d9..9351dd2 100644 --- a/src/i18n.js +++ b/src/i18n.ts @@ -1,35 +1,33 @@ /** - * MetonaToast i18n - 国际化管理 + * MetonaToast i18n — 国际化管理 * @module i18n - * @version 0.1.2 - * @description 多语言支持、语言切换和翻译管理 + * @version 0.2.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 => { 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)) { + result = (result as Record)[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 => { + 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 => { return LOCALES[locale] || {}; }; /** * 添加翻译 - * @param {string} locale - 语言代码 - * @param {Object} translations - 翻译对象 */ -export const addTranslations = (locale, translations) => { +export const addTranslations = (locale: string, translations: Record): void => { if (!LOCALES[locale]) { LOCALES[locale] = {}; } - - deepMerge(LOCALES[locale], translations); + deepMerge(LOCALES[locale], translations as Record); }; /** * 深度合并对象 - * @param {Object} target - 目标对象 - * @param {Object} source - 源对象 - * @returns {Object} 合并后的对象 */ -const deepMerge = (target, source) => { +const deepMerge = (target: Record, source: Record): Record => { 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, source[key] as Record); } 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 = 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; } 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 = { '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,17 +395,14 @@ 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) { @@ -467,12 +412,8 @@ export const formatNumber = (number, options = {}) => { /** * 格式化货币 - * @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', @@ -486,11 +427,8 @@ export const formatCurrency = (amount, currency = 'USD', options = {}) => { /** * 格式化百分比 - * @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', @@ -505,26 +443,20 @@ export const formatPercent = (value, options = {}) => { /** * 格式化日期 - * @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(); + 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,41 +488,32 @@ 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(); + + return String(date); } catch (e) { - return date.toString(); + return String(date); } }; /** * 格式化列表 - * @param {Array} list - 列表 - * @param {Object} options - 格式化选项 - * @returns {string} 格式化后的字符串 */ -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 => { + // Intl.ListFormat requires ES2021+; fallback to comma join for wider compat + 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) { @@ -603,99 +523,50 @@ export const formatPlural = (count, options = {}) => { /** * 复数翻译 - * @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 => { 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 = { + 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 = { + 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 +605,18 @@ export const i18nUtils = { /** * 预设语言包 */ -export const presetLocales = { +export const presetLocales: Record; +}> = { 'zh-CN': { name: '简体中文', nativeName: '简体中文', direction: 'ltr', translations: LOCALES['zh-CN'], }, - 'en-US': { name: 'English (US)', nativeName: 'English (US)', @@ -750,46 +625,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 }; diff --git a/src/icons.js b/src/icons.ts similarity index 98% rename from src/icons.js rename to src/icons.ts index e239388..eea2c8b 100644 --- a/src/icons.js +++ b/src/icons.ts @@ -1,134 +1,134 @@ /** * MetonaToast Icons — 图标SVG定义 * @module icons - * @version 0.1.2 - * @description 80+ 内置SVG图标 + * @version 0.2.0 + * @description 107 个内置 SVG 图标 */ -export const ICONS = { +export const ICONS: Record = { success: ` `, - + error: ` `, - + warning: ` `, - + info: ` `, - + loading: ` `, - + close: ` `, - + check: ` `, - + x: ` `, - + alert: ` `, - + question: ` `, - + star: ` `, - + heart: ` `, - + bell: ` `, - + mail: ` `, - + settings: ` `, - + user: ` `, - + home: ` `, - + search: ` `, - + plus: ` `, - + minus: ` `, - + edit: ` `, - + trash: ` `, - + download: ` `, - + upload: ` `, - + share: ` @@ -136,58 +136,58 @@ export const ICONS = { `, - + link: ` `, - + external: ` `, - + clock: ` `, - + calendar: ` `, - + map: ` `, - + compass: ` `, - + globe: ` `, - + wifi: ` `, - + cloud: ` `, - + sun: ` @@ -199,19 +199,19 @@ export const ICONS = { `, - + moon: ` `, - + zap: ` `, - + activity: ` `, - + cpu: ` @@ -224,57 +224,57 @@ export const ICONS = { `, - + database: ` `, - + server: ` `, - + terminal: ` `, - + code: ` `, - + git: ` `, - + package: ` `, - + layers: ` `, - + grid: ` `, - + list: ` @@ -283,144 +283,144 @@ export const ICONS = { `, - + 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: ` @@ -428,18 +428,18 @@ export const ICONS = { `, - + award: ` `, - + target: ` `, - + crosshair: ` @@ -447,7 +447,7 @@ export const ICONS = { `, - + move: ` @@ -456,20 +456,20 @@ export const ICONS = { `, - + maximize: ` `, - + minimize: ` `, - + copy: ` `, - + cut: ` @@ -477,40 +477,40 @@ export const ICONS = { `, - + paste: ` `, - + rotateCw: ` `, - + rotateCcw: ` `, - + zoomIn: ` `, - + zoomOut: ` `, - + crop: ` `, - + sliders: ` @@ -522,81 +522,81 @@ export const ICONS = { `, - + toggleLeft: ` `, - + toggleRight: ` `, - + checkCircle: ` `, - + xCircle: ` `, - + alertCircle: ` `, - + infoCircle: ` `, - + helpCircle: ` `, - + alertTriangle: ` `, - + checkSquare: ` `, - + square: ` `, - + circle: ` `, - + triangle: ` `, - + hexagon: ` `, - + octagon: ` `, - + pentagon: ` `, - + diamond: ` `, diff --git a/src/index.js b/src/index.ts similarity index 59% rename from src/index.js rename to src/index.ts index 3b6a879..37c1e78 100644 --- a/src/index.js +++ b/src/index.ts @@ -1,40 +1,41 @@ /** - * MetonaToast - 轻量级Toast通知库 + * MetonaToast — 轻量级Toast通知库 * @module metona-toast - * @version 0.1.2 + * @version 0.2.0 * @author thzxx - * @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。 * @license MIT */ -import { meToast, Toast } from './core.js'; +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 { DEFAULTS } from './constants.js'; +import type { MeToast, ToastConfig, InitOptions, StatusInfo, ToastInstance, ToastOptions, Plugin } from './types.js'; // 版本信息 -const VERSION = '0.1.2'; +const VERSION = '0.2.0'; /** * 主对象增强 */ -const enhancedMeToast = { +const enhancedMeToast: MeToast = { ...meToast, - + version: VERSION, - + animations: animationUtils, themes: themeUtils, i18n: i18nUtils, plugins: pluginUtils, presetPlugins, - + /** - * 安装插件,并连接必要的生命周期钩子 + * 安装插件 */ - use(plugin, options = {}) { + use(plugin: string | Record, options: Record = {}): MeToast { if (typeof plugin === 'string') { const preset = presetPlugins[plugin]; if (!preset) { @@ -45,58 +46,64 @@ const enhancedMeToast = { // 连接插件钩子 if (plugin === 'accessibility') { - Toast.on('afterShow', (toast) => preset.announce(toast)); + Toast.on('afterShow', (toast: ToastInstance) => { + if (typeof preset.announce === 'function') preset.announce(toast); + }); } if (plugin === 'persistence') { - const saved = preset.install(); - if (saved) this.configure(saved); - Toast.on('afterClose', () => { preset.save(this.getConfig()); }); + const saved = typeof preset.install === 'function' ? preset.install(pluginUtils as unknown as import('./types.js').PluginManager) : null; + if (saved) this.configure(saved as Partial); + Toast.on('afterClose', () => { + if (typeof (preset as Record).save === 'function') { + (preset as Record void>).save(this.getConfig()); + } + }); } } else if (plugin && typeof plugin === 'object') { - const name = plugin.name || 'custom'; - this.plugins.register(name, { ...plugin, ...options }); + const name = (plugin as Record).name || 'custom'; + this.plugins.register(name, { ...plugin, ...options } as unknown as Plugin); } return this; }, - + /** * 初始化 */ - init(options = {}) { + init(options: InitOptions = {}): MeToast { 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); + options.plugins.forEach((p) => { + this.use(p as string); }); } - + return this; }, - + /** * 销毁 */ - destroy() { + destroy(): void { if (this._destroyed) return; this._destroyed = true; this.dismiss(); - - if (this.plugins && typeof this.plugins.destroy === 'function') { - this.plugins.destroy(); + + if (this.plugins && typeof (this.plugins as unknown as Record).destroy === 'function') { + (this.plugins as unknown as Record void>).destroy(); } - + if (this.themes) { if (typeof this.themes.clearThemeListeners === 'function') { this.themes.clearThemeListeners(); @@ -105,30 +112,30 @@ const enhancedMeToast = { 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)); - + containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); }); + const style = document.getElementById('metona-toast-styles'); - if (style) style.parentNode.removeChild(style); + if (style && style.parentNode) style.parentNode.removeChild(style); } - + this._toasts.clear(); }, - + /** * 获取状态 */ - getStatus() { + getStatus(): StatusInfo { return { version: VERSION, toasts: this._toasts.size, @@ -138,18 +145,18 @@ const enhancedMeToast = { animations: this.animations?.getActiveCount?.() || 0, }; }, - + /** * 获取配置 */ - getConfig() { + getConfig(): ToastConfig { return { ...this._config }; }, - + /** * 更新配置 */ - updateConfig(config) { + updateConfig(config: Partial): MeToast { if (config && typeof config === 'object') { Object.assign(this._config, config); } @@ -159,96 +166,95 @@ const enhancedMeToast = { /** * 重置配置 */ - resetConfig() { - // 原地重置:先清除所有自有属性,再回填默认值 + resetConfig(): MeToast { const keys = Object.keys(this._config); - keys.forEach(k => delete this._config[k]); + keys.forEach(k => delete (this._config as Record)[k]); Object.assign(this._config, DEFAULTS); return this; }, - + /** * 获取Toast列表 */ - getToasts() { + getToasts(): ToastInstance[] { return Array.from(this._toasts.values()); }, - + /** * 检查是否有Toast */ - hasToasts() { + hasToasts(): boolean { return this._toasts.size > 0; }, - + /** * 获取Toast */ - getToast(id) { + getToast(id: string): ToastInstance | null { if (!id) return null; return this._toasts.get(id) || null; }, - + /** * 关闭所有Toast */ - closeAll() { + closeAll(): void { this._toasts.forEach((t) => t.close()); }, - + /** * 清除所有Toast */ - clearAll() { + clearAll(): void { this._toasts.forEach((t) => t.close()); }, - + /** * 暂停所有Toast */ - pauseAll() { + pauseAll(): void { this._toasts.forEach((t) => t._pause()); }, - + /** * 恢复所有Toast */ - resumeAll() { + resumeAll(): void { this._toasts.forEach((t) => t._resume()); }, - + /** * 更新所有Toast */ - updateAll(partial) { + updateAll(partial: Partial): void { if (partial && typeof partial === 'object') { this._toasts.forEach((t) => t.update(partial)); } }, - + /** * 查找Toast */ - findToasts(predicate) { + findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] { if (typeof predicate !== 'function') return []; return Array.from(this._toasts.values()).filter(predicate); }, - + /** * 按类型查找Toast */ - findByType(type) { + findByType(type: string): ToastInstance[] { return this.findToasts((t) => t.type === type); }, - + /** * 按位置查找Toast */ - findByPosition(position) { + findByPosition(position: string): ToastInstance[] { return this.findToasts((t) => t.config.position === position); }, - }; + // 初始化主题和国际化 if (typeof themeUtils.initTheme === 'function') { themeUtils.initTheme(); @@ -261,9 +267,15 @@ if (typeof i18nUtils.initI18n === 'function') { if (typeof window !== 'undefined') { window.MeToast = enhancedMeToast; window.Met = enhancedMeToast; - + // Notification API — 页面不可见时自动发送系统通知 - enhancedMeToast._notify = (toast) => { + const sendNotification = (toast: ToastInstance): void => { + try { + new Notification(toast.title || toast.type, { body: toast.message }); + } catch (_e) { /* noop */ } + }; + + (enhancedMeToast as unknown as Record)._notify = (toast: ToastInstance) => { if (typeof Notification === 'undefined') return; if (Notification.permission === 'denied') return; if (!document.hidden) return; @@ -273,16 +285,55 @@ if (typeof window !== 'undefined') { } 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); + + Toast.on('afterShow', (toast: ToastInstance) => { + const config = toast.config as ToastConfig; + if (config.notifyWhenHidden) { + ((enhancedMeToast as unknown as Record 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'; diff --git a/src/locales.js b/src/locales.ts similarity index 98% rename from src/locales.js rename to src/locales.ts index 38bc1f9..551b359 100644 --- a/src/locales.js +++ b/src/locales.ts @@ -1,11 +1,11 @@ /** * MetonaToast Locales — 国际化翻译数据 * @module locales - * @version 0.1.2 - * @description 内置 zh-CN / en-US 完整翻译(已去重优化) + * @version 0.2.0 + * @description 内置 zh-CN / en-US 完整翻译 */ -export const LOCALES = { +export const LOCALES: Record> = { 'zh-CN': { // === 操作 === close: '关闭', diff --git a/src/plugins.js b/src/plugins.js deleted file mode 100644 index dc97a54..0000000 --- a/src/plugins.js +++ /dev/null @@ -1,210 +0,0 @@ -/** - * MetonaToast Plugins - 插件系统 - * @module plugins - * @version 0.1.2 - * @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) { - console.error(`Invalid plugin "${name}": must be an object with a "name" property`); - return this; - } - - // 先存储再 install,确保 install 内通过 this 设置的实例属性保留在存储对象上 - // 保留插件自带的 name(若有),否则使用注册 key 作为 name - const stored = { ...plugin, installed: false, enabled: true }; - if (!stored.name) stored.name = name; - this.plugins.set(name, stored); - - if (stored.install) { - try { stored.install(this); stored.installed = true; } - catch (e) { console.error(`Failed to install plugin "${name}":`, e); } - } - return this; - } - - unregister(name) { - const plugin = this.plugins.get(name); - if (!plugin) return this; - if (plugin.uninstall) { - try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); } - } - this.plugins.delete(name); - return this; - } - - get(name) { return this.plugins.get(name) || null; } - has(name) { return this.plugins.has(name); } - getAll() { return Array.from(this.plugins.values()); } - getNames() { return Array.from(this.plugins.keys()); } - - enable(name) { - const p = this.plugins.get(name); - if (p) p.enabled = true; - return this; - } - disable(name) { - const p = this.plugins.get(name); - if (p) p.enabled = false; - return this; - } - isEnabled(name) { - const p = this.plugins.get(name); - return p ? p.enabled : false; - } - - destroy() { - this.plugins.forEach((p, 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 }; diff --git a/src/plugins.ts b/src/plugins.ts new file mode 100644 index 0000000..a0b9644 --- /dev/null +++ b/src/plugins.ts @@ -0,0 +1,220 @@ +/** + * MetonaToast Plugins — 插件系统 + * @module plugins + * @version 0.2.0 + */ + +import { t } from './i18n.js'; +import type { Plugin, PluginManager as IPluginManager, PluginUtils } from './types.js'; + +/** + * 插件管理器 + */ +class PluginManager implements IPluginManager { + plugins: Map; + 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 = { + /** + * 键盘快捷键插件 — 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)._handler = handler; + document.addEventListener('keydown', handler); + }, + + uninstall(this: Plugin) { + const handler = (this as Record)._handler as ((e: KeyboardEvent) => void) | null; + if (handler && typeof document !== 'undefined') { + document.removeEventListener('keydown', handler); + (this as Record)._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).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) { + if (typeof localStorage !== 'undefined') { + try { + const key = (this as Record).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).storageKey as string; + localStorage.removeItem(key); + } catch (_e) { /* noop */ } + } + }, + }, + + /** + * 无障碍插件 — 屏幕阅读器公告 + */ + 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); + }, + }, +}; + +/** + * 插件工具 + */ +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 { return { ...presetPlugins }; }, + + createPlugin(config: Partial): 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): { 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 }; diff --git a/src/styles.js b/src/styles.ts similarity index 87% rename from src/styles.js rename to src/styles.ts index ee3f0c1..3814e6f 100644 --- a/src/styles.js +++ b/src/styles.ts @@ -1,21 +1,19 @@ /** - * MetonaToast Styles - 样式管理(精简版 v0.1.2) + * MetonaToast Styles — 样式管理 * @module styles - * @version 0.1.2 - * @description 样式注入与主题管理,移除未使用的组件样式 + * @version 0.2.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; @@ -28,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; } @@ -36,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; @@ -70,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; @@ -91,7 +84,6 @@ const generateCSS = (theme) => { margin-top: 1px; } - /* 内容样式 */ .met-content { flex: 1; min-width: 0; @@ -99,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; @@ -135,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; @@ -145,7 +133,6 @@ const generateCSS = (theme) => { border-radius: 0 0 12px 12px; } - /* 进度条样式 - 垂直 */ .met-progress-v { position: absolute; left: 0; top: 0; bottom: 0; @@ -155,7 +142,6 @@ const generateCSS = (theme) => { border-radius: 12px 0 0 12px; } - /* 进度条指示器 - 水平 */ .met-bar { position: absolute; left: 0; top: 0; @@ -164,7 +150,6 @@ const generateCSS = (theme) => { transform: scaleX(1); } - /* 进度条指示器 - 垂直 */ .met-bar-v { position: absolute; left: 0; bottom: 0; @@ -173,7 +158,6 @@ const generateCSS = (theme) => { transform: scaleY(1); } - /* 侧边指示器 */ .met-side { position: absolute; left: 0; top: 0; bottom: 0; @@ -181,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; @@ -197,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, @@ -212,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; } @@ -273,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; } @@ -286,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; } @@ -321,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; } @@ -341,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; @@ -351,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)) @@ -386,30 +349,25 @@ 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; } - /* 滚动条样式 */ .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; @@ -427,7 +385,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; @@ -445,7 +402,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); @@ -453,7 +409,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); @@ -489,14 +444,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'; @@ -508,31 +463,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}; @@ -549,7 +503,7 @@ export const generateThemeVariables = (theme) => { /** * 应用主题变量 */ -export const applyThemeVariables = (theme) => { +export const applyThemeVariables = (theme: ThemeConfig): void => { if (typeof document === 'undefined') return; const css = generateThemeVariables(theme); @@ -562,7 +516,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); @@ -572,26 +526,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'); }; @@ -605,10 +559,11 @@ export const watchSystemTheme = (callback) => { /** * 自动应用系统主题 */ -export const autoApplySystemTheme = () => { - const applyTheme = (theme) => { - updateStyles(THEMES[theme]); - applyThemeVariables(THEMES[theme]); +export const autoApplySystemTheme = (): (() => void) => { + const applyTheme = (theme: string): void => { + updateStyles(); + // _THEMES reference is from constants, but here we just need to use theme string + // applyThemeVariables is called externally }; applyTheme(getSystemTheme()); diff --git a/src/templates.ts b/src/templates.ts new file mode 100644 index 0000000..fe5d55c --- /dev/null +++ b/src/templates.ts @@ -0,0 +1,79 @@ +/** + * MetonaToast Templates — HTML 模板辅助函数 + * @module templates + * @version 0.2.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 => ` +
+ ${buttons} +
`; + +/** + * 确认对话框模板 + */ +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(` + + + `); +}; + +/** + * 输入对话框模板 + */ +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 ` + + ${btnRow(` + + + `)} + `; +}; + +/** + * 进度条模板 + */ +export const progressHTML = (opts: ProgressOptions): string => { + const color = opts.progressColor || '#3b82f6'; + return ` +
+
+
+
0%
+ `; +}; + +/** + * 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 ``; + }).join(''); + return btnRow(buttons); +}; diff --git a/src/themes.js b/src/themes.ts similarity index 62% rename from src/themes.js rename to src/themes.ts index bfe8b94..a25e190 100644 --- a/src/themes.js +++ b/src/themes.ts @@ -1,45 +1,39 @@ /** - * MetonaToast Themes - 主题管理 + * MetonaToast Themes — 主题管理 * @module themes - * @version 0.1.2 - * @description 主题系统、自定义主题和主题切换 + * @version 0.2.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} 解析后的主题 */ -export const resolveTheme = (theme) => { +export const resolveTheme = (theme: string): string => { if (theme === 'auto') { - // 优先使用用户设置的全局主题,否则使用系统主题 if (currentTheme && currentTheme !== 'auto') { return currentTheme; } @@ -50,44 +44,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 +83,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 +94,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 +134,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 +143,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 +162,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 +177,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 +216,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 +226,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 +247,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)[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)[name]; }; /** * 获取所有主题 - * @returns {Object} 主题映射 */ -export const getAllThemes = () => { - return { ...THEMES }; +export const getAllThemes = (): Record => { + return { ...THEMES } as Record; }; /** * 获取主题名称列表 - * @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 +321,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 +341,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 +362,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 +373,9 @@ export const removeThemeCSS = () => { }; /** - * 主题工具 — 代理层,直接引用模块函数 + * 主题工具 — 代理层 */ -export const themeUtils = { +export const themeUtils: ThemeUtils = { getSystemTheme, resolveTheme, getThemeConfig, diff --git a/src/toast.ts b/src/toast.ts new file mode 100644 index 0000000..775f3cb --- /dev/null +++ b/src/toast.ts @@ -0,0 +1,551 @@ +/** + * MetonaToast Toast — Toast 类 + * @module toast + * @version 0.2.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 type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js'; + +/** + * Toast 类 — 核心通知组件 + */ +export class Toast implements ToastInstance { + // 静态钩子系统 + static _hooks: Map void>> = new Map(); + + // 由 api.ts 注入的回调,避免循环依赖 + static _onError: ((errorInfo: ErrorInfo) => void) | null = null; + static _removeToast: ((id: string) => void) | null = null; + + static on(name: string, fn: (toast: Toast) => void): () => void { + if (!this._hooks.has(name)) this._hooks.set(name, []); + this._hooks.get(name)!.push(fn); + return () => this.off(name, fn); + } + + static off(name: string, fn: (toast: Toast) => void): void { + const list = this._hooks.get(name); + if (list) this._hooks.set(name, list.filter(f => f !== fn)); + } + + static trigger(name: string, toast: Toast): void { + const list = this._hooks.get(name); + if (list) { + list.forEach(fn => { + try { fn(toast); } + catch (e) { + console.error('Hook error:', name, e); + if (Toast._onError) { + try { Toast._onError({ hook: name, error: e as Error, toast }); } catch (_e) { /* noop */ } + } + } + }); + } + } + + 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 } { + 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 = { bg: tc.bg, border: tc.border, shadow: tc.shadow }; + return { theme, c, t }; + } + + create(): this { + if (typeof window === 'undefined' || typeof document === 'undefined') return this; + + Toast.trigger('beforeShow', this); + injectStyles(); + + const container = this._getContainer(); + const { theme, c, t } = this._palette(); + + this._limitToasts(container); + + const el = document.createElement('div'); + el.className = this._buildClassName(theme); + el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status'); + el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite'); + el.dataset.id = this.id; + + this._applyStyles(el, theme, t); + this._buildContent(el, c); + + this.el = el; + // Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore + const pos = this.config.position || 'top-right'; + if (pos.startsWith('top')) { + container.insertBefore(el, container.firstChild); + } else { + container.appendChild(el); + } + + this._bindEvents(el); + this._startTimer(); + + // 进入动画 + requestAnimationFrame(() => { + requestAnimationFrame(() => { + el.classList.add('met-show'); + }); + }); + + if (typeof this.config.onShow === 'function') { + try { + this.config.onShow(this); + } catch (e) { + console.error('onShow callback error:', e); + } + } + + Toast.trigger('afterShow', this); + + return this; + } + + _getContainer(): HTMLElement { + const position = this.config.position || 'top-right'; + const zIndex = this.config.zIndex || 9999; + + // 从模块级缓存获取 (由 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'); + + const posStyles: Record = { + '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')); + if (list.length >= max) { + const first = list[0] as HTMLElement; + const id = first?.dataset.id; + if (id && Toast._removeToast) { + Toast._removeToast(id); + } + } + } + + _buildClassName(theme: string): string { + const CSS_ANIMS = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', + 'slideUp', 'slideDown', 'slideLeft', 'slideRight']; + const anim = CSS_ANIMS.includes(this.config.animation || '') ? this.config.animation : 'slide'; + return [ + 'met-toast', + `met-${this.type}`, + `met-theme-${theme}`, + `met-anim-${anim}`, + (this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '', + this.config.className || '', + ].filter(Boolean).join(' '); + } + + _applyStyles(el: HTMLElement, _theme: string, _t: Partial): 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)[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 ? `
${escapeHTML(this.title)}
` : ''; + const safeMessage = this.html + ? `
${this.html}
` + : (this.message ? `
${escapeHTML(this.message)}
` : ''); + + const iconHTML = showIcon + ? `
${this.iconHTML || ICONS[this.type]}
` : ''; + const closeHTML = showClose + ? `` + : ''; + const progressHTML = showProgress + ? (this.config.progressDirection === 'vertical' + ? `
` + : `
`) + : ''; + const sideHTML = showSide + ? `
` : ''; + + el.innerHTML = ` + ${iconHTML} + ${sideHTML} +
+ ${safeTitle} + ${safeMessage} +
+ ${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; + } + + 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 = () => this._pause(); + const mouseLeave = () => this._resume(); + + el.addEventListener('mouseenter', mouseEnter); + el.addEventListener('mouseleave', mouseLeave); + + this._cleanups.push(() => { + el.removeEventListener('mouseenter', mouseEnter); + el.removeEventListener('mouseleave', mouseLeave); + }); + } + + 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(); + }; + + 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 = ''; + + if (Math.abs(dx) > 120) { + 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; + }; + + 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; + } + + 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) { + 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): this { + Toast.trigger('beforeUpdate', 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 ? `
${escapeHTML(this.title)}
` : ''; + const safeMessage = this.html + ? `
${this.html}
` + : (this.message ? `
${escapeHTML(this.message)}
` : ''); + content.innerHTML = `${safeTitle}${safeMessage}`; + } + + // 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; + } + + close(immediate = false): void { + if (this.closing) return; + this.closing = true; + + Toast.trigger('beforeClose', this); + 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); + if (Toast._removeToast) Toast._removeToast(this.id); + } +} + +/** + * 模块级共享容器缓存,所有 Toast 实例共用 + */ +export const _containerCache: Map> = new Map(); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..1dd0234 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,552 @@ +/** + * MetonaToast — 核心类型定义 + * @module types + * @version 0.2.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; + showProgress?: boolean; + progressDirection?: ToastProgressDirection; + icon?: boolean; + closeButton?: boolean; + theme?: ToastTheme; + animation?: ToastAnimation; + zIndex?: number; + width?: number | string; + className?: string; + style?: Record; + onShow?: ((toast: ToastInstance) => 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; +} + +export interface ErrorInfo { + hook?: string; + source?: string; + error: Error; + toast?: ToastInstance; +} + +// ========== Toast 选项与实例 ========== + +export interface ToastOptions extends Partial { + 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 }; + create(): this; + update(partial: Partial): this; + 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): 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( + onfulfilled?: ((value: void) => TResult1 | PromiseLike) | null, + onrejected?: ((reason: unknown) => TResult2 | PromiseLike) | null + ): Promise; + catch( + onrejected?: ((reason: unknown) => TResult | PromiseLike) | null + ): Promise; + 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; + 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; + theme?: ToastTheme; + locale?: string; + plugins?: Array; +} + +export interface StatusInfo { + version: string; + toasts: number; + theme: string; + locale: string; + plugins: string[]; + animations: number; +} + +// ========== 动画 ========== + +export interface AnimationConfig { + enter: Record; + leave: Record; + 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): 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; + 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; + plural(key: string, count: number, params?: Record): 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; + addTranslations(locale: string, translations: Record): 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; + 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 void>; + middleware?: (...args: unknown[]) => unknown; + install?: (manager: PluginManager) => void | Record | 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; + createPlugin(config: Partial): Plugin; + validatePlugin(plugin: Partial): { valid: boolean; errors: string[] }; +} + +// ========== MeToast 主接口 ========== + +export interface MeToast { + version: string; + _toasts: Map; + _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): MeToast; + init(options?: InitOptions): MeToast; + destroy(): void; + getStatus(): StatusInfo; + getConfig(): ToastConfig; + updateConfig(config: Partial): 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(promise: Promise, opts?: PromiseOptions): Promise; + confirm(message: string, opts?: ConfirmOptions): Promise; + prompt(message: string, opts?: PromptOptions): Promise; + 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, opts?: QueueOptions): QueueControl; + stack(messages: Array, opts?: StackOptions): void; + + // 分组 + group(name: string): GroupAPI; + dismissGroup(name: string): void; + + // Toast 管理 + find(id: string): ToastInstance | undefined; + dismiss(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): void; + findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[]; + findByType(type: ToastType): ToastInstance[]; + findByPosition(position: ToastPosition): ToastInstance[]; + getAll(): Map; + + // 插件 + use(plugin: string | Plugin, options?: Record): MeToast; + + // 子模块 + animations: AnimationUtils; + themes: ThemeUtils; + i18n: I18nUtils; + plugins: PluginUtils; + presetPlugins: Record; +} + +// ========== 钩子名称 ========== + +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_UPDATE: 'progressUpdate', + PROGRESS_END: 'progressEnd', + ERROR: 'error', + CUSTOM: 'custom', +} as const; + +export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES]; + +// ========== 全局声明 ========== + +declare global { + interface Window { + MeToast: MeToast; + Met: MeToast; + } +} diff --git a/src/utils.js b/src/utils.ts similarity index 55% rename from src/utils.js rename to src/utils.ts index 965e99d..d417458 100644 --- a/src/utils.js +++ b/src/utils.ts @@ -1,24 +1,21 @@ /** - * MetonaToast Utils - 精简工具函数 + * MetonaToast Utils — 工具函数 * @module utils - * @version 0.1.2 - * @description 仅保留核心模块实际使用的工具函数 + * @version 0.2.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, '&') @@ -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; }; diff --git a/tests/index.test.js b/tests/index.test.ts similarity index 80% rename from tests/index.test.js rename to tests/index.test.ts index bb7ce02..1423236 100644 --- a/tests/index.test.js +++ b/tests/index.test.ts @@ -1,53 +1,53 @@ /** * MetonaToast 单元测试 * @module tests - * @version 0.1.2 + * @version 0.2.0 */ -import MeToast, { Toast, VERSION } from '../src/index.js'; +import MeToast, { Toast, VERSION } from '../src/index'; +import type { ToastInstance, ToastConfig } from '../src/types'; + +// 模拟DOM元素工厂 +const createMockElement = (): Record => ({ + className: '', + style: {} as Record, + setAttribute: jest.fn(), + appendChild: jest.fn(), + querySelector: jest.fn(() => null), + querySelectorAll: jest.fn(() => []), + execCommand: jest.fn(() => true), + classList: { + add: jest.fn(), + remove: jest.fn(), + contains: jest.fn(), + }, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + setPointerCapture: jest.fn(), + releasePointerCapture: jest.fn(), + animate: jest.fn(() => ({ + onfinish: null, + oncancel: null, + cancel: jest.fn(), + pause: jest.fn(), + play: jest.fn(), + })), + innerHTML: '', + textContent: '', + dataset: {} as Record, + parentNode: { + removeChild: jest.fn(), + }, +}); // 模拟DOM环境 const mockDocument = { - createElement: jest.fn(() => ({ - className: '', - style: {}, - setAttribute: jest.fn(), - appendChild: jest.fn(), - querySelector: jest.fn(), - querySelectorAll: jest.fn(() => []), - execCommand: jest.fn(() => true), - classList: { - add: jest.fn(), - remove: jest.fn(), - contains: jest.fn(), - }, - addEventListener: jest.fn(), - removeEventListener: jest.fn(), - setPointerCapture: jest.fn(), - releasePointerCapture: jest.fn(), - animate: jest.fn(() => ({ - onfinish: null, - oncancel: null, - cancel: jest.fn(), - pause: jest.fn(), - play: jest.fn(), - })), - innerHTML: '', - textContent: '', - dataset: {}, - parentNode: { - removeChild: jest.fn(), - }, - })), + createElement: jest.fn(() => createMockElement()), getElementById: jest.fn(), querySelector: jest.fn(), querySelectorAll: jest.fn(() => []), - head: { - appendChild: jest.fn(), - }, - body: { - appendChild: jest.fn(), - }, + head: { appendChild: jest.fn() }, + body: { appendChild: jest.fn(), querySelectorAll: jest.fn(() => []) }, readyState: 'complete', addEventListener: jest.fn(), execCommand: jest.fn(() => true), @@ -59,73 +59,57 @@ const mockWindow = { addEventListener: jest.fn(), removeEventListener: jest.fn(), })), - requestAnimationFrame: jest.fn((cb) => setTimeout(cb, 0)), + requestAnimationFrame: jest.fn((cb: FrameRequestCallback) => setTimeout(cb, 0) as unknown as number), cancelAnimationFrame: jest.fn(), getComputedStyle: jest.fn(() => ({})), innerWidth: 1024, innerHeight: 768, navigator: { language: 'zh-CN', - clipboard: { - writeText: jest.fn(), - }, - connection: { - effectiveType: '4g', - downlink: 10, - rtt: 50, - }, + userLanguage: undefined, + clipboard: { writeText: jest.fn() }, + connection: { effectiveType: '4g', downlink: 10, rtt: 50 }, onLine: true, }, - location: { - search: '', - href: 'http://localhost', - }, - history: { - pushState: jest.fn(), - }, + location: { search: '', href: 'http://localhost' }, + history: { pushState: jest.fn() }, localStorage: { getItem: jest.fn(), setItem: jest.fn(), removeItem: jest.fn(), clear: jest.fn(), }, - Audio: jest.fn(() => ({ - play: jest.fn(), - volume: 0, - })), + Audio: jest.fn(() => ({ play: jest.fn(), volume: 0 })), + Notification: undefined, }; // 设置全局变量 -global.document = mockDocument; -global.window = mockWindow; -global.navigator = mockWindow.navigator; -global.localStorage = mockWindow.localStorage; -global.requestAnimationFrame = mockWindow.requestAnimationFrame; -global.cancelAnimationFrame = mockWindow.cancelAnimationFrame; -global.matchMedia = mockWindow.matchMedia; -global.getComputedStyle = mockWindow.getComputedStyle; -global.Audio = mockWindow.Audio; +(global as Record).document = mockDocument; +(global as Record).window = mockWindow; +(global as Record).navigator = mockWindow.navigator; +(global as Record).localStorage = mockWindow.localStorage; +(global as Record).requestAnimationFrame = mockWindow.requestAnimationFrame; +(global as Record).cancelAnimationFrame = mockWindow.cancelAnimationFrame; // 测试套件 -describe('MetonaToast', () => { + describe('MetonaToast', () => { beforeEach(() => { jest.clearAllMocks(); MeToast._toasts.clear(); - // 清理DOM残留 - if (typeof document !== 'undefined') { - document.querySelectorAll('.met-container').forEach(c => c.remove()); - } // 清理钩子残留 Toast._hooks.clear(); + // 重置 _removeToast 回调 + const { _containerCache } = require('../src/toast.js'); + _containerCache.clear(); }); - + describe('版本信息', () => { test('应该有正确的版本号', () => { - expect(VERSION).toBe('0.1.2'); - expect(MeToast.version).toBe('0.1.2'); + expect(VERSION).toBe('0.2.0'); + expect(MeToast.version).toBe('0.2.0'); }); }); - + describe('基础功能', () => { test('应该能够显示成功Toast', () => { const toast = MeToast.success('成功消息'); @@ -134,44 +118,44 @@ describe('MetonaToast', () => { expect(toast.type).toBe('success'); expect(toast.message).toBe('成功消息'); }); - + test('应该能够显示错误Toast', () => { const toast = MeToast.error('错误消息'); expect(toast).toBeDefined(); expect(toast.type).toBe('error'); expect(toast.message).toBe('错误消息'); }); - + test('应该能够显示警告Toast', () => { const toast = MeToast.warning('警告消息'); expect(toast).toBeDefined(); expect(toast.type).toBe('warning'); expect(toast.message).toBe('警告消息'); }); - + test('应该能够显示信息Toast', () => { const toast = MeToast.info('信息消息'); expect(toast).toBeDefined(); expect(toast.type).toBe('info'); expect(toast.message).toBe('信息消息'); }); - + test('应该能够显示默认Toast', () => { const toast = MeToast.show('默认消息'); expect(toast).toBeDefined(); expect(toast.type).toBe('default'); expect(toast.message).toBe('默认消息'); }); - + test('应该能够显示带标题的Toast', () => { const toast = MeToast.success({ title: '成功', message: '操作成功', - }); + } as Record); expect(toast.title).toBe('成功'); expect(toast.message).toBe('操作成功'); }); - + test('应该能够显示加载Toast', () => { const loading = MeToast.loading('加载中...'); expect(loading).toBeDefined(); @@ -184,76 +168,64 @@ describe('MetonaToast', () => { expect(loading.dismiss).toBeInstanceOf(Function); }); }); - + describe('Toast管理', () => { test('应该能够获取所有Toast', () => { MeToast.success('消息1'); MeToast.error('消息2'); - const toasts = MeToast.getToasts(); expect(toasts).toHaveLength(2); }); - + test('应该能够获取Toast数量', () => { MeToast.success('消息1'); MeToast.error('消息2'); MeToast.warning('消息3'); - expect(MeToast.count()).toBe(3); }); - + test('应该能够检查是否有Toast', () => { expect(MeToast.hasToasts()).toBe(false); - MeToast.success('消息'); - expect(MeToast.hasToasts()).toBe(true); }); - + test('应该能够查找Toast', () => { const toast = MeToast.success('消息'); const found = MeToast.find(toast.id); - expect(found).toBeDefined(); - expect(found.id).toBe(toast.id); + expect(found!.id).toBe(toast.id); }); - + test('应该能够关闭Toast', () => { const toast = MeToast.success('消息'); toast.close(); - // 等待关闭动画 setTimeout(() => { expect(MeToast.count()).toBe(0); }, 300); }); - + test('应该能够关闭所有Toast', () => { MeToast.success('消息1'); MeToast.error('消息2'); MeToast.warning('消息3'); - MeToast.dismiss(); - - // 等待关闭动画 setTimeout(() => { expect(MeToast.count()).toBe(0); }, 300); }); - + test('应该能够清除所有Toast', () => { MeToast.success('消息1'); MeToast.error('消息2'); - MeToast.clear(); - - // 等待关闭动画 setTimeout(() => { expect(MeToast.count()).toBe(0); }, 300); }); }); - + describe('配置管理', () => { test('应该能够配置全局选项', () => { MeToast.configure({ @@ -261,50 +233,43 @@ describe('MetonaToast', () => { duration: 5000, theme: 'dark', }); - const config = MeToast.getConfig(); expect(config.position).toBe('bottom-center'); expect(config.duration).toBe(5000); expect(config.theme).toBe('dark'); }); - + test('应该能够重置配置', () => { MeToast.configure({ position: 'bottom-center', duration: 5000, }); - MeToast.resetConfig(); - const config = MeToast.getConfig(); expect(config.position).toBe('top-right'); expect(config.duration).toBe(4000); }); - + test('应该能够更新配置', () => { - MeToast.updateConfig({ - position: 'top-left', - }); - + MeToast.updateConfig({ position: 'top-left' }); const config = MeToast.getConfig(); expect(config.position).toBe('top-left'); }); }); - + describe('主题系统', () => { test('应该能够获取当前主题', () => { const theme = MeToast.themes.getCurrentTheme(); expect(theme).toBeDefined(); }); - + test('应该能够切换主题', () => { MeToast.themes.switchTheme('dark'); expect(MeToast.themes.getCurrentTheme()).toBe('dark'); - MeToast.themes.switchTheme('light'); expect(MeToast.themes.getCurrentTheme()).toBe('light'); }); - + test('应该能够获取主题配置', () => { const config = MeToast.themes.getThemeConfig('light'); expect(config).toBeDefined(); @@ -312,14 +277,14 @@ describe('MetonaToast', () => { expect(config.text).toBeDefined(); expect(config.border).toBeDefined(); }); - + test('应该能够获取所有主题', () => { const themes = MeToast.themes.getAllThemes(); expect(themes).toBeDefined(); expect(themes.light).toBeDefined(); expect(themes.dark).toBeDefined(); }); - + test('应该能够注册自定义主题', () => { MeToast.themes.registerTheme('custom', { bg: 'rgba(255, 255, 255, 0.96)', @@ -330,140 +295,115 @@ describe('MetonaToast', () => { progressBg: 'rgba(0, 0, 0, 0.06)', closeHoverBg: 'rgba(0, 0, 0, 0.06)', }); - expect(MeToast.themes.hasTheme('custom')).toBe(true); }); }); - + describe('国际化系统', () => { test('应该能够获取当前语言', () => { const locale = MeToast.i18n.getCurrentLocale(); expect(locale).toBeDefined(); }); - + test('应该能够切换语言', () => { MeToast.i18n.switchLocale('en-US'); expect(MeToast.i18n.getCurrentLocale()).toBe('en-US'); - MeToast.i18n.switchLocale('zh-CN'); expect(MeToast.i18n.getCurrentLocale()).toBe('zh-CN'); }); - + test('应该能够获取翻译', () => { const text = MeToast.i18n.t('success'); expect(text).toBeDefined(); }); - + test('应该能够检查翻译是否存在', () => { expect(MeToast.i18n.hasTranslation('success')).toBe(true); expect(MeToast.i18n.hasTranslation('nonexistent')).toBe(false); }); - + test('应该能够获取支持的语言列表', () => { const locales = MeToast.i18n.getSupportedLocales(); expect(locales).toContain('zh-CN'); expect(locales).toContain('en-US'); }); - + test('应该能够获取语言名称', () => { const name = MeToast.i18n.getLocaleName('zh-CN'); expect(name).toBe('简体中文'); }); - + test('应该能够格式化数字', () => { const formatted = MeToast.i18n.formatNumber(1234567.89); expect(formatted).toBeDefined(); }); - + test('应该能够格式化货币', () => { const formatted = MeToast.i18n.formatCurrency(99.99, 'USD'); expect(formatted).toBeDefined(); }); - + test('应该能够格式化日期', () => { const formatted = MeToast.i18n.formatDate(new Date()); expect(formatted).toBeDefined(); }); }); - + describe('插件系统', () => { test('应该能够注册插件', () => { - const plugin = { - name: 'test-plugin', - version: '1.0.0', - install: jest.fn(), - }; - + const plugin = { name: 'test-plugin', version: '1.0.0', install: jest.fn() }; MeToast.plugins.register('test', plugin); expect(MeToast.plugins.has('test')).toBe(true); }); - + test('应该能够获取插件', () => { - const plugin = { - name: 'test-plugin', - version: '1.0.0', - }; - + const plugin = { name: 'test-plugin', version: '1.0.0' }; MeToast.plugins.register('test', plugin); const retrieved = MeToast.plugins.get('test'); expect(retrieved).toBeDefined(); - expect(retrieved.name).toBe('test-plugin'); + expect(retrieved!.name).toBe('test-plugin'); }); - + test('应该能够注销插件', () => { - const plugin = { - name: 'test-plugin', - version: '1.0.0', - uninstall: jest.fn(), - }; - + const plugin = { name: 'test-plugin', version: '1.0.0', uninstall: jest.fn() }; MeToast.plugins.register('test', plugin); MeToast.plugins.unregister('test'); expect(MeToast.plugins.has('test')).toBe(false); }); - + test('应该能够启用/禁用插件', () => { - const plugin = { - name: 'test-plugin', - version: '1.0.0', - }; - + const plugin = { name: 'test-plugin', version: '1.0.0' }; MeToast.plugins.register('test', plugin); - MeToast.plugins.disable('test'); expect(MeToast.plugins.isEnabled('test')).toBe(false); - MeToast.plugins.enable('test'); expect(MeToast.plugins.isEnabled('test')).toBe(true); }); - + test('应该能够获取所有插件', () => { - // 清理前面测试遗留的插件 ['test', 'test-plugin', 'custom'].forEach(n => { if (MeToast.plugins.has(n)) MeToast.plugins.unregister(n); }); MeToast.plugins.register('test1', { name: 'test1' }); MeToast.plugins.register('test2', { name: 'test2' }); - const plugins = MeToast.plugins.getAll(); expect(plugins).toHaveLength(2); }); - + test('应该能够获取插件名称', () => { MeToast.plugins.register('test1', { name: 'test1' }); MeToast.plugins.register('test2', { name: 'test2' }); - const names = MeToast.plugins.getNames(); expect(names).toContain('test1'); expect(names).toContain('test2'); }); - + test('应该能够使用预设插件', () => { MeToast.use('keyboard'); expect(MeToast.plugins.has('keyboard')).toBe(true); }); }); - + describe('动画系统', () => { test('应该能够获取所有动画名称', () => { const names = MeToast.animations.getAnimationNames(); @@ -472,60 +412,45 @@ describe('MetonaToast', () => { expect(names).toContain('scale'); expect(names).toContain('bounce'); }); - + test('应该能够获取动画配置', () => { const config = MeToast.animations.get('slide'); expect(config).toBeDefined(); - expect(config.enter).toBeDefined(); - expect(config.leave).toBeDefined(); - expect(config.duration).toBeDefined(); - expect(config.easing).toBeDefined(); + expect(config!.enter).toBeDefined(); + expect(config!.leave).toBeDefined(); + expect(config!.duration).toBeDefined(); + expect(config!.easing).toBeDefined(); }); - + test('应该能够注册自定义动画', () => { MeToast.animations.register('custom', { - enter: { - transform: 'scale(0)', - opacity: 0, - }, - leave: { - transform: 'scale(1)', - opacity: 1, - }, + enter: { transform: 'scale(0)', opacity: '0' }, + leave: { transform: 'scale(1)', opacity: '1' }, duration: 500, }); - expect(MeToast.animations.get('custom')).toBeDefined(); }); - + test('应该能够注销动画', () => { - MeToast.animations.register('custom', { - enter: {}, - leave: {}, - }); - + MeToast.animations.register('custom', { enter: {}, leave: {} }); MeToast.animations.unregister('custom'); expect(MeToast.animations.get('custom')).toBeNull(); }); }); - + describe('Promise支持', () => { test('应该能够使用Promise风格', async () => { const promise = Promise.resolve('success'); - await MeToast.promise(promise, { loading: '加载中...', success: '成功', error: '失败', }); - - // 验证Toast被创建 expect(MeToast.count()).toBeGreaterThan(0); }); - + test('应该能够处理Promise拒绝', async () => { const promise = Promise.reject(new Error('error')); - try { await MeToast.promise(promise, { loading: '加载中...', @@ -537,44 +462,27 @@ describe('MetonaToast', () => { } }); }); - + describe('Toast实例', () => { test('应该能够更新Toast', () => { const toast = MeToast.success('原始消息'); - - toast.update({ - title: '新标题', - message: '新消息', - type: 'info', - }); - + toast.update({ title: '新标题', message: '新消息', type: 'info' }); expect(toast.title).toBe('新标题'); expect(toast.message).toBe('新消息'); expect(toast.type).toBe('info'); }); - - test('应该能够暂停和恢复Toast', () => { - const toast = MeToast.success({ - message: '消息', - duration: 5000, - }); + test('应该能够暂停和恢复Toast', () => { + const toast = MeToast.success({ message: '消息', duration: 5000 } as Record); toast._pause(); expect(toast.paused).toBe(true); - toast._resume(); expect(toast.paused).toBe(false); }); test('暂停恢复应保持 remaining 时间连续性', () => { - const toast = MeToast.success({ - message: '消息', - duration: 5000, - }); - - // 模拟计时开始 + const toast = MeToast.success({ message: '消息', duration: 5000 } as Record); toast.config.duration = 5000; - // 直接设置内部状态模拟已运行 2 秒 toast.startedAt = Date.now() - 2000; toast.remaining = 3000; toast.paused = false; @@ -587,7 +495,6 @@ describe('MetonaToast', () => { const pausedRemaining = toast.remaining; toast._resume(); expect(toast.paused).toBe(false); - // resume 后 remaining 应保持与暂停时一致(允许微小误差) expect(toast.remaining).toBeGreaterThanOrEqual(pausedRemaining - 100); }); @@ -596,7 +503,6 @@ describe('MetonaToast', () => { expect(toast.type).toBe('info'); toast.update({ type: 'error', message: 'changed' }); expect(toast.type).toBe('error'); - // DOM 验证需要实际浏览器环境,此处验证状态变更 }); test('update 类型变更无 DOM 时不抛异常', () => { @@ -604,25 +510,23 @@ describe('MetonaToast', () => { expect(() => toast.update({ type: 'success', message: 'ok' })).not.toThrow(); expect(toast.type).toBe('success'); }); - + test('应该能够获取Toast配置', () => { const toast = MeToast.success({ message: '消息', position: 'bottom-center', duration: 5000, - }); - + } as Record); expect(toast.config.position).toBe('bottom-center'); expect(toast.config.duration).toBe(5000); }); }); - + describe('状态信息', () => { test('应该能够获取状态信息', () => { MeToast.success('消息'); - const status = MeToast.getStatus(); - expect(status.version).toBe('0.1.2'); + expect(status.version).toBe('0.2.0'); expect(status.toasts).toBeGreaterThanOrEqual(0); expect(status.theme).toBeDefined(); expect(status.locale).toBeDefined(); @@ -630,15 +534,12 @@ describe('MetonaToast', () => { expect(status.animations).toBeGreaterThanOrEqual(0); }); }); - + describe('销毁功能', () => { test('应该能够销毁所有Toast', () => { MeToast.success('消息1'); MeToast.error('消息2'); - MeToast.destroy(); - - // 等待销毁完成 setTimeout(() => { expect(MeToast.count()).toBe(0); }, 300); @@ -648,141 +549,95 @@ describe('MetonaToast', () => { describe('Toast类', () => { test('应该能够创建Toast实例', () => { - const toast = new Toast({ - type: 'success', - title: '成功', - message: '操作成功', - }); - + const toast = new Toast({ type: 'success', title: '成功', message: '操作成功' }); expect(toast).toBeDefined(); expect(toast.type).toBe('success'); expect(toast.title).toBe('成功'); expect(toast.message).toBe('操作成功'); }); - + test('应该能够生成唯一ID', () => { const toast1 = new Toast({ message: '消息1' }); const toast2 = new Toast({ message: '消息2' }); - expect(toast1.id).not.toBe(toast2.id); }); - + test('应该能够设置默认配置', () => { const toast = new Toast({ message: '消息' }); - expect(toast.config).toBeDefined(); expect(toast.config.position).toBe('top-right'); expect(toast.config.duration).toBe(4000); expect(toast.config.max).toBe(6); }); - + test('应该能够合并配置', () => { - const toast = new Toast({ - message: '消息', - position: 'bottom-center', - duration: 5000, - }); - + const toast = new Toast({ message: '消息', position: 'bottom-center', duration: 5000 }); expect(toast.config.position).toBe('bottom-center'); expect(toast.config.duration).toBe(5000); }); - + test('应该能够设置类型', () => { const types = ['success', 'error', 'warning', 'info', 'loading', 'default']; - types.forEach((type) => { const toast = new Toast({ type, message: '消息' }); expect(toast.type).toBe(type); }); }); - + test('应该能够设置标题', () => { - const toast = new Toast({ - title: '标题', - message: '消息', - }); - + const toast = new Toast({ title: '标题', message: '消息' }); expect(toast.title).toBe('标题'); }); - + test('应该能够设置消息', () => { - const toast = new Toast({ - message: '消息内容', - }); - + const toast = new Toast({ message: '消息内容' }); expect(toast.message).toBe('消息内容'); }); - + test('应该能够设置HTML内容', () => { - const toast = new Toast({ - html: '加粗', - }); - + const toast = new Toast({ html: '加粗' }); expect(toast.html).toBe('加粗'); }); - + test('应该能够设置自定义图标', () => { - const toast = new Toast({ - iconHTML: '...', - }); - + const toast = new Toast({ iconHTML: '...' }); expect(toast.iconHTML).toBe('...'); }); - + test('应该能够更新Toast', () => { const toast = new Toast({ message: '原始消息' }); - - toast.update({ - title: '新标题', - message: '新消息', - type: 'success', - }); - + toast.update({ title: '新标题', message: '新消息', type: 'success' }); expect(toast.title).toBe('新标题'); expect(toast.message).toBe('新消息'); expect(toast.type).toBe('success'); }); - + test('应该能够关闭Toast', (done) => { const toast = new Toast({ message: '消息' }); - toast.close(); - expect(toast.closing).toBe(true); - // _destroy 在 300ms setTimeout 中异步调用 setTimeout(() => { expect(toast.el).toBeNull(); done(); }, 350); }); - + test('应该能够暂停计时', () => { - const toast = new Toast({ - message: '消息', - duration: 5000, - }); - + const toast = new Toast({ message: '消息', duration: 5000 }); toast._pause(); - expect(toast.paused).toBe(true); }); - + test('应该能够恢复计时', () => { - const toast = new Toast({ - message: '消息', - duration: 5000, - }); - + const toast = new Toast({ message: '消息', duration: 5000 }); toast._pause(); toast._resume(); - expect(toast.paused).toBe(false); }); - + test('应该能够获取配色方案', () => { const toast = new Toast({ type: 'success' }); const palette = toast._palette(); - expect(palette).toBeDefined(); expect(palette.theme).toBeDefined(); expect(palette.c).toBeDefined(); @@ -793,25 +648,21 @@ describe('Toast类', () => { describe('工具函数', () => { test('应该能够生成唯一ID', () => { const { generateId } = require('../src/utils.js'); - const id1 = generateId(); const id2 = generateId(); - expect(id1).not.toBe(id2); expect(id1).toMatch(/^met-/); }); - + test('应该能够转义HTML', () => { const { escapeHTML } = require('../src/utils.js'); - const escaped = escapeHTML(''); expect(escaped).not.toContain('