release: v0.2.0 — TypeScript 源码重构
### 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:
@@ -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/"]
|
||||
}
|
||||
@@ -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
|
||||
@@ -2,4 +2,7 @@ node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
.zcode/
|
||||
|
||||
# 本地 npm 发布凭据(含 _auth,勿提交)
|
||||
.npmrc
|
||||
|
||||
@@ -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 类型声明
|
||||
- 零依赖
|
||||
@@ -8,14 +8,14 @@
|
||||
|
||||
## 特性
|
||||
|
||||
- **零依赖** — 纯原生 JavaScript,gzip 后不到 10KB
|
||||
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB
|
||||
- **107 种图标类型** — success/error/warning/info/loading 及更多扩展类型,覆盖所有常见场景
|
||||
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,每种效果明显不同
|
||||
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
|
||||
- **国际化** — 内置 zh-CN / en-US 完整翻译(去重优化),可通过 `addTranslations()` 扩展
|
||||
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
|
||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
|
||||
- **TypeScript** — 完整类型定义,已对齐实际实现无 phantom 方法
|
||||
- **TypeScript** — 严格模式,源码级类型安全,类型从源码自动生成无手动维护
|
||||
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入监控系统
|
||||
|
||||
## 安装
|
||||
@@ -58,7 +58,7 @@ const MeToast = require('@metona-team/metona-toast');
|
||||
| Opera | 51+ |
|
||||
| iOS Safari | 11+ |
|
||||
| Android Chrome | 64+ |
|
||||
| Node.js | 18+(ESM & CJS 双格式) |
|
||||
| Node.js | 16+(ESM & CJS 双格式) |
|
||||
| TypeScript | 4.5+(完整类型声明) |
|
||||
|
||||
核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
{
|
||||
targets: {
|
||||
node: 'current',
|
||||
},
|
||||
modules: 'commonjs',
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-modules-commonjs',
|
||||
],
|
||||
};
|
||||
@@ -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 ""
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
Generated
+550
-1437
File diff suppressed because it is too large
Load Diff
+21
-20
@@ -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%",
|
||||
|
||||
+70
-98
@@ -1,5 +1,6 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
@@ -8,128 +9,99 @@ import livereload from 'rollup-plugin-livereload';
|
||||
const isDev = process.env.ROLLUP_WATCH;
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
// 基础配置
|
||||
const baseConfig = {
|
||||
input: 'src/index.js',
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
],
|
||||
};
|
||||
// 基础插件
|
||||
const basePlugins = [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
declaration: false,
|
||||
sourceMap: !isProd,
|
||||
}),
|
||||
];
|
||||
|
||||
// 开发服务器配置
|
||||
const devPlugins = isDev ? [
|
||||
serve({
|
||||
open: true,
|
||||
contentBase: ['site', 'dist'],
|
||||
port: 3000,
|
||||
port: 3001,
|
||||
}),
|
||||
livereload({
|
||||
watch: ['src', 'site'],
|
||||
}),
|
||||
] : [];
|
||||
|
||||
// 生产环境配置
|
||||
const prodPlugins = isProd ? [
|
||||
terser({
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log', 'console.warn'],
|
||||
},
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
}),
|
||||
] : [];
|
||||
|
||||
// 输出配置
|
||||
export default [
|
||||
// UMD格式(浏览器)
|
||||
// UMD(开发模式)
|
||||
{
|
||||
...baseConfig,
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.js',
|
||||
format: 'umd',
|
||||
name: 'MeToast',
|
||||
exports: 'named',
|
||||
sourcemap: !isProd,
|
||||
globals: {},
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
...devPlugins,
|
||||
...prodPlugins,
|
||||
],
|
||||
plugins: [...basePlugins, ...devPlugins],
|
||||
},
|
||||
|
||||
// ESM格式(现代浏览器/打包工具)
|
||||
{
|
||||
...baseConfig,
|
||||
output: {
|
||||
file: 'dist/metona-toast.esm.js',
|
||||
format: 'es',
|
||||
exports: 'named',
|
||||
sourcemap: !isProd,
|
||||
// 生产构建(非 dev 模式时输出全格式)
|
||||
...(isDev ? [] : [
|
||||
// ESM
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.esm.js',
|
||||
format: 'es',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
...prodPlugins,
|
||||
],
|
||||
},
|
||||
|
||||
// CommonJS格式(Node.js)
|
||||
{
|
||||
...baseConfig,
|
||||
output: {
|
||||
file: 'dist/metona-toast.cjs.js',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: !isProd,
|
||||
// CommonJS
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.cjs.js',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
...prodPlugins,
|
||||
],
|
||||
},
|
||||
|
||||
// 压缩版本(UMD)
|
||||
{
|
||||
...baseConfig,
|
||||
output: {
|
||||
file: 'dist/metona-toast.min.js',
|
||||
format: 'umd',
|
||||
name: 'MeToast',
|
||||
exports: 'named',
|
||||
sourcemap: false,
|
||||
globals: {},
|
||||
// UMD 压缩版
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.min.js',
|
||||
format: 'umd',
|
||||
name: 'MeToast',
|
||||
exports: 'named',
|
||||
sourcemap: false,
|
||||
},
|
||||
plugins: [
|
||||
...basePlugins,
|
||||
terser({
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log', 'console.warn'],
|
||||
passes: 2,
|
||||
},
|
||||
format: { comments: false },
|
||||
mangle: { toplevel: true },
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
terser({
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log', 'console.warn'],
|
||||
passes: 2,
|
||||
},
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
mangle: {
|
||||
toplevel: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// TypeScript声明文件
|
||||
{
|
||||
input: 'types/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.d.ts',
|
||||
format: 'es',
|
||||
// TypeScript 声明文件合并
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -15,7 +15,7 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT=${1:-3000}
|
||||
PORT=${1:-3001}
|
||||
|
||||
echo "✅ Python: $($PYTHON --version 2>&1)"
|
||||
echo "🌐 启动服务器..."
|
||||
|
||||
+2
-2
@@ -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
@@ -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>
|
||||
|
||||
+8
-8
@@ -80,7 +80,7 @@
|
||||
|
||||
<header class="hero">
|
||||
<h1><span>MetonaToast</span></h1>
|
||||
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。纯原生 JavaScript,gzip 不到 10KB。</p>
|
||||
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式源码,gzip 不到 10KB。</p>
|
||||
<div class="badges">
|
||||
<span>📦 <10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
|
||||
</div>
|
||||
@@ -126,8 +126,8 @@
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>零依赖</h3>
|
||||
<p>纯原生 JavaScript 实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
||||
<h3>零依赖</h3>
|
||||
<p>TypeScript 严格模式,纯原生实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🛡️</div>
|
||||
@@ -141,8 +141,8 @@
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">📘</div>
|
||||
<h3>TypeScript</h3>
|
||||
<p>完整类型定义,与实现严格对齐零 phantom 方法。<code>MeToast.success()</code>、<code>.confirm()</code>、<code>.action()</code> 全部有类型。</p>
|
||||
<h3>TypeScript</h3>
|
||||
<p>严格模式源码,类型从源码自动生成。strictNullChecks、noImplicitAny 全开,零类型漂移。</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
@@ -173,8 +173,8 @@
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">📦</div>
|
||||
<h3>Node.js</h3>
|
||||
<p>18+</p>
|
||||
<h3>Node.js</h3>
|
||||
<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>
|
||||
|
||||
@@ -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)',
|
||||
});
|
||||
@@ -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
@@ -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 };
|
||||
@@ -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
File diff suppressed because it is too large
Load Diff
+101
-268
@@ -1,35 +1,33 @@
|
||||
/**
|
||||
* MetonaToast i18n - 国际化管理
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 0.1.2
|
||||
* @description 多语言支持、语言切换和翻译管理
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
import { LOCALES } from './constants.js';
|
||||
import type { I18nUtils, LocaleInfo } from './types.js';
|
||||
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
let localeListeners = new Set();
|
||||
const localeListeners: Set<(locale: string) => void> = new Set();
|
||||
let fallbackLocale = 'zh-CN';
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
* @returns {string} 当前语言
|
||||
*/
|
||||
export const getCurrentLocale = () => {
|
||||
export const getCurrentLocale = (): string => {
|
||||
return currentLocale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置当前语言
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const setCurrentLocale = (locale) => {
|
||||
export const setCurrentLocale = (locale: string): void => {
|
||||
if (!LOCALES[locale]) {
|
||||
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
||||
locale = fallbackLocale;
|
||||
}
|
||||
|
||||
|
||||
currentLocale = locale;
|
||||
notifyLocaleListeners(locale);
|
||||
saveLocale(locale);
|
||||
@@ -37,130 +35,104 @@ export const setCurrentLocale = (locale) => {
|
||||
|
||||
/**
|
||||
* 获取回退语言
|
||||
* @returns {string} 回退语言
|
||||
*/
|
||||
export const getFallbackLocale = () => {
|
||||
export const getFallbackLocale = (): string => {
|
||||
return fallbackLocale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置回退语言
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const setFallbackLocale = (locale) => {
|
||||
export const setFallbackLocale = (locale: string): void => {
|
||||
if (!LOCALES[locale]) {
|
||||
console.warn(`Fallback locale "${locale}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
fallbackLocale = locale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
* @param {string} key - 翻译键
|
||||
* @param {Object} params - 插值参数
|
||||
* @returns {string} 翻译后的字符串
|
||||
*/
|
||||
export const t = (key, params = {}) => {
|
||||
// 尝试当前语言
|
||||
export const t = (key: string, params: Record<string, string | number> = {}): string => {
|
||||
const currentTranslation = getTranslation(currentLocale, key);
|
||||
if (currentTranslation !== undefined) {
|
||||
return interpolate(currentTranslation, params);
|
||||
}
|
||||
|
||||
// 尝试回退语言
|
||||
|
||||
if (currentLocale !== fallbackLocale) {
|
||||
const fallbackTranslation = getTranslation(fallbackLocale, key);
|
||||
if (fallbackTranslation !== undefined) {
|
||||
return interpolate(fallbackTranslation, params);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回键名
|
||||
|
||||
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @param {string} key - 翻译键
|
||||
* @returns {string|undefined} 翻译字符串
|
||||
*/
|
||||
const getTranslation = (locale, key) => {
|
||||
const getTranslation = (locale: string, key: string): string | undefined => {
|
||||
const localeData = LOCALES[locale];
|
||||
if (!localeData) return undefined;
|
||||
|
||||
// 支持嵌套键 (如 'common.close')
|
||||
|
||||
const keys = key.split('.');
|
||||
let result = localeData;
|
||||
|
||||
let result: unknown = localeData;
|
||||
|
||||
for (const k of keys) {
|
||||
if (result && typeof result === 'object' && k in result) {
|
||||
result = result[k];
|
||||
if (result && typeof result === 'object' && k in (result as Record<string, unknown>)) {
|
||||
result = (result as Record<string, unknown>)[k];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return typeof result === 'string' ? result : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 插值函数
|
||||
* @param {string} str - 包含占位符的字符串
|
||||
* @param {Object} params - 插值参数
|
||||
* @returns {string} 插值后的字符串
|
||||
*/
|
||||
const interpolate = (str, params) => {
|
||||
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return params[key] !== undefined ? params[key] : match;
|
||||
const interpolate = (str: string, params: Record<string, string | number>): string => {
|
||||
return str.replace(/\{(\w+)\}/g, (_match, key: string) => {
|
||||
return params[key] !== undefined ? String(params[key]) : _match;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查翻译是否存在
|
||||
* @param {string} key - 翻译键
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
export const hasTranslation = (key) => {
|
||||
export const hasTranslation = (key: string): boolean => {
|
||||
return getTranslation(currentLocale, key) !== undefined ||
|
||||
getTranslation(fallbackLocale, key) !== undefined;
|
||||
getTranslation(fallbackLocale, key) !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {Object} 翻译对象
|
||||
*/
|
||||
export const getTranslations = (locale) => {
|
||||
export const getTranslations = (locale: string): Record<string, unknown> => {
|
||||
return LOCALES[locale] || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @param {Object} translations - 翻译对象
|
||||
*/
|
||||
export const addTranslations = (locale, translations) => {
|
||||
export const addTranslations = (locale: string, translations: Record<string, unknown>): void => {
|
||||
if (!LOCALES[locale]) {
|
||||
LOCALES[locale] = {};
|
||||
}
|
||||
|
||||
deepMerge(LOCALES[locale], translations);
|
||||
deepMerge(LOCALES[locale], translations as Record<string, string>);
|
||||
};
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
* @param {Object} target - 目标对象
|
||||
* @param {Object} source - 源对象
|
||||
* @returns {Object} 合并后的对象
|
||||
*/
|
||||
const deepMerge = (target, source) => {
|
||||
const deepMerge = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
|
||||
for (const key in source) {
|
||||
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
||||
deepMerge(target[key], source[key]);
|
||||
deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
@@ -170,32 +142,29 @@ const deepMerge = (target, source) => {
|
||||
|
||||
/**
|
||||
* 移除翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @param {string} key - 翻译键
|
||||
*/
|
||||
export const removeTranslation = (locale, key) => {
|
||||
export const removeTranslation = (locale: string, key: string): void => {
|
||||
const localeData = LOCALES[locale];
|
||||
if (!localeData) return;
|
||||
|
||||
|
||||
const keys = key.split('.');
|
||||
let current = localeData;
|
||||
|
||||
let current: Record<string, unknown> = localeData;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
|
||||
current = current[keys[i]];
|
||||
current = current[keys[i]] as Record<string, unknown>;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
delete current[keys[keys.length - 1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除翻译
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const clearTranslations = (locale) => {
|
||||
export const clearTranslations = (locale: string): void => {
|
||||
if (LOCALES[locale]) {
|
||||
LOCALES[locale] = {};
|
||||
}
|
||||
@@ -203,28 +172,23 @@ export const clearTranslations = (locale) => {
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* @returns {Array} 语言代码数组
|
||||
*/
|
||||
export const getSupportedLocales = () => {
|
||||
export const getSupportedLocales = (): string[] => {
|
||||
return Object.keys(LOCALES);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查语言是否支持
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {boolean} 是否支持
|
||||
*/
|
||||
export const isLocaleSupported = (locale) => {
|
||||
export const isLocaleSupported = (locale: string): boolean => {
|
||||
return locale in LOCALES;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言名称
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {string} 语言名称
|
||||
*/
|
||||
export const getLocaleName = (locale) => {
|
||||
const names = {
|
||||
export const getLocaleName = (locale: string): string => {
|
||||
const names: Record<string, string> = {
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '繁體中文',
|
||||
'en-US': 'English (US)',
|
||||
@@ -304,26 +268,22 @@ export const getLocaleName = (locale) => {
|
||||
've': 'Tshivenda',
|
||||
'nr': 'isiNdebele',
|
||||
};
|
||||
|
||||
|
||||
return names[locale] || locale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言方向
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {string} 语言方向 ('ltr' 或 'rtl')
|
||||
*/
|
||||
export const getLocaleDirection = (locale) => {
|
||||
export const getLocaleDirection = (locale: string): 'ltr' | 'rtl' => {
|
||||
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
||||
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言信息
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {Object} 语言信息
|
||||
*/
|
||||
export const getLocaleInfo = (locale) => {
|
||||
export const getLocaleInfo = (locale: string): LocaleInfo => {
|
||||
return {
|
||||
code: locale,
|
||||
name: getLocaleName(locale),
|
||||
@@ -336,17 +296,15 @@ export const getLocaleInfo = (locale) => {
|
||||
|
||||
/**
|
||||
* 获取所有语言信息
|
||||
* @returns {Array} 语言信息数组
|
||||
*/
|
||||
export const getAllLocaleInfo = () => {
|
||||
export const getAllLocaleInfo = (): LocaleInfo[] => {
|
||||
return getSupportedLocales().map(getLocaleInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存语言到本地存储
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const saveLocale = (locale) => {
|
||||
export const saveLocale = (locale: string): void => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('metona-toast-locale', locale);
|
||||
@@ -358,9 +316,8 @@ export const saveLocale = (locale) => {
|
||||
|
||||
/**
|
||||
* 从本地存储加载语言
|
||||
* @returns {string} 语言代码
|
||||
*/
|
||||
export const loadLocale = () => {
|
||||
export const loadLocale = (): string => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
return localStorage.getItem('metona-toast-locale') || getDefaultLocale();
|
||||
@@ -373,50 +330,43 @@ export const loadLocale = () => {
|
||||
|
||||
/**
|
||||
* 获取默认语言
|
||||
* @returns {string} 语言代码
|
||||
*/
|
||||
export const getDefaultLocale = () => {
|
||||
export const getDefaultLocale = (): string => {
|
||||
if (typeof navigator !== 'undefined') {
|
||||
// 尝试从浏览器获取语言
|
||||
const browserLocale = navigator.language || navigator.userLanguage;
|
||||
const browserLocale = navigator.language || (navigator as unknown as { userLanguage?: string }).userLanguage;
|
||||
if (browserLocale && isLocaleSupported(browserLocale)) {
|
||||
return browserLocale;
|
||||
}
|
||||
|
||||
// 尝试语言代码的前两位
|
||||
|
||||
const shortLocale = browserLocale?.split('-')[0];
|
||||
if (shortLocale && isLocaleSupported(shortLocale)) {
|
||||
return shortLocale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return fallbackLocale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化国际化系统
|
||||
*/
|
||||
export const initI18n = () => {
|
||||
export const initI18n = (): void => {
|
||||
const savedLocale = loadLocale();
|
||||
setCurrentLocale(savedLocale);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
* @param {string} locale - 新语言
|
||||
*/
|
||||
export const switchLocale = (locale) => {
|
||||
export const switchLocale = (locale: string): void => {
|
||||
setCurrentLocale(locale);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加语言监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
* @returns {Function} 移除监听器函数
|
||||
*/
|
||||
export const addLocaleListener = (listener) => {
|
||||
export const addLocaleListener = (listener: (locale: string) => void): () => void => {
|
||||
localeListeners.add(listener);
|
||||
|
||||
return () => {
|
||||
localeListeners.delete(listener);
|
||||
};
|
||||
@@ -424,17 +374,15 @@ export const addLocaleListener = (listener) => {
|
||||
|
||||
/**
|
||||
* 移除语言监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
*/
|
||||
export const removeLocaleListener = (listener) => {
|
||||
export const removeLocaleListener = (listener: (locale: string) => void): void => {
|
||||
localeListeners.delete(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知语言监听器
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
const notifyLocaleListeners = (locale) => {
|
||||
const notifyLocaleListeners = (locale: string): void => {
|
||||
localeListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(locale);
|
||||
@@ -447,17 +395,14 @@ const notifyLocaleListeners = (locale) => {
|
||||
/**
|
||||
* 清除所有语言监听器
|
||||
*/
|
||||
export const clearLocaleListeners = () => {
|
||||
export const clearLocaleListeners = (): void => {
|
||||
localeListeners.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} number - 数字
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatNumber = (number, options = {}) => {
|
||||
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
} catch (e) {
|
||||
@@ -467,12 +412,8 @@ export const formatNumber = (number, options = {}) => {
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param {number} amount - 金额
|
||||
* @param {string} currency - 货币代码
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
export const formatCurrency = (amount: number, currency = 'USD', options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, {
|
||||
style: 'currency',
|
||||
@@ -486,11 +427,8 @@ export const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param {number} value - 值
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatPercent = (value, options = {}) => {
|
||||
export const formatPercent = (value: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, {
|
||||
style: 'percent',
|
||||
@@ -505,26 +443,20 @@ export const formatPercent = (value, options = {}) => {
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date|number|string} date - 日期
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatDate = (date, options = {}) => {
|
||||
export const formatDate = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
|
||||
try {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
} catch (e) {
|
||||
return date.toString();
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|number|string} date - 日期
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatTime = (date, options = {}) => {
|
||||
export const formatTime = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
|
||||
return formatDate(date, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
@@ -535,22 +467,19 @@ export const formatTime = (date, options = {}) => {
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {Date|number|string} date - 日期
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatRelativeTime = (date, options = {}) => {
|
||||
export const formatRelativeTime = (date: Date | number | string, options: Intl.RelativeTimeFormatOptions = {}): string => {
|
||||
try {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
const now = new Date();
|
||||
const diff = dateObj - now;
|
||||
|
||||
const diff = dateObj.getTime() - now.getTime();
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat(currentLocale, {
|
||||
numeric: 'auto',
|
||||
...options,
|
||||
});
|
||||
|
||||
const units = [
|
||||
|
||||
const units: Array<{ unit: Intl.RelativeTimeFormatUnit; ms: number }> = [
|
||||
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
|
||||
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
|
||||
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
|
||||
@@ -559,41 +488,32 @@ export const formatRelativeTime = (date, options = {}) => {
|
||||
{ unit: 'minute', ms: 60 * 1000 },
|
||||
{ unit: 'second', ms: 1000 },
|
||||
];
|
||||
|
||||
|
||||
for (const { unit, ms } of units) {
|
||||
if (Math.abs(diff) >= ms || unit === 'second') {
|
||||
const value = Math.round(diff / ms);
|
||||
return rtf.format(value, unit);
|
||||
}
|
||||
}
|
||||
|
||||
return date.toString();
|
||||
|
||||
return String(date);
|
||||
} catch (e) {
|
||||
return date.toString();
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化列表
|
||||
* @param {Array} list - 列表
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatList = (list, options = {}) => {
|
||||
try {
|
||||
return new Intl.ListFormat(currentLocale, options).format(list);
|
||||
} catch (e) {
|
||||
return list.join(', ');
|
||||
}
|
||||
export const formatList = (list: string[], _options: Record<string, 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,99 +523,50 @@ export const formatPlural = (count, options = {}) => {
|
||||
|
||||
/**
|
||||
* 复数翻译
|
||||
* @param {string} key - 翻译键
|
||||
* @param {number} count - 数量
|
||||
* @param {Object} params - 插值参数
|
||||
* @returns {string} 翻译后的字符串
|
||||
*/
|
||||
export const plural = (key, count, params = {}) => {
|
||||
export const plural = (key: string, count: number, params: Record<string, string | number> = {}): string => {
|
||||
const pluralForm = formatPlural(count);
|
||||
const pluralKey = `${key}.${pluralForm}`;
|
||||
|
||||
|
||||
if (hasTranslation(pluralKey)) {
|
||||
return t(pluralKey, { ...params, count });
|
||||
}
|
||||
|
||||
// 尝试通用形式
|
||||
|
||||
if (hasTranslation(key)) {
|
||||
return t(key, { ...params, count });
|
||||
}
|
||||
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* 日期时间格式化选项
|
||||
*/
|
||||
export const dateTimeFormats = {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
},
|
||||
medium: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
},
|
||||
time: {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
},
|
||||
date: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
},
|
||||
weekday: {
|
||||
weekday: 'long',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
export const dateTimeFormats: Record<string, Intl.DateTimeFormatOptions> = {
|
||||
short: { year: 'numeric', month: 'short', day: 'numeric' },
|
||||
medium: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
||||
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
date: { year: 'numeric', month: 'long', day: 'numeric' },
|
||||
weekday: { weekday: 'long' },
|
||||
month: { month: 'long' },
|
||||
year: { year: 'numeric' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 数字格式化选项
|
||||
*/
|
||||
export const numberFormats = {
|
||||
integer: {
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
decimal: {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
percent: {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
currency: {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
},
|
||||
export const numberFormats: Record<string, Intl.NumberFormatOptions> = {
|
||||
integer: { maximumFractionDigits: 0 },
|
||||
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
||||
percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 },
|
||||
currency: { style: 'currency', currency: 'USD' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 国际化工具 — 代理层,直接引用模块函数
|
||||
* 国际化工具 — 代理层
|
||||
*/
|
||||
export const i18nUtils = {
|
||||
export const i18nUtils: I18nUtils = {
|
||||
t,
|
||||
plural,
|
||||
getCurrentLocale,
|
||||
@@ -734,14 +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 };
|
||||
+109
-109
@@ -1,134 +1,134 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 0.1.2
|
||||
* @description 80+ 内置SVG图标
|
||||
* @version 0.2.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
|
||||
export const ICONS = {
|
||||
export const ICONS: Record<string, string> = {
|
||||
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
error: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
warning: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
info: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
loading: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" class="met-spin"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
close: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
check: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
x: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
alert: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
question: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
star: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
heart: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
bell: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
mail: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
settings: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
user: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
home: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
search: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
plus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
minus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
edit: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
trash: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
download: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
upload: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
share: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="5" r="3"/>
|
||||
<circle cx="6" cy="12" r="3"/>
|
||||
@@ -136,58 +136,58 @@ export const ICONS = {
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
link: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
external: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
clock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
calendar: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
map: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="18"/>
|
||||
<line x1="16" y1="6" x2="16" y2="22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
compass: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
globe: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
wifi: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
|
||||
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
cloud: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sun: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
@@ -199,19 +199,19 @@ export const ICONS = {
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
moon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
zap: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
activity: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
cpu: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
|
||||
<rect x="9" y="9" width="6" height="6"/>
|
||||
@@ -224,57 +224,57 @@ export const ICONS = {
|
||||
<line x1="1" y1="9" x2="4" y2="9"/>
|
||||
<line x1="1" y1="14" x2="4" y2="14"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
database: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
server: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
terminal: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5"/>
|
||||
<line x1="12" y1="19" x2="20" y2="19"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
code: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
<polyline points="8 6 2 12 8 18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
git: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="18" r="3"/>
|
||||
<circle cx="6" cy="6" r="3"/>
|
||||
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
|
||||
<line x1="6" y1="9" x2="6" y2="21"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
package: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
layers: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
||||
<polyline points="2 17 12 22 22 17"/>
|
||||
<polyline points="2 12 12 17 22 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
grid: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
list: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||
@@ -283,144 +283,144 @@ export const ICONS = {
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
filter: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sort: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<polyline points="19 12 12 19 5 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
refresh: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<polyline points="1 20 1 14 7 14"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sync: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<polyline points="1 20 1 14 7 14"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
power: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
|
||||
<line x1="12" y1="2" x2="12" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
battery: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="6" width="18" height="12" rx="2" ry="2"/>
|
||||
<line x1="23" y1="13" x2="23" y2="11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
bluetooth: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
volume: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
mic: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
camera: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
||||
<circle cx="12" cy="13" r="4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
image: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
video: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
music: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18V5l12-2v13"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
<circle cx="18" cy="16" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
file: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
|
||||
<polyline points="13 2 13 9 20 9"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
folder: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
clipboard: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
save: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||
<polyline points="7 3 7 8 15 8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
print: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 6 2 18 2 18 9"/>
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/>
|
||||
<rect x="6" y="14" width="12" height="8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
eye: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
eyeOff: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
lock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
unlock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
shield: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
key: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
flag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
|
||||
<line x1="4" y1="22" x2="4" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
bookmark: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
tag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
|
||||
<line x1="7" y1="7" x2="7.01" y2="7"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
gift: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 12 20 22 4 22 4 12"/>
|
||||
<rect x="2" y="7" width="20" height="5"/>
|
||||
@@ -428,18 +428,18 @@ export const ICONS = {
|
||||
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/>
|
||||
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
award: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="8" r="7"/>
|
||||
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
target: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<circle cx="12" cy="12" r="6"/>
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
crosshair: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="22" y1="12" x2="18" y2="12"/>
|
||||
@@ -447,7 +447,7 @@ export const ICONS = {
|
||||
<line x1="12" y1="6" x2="12" y2="2"/>
|
||||
<line x1="12" y1="22" x2="12" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
move: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="5 9 2 12 5 15"/>
|
||||
<polyline points="9 5 12 2 15 5"/>
|
||||
@@ -456,20 +456,20 @@ export const ICONS = {
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<line x1="12" y1="2" x2="12" y2="22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
maximize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
minimize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
copy: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
cut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="6" cy="6" r="3"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
@@ -477,40 +477,40 @@ export const ICONS = {
|
||||
<line x1="14.47" y1="14.48" x2="20" y2="20"/>
|
||||
<line x1="8.12" y1="8.12" x2="12" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
paste: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
rotateCw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
rotateCcw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"/>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
zoomIn: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
<line x1="11" y1="8" x2="11" y2="14"/>
|
||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
zoomOut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
crop: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"/>
|
||||
<path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sliders: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="4" y1="21" x2="4" y2="14"/>
|
||||
<line x1="4" y1="10" x2="4" y2="3"/>
|
||||
@@ -522,81 +522,81 @@ export const ICONS = {
|
||||
<line x1="9" y1="8" x2="15" y2="8"/>
|
||||
<line x1="17" y1="16" x2="23" y2="16"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
toggleLeft: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
|
||||
<circle cx="8" cy="12" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
toggleRight: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
|
||||
<circle cx="16" cy="12" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
checkCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
xCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
alertCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
infoCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
helpCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
alertTriangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
checkSquare: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 11 12 14 22 4"/>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
square: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
circle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
triangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
hexagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
octagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
pentagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 22 8.5 19 20 5 20 2 8.5 12 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
diamond: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4.5" y="4.5" width="15" height="15" rx="1" transform="rotate(45 12 12)"/>
|
||||
</svg>`,
|
||||
+130
-79
@@ -1,40 +1,41 @@
|
||||
/**
|
||||
* MetonaToast - 轻量级Toast通知库
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.1.2
|
||||
* @version 0.2.0
|
||||
* @author thzxx
|
||||
* @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { meToast, Toast } from './core.js';
|
||||
import { meToast } from './api.js';
|
||||
import { Toast } from './toast.js';
|
||||
import { animationUtils } from './animations.js';
|
||||
import { themeUtils } from './themes.js';
|
||||
import { i18nUtils } from './i18n.js';
|
||||
import { pluginUtils, presetPlugins } from './plugins.js';
|
||||
import { DEFAULTS } from './constants.js';
|
||||
import type { MeToast, ToastConfig, InitOptions, StatusInfo, ToastInstance, ToastOptions, Plugin } from './types.js';
|
||||
|
||||
// 版本信息
|
||||
const VERSION = '0.1.2';
|
||||
const VERSION = '0.2.0';
|
||||
|
||||
/**
|
||||
* 主对象增强
|
||||
*/
|
||||
const enhancedMeToast = {
|
||||
const enhancedMeToast: MeToast = {
|
||||
...meToast,
|
||||
|
||||
|
||||
version: VERSION,
|
||||
|
||||
|
||||
animations: animationUtils,
|
||||
themes: themeUtils,
|
||||
i18n: i18nUtils,
|
||||
plugins: pluginUtils,
|
||||
presetPlugins,
|
||||
|
||||
|
||||
/**
|
||||
* 安装插件,并连接必要的生命周期钩子
|
||||
* 安装插件
|
||||
*/
|
||||
use(plugin, options = {}) {
|
||||
use(plugin: string | Record<string, unknown>, options: Record<string, unknown> = {}): MeToast {
|
||||
if (typeof plugin === 'string') {
|
||||
const preset = presetPlugins[plugin];
|
||||
if (!preset) {
|
||||
@@ -45,58 +46,64 @@ const enhancedMeToast = {
|
||||
|
||||
// 连接插件钩子
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast) => preset.announce(toast));
|
||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
if (typeof preset.announce === 'function') preset.announce(toast);
|
||||
});
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
const saved = preset.install();
|
||||
if (saved) this.configure(saved);
|
||||
Toast.on('afterClose', () => { preset.save(this.getConfig()); });
|
||||
const saved = typeof preset.install === 'function' ? preset.install(pluginUtils as unknown as import('./types.js').PluginManager) : null;
|
||||
if (saved) this.configure(saved as Partial<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;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init(options = {}) {
|
||||
init(options: InitOptions = {}): MeToast {
|
||||
if (options.config) {
|
||||
this.configure(options.config);
|
||||
}
|
||||
|
||||
|
||||
if (options.theme) {
|
||||
this.themes.switchTheme(options.theme);
|
||||
}
|
||||
|
||||
|
||||
if (options.locale) {
|
||||
this.i18n.switchLocale(options.locale);
|
||||
}
|
||||
|
||||
|
||||
if (options.plugins && Array.isArray(options.plugins)) {
|
||||
options.plugins.forEach((plugin) => {
|
||||
this.use(plugin);
|
||||
options.plugins.forEach((p) => {
|
||||
this.use(p as string);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy() {
|
||||
destroy(): void {
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
this.dismiss();
|
||||
|
||||
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
||||
this.plugins.destroy();
|
||||
|
||||
if (this.plugins && typeof (this.plugins as unknown as Record<string, unknown>).destroy === 'function') {
|
||||
(this.plugins as unknown as Record<string, () => void>).destroy();
|
||||
}
|
||||
|
||||
|
||||
if (this.themes) {
|
||||
if (typeof this.themes.clearThemeListeners === 'function') {
|
||||
this.themes.clearThemeListeners();
|
||||
@@ -105,30 +112,30 @@ const enhancedMeToast = {
|
||||
this.themes.unwatchSystemTheme();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||
this.i18n.clearLocaleListeners();
|
||||
}
|
||||
|
||||
|
||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
||||
this.animations.cancelAll();
|
||||
}
|
||||
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach((c) => c.parentNode && c.parentNode.removeChild(c));
|
||||
|
||||
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||
|
||||
const style = document.getElementById('metona-toast-styles');
|
||||
if (style) style.parentNode.removeChild(style);
|
||||
if (style && style.parentNode) style.parentNode.removeChild(style);
|
||||
}
|
||||
|
||||
|
||||
this._toasts.clear();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*/
|
||||
getStatus() {
|
||||
getStatus(): StatusInfo {
|
||||
return {
|
||||
version: VERSION,
|
||||
toasts: this._toasts.size,
|
||||
@@ -138,18 +145,18 @@ const enhancedMeToast = {
|
||||
animations: this.animations?.getActiveCount?.() || 0,
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig() {
|
||||
getConfig(): ToastConfig {
|
||||
return { ...this._config };
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(config) {
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast {
|
||||
if (config && typeof config === 'object') {
|
||||
Object.assign(this._config, config);
|
||||
}
|
||||
@@ -159,96 +166,95 @@ const enhancedMeToast = {
|
||||
/**
|
||||
* 重置配置
|
||||
*/
|
||||
resetConfig() {
|
||||
// 原地重置:先清除所有自有属性,再回填默认值
|
||||
resetConfig(): MeToast {
|
||||
const keys = Object.keys(this._config);
|
||||
keys.forEach(k => delete this._config[k]);
|
||||
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
||||
Object.assign(this._config, DEFAULTS);
|
||||
return this;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 获取Toast列表
|
||||
*/
|
||||
getToasts() {
|
||||
getToasts(): ToastInstance[] {
|
||||
return Array.from(this._toasts.values());
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 检查是否有Toast
|
||||
*/
|
||||
hasToasts() {
|
||||
hasToasts(): boolean {
|
||||
return this._toasts.size > 0;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 获取Toast
|
||||
*/
|
||||
getToast(id) {
|
||||
getToast(id: string): ToastInstance | null {
|
||||
if (!id) return null;
|
||||
return this._toasts.get(id) || null;
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 关闭所有Toast
|
||||
*/
|
||||
closeAll() {
|
||||
closeAll(): void {
|
||||
this._toasts.forEach((t) => t.close());
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 清除所有Toast
|
||||
*/
|
||||
clearAll() {
|
||||
clearAll(): void {
|
||||
this._toasts.forEach((t) => t.close());
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 暂停所有Toast
|
||||
*/
|
||||
pauseAll() {
|
||||
pauseAll(): void {
|
||||
this._toasts.forEach((t) => t._pause());
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 恢复所有Toast
|
||||
*/
|
||||
resumeAll() {
|
||||
resumeAll(): void {
|
||||
this._toasts.forEach((t) => t._resume());
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 更新所有Toast
|
||||
*/
|
||||
updateAll(partial) {
|
||||
updateAll(partial: Partial<ToastOptions>): void {
|
||||
if (partial && typeof partial === 'object') {
|
||||
this._toasts.forEach((t) => t.update(partial));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 查找Toast
|
||||
*/
|
||||
findToasts(predicate) {
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
|
||||
if (typeof predicate !== 'function') return [];
|
||||
return Array.from(this._toasts.values()).filter(predicate);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 按类型查找Toast
|
||||
*/
|
||||
findByType(type) {
|
||||
findByType(type: string): ToastInstance[] {
|
||||
return this.findToasts((t) => t.type === type);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* 按位置查找Toast
|
||||
*/
|
||||
findByPosition(position) {
|
||||
findByPosition(position: string): ToastInstance[] {
|
||||
return this.findToasts((t) => t.config.position === position);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
// 初始化主题和国际化
|
||||
if (typeof themeUtils.initTheme === 'function') {
|
||||
themeUtils.initTheme();
|
||||
@@ -261,9 +267,15 @@ if (typeof i18nUtils.initI18n === 'function') {
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MeToast = enhancedMeToast;
|
||||
window.Met = enhancedMeToast;
|
||||
|
||||
|
||||
// Notification API — 页面不可见时自动发送系统通知
|
||||
enhancedMeToast._notify = (toast) => {
|
||||
const sendNotification = (toast: ToastInstance): void => {
|
||||
try {
|
||||
new Notification(toast.title || toast.type, { body: toast.message });
|
||||
} catch (_e) { /* noop */ }
|
||||
};
|
||||
|
||||
(enhancedMeToast as unknown as Record<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';
|
||||
@@ -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
@@ -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
@@ -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 };
|
||||
@@ -1,21 +1,19 @@
|
||||
/**
|
||||
* MetonaToast Styles - 样式管理(精简版 v0.1.2)
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @version 0.1.2
|
||||
* @description 样式注入与主题管理,移除未使用的组件样式
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
import type { ThemeConfig } from './types.js';
|
||||
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
let styleElement: HTMLStyleElement | null = null;
|
||||
|
||||
/**
|
||||
* 生成CSS样式
|
||||
*/
|
||||
const generateCSS = (theme) => {
|
||||
const generateCSS = (): string => {
|
||||
return `
|
||||
/* MetonaToast 基础样式 */
|
||||
.met-container {
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
@@ -28,7 +26,6 @@ const generateCSS = (theme) => {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 位置样式 */
|
||||
.met-container.top-left { top: 0; left: 0; align-items: flex-start; }
|
||||
.met-container.top-center { top: 0; left: 0; right: 0; align-items: center; }
|
||||
.met-container.top-right { top: 0; right: 0; align-items: flex-end; }
|
||||
@@ -36,7 +33,6 @@ const generateCSS = (theme) => {
|
||||
.met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; }
|
||||
.met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; }
|
||||
|
||||
/* Toast基础样式 */
|
||||
.met-toast {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -70,17 +66,14 @@ const generateCSS = (theme) => {
|
||||
perspective: 1000;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.met-toast:hover {
|
||||
box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important;
|
||||
}
|
||||
|
||||
/* 可点击状态 */
|
||||
.met-toast.met-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.met-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -91,7 +84,6 @@ const generateCSS = (theme) => {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* 内容样式 */
|
||||
.met-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -99,21 +91,18 @@ const generateCSS = (theme) => {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.met-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
/* 消息样式 */
|
||||
.met-message {
|
||||
font-size: 13px;
|
||||
opacity: 0.85;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 关闭按钮样式 */
|
||||
.met-close {
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
@@ -135,7 +124,6 @@ const generateCSS = (theme) => {
|
||||
background: var(--met-close-hover-bg, rgba(0,0,0,0.06));
|
||||
}
|
||||
|
||||
/* 进度条样式 - 水平 */
|
||||
.met-progress {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
@@ -145,7 +133,6 @@ const generateCSS = (theme) => {
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
/* 进度条样式 - 垂直 */
|
||||
.met-progress-v {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
@@ -155,7 +142,6 @@ const generateCSS = (theme) => {
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
/* 进度条指示器 - 水平 */
|
||||
.met-bar {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
@@ -164,7 +150,6 @@ const generateCSS = (theme) => {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* 进度条指示器 - 垂直 */
|
||||
.met-bar-v {
|
||||
position: absolute;
|
||||
left: 0; bottom: 0;
|
||||
@@ -173,7 +158,6 @@ const generateCSS = (theme) => {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
/* 侧边指示器 */
|
||||
.met-side {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
@@ -181,13 +165,11 @@ const generateCSS = (theme) => {
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.met-spin {
|
||||
animation: met-rot 1s linear infinite;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
/* 离开状态 */
|
||||
.met-toast.met-leaving {
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
@@ -197,7 +179,6 @@ const generateCSS = (theme) => {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* === 进入动画:初始隐藏态 === */
|
||||
.met-anim-slide.met-toast,
|
||||
.met-anim-fade.met-toast,
|
||||
.met-anim-scale.met-toast,
|
||||
@@ -212,7 +193,6 @@ const generateCSS = (theme) => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* === 进入动画:@keyframes 定义 === */
|
||||
@keyframes met-slide-in {
|
||||
0% { transform: translateX(80px); opacity: 0; }
|
||||
65% { transform: translateX(-6px); opacity: 1; }
|
||||
@@ -273,7 +253,6 @@ const generateCSS = (theme) => {
|
||||
100% { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* === 进入动画:绑定到 .met-show === */
|
||||
.met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
.met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; }
|
||||
.met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
||||
@@ -286,7 +265,6 @@ const generateCSS = (theme) => {
|
||||
.met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
.met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.met-container { padding: 12px !important; }
|
||||
.met-toast { width: 100% !important; min-width: 0 !important; }
|
||||
@@ -321,10 +299,6 @@ const generateCSS = (theme) => {
|
||||
.met-toast { width: 100%; min-width: 0; }
|
||||
}
|
||||
|
||||
/* 无障碍支持 */
|
||||
.met-toast[role="alert"] { }
|
||||
.met-toast[role="status"] { }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.met-toast { transition: opacity 0.15s !important; }
|
||||
.met-toast.met-show { animation: none !important; opacity: 1 !important; }
|
||||
@@ -341,7 +315,6 @@ const generateCSS = (theme) => {
|
||||
.met-close { border: 2px solid currentColor; border-radius: 4px; }
|
||||
}
|
||||
|
||||
/* 焦点样式 */
|
||||
.met-toast:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
@@ -351,32 +324,22 @@ const generateCSS = (theme) => {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 触摸设备优化 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.met-toast:hover { box-shadow: inherit; }
|
||||
.met-toast { min-height: 48px; }
|
||||
.met-close { min-width: 48px; min-height: 48px; }
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.met-toast { min-height: 52px; padding: 16px 18px; }
|
||||
.met-close { min-width: 52px; min-height: 52px; padding: 8px; }
|
||||
.met-icon { width: 28px; height: 28px; }
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.met-container { display: none !important; }
|
||||
.met-toast { display: none !important; }
|
||||
}
|
||||
|
||||
/* 全屏/画中画模式 */
|
||||
:fullscreen .met-container,
|
||||
:picture-in-picture .met-container {
|
||||
z-index: 2147483647;
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
@supports (padding: max(0px)) {
|
||||
.met-container {
|
||||
padding: max(24px, env(safe-area-inset-top))
|
||||
@@ -386,30 +349,25 @@ const generateCSS = (theme) => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 按下状态 */
|
||||
.met-toast:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 类型左侧边框 */
|
||||
.met-toast.met-success { border-left: 4px solid #10b981; }
|
||||
.met-toast.met-error { border-left: 4px solid #ef4444; }
|
||||
.met-toast.met-warning { border-left: 4px solid #f59e0b; }
|
||||
.met-toast.met-info { border-left: 4px solid #3b82f6; }
|
||||
.met-toast.met-loading { border-left: 4px solid #6366f1; }
|
||||
|
||||
/* 进度条过渡 */
|
||||
.met-bar, .met-bar-v {
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
||||
.met-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||
.met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
|
||||
|
||||
/* 暗色主题特定样式 */
|
||||
.met-toast.met-theme-dark {
|
||||
background: rgba(28, 32, 40, 0.94);
|
||||
color: #e6e8eb;
|
||||
@@ -427,7 +385,6 @@ const generateCSS = (theme) => {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* 亮色主题特定样式 */
|
||||
.met-toast.met-theme-light {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
color: #1f2937;
|
||||
@@ -445,7 +402,6 @@ const generateCSS = (theme) => {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 自定义主题支持 */
|
||||
.met-toast[data-theme] {
|
||||
--met-bg: var(--met-theme-bg);
|
||||
--met-text: var(--met-theme-text);
|
||||
@@ -453,7 +409,6 @@ const generateCSS = (theme) => {
|
||||
--met-shadow: var(--met-theme-shadow);
|
||||
}
|
||||
|
||||
/* 暗色模式自动检测 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.met-theme-auto .met-toast {
|
||||
background: rgba(28, 32, 40, 0.94);
|
||||
@@ -489,14 +444,14 @@ const generateCSS = (theme) => {
|
||||
/**
|
||||
* 注入样式
|
||||
*/
|
||||
export const injectStyles = (theme = THEMES.light) => {
|
||||
export const injectStyles = (): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
if (styleElement && document.getElementById('metona-toast-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const css = generateCSS(theme);
|
||||
const css = generateCSS();
|
||||
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = 'metona-toast-styles';
|
||||
@@ -508,31 +463,30 @@ export const injectStyles = (theme = THEMES.light) => {
|
||||
/**
|
||||
* 更新样式
|
||||
*/
|
||||
export const updateStyles = (theme) => {
|
||||
export const updateStyles = (): void => {
|
||||
if (!styleElement) {
|
||||
injectStyles(theme);
|
||||
injectStyles();
|
||||
return;
|
||||
}
|
||||
|
||||
const css = generateCSS(theme);
|
||||
const css = generateCSS();
|
||||
styleElement.textContent = css.replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除样式
|
||||
*/
|
||||
export const removeStyles = () => {
|
||||
export const removeStyles = (): void => {
|
||||
if (styleElement && styleElement.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
}
|
||||
|
||||
styleElement = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成主题CSS变量
|
||||
*/
|
||||
export const generateThemeVariables = (theme) => {
|
||||
export const generateThemeVariables = (theme: ThemeConfig): string => {
|
||||
return `
|
||||
:root {
|
||||
--met-theme-bg: ${theme.bg};
|
||||
@@ -549,7 +503,7 @@ export const generateThemeVariables = (theme) => {
|
||||
/**
|
||||
* 应用主题变量
|
||||
*/
|
||||
export const applyThemeVariables = (theme) => {
|
||||
export const applyThemeVariables = (theme: ThemeConfig): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const css = generateThemeVariables(theme);
|
||||
@@ -562,7 +516,7 @@ export const applyThemeVariables = (theme) => {
|
||||
/**
|
||||
* 清除主题变量
|
||||
*/
|
||||
export const clearThemeVariables = () => {
|
||||
export const clearThemeVariables = (): void => {
|
||||
const customStyle = document.getElementById('metona-toast-custom-styles');
|
||||
if (customStyle && customStyle.parentNode) {
|
||||
customStyle.parentNode.removeChild(customStyle);
|
||||
@@ -572,26 +526,26 @@ export const clearThemeVariables = () => {
|
||||
/**
|
||||
* 获取系统主题
|
||||
*/
|
||||
export const getSystemTheme = () => {
|
||||
export const getSystemTheme = (): string => {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
|
||||
return window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听系统主题变化
|
||||
*/
|
||||
export const watchSystemTheme = (callback) => {
|
||||
export const watchSystemTheme = (callback: (theme: string) => void): (() => void) => {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handler = (e) => {
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
callback(e.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
@@ -605,10 +559,11 @@ export const watchSystemTheme = (callback) => {
|
||||
/**
|
||||
* 自动应用系统主题
|
||||
*/
|
||||
export const autoApplySystemTheme = () => {
|
||||
const applyTheme = (theme) => {
|
||||
updateStyles(THEMES[theme]);
|
||||
applyThemeVariables(THEMES[theme]);
|
||||
export const autoApplySystemTheme = (): (() => void) => {
|
||||
const applyTheme = (theme: string): void => {
|
||||
updateStyles();
|
||||
// _THEMES reference is from constants, but here we just need to use theme string
|
||||
// applyThemeVariables is called externally
|
||||
};
|
||||
|
||||
applyTheme(getSystemTheme());
|
||||
@@ -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);
|
||||
};
|
||||
+71
-125
@@ -1,45 +1,39 @@
|
||||
/**
|
||||
* MetonaToast Themes - 主题管理
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 0.1.2
|
||||
* @description 主题系统、自定义主题和主题切换
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
import { prefersDark } from './utils.js';
|
||||
import type { ThemeConfig, ThemePreview, ThemeUtils } from './types.js';
|
||||
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
let themeListeners = new Set();
|
||||
let systemThemeMediaQuery = null;
|
||||
let systemThemeListener = null;
|
||||
const themeListeners: Set<(theme: string, resolved: string) => void> = new Set();
|
||||
let systemThemeMediaQuery: MediaQueryList | null = null;
|
||||
let systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null;
|
||||
|
||||
/**
|
||||
* 获取系统主题
|
||||
* @returns {string} 系统主题 ('light' 或 'dark')
|
||||
*/
|
||||
export const getSystemTheme = () => {
|
||||
export const getSystemTheme = (): 'light' | 'dark' => {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return prefersDark() ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题(别名)
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {string} 解析后的主题
|
||||
*/
|
||||
export const getTheme = (theme) => {
|
||||
export const getTheme = (theme: string): string => {
|
||||
return resolveTheme(theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析主题
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {string} 解析后的主题
|
||||
*/
|
||||
export const resolveTheme = (theme) => {
|
||||
export const resolveTheme = (theme: string): string => {
|
||||
if (theme === 'auto') {
|
||||
// 优先使用用户设置的全局主题,否则使用系统主题
|
||||
if (currentTheme && currentTheme !== 'auto') {
|
||||
return currentTheme;
|
||||
}
|
||||
@@ -50,44 +44,37 @@ export const resolveTheme = (theme) => {
|
||||
|
||||
/**
|
||||
* 获取主题配置
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {Object} 主题配置
|
||||
*/
|
||||
export const getThemeConfig = (theme) => {
|
||||
export const getThemeConfig = (theme: string): ThemeConfig => {
|
||||
const resolved = resolveTheme(theme);
|
||||
return THEMES[resolved] || THEMES.light;
|
||||
return (THEMES[resolved] as ThemeConfig) || (THEMES.light as ThemeConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
export const applyTheme = (theme) => {
|
||||
export const applyTheme = (theme: string): void => {
|
||||
currentTheme = theme;
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
// 应用主题到文档
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-met-theme', resolved);
|
||||
document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto');
|
||||
document.documentElement.classList.add(`met-theme-${resolved}`);
|
||||
|
||||
// 设置CSS变量
|
||||
|
||||
const config = getThemeConfig(theme);
|
||||
setThemeVariables(config);
|
||||
}
|
||||
|
||||
// 通知监听器
|
||||
|
||||
notifyThemeListeners(theme, resolved);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置主题CSS变量
|
||||
* @param {Object} config - 主题配置
|
||||
*/
|
||||
export const setThemeVariables = (config) => {
|
||||
export const setThemeVariables = (config: ThemeConfig): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--met-bg', config.bg);
|
||||
root.style.setProperty('--met-text', config.text);
|
||||
@@ -96,8 +83,7 @@ export const setThemeVariables = (config) => {
|
||||
root.style.setProperty('--met-hover-shadow', config.hoverShadow);
|
||||
root.style.setProperty('--met-progress-bg', config.progressBg);
|
||||
root.style.setProperty('--met-close-hover-bg', config.closeHoverBg);
|
||||
|
||||
// 设置颜色变量
|
||||
|
||||
root.style.setProperty('--met-success', '#10b981');
|
||||
root.style.setProperty('--met-error', '#ef4444');
|
||||
root.style.setProperty('--met-warning', '#f59e0b');
|
||||
@@ -108,51 +94,39 @@ export const setThemeVariables = (config) => {
|
||||
/**
|
||||
* 清除主题CSS变量
|
||||
*/
|
||||
export const clearThemeVariables = () => {
|
||||
export const clearThemeVariables = (): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const root = document.documentElement;
|
||||
const variables = [
|
||||
'--met-bg',
|
||||
'--met-text',
|
||||
'--met-border',
|
||||
'--met-shadow',
|
||||
'--met-hover-shadow',
|
||||
'--met-progress-bg',
|
||||
'--met-close-hover-bg',
|
||||
'--met-success',
|
||||
'--met-error',
|
||||
'--met-warning',
|
||||
'--met-info',
|
||||
'--met-loading',
|
||||
'--met-bg', '--met-text', '--met-border', '--met-shadow',
|
||||
'--met-hover-shadow', '--met-progress-bg', '--met-close-hover-bg',
|
||||
'--met-success', '--met-error', '--met-warning', '--met-info', '--met-loading',
|
||||
];
|
||||
|
||||
variables.forEach((variable) => {
|
||||
root.style.removeProperty(variable);
|
||||
|
||||
variables.forEach((v) => {
|
||||
root.style.removeProperty(v);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
* @returns {string} 当前主题
|
||||
*/
|
||||
export const getCurrentTheme = () => {
|
||||
export const getCurrentTheme = (): string => {
|
||||
return currentTheme;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取解析后的主题
|
||||
* @returns {string} 解析后的主题
|
||||
*/
|
||||
export const getResolvedTheme = () => {
|
||||
export const getResolvedTheme = (): string => {
|
||||
return resolveTheme(currentTheme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
* @param {string} theme - 新主题
|
||||
*/
|
||||
export const switchTheme = (theme) => {
|
||||
export const switchTheme = (theme: string): void => {
|
||||
applyTheme(theme);
|
||||
saveTheme(theme);
|
||||
};
|
||||
@@ -160,7 +134,7 @@ export const switchTheme = (theme) => {
|
||||
/**
|
||||
* 切换亮色/暗色主题
|
||||
*/
|
||||
export const toggleTheme = () => {
|
||||
export const toggleTheme = (): void => {
|
||||
const resolved = getResolvedTheme();
|
||||
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
||||
switchTheme(newTheme);
|
||||
@@ -169,15 +143,14 @@ export const toggleTheme = () => {
|
||||
/**
|
||||
* 重置为自动主题
|
||||
*/
|
||||
export const resetToAuto = () => {
|
||||
export const resetToAuto = (): void => {
|
||||
switchTheme('auto');
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存主题到本地存储
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
export const saveTheme = (theme) => {
|
||||
export const saveTheme = (theme: string): void => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('metona-toast-theme', theme);
|
||||
@@ -189,9 +162,8 @@ export const saveTheme = (theme) => {
|
||||
|
||||
/**
|
||||
* 从本地存储加载主题
|
||||
* @returns {string} 主题名称
|
||||
*/
|
||||
export const loadTheme = () => {
|
||||
export const loadTheme = (): string => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
return localStorage.getItem('metona-toast-theme') || 'auto';
|
||||
@@ -205,41 +177,36 @@ export const loadTheme = () => {
|
||||
/**
|
||||
* 初始化主题系统
|
||||
*/
|
||||
export const initTheme = () => {
|
||||
// 加载保存的主题
|
||||
export const initTheme = (): void => {
|
||||
const savedTheme = loadTheme();
|
||||
applyTheme(savedTheme);
|
||||
|
||||
// 监听系统主题变化
|
||||
watchSystemTheme();
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听系统主题变化
|
||||
*/
|
||||
export const watchSystemTheme = () => {
|
||||
export const watchSystemTheme = (): void => {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||||
|
||||
// 移除旧的监听器
|
||||
|
||||
if (systemThemeMediaQuery && systemThemeListener) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||
}
|
||||
|
||||
// 创建新的监听器
|
||||
|
||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
systemThemeListener = (e) => {
|
||||
systemThemeListener = () => {
|
||||
if (currentTheme === 'auto') {
|
||||
applyTheme('auto');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止监听系统主题变化
|
||||
*/
|
||||
export const unwatchSystemTheme = () => {
|
||||
export const unwatchSystemTheme = (): void => {
|
||||
if (systemThemeMediaQuery && systemThemeListener) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||
systemThemeMediaQuery = null;
|
||||
@@ -249,12 +216,9 @@ export const unwatchSystemTheme = () => {
|
||||
|
||||
/**
|
||||
* 添加主题监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
* @returns {Function} 移除监听器函数
|
||||
*/
|
||||
export const addThemeListener = (listener) => {
|
||||
export const addThemeListener = (listener: (theme: string, resolved: string) => void): () => void => {
|
||||
themeListeners.add(listener);
|
||||
|
||||
return () => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
@@ -262,18 +226,15 @@ export const addThemeListener = (listener) => {
|
||||
|
||||
/**
|
||||
* 移除主题监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
*/
|
||||
export const removeThemeListener = (listener) => {
|
||||
export const removeThemeListener = (listener: (theme: string, resolved: string) => void): void => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知主题监听器
|
||||
* @param {string} theme - 主题名称
|
||||
* @param {string} resolved - 解析后的主题
|
||||
*/
|
||||
const notifyThemeListeners = (theme, resolved) => {
|
||||
const notifyThemeListeners = (theme: string, resolved: string): void => {
|
||||
themeListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(theme, resolved);
|
||||
@@ -286,74 +247,64 @@ const notifyThemeListeners = (theme, resolved) => {
|
||||
/**
|
||||
* 清除所有主题监听器
|
||||
*/
|
||||
export const clearThemeListeners = () => {
|
||||
export const clearThemeListeners = (): void => {
|
||||
themeListeners.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册自定义主题
|
||||
* @param {string} name - 主题名称
|
||||
* @param {Object} config - 主题配置
|
||||
*/
|
||||
export const registerTheme = (name, config) => {
|
||||
THEMES[name] = {
|
||||
bg: config.bg || THEMES.light.bg,
|
||||
text: config.text || THEMES.light.text,
|
||||
border: config.border || THEMES.light.border,
|
||||
shadow: config.shadow || THEMES.light.shadow,
|
||||
hoverShadow: config.hoverShadow || THEMES.light.hoverShadow,
|
||||
progressBg: config.progressBg || THEMES.light.progressBg,
|
||||
closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg,
|
||||
export const registerTheme = (name: string, config: ThemeConfig): void => {
|
||||
(THEMES as Record<string, ThemeConfig>)[name] = {
|
||||
bg: config.bg || (THEMES.light as ThemeConfig).bg,
|
||||
text: config.text || (THEMES.light as ThemeConfig).text,
|
||||
border: config.border || (THEMES.light as ThemeConfig).border,
|
||||
shadow: config.shadow || (THEMES.light as ThemeConfig).shadow,
|
||||
hoverShadow: config.hoverShadow || (THEMES.light as ThemeConfig).hoverShadow,
|
||||
progressBg: config.progressBg || (THEMES.light as ThemeConfig).progressBg,
|
||||
closeHoverBg: config.closeHoverBg || (THEMES.light as ThemeConfig).closeHoverBg,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注销自定义主题
|
||||
* @param {string} name - 主题名称
|
||||
*/
|
||||
export const unregisterTheme = (name) => {
|
||||
export const unregisterTheme = (name: string): void => {
|
||||
if (name === 'light' || name === 'dark' || name === 'auto') {
|
||||
console.warn('Cannot unregister built-in theme:', name);
|
||||
return;
|
||||
}
|
||||
|
||||
delete THEMES[name];
|
||||
delete (THEMES as Record<string, ThemeConfig>)[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有主题
|
||||
* @returns {Object} 主题映射
|
||||
*/
|
||||
export const getAllThemes = () => {
|
||||
return { ...THEMES };
|
||||
export const getAllThemes = (): Record<string, ThemeConfig> => {
|
||||
return { ...THEMES } as Record<string, ThemeConfig>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题名称列表
|
||||
* @returns {Array} 主题名称数组
|
||||
*/
|
||||
export const getThemeNames = () => {
|
||||
export const getThemeNames = (): string[] => {
|
||||
return Object.keys(THEMES);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查主题是否存在
|
||||
* @param {string} name - 主题名称
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
export const hasTheme = (name) => {
|
||||
export const hasTheme = (name: string): boolean => {
|
||||
return name in THEMES;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题预览
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {Object} 主题预览
|
||||
*/
|
||||
export const getThemePreview = (theme) => {
|
||||
export const getThemePreview = (theme: string): ThemePreview => {
|
||||
const config = getThemeConfig(theme);
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
|
||||
return {
|
||||
name: theme,
|
||||
resolved,
|
||||
@@ -370,13 +321,11 @@ export const getThemePreview = (theme) => {
|
||||
|
||||
/**
|
||||
* 生成主题CSS
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {string} CSS字符串
|
||||
*/
|
||||
export const generateThemeCSS = (theme) => {
|
||||
export const generateThemeCSS = (theme: string): string => {
|
||||
const config = getThemeConfig(theme);
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
|
||||
return `
|
||||
.met-theme-${resolved} {
|
||||
--met-bg: ${config.bg};
|
||||
@@ -392,21 +341,18 @@ export const generateThemeCSS = (theme) => {
|
||||
|
||||
/**
|
||||
* 应用主题CSS
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
export const applyThemeCSS = (theme) => {
|
||||
export const applyThemeCSS = (theme: string): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const css = generateThemeCSS(theme);
|
||||
const styleId = 'metona-toast-theme-css';
|
||||
|
||||
// 移除旧样式
|
||||
|
||||
const oldStyle = document.getElementById(styleId);
|
||||
if (oldStyle) {
|
||||
oldStyle.remove();
|
||||
}
|
||||
|
||||
// 添加新样式
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = css;
|
||||
@@ -416,9 +362,9 @@ export const applyThemeCSS = (theme) => {
|
||||
/**
|
||||
* 移除主题CSS
|
||||
*/
|
||||
export const removeThemeCSS = () => {
|
||||
export const removeThemeCSS = (): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const styleId = 'metona-toast-theme-css';
|
||||
const style = document.getElementById(styleId);
|
||||
if (style) {
|
||||
@@ -427,9 +373,9 @@ export const removeThemeCSS = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题工具 — 代理层,直接引用模块函数
|
||||
* 主题工具 — 代理层
|
||||
*/
|
||||
export const themeUtils = {
|
||||
export const themeUtils: ThemeUtils = {
|
||||
getSystemTheme,
|
||||
resolveTheme,
|
||||
getThemeConfig,
|
||||
+551
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
/**
|
||||
* MetonaToast Utils - 精简工具函数
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 0.1.2
|
||||
* @description 仅保留核心模块实际使用的工具函数
|
||||
* @version 0.2.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {string} 唯一ID
|
||||
*/
|
||||
export const generateId = () => {
|
||||
export const generateId = (): string => {
|
||||
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
||||
};
|
||||
|
||||
/**
|
||||
* HTML转义
|
||||
* @param {string} s - 输入字符串
|
||||
* @returns {string} 转义后的字符串
|
||||
* @param s - 输入字符串
|
||||
*/
|
||||
export const escapeHTML = (s) => {
|
||||
export const escapeHTML = (s: string | null | undefined): string => {
|
||||
if (typeof document === 'undefined') {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&')
|
||||
@@ -35,10 +32,9 @@ export const escapeHTML = (s) => {
|
||||
|
||||
/**
|
||||
* 检测暗色模式偏好
|
||||
* @returns {boolean} 是否偏好暗色模式
|
||||
*/
|
||||
export const prefersDark = () => {
|
||||
export const prefersDark = (): boolean => {
|
||||
return typeof window !== 'undefined' &&
|
||||
window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
typeof window.matchMedia === 'function' &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
};
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"]
|
||||
}
|
||||
Vendored
-1046
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user