release: v0.2.0 — TypeScript 源码重构
CI / test (18.x) (push) Successful in 9m52s
CI / test (22.x) (push) Canceled after 0s
CI / test (24.x) (push) Canceled after 0s
CI / test (20.x) (push) Canceled after 2m49s

### Changed
- 全部源码从 JavaScript 迁移到 TypeScript (strict mode)
- core.js (1244行) 拆分为 toast.ts + api.ts + templates.ts
- 删除手动维护的 types/index.d.ts,类型从源码自动生成
- 构建工具链: Babel → ts-jest, 新增 @rollup/plugin-typescript
- 新增 rollup-plugin-dts 生成合并 .d.ts

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

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

### Removed
- babel.config.js, types/ 目录, 所有 src/*.js
This commit is contained in:
tianhao
2026-07-25 11:22:07 +08:00
parent ee2b685f64
commit 9a6cd33d90
36 changed files with 3673 additions and 5473 deletions
+28
View File
@@ -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/"]
}
+40
View File
@@ -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
+3
View File
@@ -2,4 +2,7 @@ node_modules/
dist/
coverage/
.DS_Store
.zcode/
# 本地 npm 发布凭据(含 _auth,勿提交)
.npmrc
+47
View File
@@ -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 类型声明
- 零依赖
+3 -3
View File
@@ -8,14 +8,14 @@
## 特性
- **零依赖** — 纯原生 JavaScriptgzip 后不到 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 APIChrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
-16
View File
@@ -1,16 +0,0 @@
module.exports = {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
modules: 'commonjs',
},
],
],
plugins: [
'@babel/plugin-transform-modules-commonjs',
],
};
+8 -66
View File
@@ -1,83 +1,25 @@
#!/bin/bash
# MetonaToast 构建脚本
# 构建脚本 — 安装依赖 → 测试 → 构建
echo "🍞 MetonaToast 构建脚本"
echo "========================"
echo "🔨 MetonaToast 构建脚本"
# 检查Node.js
if ! command -v node &> /dev/null; then
echo "❌ 错误: 未找到Node.js"
echo "❌ 未找到 Node.js"
exit 1
fi
# 检查npm
if ! command -v npm &> /dev/null; then
echo "❌ 错误: 未找到npm"
exit 1
fi
echo "✅ Node.js: $(node -v)"
echo "✅ Node.js版本: $(node -v)"
echo "✅ npm版本: $(npm -v)"
# 安装依赖
echo ""
echo "📦 安装依赖..."
npm install
npm install || exit 1
if [ $? -ne 0 ]; then
echo "❌ 依赖安装失败"
exit 1
fi
echo "✅ 依赖安装完成"
# 运行测试
echo ""
echo "🧪 运行测试..."
npm test
npm test || exit 1
if [ $? -ne 0 ]; then
echo "❌ 测试失败"
exit 1
fi
echo "🔨 构建..."
npm run build || exit 1
echo "✅ 测试通过"
# 构建项目
echo ""
echo "🔨 构建项目..."
npm run build
if [ $? -ne 0 ]; then
echo "❌ 构建失败"
exit 1
fi
echo "✅ 构建完成"
# 显示构建结果
echo ""
echo "📊 构建结果:"
echo "========================"
ls -lh dist/
echo ""
echo "✅ 构建成功完成!"
echo ""
echo "📁 文件结构:"
echo " - dist/metona-toast.js (UMD格式)"
echo " - dist/metona-toast.esm.js (ES Module格式)"
echo " - dist/metona-toast.cjs.js (CommonJS格式)"
echo " - dist/metona-toast.min.js (压缩版本)"
echo " - dist/metona-toast.d.ts (TypeScript声明)"
echo ""
echo "🚀 使用方法:"
echo " 1. 浏览器: <script src='dist/metona-toast.js'></script>"
echo " 2. ES Module: import MeToast from 'dist/metona-toast.esm.js'"
echo " 3. CommonJS: const MeToast = require('dist/metona-toast.cjs.js')"
echo ""
echo "📝 示例:"
echo " 打开 site/index.html 查看官网"
echo " 打开 site/demo.html 查看演示"
echo ""
+34
View File
@@ -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,
};
-17
View File
@@ -1,17 +0,0 @@
module.exports = {
testEnvironment: 'jsdom',
transform: {
'^.+\\.js$': 'babel-jest',
},
transformIgnorePatterns: [
'/node_modules/(?!(@rollup)/)',
],
moduleFileExtensions: ['js', 'json'],
collectCoverageFrom: [
'src/**/*.js',
'!src/index.js',
],
coverageDirectory: 'coverage',
coverageReporters: ['text', 'lcov'],
verbose: true,
};
+550 -1437
View File
File diff suppressed because it is too large Load Diff
+21 -20
View File
@@ -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%",
+32 -60
View File
@@ -1,5 +1,6 @@
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import typescript from '@rollup/plugin-typescript';
import terser from '@rollup/plugin-terser';
import dts from 'rollup-plugin-dts';
import serve from 'rollup-plugin-serve';
@@ -8,104 +9,79 @@ 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: [
// 基础插件
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格式(现代浏览器/打包工具)
// 生产构建(非 dev 模式时输出全格式)
...(isDev ? [] : [
// ESM
{
...baseConfig,
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.esm.js',
format: 'es',
exports: 'named',
sourcemap: !isProd,
sourcemap: true,
},
plugins: [
...baseConfig.plugins,
...prodPlugins,
],
plugins: basePlugins,
},
// CommonJS格式(Node.js
// CommonJS
{
...baseConfig,
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.cjs.js',
format: 'cjs',
exports: 'named',
sourcemap: !isProd,
sourcemap: true,
},
plugins: [
...baseConfig.plugins,
...prodPlugins,
],
plugins: basePlugins,
},
// 压缩版本(UMD
// UMD 压缩版
{
...baseConfig,
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.min.js',
format: 'umd',
name: 'MeToast',
exports: 'named',
sourcemap: false,
globals: {},
},
plugins: [
...baseConfig.plugins,
...basePlugins,
terser({
compress: {
drop_console: true,
@@ -113,23 +89,19 @@ export default [
pure_funcs: ['console.log', 'console.warn'],
passes: 2,
},
format: {
comments: false,
},
mangle: {
toplevel: true,
},
format: { comments: false },
mangle: { toplevel: true },
}),
],
},
// TypeScript声明文件
// TypeScript 声明文件合并
{
input: 'types/index.d.ts',
input: 'src/index.ts',
output: {
file: 'dist/metona-toast.d.ts',
format: 'es',
},
plugins: [dts()],
},
]),
];
+1 -1
View File
@@ -15,7 +15,7 @@ else
exit 1
fi
PORT=${1:-3000}
PORT=${1:-3001}
echo "✅ Python: $($PYTHON --version 2>&1)"
echo "🌐 启动服务器..."
+2 -2
View File
@@ -244,7 +244,7 @@
<!-- 14. 错误回调 -->
<div class="card">
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.1.2</span></div>
<div class="label">🛡️ onError 错误捕获 <span style="color:#34d399;font-size:10px;">v0.2.0</span></div>
<div class="btn-row">
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
@@ -282,7 +282,7 @@
</main>
<footer>
MetonaToast v0.1.2 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
MetonaToast v0.2.0 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
</footer>
<script src="../dist/metona-toast.js"></script>
+1 -1
View File
@@ -87,7 +87,7 @@
<main class="main">
<h1>API 文档</h1>
<p>MetonaToast v0.1.2 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
<p>MetonaToast v0.2.0 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
<!-- ===== 基础通知 ===== -->
<h2 id="show">show(message, opts?)</h2>
+5 -5
View File
@@ -80,7 +80,7 @@
<header class="hero">
<h1><span>MetonaToast</span></h1>
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。纯原生 JavaScriptgzip 不到 10KB。</p>
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式源码gzip 不到 10KB。</p>
<div class="badges">
<span>📦 &lt;10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
</div>
@@ -127,7 +127,7 @@
<div class="feature">
<div class="feature-icon"></div>
<h3>零依赖</h3>
<p>纯原生 JavaScript 实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
<p>TypeScript 严格模式,纯原生实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
</div>
<div class="feature">
<div class="feature-icon">🛡️</div>
@@ -142,7 +142,7 @@
<div class="feature">
<div class="feature-icon">📘</div>
<h3>TypeScript</h3>
<p>完整类型定义,与实现严格对齐零 phantom 方法。<code>MeToast.success()</code><code>.confirm()</code><code>.action()</code> 全部有类型</p>
<p>严格模式源码,类型从源码自动生成。strictNullChecks、noImplicitAny 全开,零类型漂移</p>
</div>
</div>
</main>
@@ -174,7 +174,7 @@
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">📦</div>
<h3>Node.js</h3>
<p>18+</p>
<p>16+</p>
</div>
<div class="feature" style="text-align:center;padding:20px 16px;">
<div style="font-size:28px;margin-bottom:6px;">📘</div>
@@ -247,7 +247,7 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
</section>
<footer>
<p>MetonaToast v0.1.2 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
<p>MetonaToast v0.2.0 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
</footer>
<script src="../dist/metona-toast.js"></script>
-111
View File
@@ -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)',
});
+89
View File
@@ -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<string, AnimationConfig> = 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<AnimationConfig>): void {
animationMap.set(name, {
name,
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
},
unregister(name: string): void {
animationMap.delete(name);
},
get(name: string): AnimationConfig | null {
return animationMap.get(name) || null;
},
getAnimationNames(): string[] {
return Array.from(animationMap.keys());
},
getActiveCount(): number {
return animationMap.size;
},
cancelAll(): void {
// CSS动画由浏览器原生管理,无需手动取消
},
reset(): void {
animationMap.clear();
Object.entries(ANIMATIONS).forEach(([name, config]) => {
animationMap.set(name, {
name,
enter: config.enter,
leave: config.leave,
duration: config.duration,
easing: config.easing,
});
});
},
destroy(): void {
animationMap.clear();
},
};
/**
* 动画预设(占位,兼容旧API
*/
export const animationPresets: Record<string, AnimationConfig> = {};
/**
* 创建自定义动画配置
*/
export const createAnimation = (config: Partial<AnimationConfig>): AnimationConfig => ({
enter: config.enter || {},
leave: config.leave || {},
duration: config.duration || 300,
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
});
+617
View File
@@ -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<string, unknown>) || {}),
type: (second as Record<string, unknown>)?.type as string || defaultType,
message: first,
};
}
if (first && typeof first === 'object') {
return {
...(first as Record<string, unknown>),
type: (first as Record<string, unknown>).type as string || defaultType,
} as ToastOptions;
}
return {
type: defaultType,
message: '',
};
};
/**
* meToast API 对象
*/
const meToast: MeToast = {
_toasts: new Map<string, ToastInstance>(),
_config: { ...DEFAULTS } as unknown as ToastConfig,
version: '0.2.0',
configure(opts: Partial<ToastConfig>): MeToast {
if (!opts || typeof opts !== 'object') return this;
Object.assign(this._config, opts);
if (opts.theme) {
applyTheme(opts.theme);
}
if (opts.locale) {
setCurrentLocale(opts.locale);
}
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<ToastOptions>) => { toast.update(p); return this; },
dismiss: () => toast.close(),
};
},
promise<T>(promise: Promise<T>, opts: PromiseOptions = {}): Promise<T> {
if (!promise || typeof (promise as unknown as { then?: unknown }).then !== 'function') {
console.error('MeToast.promise: first argument must be a Promise');
return Promise.reject(new Error('Invalid promise'));
}
const loadingMsg = opts.loading || t('loading');
const successMsg = opts.success || t('success');
const errorMsg = opts.error || t('error');
const ctrl = this.loading(loadingMsg);
return Promise.resolve(promise)
.then((data: T) => {
ctrl.success(successMsg);
return data;
})
.catch((err: unknown) => {
ctrl.error(errorMsg);
throw err;
});
},
_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<boolean> {
if (typeof message !== 'string') {
console.error('MeToast.confirm: message must be a string');
return Promise.resolve(false);
}
return new Promise<boolean>((resolve) => {
let resolved = false;
const safeResolve = (val: boolean) => {
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
};
const safetyTimeout = setTimeout(() => safeResolve(false), 10000);
const toast = this._emit({
...opts,
type: opts.type || 'warning',
message,
duration: 0,
closeButton: false,
closeOnClick: false,
draggable: false,
html: confirmHTML(opts),
});
setTimeout(() => {
const confirmBtn = toast.el?.querySelector('.met-confirm-btn') as HTMLElement | null;
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
if (confirmBtn) {
confirmBtn.addEventListener('click', () => {
toast.close();
safeResolve(true);
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
toast.close();
safeResolve(false);
});
}
}, 0);
});
},
prompt(message: string, opts: PromptOptions = {}): Promise<string | null> {
if (typeof message !== 'string') {
console.error('MeToast.prompt: message must be a string');
return Promise.resolve(null);
}
return new Promise<string | null>((resolve) => {
let resolved = false;
const safeResolve = (val: string | null) => {
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
};
const safetyTimeout = setTimeout(() => safeResolve(null), 10000);
const toast = this._emit({
...opts,
type: opts.type || 'info',
message,
duration: 0,
closeButton: false,
closeOnClick: false,
draggable: false,
html: promptHTML(opts),
});
setTimeout(() => {
const input = toast.el?.querySelector('.met-input') as HTMLInputElement | null;
const submitBtn = toast.el?.querySelector('.met-submit-btn') as HTMLElement | null;
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
if (input) {
input.focus();
input.addEventListener('keydown', (e: KeyboardEvent) => {
if (e.key === 'Enter') {
toast.close();
safeResolve(input.value);
}
});
}
if (submitBtn) {
submitBtn.addEventListener('click', () => {
toast.close();
safeResolve(input?.value || null);
});
}
if (cancelBtn) {
cancelBtn.addEventListener('click', () => {
toast.close();
safeResolve(null);
});
}
}, 0);
});
},
progress(messageOrOpts: string | ToastOptions, opts: ProgressOptions = {}): ProgressControl {
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
normalized.duration = 0;
normalized.closeButton = false;
normalized.showProgress = false;
const toast = this._emit({
...normalized,
html: progressHTML(opts),
});
return {
id: toast.id,
setProgress(percent: number) {
const fill = toast.el?.querySelector('.met-progress-fill') as HTMLElement | null;
const text = toast.el?.querySelector('.met-progress-text') as HTMLElement | null;
if (fill) {
fill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
}
if (text) {
text.textContent = `${Math.round(percent)}%`;
}
},
complete(message?: string) {
this.setProgress(100);
setTimeout(() => {
toast.update({
type: 'success',
message: message || t('success'),
html: '',
});
setTimeout(() => toast.close(), 1000);
}, 300);
},
error(message?: string) {
toast.update({
type: 'error',
message: message || t('error'),
html: '',
});
setTimeout(() => toast.close(), 2000);
},
dismiss() {
toast.close();
},
};
},
countdown(message: string, seconds = 10, opts: CountdownOptions = {}): CountdownControl {
if (typeof message !== 'string') {
console.error('MeToast.countdown: message must be a string');
return { id: '', cancel: () => {}, pause: () => {}, resume: () => {} };
}
let remaining = Math.max(1, parseInt(String(seconds)) || 10);
let timer: ReturnType<typeof setInterval> | null = null;
const toast = this._emit({
...opts,
type: opts.type || 'warning',
message: message.replace(/\{seconds\}/g, String(remaining)),
duration: 0,
closeButton: true,
showProgress: false,
});
const tick = (): void => {
remaining--;
if (remaining <= 0) {
if (timer !== null) clearInterval(timer);
toast.close();
if (typeof opts.onComplete === 'function') {
try { opts.onComplete(); } catch (e) { console.error('countdown onComplete error:', e); }
}
return;
}
toast.update({
message: message.replace(/\{seconds\}/g, String(remaining)),
});
};
timer = setInterval(tick, 1000);
return {
id: toast.id,
cancel() { if (timer !== null) clearInterval(timer); toast.close(); },
pause() { if (timer !== null) clearInterval(timer); },
resume() { if (timer !== null) clearInterval(timer); timer = setInterval(tick, 1000); },
};
},
action(messageOrOpts: string | ToastOptions, actions: ActionButton[] = [], opts: ToastOptions = {}): ActionControl {
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
normalized.duration = opts.duration ?? 0;
normalized.closeButton = opts.closeButton ?? true;
const toast = this._emit({
...normalized,
html: (normalized.html || '') + actionHTML(actions),
});
if (Array.isArray(actions)) {
setTimeout(() => {
actions.forEach((a, i) => {
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
if (btn && typeof a.onClick === 'function') {
btn.addEventListener('click', () => {
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<string | ToastOptions>, opts: QueueOptions = {}): QueueControl {
if (!Array.isArray(messages)) {
console.error('MeToast.queue: messages must be an array');
const p = Promise.resolve();
return { then: (fn) => p.then(fn), catch: (rj) => p.catch(rj), cancel: () => {} };
}
let cancelled = false;
const promise = new Promise<void>((resolve) => {
let index = 0;
const delay = opts.delay || 1000;
const userOnClose = opts.onClose;
const showNext = (): void => {
if (cancelled || index >= messages.length) {
resolve();
return;
}
const message = messages[index];
index++;
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
const msgObj: ToastOptions = (typeof message === 'object' && message !== null) ? message as ToastOptions : {};
const msgOnClose = msgObj.onClose;
this._emit({
...opts,
...msgObj,
message: msg,
duration: msgObj.duration || opts.duration || 3000,
onClose: (toast: ToastInstance) => {
if (typeof msgOnClose === 'function') {
try { msgOnClose(toast); } catch (_e) { /* noop */ }
}
if (typeof userOnClose === 'function') {
try { userOnClose(toast); } catch (_e) { /* noop */ }
}
setTimeout(showNext, delay);
},
} as ToastOptions);
};
showNext();
});
return {
then: (fn, rj) => promise.then(fn, rj),
catch: (rj) => promise.catch(rj),
cancel: () => { cancelled = true; },
};
},
stack(messages: Array<string | ToastOptions>, opts: StackOptions = {}): void {
if (!Array.isArray(messages)) {
console.error('MeToast.stack: messages must be an array');
return;
}
messages.forEach((message, index) => {
setTimeout(() => {
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
this._emit({
...opts,
...((typeof message === 'object' && message !== null) ? message as ToastOptions : {}),
message: msg,
} as ToastOptions);
}, index * (opts.stagger || 100));
});
},
dismiss(id?: string): void {
if (id) {
const t = this._toasts.get(id);
if (t) t.close();
return;
}
this._toasts.forEach(t => t.close());
},
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<string, ToastInstance> {
return new Map(this._toasts);
},
count(): number {
return this._toasts.size;
},
group(name: string): GroupAPI {
const self = this;
const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action'];
const g: GroupAPI = { _group: name } as unknown as GroupAPI;
methods.forEach(m => {
(g as unknown as Record<string, (...args: unknown[]) => unknown>)[m] = (...args: unknown[]) => {
const lastArg = args[args.length - 1];
const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg);
const selfMethods = self as unknown as Record<string, (...a: unknown[]) => unknown>;
if (isObj && args.length === 1) {
return selfMethods[m]({ ...(lastArg as Record<string, unknown>), group: name });
}
if (isObj) {
args.pop();
return selfMethods[m](...args, { ...(lastArg as Record<string, unknown>), group: name });
}
return selfMethods[m](...args, { group: name });
};
});
g.dismiss = () => self.dismissGroup(name);
g.count = () => self._groupCount(name);
return g;
},
dismissGroup(name: string): void {
this._toasts.forEach(t => { if (t.group === name) t.close(); });
},
_groupCount(name: string): number {
let c = 0;
this._toasts.forEach(t => { if (t.group === name) c++; });
return c;
},
getToasts(): ToastInstance[] {
return Array.from(this._toasts.values());
},
hasToasts(): boolean {
return this._toasts.size > 0;
},
getToast(id: string): ToastInstance | null {
if (!id) return null;
return this._toasts.get(id) || null;
},
closeAll(): void {
this._toasts.forEach(t => t.close());
},
clearAll(): void {
this._toasts.forEach(t => t.close());
},
pauseAll(): void {
this._toasts.forEach(t => t._pause());
},
resumeAll(): void {
this._toasts.forEach(t => t._resume());
},
updateAll(partial: Partial<ToastOptions>): void {
if (partial && typeof partial === 'object') {
this._toasts.forEach(t => t.update(partial));
}
},
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
if (typeof predicate !== 'function') return [];
return Array.from(this._toasts.values()).filter(predicate);
},
findByType(type: string): ToastInstance[] {
return this.findToasts(t => t.type === type);
},
findByPosition(position: string): ToastInstance[] {
return this.findToasts(t => t.config.position === position);
},
// ====== 以下由 index.ts 增强 ======
init(_options?: Record<string, unknown>): MeToast { return this; },
getStatus() { return { version: '0.2.0', toasts: 0, theme: '', locale: '', plugins: [], animations: 0 }; },
getConfig(): ToastConfig { return { ...this._config }; },
updateConfig(_config: Partial<ToastConfig>): MeToast { return this; },
resetConfig(): MeToast { return this; },
use(_plugin: string | Record<string, unknown>, _options?: Record<string, unknown>): 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 };
+60 -72
View File
@@ -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<string, string>,
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<string | Record<string, unknown>>,
});
// Icons — 从 icons.js 导入并重新导出(保持向后兼容)
import { ICONS } from './icons.js';
export { ICONS };
/**
*
*/
export const TYPE_COLORS = {
export const TYPE_COLORS: Record<string, { fg: string; bg: string }> = {
success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
@@ -179,70 +158,75 @@ export const TYPE_COLORS = {
/**
*
*/
export const ANIMATIONS = {
export const ANIMATIONS: Record<string, {
enter: Record<string, string>;
leave: Record<string, string>;
duration: number;
easing: string;
}> = {
slide: {
enter: { transform: 'translateX(80px)', opacity: 0 },
leave: { transform: 'translateX(120%)', opacity: 0 },
enter: { transform: 'translateX(80px)', opacity: '0' },
leave: { transform: 'translateX(120%)', opacity: '0' },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
fade: {
enter: { opacity: 0, filter: 'blur(3px)' },
leave: { opacity: 0, filter: 'blur(3px)' },
enter: { opacity: '0', filter: 'blur(3px)' },
leave: { opacity: '0', filter: 'blur(3px)' },
duration: 500,
easing: 'ease',
},
scale: {
enter: { transform: 'scale(0.55)', opacity: 0 },
leave: { transform: 'scale(0.55)', opacity: 0 },
enter: { transform: 'scale(0.55)', opacity: '0' },
leave: { transform: 'scale(0.55)', opacity: '0' },
duration: 450,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
},
bounce: {
enter: { transform: 'translateY(-80px)', opacity: 0 },
leave: { transform: 'translateY(20px)', opacity: 0 },
enter: { transform: 'translateY(-80px)', opacity: '0' },
leave: { transform: 'translateY(20px)', opacity: '0' },
duration: 650,
easing: 'ease',
},
flip: {
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: 0 },
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: 0 },
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: '0' },
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: '0' },
duration: 500,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
rotate: {
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: 0 },
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: 0 },
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: '0' },
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: '0' },
duration: 500,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
zoom: {
enter: { transform: 'scale(0.1)', opacity: 0 },
leave: { transform: 'scale(0.1)', opacity: 0 },
enter: { transform: 'scale(0.1)', opacity: '0' },
leave: { transform: 'scale(0.1)', opacity: '0' },
duration: 500,
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
},
slideUp: {
enter: { transform: 'translateY(60px)', opacity: 0 },
leave: { transform: 'translateY(-120%)', opacity: 0 },
enter: { transform: 'translateY(60px)', opacity: '0' },
leave: { transform: 'translateY(-120%)', opacity: '0' },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
slideDown: {
enter: { transform: 'translateY(-60px)', opacity: 0 },
leave: { transform: 'translateY(120%)', opacity: 0 },
enter: { transform: 'translateY(-60px)', opacity: '0' },
leave: { transform: 'translateY(120%)', opacity: '0' },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
slideLeft: {
enter: { transform: 'translateX(-90px)', opacity: 0 },
leave: { transform: 'translateX(120%)', opacity: 0 },
enter: { transform: 'translateX(-90px)', opacity: '0' },
leave: { transform: 'translateX(120%)', opacity: '0' },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
slideRight: {
enter: { transform: 'translateX(90px)', opacity: 0 },
leave: { transform: 'translateX(-120%)', opacity: 0 },
enter: { transform: 'translateX(90px)', opacity: '0' },
leave: { transform: 'translateX(-120%)', opacity: '0' },
duration: 400,
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
},
@@ -251,7 +235,15 @@ export const ANIMATIONS = {
/**
*
*/
export const THEMES = {
export const THEMES: Record<string, {
bg: string;
text: string;
border: string;
shadow: string;
hoverShadow: string;
progressBg: string;
closeHoverBg: string;
}> = {
light: {
bg: 'rgba(255, 255, 255, 0.96)',
text: '#1f2937',
@@ -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;
-1243
View File
File diff suppressed because it is too large Load Diff
+80 -247
View File
@@ -1,30 +1,28 @@
/**
* 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;
@@ -37,39 +35,31 @@ export const setCurrentLocale = (locale) => {
/**
* 退
* @returns {string} 退
*/
export const getFallbackLocale = () => {
export const getFallbackLocale = (): string => {
return fallbackLocale;
};
/**
* 退
* @param {string} locale -
*/
export const setFallbackLocale = (locale) => {
export const setFallbackLocale = (locale: string): void => {
if (!LOCALES[locale]) {
console.warn(`Fallback locale "${locale}" not found`);
return;
}
fallbackLocale = locale;
};
/**
*
* @param {string} key -
* @param {Object} params -
* @returns {string}
*/
export const t = (key, params = {}) => {
// 尝试当前语言
export const t = (key: string, params: Record<string, string | number> = {}): string => {
const currentTranslation = getTranslation(currentLocale, key);
if (currentTranslation !== undefined) {
return interpolate(currentTranslation, params);
}
// 尝试回退语言
if (currentLocale !== fallbackLocale) {
const fallbackTranslation = getTranslation(fallbackLocale, key);
if (fallbackTranslation !== undefined) {
@@ -77,28 +67,23 @@ export const t = (key, params = {}) => {
}
}
// 返回键名
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
return key;
};
/**
*
* @param {string} locale -
* @param {string} key -
* @returns {string|undefined}
*/
const getTranslation = (locale, key) => {
const getTranslation = (locale: string, key: string): string | undefined => {
const localeData = LOCALES[locale];
if (!localeData) return undefined;
// 支持嵌套键 (如 'common.close')
const keys = key.split('.');
let result = localeData;
let result: unknown = localeData;
for (const k of keys) {
if (result && typeof result === 'object' && k in result) {
result = result[k];
if (result && typeof result === 'object' && k in (result as Record<string, unknown>)) {
result = (result as Record<string, unknown>)[k];
} else {
return undefined;
}
@@ -109,58 +94,45 @@ const getTranslation = (locale, key) => {
/**
*
* @param {string} str -
* @param {Object} params -
* @returns {string}
*/
const interpolate = (str, params) => {
return str.replace(/\{(\w+)\}/g, (match, key) => {
return params[key] !== undefined ? params[key] : match;
const interpolate = (str: string, params: Record<string, string | number>): string => {
return str.replace(/\{(\w+)\}/g, (_match, key: string) => {
return params[key] !== undefined ? String(params[key]) : _match;
});
};
/**
*
* @param {string} key -
* @returns {boolean}
*/
export const hasTranslation = (key) => {
export const hasTranslation = (key: string): boolean => {
return getTranslation(currentLocale, key) !== undefined ||
getTranslation(fallbackLocale, key) !== undefined;
};
/**
*
* @param {string} locale -
* @returns {Object}
*/
export const getTranslations = (locale) => {
export const getTranslations = (locale: string): Record<string, unknown> => {
return LOCALES[locale] || {};
};
/**
*
* @param {string} locale -
* @param {Object} translations -
*/
export const addTranslations = (locale, translations) => {
export const addTranslations = (locale: string, translations: Record<string, unknown>): void => {
if (!LOCALES[locale]) {
LOCALES[locale] = {};
}
deepMerge(LOCALES[locale], translations);
deepMerge(LOCALES[locale], translations as Record<string, string>);
};
/**
*
* @param {Object} target -
* @param {Object} source -
* @returns {Object}
*/
const deepMerge = (target, source) => {
const deepMerge = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
for (const key in source) {
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
deepMerge(target[key], source[key]);
deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
} else {
target[key] = source[key];
}
@@ -170,19 +142,17 @@ const deepMerge = (target, source) => {
/**
*
* @param {string} locale -
* @param {string} key -
*/
export const removeTranslation = (locale, key) => {
export const removeTranslation = (locale: string, key: string): void => {
const localeData = LOCALES[locale];
if (!localeData) return;
const keys = key.split('.');
let current = localeData;
let current: Record<string, unknown> = localeData;
for (let i = 0; i < keys.length - 1; i++) {
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
current = current[keys[i]];
current = current[keys[i]] as Record<string, unknown>;
} else {
return;
}
@@ -193,9 +163,8 @@ export const removeTranslation = (locale, key) => {
/**
*
* @param {string} locale -
*/
export const clearTranslations = (locale) => {
export const clearTranslations = (locale: string): void => {
if (LOCALES[locale]) {
LOCALES[locale] = {};
}
@@ -203,28 +172,23 @@ export const clearTranslations = (locale) => {
/**
*
* @returns {Array}
*/
export const getSupportedLocales = () => {
export const getSupportedLocales = (): string[] => {
return Object.keys(LOCALES);
};
/**
*
* @param {string} locale -
* @returns {boolean}
*/
export const isLocaleSupported = (locale) => {
export const isLocaleSupported = (locale: string): boolean => {
return locale in LOCALES;
};
/**
*
* @param {string} locale -
* @returns {string}
*/
export const getLocaleName = (locale) => {
const names = {
export const getLocaleName = (locale: string): string => {
const names: Record<string, string> = {
'zh-CN': '简体中文',
'zh-TW': '繁體中文',
'en-US': 'English (US)',
@@ -310,20 +274,16 @@ export const getLocaleName = (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,17 +330,14 @@ 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;
@@ -396,27 +350,23 @@ export const getDefaultLocale = () => {
/**
*
*/
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 },
@@ -567,33 +496,24 @@ export const formatRelativeTime = (date, options = {}) => {
}
}
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) {
export const formatList = (list: string[], _options: Record<string, unknown> = {}): 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,12 +523,8 @@ 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, string | number> = {}): string => {
const pluralForm = formatPlural(count);
const pluralKey = `${key}.${pluralForm}`;
@@ -616,7 +532,6 @@ export const plural = (key, count, params = {}) => {
return t(pluralKey, { ...params, count });
}
// 尝试通用形式
if (hasTranslation(key)) {
return t(key, { ...params, count });
}
@@ -627,75 +542,31 @@ export const plural = (key, count, params = {}) => {
/**
*
*/
export const dateTimeFormats = {
short: {
year: 'numeric',
month: 'short',
day: 'numeric',
},
medium: {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: 'numeric',
minute: 'numeric',
},
long: {
year: 'numeric',
month: 'long',
day: 'numeric',
weekday: 'long',
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
time: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
date: {
year: 'numeric',
month: 'long',
day: 'numeric',
},
weekday: {
weekday: 'long',
},
month: {
month: 'long',
},
year: {
year: 'numeric',
},
export const dateTimeFormats: Record<string, Intl.DateTimeFormatOptions> = {
short: { year: 'numeric', month: 'short', day: 'numeric' },
medium: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', second: 'numeric' },
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
date: { year: 'numeric', month: 'long', day: 'numeric' },
weekday: { weekday: 'long' },
month: { month: 'long' },
year: { year: 'numeric' },
};
/**
*
*/
export const numberFormats = {
integer: {
maximumFractionDigits: 0,
},
decimal: {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
},
percent: {
style: 'percent',
minimumFractionDigits: 0,
maximumFractionDigits: 2,
},
currency: {
style: 'currency',
currency: 'USD',
},
export const numberFormats: Record<string, Intl.NumberFormatOptions> = {
integer: { maximumFractionDigits: 0 },
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 },
currency: { style: 'currency', currency: 'USD' },
};
/**
*
*
*/
export const i18nUtils = {
export const i18nUtils: I18nUtils = {
t,
plural,
getCurrentLocale,
@@ -734,14 +605,18 @@ export const i18nUtils = {
/**
*
*/
export const presetLocales = {
export const presetLocales: Record<string, {
name: string;
nativeName: string;
direction: 'ltr' | 'rtl';
translations: Record<string, string>;
}> = {
'zh-CN': {
name: '简体中文',
nativeName: '简体中文',
direction: 'ltr',
translations: LOCALES['zh-CN'],
},
'en-US': {
name: 'English (US)',
nativeName: 'English (US)',
@@ -750,46 +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 };
+3 -3
View File
@@ -1,11 +1,11 @@
/**
* 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<string, string> = {
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"/>
</svg>`,
+99 -48
View File
@@ -1,26 +1,27 @@
/**
* 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,
@@ -32,9 +33,9 @@ const enhancedMeToast = {
presetPlugins,
/**
*
*
*/
use(plugin, options = {}) {
use(plugin: string | Record<string, unknown>, options: Record<string, unknown> = {}): MeToast {
if (typeof plugin === 'string') {
const preset = presetPlugins[plugin];
if (!preset) {
@@ -45,16 +46,22 @@ 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<ToastConfig>);
Toast.on('afterClose', () => {
if (typeof (preset as Record<string, unknown>).save === 'function') {
(preset as Record<string, (c: unknown) => 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<string, string>).name || 'custom';
this.plugins.register(name, { ...plugin, ...options } as unknown as Plugin);
}
return this;
@@ -63,7 +70,7 @@ const enhancedMeToast = {
/**
*
*/
init(options = {}) {
init(options: InitOptions = {}): MeToast {
if (options.config) {
this.configure(options.config);
}
@@ -77,8 +84,8 @@ const enhancedMeToast = {
}
if (options.plugins && Array.isArray(options.plugins)) {
options.plugins.forEach((plugin) => {
this.use(plugin);
options.plugins.forEach((p) => {
this.use(p as string);
});
}
@@ -88,13 +95,13 @@ const enhancedMeToast = {
/**
*
*/
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<string, unknown>).destroy === 'function') {
(this.plugins as unknown as Record<string, () => void>).destroy();
}
if (this.themes) {
@@ -116,10 +123,10 @@ const enhancedMeToast = {
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();
@@ -128,7 +135,7 @@ const enhancedMeToast = {
/**
*
*/
getStatus() {
getStatus(): StatusInfo {
return {
version: VERSION,
toasts: this._toasts.size,
@@ -142,14 +149,14 @@ const enhancedMeToast = {
/**
*
*/
getConfig() {
getConfig(): ToastConfig {
return { ...this._config };
},
/**
*
*/
updateConfig(config) {
updateConfig(config: Partial<ToastConfig>): MeToast {
if (config && typeof config === 'object') {
Object.assign(this._config, config);
}
@@ -159,10 +166,9 @@ 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<string, unknown>)[k]);
Object.assign(this._config, DEFAULTS);
return this;
},
@@ -170,21 +176,21 @@ const enhancedMeToast = {
/**
* 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;
},
@@ -192,35 +198,35 @@ const enhancedMeToast = {
/**
* 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<ToastOptions>): void {
if (partial && typeof partial === 'object') {
this._toasts.forEach((t) => t.update(partial));
}
@@ -229,7 +235,7 @@ const enhancedMeToast = {
/**
* Toast
*/
findToasts(predicate) {
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
if (typeof predicate !== 'function') return [];
return Array.from(this._toasts.values()).filter(predicate);
},
@@ -237,18 +243,18 @@ const enhancedMeToast = {
/**
* 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();
@@ -263,7 +269,13 @@ if (typeof window !== 'undefined') {
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<string, unknown>)._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<string, (t: ToastInstance) => void>)._notify)(toast);
}
});
}
// 导出
export default enhancedMeToast;
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
// 类型重导出(供 TypeScript 消费者 import type 使用)
export type {
ToastType,
ToastPosition,
ToastTheme,
ToastAnimation,
ToastProgressDirection,
ToastConfig,
ToastOptions,
ToastInstance,
TypeColor,
ErrorInfo,
LoadingControl,
ProgressControl,
CountdownControl,
ActionControl,
QueueControl,
GroupAPI,
ActionButton,
PromiseOptions,
ConfirmOptions,
PromptOptions,
ProgressOptions,
CountdownOptions,
QueueOptions,
StackOptions,
InitOptions,
StatusInfo,
AnimationConfig,
AnimationUtils,
ThemeConfig,
ThemePreview,
ThemeUtils,
LocaleInfo,
I18nUtils,
Plugin,
PluginManager,
PluginUtils,
} from './types.js';
+3 -3
View File
@@ -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<string, Record<string, string>> = {
'zh-CN': {
// === 操作 ===
close: '关闭',
-210
View File
@@ -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 };
+220
View File
@@ -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<string, Plugin>;
initialized: boolean;
constructor() {
this.plugins = new Map();
this.initialized = false;
}
register(name: string, plugin: Plugin): PluginManager {
if (this.plugins.has(name)) {
console.warn(`Plugin "${name}" is already registered`);
return this;
}
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
return this;
}
const stored: Plugin = { ...plugin, installed: false, enabled: true };
if (!stored.name) stored.name = name;
this.plugins.set(name, stored);
if (stored.install) {
try {
stored.install(this);
stored.installed = true;
} catch (e) {
console.error(`Failed to install plugin "${name}":`, e);
}
}
return this;
}
unregister(name: string): PluginManager {
const plugin = this.plugins.get(name);
if (!plugin) return this;
if (plugin.uninstall) {
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
}
this.plugins.delete(name);
return this;
}
get(name: string): Plugin | null { return this.plugins.get(name) || null; }
has(name: string): boolean { return this.plugins.has(name); }
getAll(): Plugin[] { return Array.from(this.plugins.values()); }
getNames(): string[] { return Array.from(this.plugins.keys()); }
enable(name: string): PluginManager {
const p = this.plugins.get(name);
if (p) p.enabled = true;
return this;
}
disable(name: string): PluginManager {
const p = this.plugins.get(name);
if (p) p.enabled = false;
return this;
}
isEnabled(name: string): boolean {
const p = this.plugins.get(name);
return p ? !!p.enabled : false;
}
destroy(): void {
this.plugins.forEach((p) => {
if (p.destroy) { try { p.destroy(this); } catch (_e) { /* noop */ } }
});
this.plugins.clear();
}
}
/**
* 预设插件
*/
const presetPlugins: Record<string, Plugin> = {
/**
* 键盘快捷键插件 — ESC 关闭所有 Toast
*/
keyboard: {
name: 'keyboard',
version: '1.0.0',
description: 'ESC 关闭所有 Toast',
_handler: null as ((e: KeyboardEvent) => void) | null,
install(this: Plugin) {
if (typeof document === 'undefined') return;
const handler = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
try { (btn as HTMLElement).click(); } catch (_e) { /* noop */ }
});
}
};
(this as Record<string, unknown>)._handler = handler;
document.addEventListener('keydown', handler);
},
uninstall(this: Plugin) {
const handler = (this as Record<string, unknown>)._handler as ((e: KeyboardEvent) => void) | null;
if (handler && typeof document !== 'undefined') {
document.removeEventListener('keydown', handler);
(this as Record<string, unknown>)._handler = null;
}
},
},
/**
* 持久化插件 — 自动保存/加载配置到 localStorage
*/
persistence: {
name: 'persistence',
version: '1.0.0',
description: '自动持久化配置到 localStorage',
storageKey: 'metona-toast-config',
install(this: Plugin) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).storageKey as string;
const saved = localStorage.getItem(key);
return saved ? JSON.parse(saved) : null;
} catch (_e) { return null; }
}
return null;
},
save(this: Plugin, config: Record<string, unknown>) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).storageKey as string;
localStorage.setItem(key, JSON.stringify(config));
} catch (_e) { /* noop */ }
}
},
uninstall(this: Plugin) {
if (typeof localStorage !== 'undefined') {
try {
const key = (this as Record<string, unknown>).storageKey as string;
localStorage.removeItem(key);
} catch (_e) { /* noop */ }
}
},
},
/**
* 无障碍插件 — 屏幕阅读器公告
*/
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<string, Plugin> { return { ...presetPlugins }; },
createPlugin(config: Partial<Plugin>): Plugin {
return {
name: config.name || 'custom',
version: config.version || '1.0.0',
description: config.description || '',
hooks: config.hooks || {},
install: config.install || undefined,
uninstall: config.uninstall || undefined,
...config,
};
},
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] } {
const errors: string[] = [];
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
return { valid: errors.length === 0, errors };
},
};
const defaultPluginManager = new PluginManager();
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
+22 -67
View File
@@ -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,7 +526,7 @@ export const clearThemeVariables = () => {
/**
*
*/
export const getSystemTheme = () => {
export const getSystemTheme = (): string => {
if (typeof window === 'undefined') return 'light';
return window.matchMedia &&
@@ -584,14 +538,14 @@ export const getSystemTheme = () => {
/**
*
*/
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());
+79
View File
@@ -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 => `
<div style="display: flex; gap: 8px; margin-top: 12px;">
${buttons}
</div>`;
/**
* 确认对话框模板
*/
export const confirmHTML = (opts: ConfirmOptions): string => {
const confirmText = opts.confirmText || t('confirm');
const cancelText = opts.cancelText || t('cancel');
const confirmColor = opts.confirmColor || '#10b981';
const cancelColor = opts.cancelColor || '#6b7280';
return btnRow(`
<button class="met-confirm-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${confirmColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(confirmText)}</button>
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
`);
};
/**
* 输入对话框模板
*/
export const promptHTML = (opts: PromptOptions): string => {
const submitText = opts.submitText || t('confirm');
const cancelText = opts.cancelText || t('cancel');
const submitColor = opts.submitColor || '#3b82f6';
const cancelColor = opts.cancelColor || '#6b7280';
return `
<input type="${opts.inputType || 'text'}"
class="met-input"
placeholder="${escapeHTML(opts.placeholder || '')}"
value="${escapeHTML(opts.defaultValue || '')}"
style="width:100%;padding:8px 12px;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:13px;margin-top:8px;background:transparent;color:inherit;box-sizing:border-box">
${btnRow(`
<button class="met-submit-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${submitColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(submitText)}</button>
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
`)}
`;
};
/**
* 进度条模板
*/
export const progressHTML = (opts: ProgressOptions): string => {
const color = opts.progressColor || '#3b82f6';
return `
<div class="met-progress-bar" style="width:100%;height:8px;background:rgba(0,0,0,0.1);border-radius:4px;margin-top:12px;overflow:hidden">
<div class="met-progress-fill" style="width:0%;height:100%;background:${color};border-radius:4px;transition:width 0.3s ease"></div>
</div>
<div class="met-progress-text" style="font-size:12px;opacity:0.7;margin-top:4px;text-align:right">0%</div>
`;
};
/**
* Action Toast 模板
*/
export const actionHTML = (actions: ActionButton[]): string => {
if (!Array.isArray(actions) || actions.length === 0) return '';
const buttons = actions.map((a, i) => {
const bg = a.style?.background || a.color || '#6366f1';
const cls = `met-action-btn-${i}`;
return `<button class="${cls}" style="flex:1;padding:6px 12px;border:none;border-radius:6px;background:${bg};color:white;cursor:pointer;font-size:12px;font-weight:500">${escapeHTML(a.text || '')}</button>`;
}).join('');
return btnRow(buttons);
};
+55 -109
View File
@@ -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,42 +44,35 @@ 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;
@@ -97,7 +84,6 @@ export const setThemeVariables = (config) => {
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,29 +177,24 @@ 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');
}
@@ -239,7 +206,7 @@ export const watchSystemTheme = () => {
/**
*
*/
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,71 +247,61 @@ const notifyThemeListeners = (theme, resolved) => {
/**
*
*/
export const clearThemeListeners = () => {
export const clearThemeListeners = (): void => {
themeListeners.clear();
};
/**
*
* @param {string} name -
* @param {Object} config -
*/
export const registerTheme = (name, config) => {
THEMES[name] = {
bg: config.bg || THEMES.light.bg,
text: config.text || THEMES.light.text,
border: config.border || THEMES.light.border,
shadow: config.shadow || THEMES.light.shadow,
hoverShadow: config.hoverShadow || THEMES.light.hoverShadow,
progressBg: config.progressBg || THEMES.light.progressBg,
closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg,
export const registerTheme = (name: string, config: ThemeConfig): void => {
(THEMES as Record<string, ThemeConfig>)[name] = {
bg: config.bg || (THEMES.light as ThemeConfig).bg,
text: config.text || (THEMES.light as ThemeConfig).text,
border: config.border || (THEMES.light as ThemeConfig).border,
shadow: config.shadow || (THEMES.light as ThemeConfig).shadow,
hoverShadow: config.hoverShadow || (THEMES.light as ThemeConfig).hoverShadow,
progressBg: config.progressBg || (THEMES.light as ThemeConfig).progressBg,
closeHoverBg: config.closeHoverBg || (THEMES.light as ThemeConfig).closeHoverBg,
};
};
/**
*
* @param {string} name -
*/
export const unregisterTheme = (name) => {
export const unregisterTheme = (name: string): void => {
if (name === 'light' || name === 'dark' || name === 'auto') {
console.warn('Cannot unregister built-in theme:', name);
return;
}
delete THEMES[name];
delete (THEMES as Record<string, ThemeConfig>)[name];
};
/**
*
* @returns {Object}
*/
export const getAllThemes = () => {
return { ...THEMES };
export const getAllThemes = (): Record<string, ThemeConfig> => {
return { ...THEMES } as Record<string, ThemeConfig>;
};
/**
*
* @returns {Array}
*/
export const getThemeNames = () => {
export const getThemeNames = (): string[] => {
return Object.keys(THEMES);
};
/**
*
* @param {string} name -
* @returns {boolean}
*/
export const hasTheme = (name) => {
export const hasTheme = (name: string): boolean => {
return name in THEMES;
};
/**
*
* @param {string} theme -
* @returns {Object}
*/
export const getThemePreview = (theme) => {
export const getThemePreview = (theme: string): ThemePreview => {
const config = getThemeConfig(theme);
const resolved = resolveTheme(theme);
@@ -370,10 +321,8 @@ 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);
@@ -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,7 +362,7 @@ export const applyThemeCSS = (theme) => {
/**
* CSS
*/
export const removeThemeCSS = () => {
export const removeThemeCSS = (): void => {
if (typeof document === 'undefined') return;
const styleId = 'metona-toast-theme-css';
@@ -427,9 +373,9 @@ export const removeThemeCSS = () => {
};
/**
*
*
*/
export const themeUtils = {
export const themeUtils: ThemeUtils = {
getSystemTheme,
resolveTheme,
getThemeConfig,
+551
View File
@@ -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<string, Array<(toast: Toast) => 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<ThemeConfig> } {
const theme = getTheme(this.config.theme || 'auto');
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
const tc = (THEMES[theme] as ThemeConfig) || (THEMES.light as ThemeConfig);
const t: Partial<ThemeConfig> = { bg: tc.bg, border: tc.border, shadow: tc.shadow };
return { theme, c, t };
}
create(): this {
if (typeof window === 'undefined' || typeof document === 'undefined') return this;
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<string, string> = {
'top-left': 'top:0;left:0;align-items:flex-start',
'top-center': 'top:0;left:0;right:0;align-items:center',
'top-right': 'top:0;right:0;align-items:flex-end',
'bottom-left': 'bottom:0;left:0;align-items:flex-start',
'bottom-center': 'bottom:0;left:0;right:0;align-items:center',
'bottom-right': 'bottom:0;right:0;align-items:flex-end',
};
el.style.cssText = `
display:flex;
flex-direction:column;
gap:${this.config.gap || 12}px;
box-sizing:border-box;
padding:${this.config.offset || 24}px;
max-width:100vw;
position:fixed;
z-index:${zIndex};
pointer-events:none;
${posStyles[position] || 'top:0;right:0;align-items:flex-end'}
`;
document.body.appendChild(el);
_containerCache.get(document.body)!.set(position, el);
return el;
}
_limitToasts(container: HTMLElement): void {
const max = this.config.max || 6;
const list = Array.from(container.querySelectorAll('.met-toast'));
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<ThemeConfig>): void {
const widthValue = typeof this.config.width === 'number'
? `${this.config.width}px`
: (typeof this.config.width === 'string' ? this.config.width : '360px');
const set = (p: string, v: string) => el.style.setProperty(p, v, 'important');
set('position', 'relative');
set('flex-shrink', '0');
set('min-width', '240px');
set('max-width', 'calc(100vw - 48px)');
set('width', widthValue);
// 用户自定义样式最后应用
if (this.config.style && Object.keys(this.config.style).length > 0) {
Object.entries(this.config.style).forEach(([k, v]) => { (el.style as unknown as Record<string, string>)[k] = v; });
}
}
_buildContent(el: HTMLElement, c: TypeColor): void {
// 自定义渲染函数 — 完全接管 DOM 构建
if (typeof this.config.render === 'function') {
el.innerHTML = this.config.render(this);
this.barEl = el.querySelector('.met-bar, .met-bar-v');
return;
}
const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]);
const showClose = this.config.closeButton !== false;
const showProgress = this.config.showProgress !== false && (this.config.duration || 0) > 0;
const showSide = (!showIcon && this.type !== 'default');
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
const safeMessage = this.html
? `<div class="met-message">${this.html}</div>`
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
const iconHTML = showIcon
? `<div class="met-icon" style="color:${c.fg}">${this.iconHTML || ICONS[this.type]}</div>` : '';
const closeHTML = showClose
? `<button class="met-close" type="button" aria-label="${t('close')}">
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"/>
<line x1="6" y1="6" x2="18" y2="18"/>
</svg>
</button>`
: '';
const progressHTML = showProgress
? (this.config.progressDirection === 'vertical'
? `<div class="met-progress-v"><div class="met-bar-v" style="background:${c.fg}"></div></div>`
: `<div class="met-progress"><div class="met-bar" style="background:${c.fg}"></div></div>`)
: '';
const sideHTML = showSide
? `<div class="met-side" style="background:${c.fg}"></div>` : '';
el.innerHTML = `
${iconHTML}
${sideHTML}
<div class="met-content">
${safeTitle}
${safeMessage}
</div>
${closeHTML}
${progressHTML}
`;
this.barEl = el.querySelector('.met-bar, .met-bar-v');
}
_bindEvents(el: HTMLElement): void {
const eventHandler = (e: Event) => {
const target = e.target as HTMLElement;
if (target.closest('.met-close')) {
e.stopPropagation();
this.close();
return;
}
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<ToastOptions>): 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 ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
const safeMessage = this.html
? `<div class="met-message">${this.html}</div>`
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
content.innerHTML = `${safeTitle}${safeMessage}`;
}
// 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<Node, Map<string, HTMLElement>> = new Map();
+552
View File
@@ -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<string, string>;
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<string | Plugin>;
}
export interface ErrorInfo {
hook?: string;
source?: string;
error: Error;
toast?: ToastInstance;
}
// ========== Toast 选项与实例 ==========
export interface ToastOptions extends Partial<ToastConfig> {
type?: string;
title?: string;
message?: string;
content?: string;
html?: string;
iconHTML?: string;
id?: string;
}
export interface ToastInstance {
id: string;
type: string;
title: string;
message: string;
html: string;
iconHTML: string;
config: ToastConfig;
el: HTMLElement | null;
barEl: HTMLElement | null;
rafId: number | null;
remaining: number;
startedAt: number;
paused: boolean;
closing: boolean;
group: string | null;
_cleanups: Array<() => void>;
_palette(): { theme: string; c: TypeColor; t: Partial<ThemeConfig> };
create(): this;
update(partial: Partial<ToastOptions>): this;
close(immediate?: boolean): void;
_pause(): void;
_resume(): void;
_destroy(): void;
}
// ========== 控制对象 ==========
export interface LoadingControl {
id: string;
success(message?: string, opts?: ToastOptions): ToastInstance | null;
error(message?: string, opts?: ToastOptions): ToastInstance | null;
info(message?: string, opts?: ToastOptions): ToastInstance | null;
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
update(partial: Partial<ToastOptions>): unknown;
dismiss(): void;
}
export interface ProgressControl {
id: string;
setProgress(percent: number): void;
complete(message?: string): void;
error(message?: string): void;
dismiss(): void;
}
export interface CountdownControl {
id: string;
cancel(): void;
pause(): void;
resume(): void;
}
export interface ActionControl {
id: string;
toast: ToastInstance;
dismiss(): void;
}
export interface QueueControl {
then<TResult1 = void, TResult2 = never>(
onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
): Promise<TResult1 | TResult2>;
catch<TResult = never>(
onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
): Promise<void | TResult>;
cancel(): void;
}
export interface GroupAPI {
_group: string;
show: (...args: unknown[]) => ToastInstance;
success: (...args: unknown[]) => ToastInstance;
error: (...args: unknown[]) => ToastInstance;
warning: (...args: unknown[]) => ToastInstance;
info: (...args: unknown[]) => ToastInstance;
loading: (...args: unknown[]) => LoadingControl;
action: (...args: unknown[]) => ActionControl;
dismiss(): void;
count(): number;
}
// ========== Action 按钮 ==========
export interface ActionButton {
text: string;
onClick: (toast: ToastInstance) => void;
color?: string;
style?: Record<string, string>;
close?: boolean;
}
// ========== Options 接口 ==========
export interface PromiseOptions extends ToastOptions {
loading?: string;
success?: string;
error?: string;
}
export interface ConfirmOptions extends ToastOptions {
confirmText?: string;
confirmColor?: string;
cancelText?: string;
cancelColor?: string;
}
export interface PromptOptions extends ToastOptions {
inputType?: string;
placeholder?: string;
defaultValue?: string;
submitText?: string;
submitColor?: string;
cancelText?: string;
cancelColor?: string;
}
export interface ProgressOptions extends ToastOptions {
progressColor?: string;
}
export interface CountdownOptions extends ToastOptions {
onComplete?: (() => void) | null;
}
export interface QueueOptions extends ToastOptions {
delay?: number;
}
export interface StackOptions extends ToastOptions {
stagger?: number;
}
export interface InitOptions {
config?: Partial<ToastConfig>;
theme?: ToastTheme;
locale?: string;
plugins?: Array<string | Plugin>;
}
export interface StatusInfo {
version: string;
toasts: number;
theme: string;
locale: string;
plugins: string[];
animations: number;
}
// ========== 动画 ==========
export interface AnimationConfig {
enter: Record<string, string>;
leave: Record<string, string>;
duration: number;
easing: string;
name?: string;
delay?: number;
iterations?: number;
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
}
export interface AnimationUtils {
register(name: string, config: Partial<AnimationConfig>): void;
unregister(name: string): void;
get(name: string): AnimationConfig | null;
getAnimationNames(): string[];
getActiveCount(): number;
cancelAll(): void;
reset(): void;
destroy(): void;
}
// ========== 主题 ==========
export interface ThemeConfig {
bg: string;
text: string;
border: string;
shadow: string;
hoverShadow: string;
progressBg: string;
closeHoverBg: string;
backdropFilter?: string;
}
export interface ThemePreview {
name: string;
resolved: string;
colors: {
background: string;
text: string;
border: string;
};
isDark: boolean;
isLight: boolean;
isAuto: boolean;
}
export interface ThemeUtils {
getSystemTheme(): 'light' | 'dark';
resolveTheme(theme: string): string;
getThemeConfig(theme: string): ThemeConfig;
applyTheme(theme: string): void;
getCurrentTheme(): string;
getResolvedTheme(): string;
switchTheme(theme: string): void;
toggleTheme(): void;
resetToAuto(): void;
initTheme(): void;
watchSystemTheme(): void;
unwatchSystemTheme(): void;
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
clearThemeListeners(): void;
registerTheme(name: string, config: ThemeConfig): void;
unregisterTheme(name: string): void;
getAllThemes(): Record<string, ThemeConfig>;
getThemeNames(): string[];
hasTheme(name: string): boolean;
getThemePreview(theme: string): ThemePreview;
generateThemeCSS(theme: string): string;
applyThemeCSS(theme: string): void;
removeThemeCSS(): void;
saveTheme(theme: string): void;
loadTheme(): string;
}
// ========== 国际化 ==========
export interface LocaleInfo {
code: string;
name: string;
direction: 'ltr' | 'rtl';
isSupported: boolean;
isCurrent: boolean;
isFallback: boolean;
}
export interface I18nUtils {
t(key: string, params?: Record<string, string | number>): string;
plural(key: string, count: number, params?: Record<string, string | number>): string;
getCurrentLocale(): string;
setCurrentLocale(locale: string): void;
switchLocale(locale: string): void;
getFallbackLocale(): string;
setFallbackLocale(locale: string): void;
hasTranslation(key: string): boolean;
getTranslations(locale: string): Record<string, unknown>;
addTranslations(locale: string, translations: Record<string, unknown>): void;
removeTranslation(locale: string, key: string): void;
clearTranslations(locale: string): void;
getSupportedLocales(): string[];
isLocaleSupported(locale: string): boolean;
getLocaleName(locale: string): string;
getLocaleDirection(locale: string): 'ltr' | 'rtl';
getLocaleInfo(locale: string): LocaleInfo;
getAllLocaleInfo(): LocaleInfo[];
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
formatList(list: string[], options?: Record<string, unknown>): string;
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
addLocaleListener(listener: (locale: string) => void): () => void;
removeLocaleListener(listener: (locale: string) => void): void;
clearLocaleListeners(): void;
initI18n(): void;
saveLocale(locale: string): void;
loadLocale(): string;
getDefaultLocale(): string;
}
// ========== 插件 ==========
export interface Plugin {
name: string;
version?: string;
description?: string;
hooks?: Record<string, (...args: unknown[]) => void>;
middleware?: (...args: unknown[]) => unknown;
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
uninstall?: (manager: PluginManager) => void;
init?: (manager: PluginManager) => void;
destroy?: (manager: PluginManager) => void;
installed?: boolean;
enabled?: boolean;
[key: string]: unknown;
}
export interface PluginManager {
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
destroy(): void;
}
export interface PluginUtils {
createManager(): PluginManager;
register(name: string, plugin: Plugin): PluginManager;
unregister(name: string): PluginManager;
get(name: string): Plugin | null;
has(name: string): boolean;
getAll(): Plugin[];
getNames(): string[];
enable(name: string): PluginManager;
disable(name: string): PluginManager;
isEnabled(name: string): boolean;
getPreset(name: string): Plugin | null;
getAllPresets(): Record<string, Plugin>;
createPlugin(config: Partial<Plugin>): Plugin;
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] };
}
// ========== MeToast 主接口 ==========
export interface MeToast {
version: string;
_toasts: Map<string, ToastInstance>;
_config: ToastConfig;
_destroyed?: boolean;
// 内部方法
_emit(opts: ToastOptions): ToastInstance;
_remove(id: string): void;
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
_groupCount(name: string): number;
// 配置
configure(opts: Partial<ToastConfig>): MeToast;
init(options?: InitOptions): MeToast;
destroy(): void;
getStatus(): StatusInfo;
getConfig(): ToastConfig;
updateConfig(config: Partial<ToastConfig>): MeToast;
resetConfig(): MeToast;
// Toast 方法
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
// 分组
group(name: string): GroupAPI;
dismissGroup(name: string): void;
// Toast 管理
find(id: string): ToastInstance | undefined;
dismiss(id?: string): void;
clear(position?: ToastPosition): void;
getToasts(): ToastInstance[];
count(): number;
hasToasts(): boolean;
getToast(id: string): ToastInstance | null;
closeAll(): void;
clearAll(): void;
pauseAll(): void;
resumeAll(): void;
updateAll(partial: Partial<ToastOptions>): void;
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
findByType(type: ToastType): ToastInstance[];
findByPosition(position: ToastPosition): ToastInstance[];
getAll(): Map<string, ToastInstance>;
// 插件
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
// 子模块
animations: AnimationUtils;
themes: ThemeUtils;
i18n: I18nUtils;
plugins: PluginUtils;
presetPlugins: Record<string, Plugin>;
}
// ========== 钩子名称 ==========
export const HOOK_NAMES = {
BEFORE_INIT: 'beforeInit',
AFTER_INIT: 'afterInit',
BEFORE_DESTROY: 'beforeDestroy',
AFTER_DESTROY: 'afterDestroy',
BEFORE_SHOW: 'beforeShow',
AFTER_SHOW: 'afterShow',
BEFORE_CLOSE: 'beforeClose',
AFTER_CLOSE: 'afterClose',
BEFORE_UPDATE: 'beforeUpdate',
AFTER_UPDATE: 'afterUpdate',
CONFIG_CHANGE: 'configChange',
THEME_CHANGE: 'themeChange',
LOCALE_CHANGE: 'localeChange',
CLICK: 'click',
HOVER: 'hover',
DRAG_START: 'dragStart',
DRAG_END: 'dragEnd',
ANIMATION_START: 'animationStart',
ANIMATION_END: 'animationEnd',
PROGRESS_START: 'progressStart',
PROGRESS_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;
}
}
+7 -11
View File
@@ -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, '&amp;')
@@ -35,10 +32,9 @@ export const escapeHTML = (s) => {
/**
*
* @returns {boolean}
*/
export const prefersDark = () => {
export const prefersDark = (): boolean => {
return typeof window !== 'undefined' &&
window.matchMedia &&
typeof window.matchMedia === 'function' &&
window.matchMedia('(prefers-color-scheme: dark)').matches;
};
File diff suppressed because it is too large Load Diff
+24
View File
@@ -0,0 +1,24 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ES2020",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"moduleResolution": "bundler",
"strict": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"declaration": false,
"declarationMap": false,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src",
"baseUrl": ".",
"resolveJsonModule": true,
"isolatedModules": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist", "tests", "site"]
}
-1046
View File
File diff suppressed because it is too large Load Diff