Compare commits
19
Commits
a9a0be8f21
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f8b9d2f75 | ||
|
|
2516a3a36b | ||
|
|
a4a757b15b | ||
|
|
95f5652e9c | ||
|
|
dd061025e0 | ||
|
|
1f718c4ef8 | ||
|
|
bbff631f4f | ||
|
|
2937ce5e2d | ||
|
|
cb8dd623db | ||
|
|
8014dedf93 | ||
|
|
bc1128bd1f | ||
|
|
cca2675fb4 | ||
|
|
32a3bdf86e | ||
|
|
9a6cd33d90 | ||
|
|
ee2b685f64 | ||
|
|
5c5627179a | ||
|
|
f7d74d152d | ||
|
|
eebd669b7f | ||
|
|
a6c6714b54 |
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"root": true,
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2020": true,
|
||||
"node": true,
|
||||
"jest": true
|
||||
},
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
"ecmaVersion": 2020,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": ["@typescript-eslint"],
|
||||
"extends": ["eslint:recommended"],
|
||||
"rules": {
|
||||
"no-console": "off",
|
||||
"no-unused-vars": "off",
|
||||
"@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_", "varsIgnorePattern": "^_", "caughtErrorsIgnorePattern": "^_" }],
|
||||
"@typescript-eslint/no-explicit-any": "warn",
|
||||
"prefer-const": "warn",
|
||||
"no-undef": "off",
|
||||
"no-empty": "off",
|
||||
"no-useless-escape": "off",
|
||||
"no-control-regex": "off",
|
||||
"no-regex-spaces": "off",
|
||||
"no-constant-condition": ["warn", { "checkLoops": false }]
|
||||
},
|
||||
"ignorePatterns": ["dist/", "coverage/", "node_modules/"]
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# 统一文本文件行尾为 LF(跨平台协作)
|
||||
* text=auto eol=lf
|
||||
|
||||
# 二进制文件不进行行尾转换
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
*.ico binary
|
||||
*.gif binary
|
||||
*.svg text
|
||||
@@ -0,0 +1,50 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: debian-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
node-version: [18.x, 20.x, 22.x, 24.x]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
- name: 'Run tests (coverage gate: lines >= 95%)'
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||
env:
|
||||
NODE_OPTIONS: --max-old-space-size=4096
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Bundle size
|
||||
run: |
|
||||
echo "=== Build Output ==="
|
||||
ls -lh dist/
|
||||
echo ""
|
||||
echo "=== Minified Gzip Size ==="
|
||||
gzip -c dist/metona-toast.min.js | wc -c | xargs echo "min.js gzip:"
|
||||
gzip -c dist/metona-toast.esm.js | wc -c | xargs echo "esm.js gzip:"
|
||||
@@ -0,0 +1,36 @@
|
||||
name: Publish
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ['v*']
|
||||
|
||||
jobs:
|
||||
publish:
|
||||
runs-on: debian-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
registry-url: https://git.metona.cn/api/packages/MetonaTeam/npm/
|
||||
cache: 'npm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Type check
|
||||
run: npm run typecheck
|
||||
|
||||
- name: Run tests
|
||||
run: npx jest --forceExit --maxWorkers=2 --no-cache --coverage
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
- name: Publish to Gitea npm registry
|
||||
run: npm publish --access public
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.PACKAGES_TOKEN }}
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
node_modules/
|
||||
dist/
|
||||
coverage/
|
||||
.DS_Store
|
||||
.zcode/
|
||||
|
||||
# 本地 npm 发布凭据(含 _auth,勿提交)
|
||||
.npmrc
|
||||
|
||||
+125
@@ -0,0 +1,125 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to MetonaToast will be documented in this file.
|
||||
|
||||
## [0.5.0] - 2026-08-08
|
||||
|
||||
### Changed
|
||||
- **覆盖率 96.42%(行)**:新增 59 个测试用例(共 385),覆盖 DOM 交互事件(click/drag/hover/animation)、confirm/prompt/action 按钮交互、Intl/localStorage 异常路径、React 渲染生命周期
|
||||
- **测试环境重构**:SSR 分支测试用 `Object.defineProperty` 真正覆盖 document/window(jsdom getter 使此前赋值无效);新增 `tests/dom.test.ts`(jsdom 真实 DOM 事件驱动)、`tests/setup.ts`(TextEncoder 补丁)
|
||||
- **jest 覆盖率门禁**:`lines >= 95%`,未达标测试即失败
|
||||
|
||||
### Fixed
|
||||
- **`Toast.off` 空数组残留**:移除全部钩子后删除 Map key(此前空数组残留导致 `_hooks.has()` 误报 true)
|
||||
- **`Toast.on` 返回的取消函数 this 绑定**:改用类名引用,任意调用上下文可用
|
||||
- **action 按钮 `close:false` 失效**:按钮 click 冒泡到 toast 的 closeOnClick 处理器导致总是关闭,已 stopPropagation 修复
|
||||
- **`use()` 重复注册钩子**:accessibility/persistence 预设的钩子注册改为防重复(先卸载旧钩子),并在插件 `uninstall` 中一并清理
|
||||
- **i18n catch 引用错误**:`catch {}` 中残留 `e` 引用导致的 ReferenceError(saveLocale/loadLocale/notifyLocaleListeners)
|
||||
- **删除残缺死代码**:`styles.autoApplySystemTheme`(无实际效果的占位实现)
|
||||
|
||||
### Removed
|
||||
- **lint 零警告**:`no-console` 规则关闭(库的错误报告依赖 console,生产构建已由 terser `drop_console` 剥离);catch 变量通过 `caughtErrorsIgnorePattern` 豁免
|
||||
|
||||
### Added
|
||||
- **Gitea Actions 发布自动化**:`publish.yml` — 推送 `v*` tag 时自动 测试+构建+发布到 Gitea npm registry(需配置 `PACKAGES_TOKEN` secret)
|
||||
- **CI 强化**:lint 改为必过(移除 continue-on-error);测试命令带覆盖率门禁
|
||||
- **CJS 产物兼容性修复**:包声明 `type: module` 下 `.js` 产物被 Node 按 ESM 解析导致 `require()` 失效,CJS 产物改 `.cjs` 扩展名并将 default 提升为 `module.exports`(`require()` 直接返回 MeToast,兼容 `.default`/`.MeToast`/`.Met`/`.Toast`/`.VERSION` 全部用法)
|
||||
|
||||
## [0.4.0] - 2026-08-08
|
||||
|
||||
### Added
|
||||
- **React 适配器子包**:`@metona-team/metona-toast/react` — `useToast()` hook(组件卸载时自动移除本组件创建的 Toast)+ 声明式 `<Toast />` 组件(props 变化更新、卸载自动清理)。主包保持零依赖,`react` 为 optional peerDependency
|
||||
- **`dedupe` 预设插件**:相同 type+message 的 Toast 自动去重(更新已有实例而非重复弹出),支持 uninstall
|
||||
- **`dragThreshold` 配置项**:拖拽关闭阈值可配置(默认 120px,原为硬编码)
|
||||
- **RTL 完整适配**:容器设置 `dir="rtl"`,进度条/side 条/类型色条/圆角正确镜像
|
||||
- **`update({ duration })` 支持**:动态变更显示时长并自动重启/停止计时器(此前 update 忽略 duration)
|
||||
- **幽灵实例修复**:`_emit` 检查 `create()` 结果,beforeShow 拦截后的 toast 不再注册到 `_toasts`(此前会残留幽灵记录,影响 count 与去重类插件)
|
||||
|
||||
### Changed
|
||||
- **loading 链式转换 id 稳定**:`loading.success/error/info/warning` 由"关闭旧 + 新建"改为原地 `update()` 同一实例,返回的 Toast id 与 loading id 一致;转换后 duration 自动恢复默认值(自动关闭)。注:转换不再产生新的入场动画,改为平滑更新;关闭按钮/进度条保持 loading 配置(如需默认外观,请通过 opts 覆盖)
|
||||
|
||||
## [0.3.0] - 2026-08-08
|
||||
|
||||
### Fixed
|
||||
- **自定义动画真实生效**:`animations.register()` 注册的动画此前仅写入内存 Map,实际渲染始终 fallback 到 slide。现在注册表驱动 `_buildClassName` 并动态注入对应 `@keyframes` + 类规则,README 的注册教程名实相符
|
||||
- **`max` 超限真正关闭最早的 toast**:此前超限时仅从内存 Map 删除记录,DOM 上的旧 toast 依然显示。现在通过实例注册表找到最早的 toast 并真正 `close()`(触发离场动画与生命周期回调)
|
||||
- **`destroy()` 全量清理**:新增清理 `Toast._hooks` 钩子、`Toast._registry` 实例注册表、`metona-toast-theme-css` / `metona-toast-custom-styles` / `metona-toast-anim-styles` 样式标签;动画注册表改为真正清空
|
||||
- **`applyThemeVariables` 重复注入**:修复多次调用追加相同 id 的 style 标签问题(改为先移除旧元素)
|
||||
- **`init()` 复活**:`destroy()` 后调用 `init()` 重置 `_destroyed` 标志,恢复可初始化状态
|
||||
- **失效脚本清理**:删除 `docs`(typedoc 未安装)、`example`(examples/ 目录不存在)脚本,新增 `serve` 脚本
|
||||
- **`AnimationConfig.enter/leave` 类型放宽**:支持 `opacity: 0` 等数值属性(此前仅 `string`,README 示例在 TS 严格模式下报错)
|
||||
|
||||
### Added
|
||||
- **钩子系统完整化**:此前 `HOOK_NAMES` 声明 20+ 钩子但多数永不触发。现在全部声明均有触发点:
|
||||
- 拦截语义:`beforeClose` / `beforeUpdate` 返回 `false` 可分别阻止关闭、更新
|
||||
- 新触发点:`click` / `hover` / `dragStart` / `dragEnd` / `animationStart` / `animationEnd` / `progressStart` / `progressEnd` / `configChange` / `themeChange` / `localeChange` / `beforeInit` / `afterInit` / `beforeDestroy` / `afterDestroy`
|
||||
- **`init({ plugins })` 支持插件对象**:与 `use()` 一致,可传 `string | Plugin`
|
||||
|
||||
### Changed
|
||||
- **VERSION 唯一来源**:版本号统一在 `src/constants.ts` 维护,`api.ts` / `index.ts` 引用,消除三处硬编码漂移
|
||||
- **`HOOK_NAMES` 收敛**:移除永不触发的 `progressUpdate` / `custom` 声明
|
||||
- 版本升级至 **v0.3.0**(含行为增强,按 semver minor 处理)
|
||||
|
||||
## [0.2.1] - 2026-07-25
|
||||
|
||||
### Fixed
|
||||
- **`auto` 主题配置死值**:`THEMES.auto` 改为合法 CSS 值(复用 light 默认值),避免 `getThemeConfig('auto')` 返回无意义字符串
|
||||
- **`resolveTheme` 循环逻辑**:`resolveTheme('auto')` 始终检测系统主题,不再依赖 `currentTheme` 全局状态
|
||||
- **`formatList` 国际化**:优先使用 `Intl.ListFormat`,降级到逗号拼接,支持各语言原生列表格式
|
||||
- **RTL 容器定位**:RTL 语言自动翻转 left/right 位置,容器创建时正确适配
|
||||
- **`_containerCache` 泄露**:`index.ts` 的 `destroy()` 中增加 `_containerCache.clear()`,与 `api.ts` 保持一致
|
||||
|
||||
### Changed
|
||||
- **代码去重**:消除 `index.ts` 与 `api.ts` 之间 11 个方法的重复定义,统一在 `api.ts` 实现
|
||||
- **Toast 回调注入**:`Toast._onError` / `Toast._removeToast` 改为 `Toast.setCallbacks()` 静态方法,消除隐式契约
|
||||
- **Stub 方法替换**:`api.ts` 中 7 个 stub 方法替换为完整实现(init/use/destroy/getStatus/getConfig/updateConfig/resetConfig)
|
||||
- **ESLint 规则**:新增 `@typescript-eslint/no-explicit-any` (warn) 和 `prefer-const` (warn)
|
||||
|
||||
### Added
|
||||
- **`beforeShow` 拦截**:钩子函数返回 `false` 可阻止 Toast 显示;配置 `onBeforeShow` 回调支持同样能力
|
||||
- **`updatePosition()` 方法**:允许运行时移动 Toast 到不同位置容器
|
||||
- **`remove()` + `removeToast()` 方法**:立即移除 Toast(不触发离场动画),提供无动画的快速清除 API
|
||||
|
||||
## [0.2.0] - 2026-07-25
|
||||
|
||||
### Changed
|
||||
- **TypeScript 源码重构**:全部源码从 JavaScript 迁移到 TypeScript
|
||||
- 删除手动维护的 `types/index.d.ts`,类型由源码自动生成
|
||||
- `core.js` (1244行) 拆分为 `toast.ts` + `api.ts` + `templates.ts`
|
||||
- 构建工具链:Babel → `ts-jest`,新增 `@rollup/plugin-typescript`
|
||||
- 测试迁移至 TypeScript (`tests/index.test.ts`)
|
||||
|
||||
### Added
|
||||
- `tsconfig.json` (strict: true)
|
||||
- `src/types.ts` 核心类型模块
|
||||
- `.eslintrc.json` ESLint 配置
|
||||
- `.gitea/workflows/ci.yml` CI 工作流
|
||||
- `CHANGELOG.md`
|
||||
|
||||
### Removed
|
||||
- `babel.config.js`
|
||||
- `types/` 目录
|
||||
|
||||
## [0.1.2] - 2026-07-24
|
||||
|
||||
### Fixed
|
||||
- 补全和修正 TypeScript 类型声明
|
||||
- 全面修复、优化与增强
|
||||
|
||||
## [0.1.1] - 2026-07-23
|
||||
|
||||
### Changed
|
||||
- 版本号重置,更新文档页面
|
||||
|
||||
## [0.1.0] - 2026-07-22
|
||||
|
||||
### Added
|
||||
- 初始发布
|
||||
- 107 种内置图标
|
||||
- 11 种动画效果
|
||||
- 4 种主题(light/dark/auto/warm)
|
||||
- 国际化支持 (zh-CN/en-US)
|
||||
- 插件系统 (keyboard/persistence/accessibility)
|
||||
- 拖拽关闭、进度条、倒计时等高级特性
|
||||
- TypeScript 类型声明
|
||||
- 零依赖
|
||||
@@ -5,17 +5,21 @@
|
||||
[](https://www.npmjs.com/package/@metona-team/metona-toast)
|
||||
[](https://bundlephobia.com/package/@metona-team/metona-toast)
|
||||
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
||||
[](https://git.metona.cn/MetonaTeam/MetonaToast)
|
||||
|
||||
## 特性
|
||||
|
||||
- **零依赖** — 纯原生 JavaScript,gzip 后不到 10KB
|
||||
- **80+ 图标类型** — success/error/warning/info/loading 及更多扩展类型
|
||||
- **11 种动画** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,每种效果明显不同
|
||||
- **主题系统** — light/dark/auto/warm 四种内置主题,支持 `registerTheme()` 注册自定义主题
|
||||
- **国际化** — 内置 zh-CN / en-US 完整翻译,可通过 `addTranslations()` 扩展
|
||||
- **插件系统** — 3 款预设插件 (keyboard/persistence/accessibility) + 自定义插件
|
||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭
|
||||
- **TypeScript** — 完整类型定义
|
||||
- **零依赖** — TypeScript 严格模式源码,gzip 后不到 10KB,无任何运行时依赖
|
||||
- **107 种图标类型** — success/error/warning/info/loading 及 100+ 扩展类型,覆盖所有常见场景
|
||||
- **11 种动画 + 自定义** — slide/fade/scale/bounce/flip/rotate/zoom/slideUp/slideDown/slideLeft/slideRight,`animations.register()` 可注册任意自定义动画(动态注入 keyframes,真实生效)
|
||||
- **主题系统** — light/dark/auto/warm 四种内置主题,`registerTheme()` 注册自定义主题,跟随系统主题自动切换
|
||||
- **国际化** — 内置 zh-CN / en-US 完整翻译,`addTranslations()` 扩展任意语言,RTL 语言完整适配
|
||||
- **插件系统** — 4 款预设插件 (keyboard / persistence / accessibility / dedupe) + 自定义插件
|
||||
- **完整钩子系统** — 17 个生命周期与事件钩子,`beforeShow` / `beforeClose` / `beforeUpdate` 支持返回 `false` 拦截
|
||||
- **可拖拽关闭** — 拖动 Toast 任意方向即可关闭,阈值 `dragThreshold` 可配置
|
||||
- **React 适配器** — `@metona-team/metona-toast/react` 子路径导入,`useToast()` hook + 声明式 `<Toast />` 组件,主包保持零依赖
|
||||
- **全局错误回调** — `onError` 捕获所有钩子异常和定时器错误,方便接入 Sentry、DataDog 等监控系统
|
||||
- **工程质量** — 385 个测试全过,行覆盖率 96.42%,lint 零警告,CI 含覆盖率门禁(≥95%)
|
||||
|
||||
## 安装
|
||||
|
||||
@@ -57,14 +61,14 @@ const MeToast = require('@metona-team/metona-toast');
|
||||
| Opera | 51+ |
|
||||
| iOS Safari | 11+ |
|
||||
| Android Chrome | 64+ |
|
||||
| Node.js | 18+(ESM & CJS 双格式) |
|
||||
| TypeScript | 4.5+(完整类型声明) |
|
||||
| Node.js | 16+(ESM & CJS 双格式) |
|
||||
| TypeScript | 4.5+(完整类型声明,类型从源码自动生成) |
|
||||
|
||||
核心依赖:Web Animations API(Chrome 64+, Firefox 63+, Safari 11+)、Pointer Events、CSS Grid、CSS Custom Properties。
|
||||
|
||||
## 快速开始
|
||||
|
||||
浏览器中 `window` 上会注册两个全局变量:`MeToast` 和 `Met`,两者完全等价,任选其一使用。
|
||||
浏览器 `window` 上注册两个全局变量:`MeToast` 和 `Met`,完全等价。
|
||||
|
||||
```javascript
|
||||
import MeToast from '@metona-team/metona-toast';
|
||||
@@ -81,11 +85,11 @@ MeToast.info('系统维护中');
|
||||
// 带标题
|
||||
MeToast.success({ title: '保存成功', message: '文件已同步到云端' });
|
||||
|
||||
// 加载 → 成功链式转换
|
||||
// 加载 → 链式转换(原地更新同一实例,id 稳定)
|
||||
const loading = MeToast.loading('正在提交...');
|
||||
setTimeout(() => loading.success('提交成功!'), 2000);
|
||||
|
||||
// Promise 风格
|
||||
// Promise 风格 — 自动 loading → success/error
|
||||
await MeToast.promise(fetch('/api/data'), {
|
||||
loading: '加载中...',
|
||||
success: '加载完成!',
|
||||
@@ -99,53 +103,65 @@ await MeToast.promise(fetch('/api/data'), {
|
||||
MeToast.configure({
|
||||
position: 'top-right', // top-left | top-center | top-right | bottom-left | bottom-center | bottom-right
|
||||
duration: 4000, // 显示时长(ms),0 为不自动关闭
|
||||
max: 6, // 同时最多显示条数
|
||||
theme: 'auto', // light | dark | auto | warm
|
||||
animation: 'slide', // slide | fade | scale | bounce | flip | rotate | zoom | slideUp | slideDown | slideLeft | slideRight
|
||||
max: 6, // 同一位置最多显示条数,超出关闭最早的
|
||||
gap: 12, // Toast 间距(px)
|
||||
offset: 24, // 容器距屏幕边缘(px)
|
||||
theme: 'auto', // light | dark | auto(跟随系统)| warm
|
||||
animation: 'slide', // 11 种内置动画或自定义动画名
|
||||
pauseOnHover: true, // 悬停暂停计时
|
||||
closeOnClick: true, // 点击关闭
|
||||
showProgress: true, // 显示进度条
|
||||
draggable: true, // 允许拖拽关闭
|
||||
dragThreshold: 120, // 拖拽关闭阈值(px)
|
||||
showProgress: true, // 显示进度条
|
||||
progressDirection: 'horizontal', // horizontal | vertical
|
||||
zIndex: 9999,
|
||||
width: 360,
|
||||
locale: 'zh-CN', // 语言
|
||||
onError: ({ hook, source, error, toast }) => {
|
||||
// 钩子/定时器异常全局捕获,可接入监控系统
|
||||
console.error('Toast error:', hook, error);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 动画效果
|
||||
|
||||
11 种动画,每种感官差异明显:
|
||||
11 种内置动画,每种感官差异明显:
|
||||
|
||||
| 动画 | 效果描述 |
|
||||
|------|----------|
|
||||
| `slide` | 从右侧滑入 + 轻微过冲回弹 |
|
||||
| `fade` | 纯淡入(blur→清晰),从容优雅 |
|
||||
| `scale` | 弹性放大(0.55→1.07→1.0) |
|
||||
| `bounce` | 从天而降四段弹跳 |
|
||||
| `flip` | 3D 翻转入场 + 回摆 |
|
||||
| `rotate` | 旋转摇摆进入 |
|
||||
| `zoom` | 从中心爆发式弹出 |
|
||||
| `slideUp` | 从下方弹入 |
|
||||
| `slideDown` | 从上方弹入 |
|
||||
| `slideLeft` | 从左侧滑入 |
|
||||
| `slideRight` | 从右侧滑入 |
|
||||
| 动画 | 效果描述 | 时长 |
|
||||
|------|----------|------|
|
||||
| `slide` | 从右侧滑入 + 轻微过冲回弹 | 400ms |
|
||||
| `fade` | 纯淡入(blur→清晰),从容优雅 | 500ms |
|
||||
| `scale` | 弹性放大(0.55→1.07→1.0) | 450ms |
|
||||
| `bounce` | 从天而降四段弹跳 | 650ms |
|
||||
| `flip` | 3D 翻转入场 + 回摆 | 500ms |
|
||||
| `rotate` | 旋转摇摆进入 | 500ms |
|
||||
| `zoom` | 从中心爆发式弹出 | 500ms |
|
||||
| `slideUp` | 从下方弹入 | 400ms |
|
||||
| `slideDown` | 从上方弹入 | 400ms |
|
||||
| `slideLeft` | 从左侧滑入 | 400ms |
|
||||
| `slideRight` | 从右侧滑入 | 400ms |
|
||||
|
||||
```javascript
|
||||
MeToast.success('弹跳动画', { animation: 'bounce' });
|
||||
|
||||
// 注册自定义动画
|
||||
// 注册自定义动画 — 动态注入 keyframes,真实生效
|
||||
MeToast.animations.register('myAnim', {
|
||||
enter: { transform: 'rotate(-30deg) scale(0.5)', opacity: 0 },
|
||||
leave: { transform: 'rotate(0) scale(1)', opacity: 1 },
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
});
|
||||
MeToast.success('自定义动画', { animation: 'myAnim' });
|
||||
```
|
||||
|
||||
## 主题
|
||||
|
||||
```javascript
|
||||
// 切换主题
|
||||
// 切换主题(自动持久化到 localStorage)
|
||||
MeToast.themes.switchTheme('dark');
|
||||
MeToast.themes.switchTheme('warm');
|
||||
MeToast.themes.toggleTheme(); // light ↔ dark
|
||||
|
||||
// 注册自定义主题
|
||||
MeToast.themes.registerTheme('ocean', {
|
||||
@@ -184,6 +200,18 @@ await MeToast.queue(['步骤一', '步骤二', '步骤三'], { delay: 800 });
|
||||
|
||||
// 堆叠展示(同时错峰)
|
||||
MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
||||
|
||||
// Action Toast — 内嵌操作按钮(close:false 可多次点击不关闭)
|
||||
MeToast.action('文件已删除', [
|
||||
{ text: '撤销', onClick: () => restore(), color: '#3b82f6' },
|
||||
{ text: '查看', onClick: () => open(), color: '#10b981', close: false },
|
||||
]);
|
||||
|
||||
// 分组管理
|
||||
const orders = MeToast.group('orders');
|
||||
orders.success('订单已创建');
|
||||
orders.count(); // 该组 toast 数量
|
||||
orders.dismiss(); // 一键关闭整组
|
||||
```
|
||||
|
||||
## 插件
|
||||
@@ -193,8 +221,9 @@ MeToast.stack(['消息1', '消息2', '消息3'], { stagger: 150 });
|
||||
MeToast.use('keyboard'); // ESC 关闭所有 Toast
|
||||
MeToast.use('persistence'); // 配置持久化到 localStorage
|
||||
MeToast.use('accessibility'); // 屏幕阅读器朗读公告
|
||||
MeToast.use('dedupe'); // 相同 type+message 自动去重(更新已有实例)
|
||||
|
||||
// 自定义插件(通过 install 中注册 Toast 生命周期钩子)
|
||||
// 自定义插件(install 中注册生命周期钩子)
|
||||
import MeToast, { Toast } from '@metona-team/metona-toast';
|
||||
|
||||
MeToast.use({
|
||||
@@ -204,6 +233,36 @@ MeToast.use({
|
||||
console.log('Toast shown:', toast.message);
|
||||
});
|
||||
},
|
||||
uninstall() {
|
||||
console.log('plugin removed');
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 钩子系统
|
||||
|
||||
`Toast.on(name, handler)` 注册钩子,返回取消函数。`beforeShow` / `beforeClose` / `beforeUpdate` 的 handler 返回 `false` 可拦截对应操作。
|
||||
|
||||
| 钩子 | 触发时机 | 拦截 |
|
||||
|------|----------|------|
|
||||
| `beforeInit` / `afterInit` | `init()` 前后 | — |
|
||||
| `beforeDestroy` / `afterDestroy` | `destroy()` 前后 | — |
|
||||
| `beforeShow` / `afterShow` | Toast 显示前后 | ✅ 返回 false 阻止显示 |
|
||||
| `beforeClose` / `afterClose` | Toast 关闭前后 | ✅ 返回 false 阻止关闭 |
|
||||
| `beforeUpdate` / `afterUpdate` | `update()` 前后 | ✅ 返回 false 阻止更新 |
|
||||
| `configChange` | `configure`/`updateConfig`/`resetConfig` | — |
|
||||
| `themeChange` | 主题切换 | — |
|
||||
| `localeChange` | 语言切换 | — |
|
||||
| `click` | 点击 Toast 主体 | — |
|
||||
| `hover` | 鼠标悬停进出 | — |
|
||||
| `dragStart` / `dragEnd` | 拖拽开始/结束 | — |
|
||||
| `animationStart` / `animationEnd` | 入场动画开始/结束 | — |
|
||||
| `progressStart` / `progressEnd` | 倒计时开始/归零 | — |
|
||||
|
||||
```javascript
|
||||
// 拦截示例:指定类型禁止显示
|
||||
Toast.on('beforeShow', (toast) => {
|
||||
if (toast.type === 'error' && !isAllowed) return false;
|
||||
});
|
||||
```
|
||||
|
||||
@@ -212,7 +271,7 @@ MeToast.use({
|
||||
```javascript
|
||||
MeToast.i18n.switchLocale('en-US');
|
||||
|
||||
// 添加语言
|
||||
// 添加语言(嵌套结构支持深度合并)
|
||||
MeToast.i18n.addTranslations('ja', {
|
||||
success: '成功',
|
||||
error: 'エラー',
|
||||
@@ -220,21 +279,30 @@ MeToast.i18n.addTranslations('ja', {
|
||||
cancel: 'キャンセル',
|
||||
});
|
||||
|
||||
// 格式化工具
|
||||
// 格式化工具(基于 Intl)
|
||||
MeToast.i18n.formatNumber(1234567); // "1,234,567"
|
||||
MeToast.i18n.formatCurrency(99, 'CNY'); // "¥99.00"
|
||||
MeToast.i18n.formatDate(new Date());
|
||||
MeToast.i18n.formatRelativeTime(Date.now() + 3600000); // "1 小时后"
|
||||
```
|
||||
|
||||
## Toast 管理
|
||||
|
||||
```javascript
|
||||
MeToast.count(); // 当前数量
|
||||
MeToast.getToasts(); // 获取所有实例
|
||||
MeToast.getAll(); // 获取原生 Map<id, ToastInstance>
|
||||
MeToast.getToasts(); // 获取所有实例数组
|
||||
MeToast.find(id); // 查找实例(不存在返回 undefined)
|
||||
MeToast.findByType(type); // 按类型过滤
|
||||
MeToast.findByPosition(pos);// 按位置过滤
|
||||
MeToast.dismiss(); // 关闭所有
|
||||
MeToast.dismiss(id); // 关闭指定
|
||||
MeToast.removeToast(id); // 立即移除(无离场动画)
|
||||
MeToast.clear(position?); // 按位置清除
|
||||
MeToast.pauseAll(); // 暂停所有计时
|
||||
MeToast.resumeAll(); // 恢复所有计时
|
||||
MeToast.destroy(); // 完全销毁
|
||||
MeToast.updateAll(partial); // 批量更新
|
||||
MeToast.destroy(); // 完全销毁(支持重复调用,init() 可恢复)
|
||||
```
|
||||
|
||||
## 回调
|
||||
@@ -242,12 +310,63 @@ MeToast.destroy(); // 完全销毁
|
||||
```javascript
|
||||
MeToast.success({
|
||||
message: '操作成功',
|
||||
onBeforeShow: (toast) => true, // 返回 false 阻止显示
|
||||
onShow: (toast) => console.log('显示:', toast.id),
|
||||
onClose: (toast) => console.log('关闭:', toast.id),
|
||||
onClick: (toast) => console.log('点击:', toast.id),
|
||||
onUpdate: (toast) => console.log('更新:', toast.id),
|
||||
});
|
||||
|
||||
// 全局错误回调 — 捕获钩子异常和定时器错误
|
||||
MeToast.configure({
|
||||
onError: ({ hook, source, error, toast }) => {
|
||||
reportError(error, { hook, source, toastId: toast?.id });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## React 适配器
|
||||
|
||||
主包保持零依赖;React 适配器通过子路径导入,`react` 为 optional peerDependency,未使用 React 的项目不受影响。
|
||||
|
||||
```bash
|
||||
npm install @metona-team/metona-toast react
|
||||
```
|
||||
|
||||
```tsx
|
||||
import { useToast, Toast } from '@metona-team/metona-toast/react';
|
||||
|
||||
function SubmitButton() {
|
||||
// 本组件创建的 Toast 会在组件卸载时自动移除
|
||||
const toast = useToast();
|
||||
|
||||
const submit = async () => {
|
||||
const loading = toast.loading('正在提交...');
|
||||
try {
|
||||
await api.submit();
|
||||
loading.success('提交成功!');
|
||||
} catch (e) {
|
||||
loading.error('提交失败');
|
||||
}
|
||||
};
|
||||
|
||||
return <button onClick={submit}>提交</button>;
|
||||
}
|
||||
|
||||
// 声明式 Toast — props 变化时更新内容,组件卸载时自动移除(autoClose 默认 true)
|
||||
function SaveIndicator({ saving }: { saving: boolean }) {
|
||||
return saving ? <Toast type="info" message="正在保存..." /> : null;
|
||||
}
|
||||
```
|
||||
|
||||
## 工程质量
|
||||
|
||||
- **385 个测试** 全部通过(单元 + DOM 交互 + React 渲染 + SSR 环境隔离)
|
||||
- **行覆盖率 96.42%**,jest 覆盖率门禁 `lines >= 95%`,未达标构建即失败
|
||||
- **lint 零警告**(TypeScript ESLint 严格检查)
|
||||
- **CI**(Gitea Actions):Node 18/20/22/24 矩阵 — typecheck → lint → 测试+覆盖率门禁 → 构建 → bundle size 检查
|
||||
- **自动发布**:推送 `v*` tag 触发测试、构建并发布到 Gitea npm registry
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', { targets: { node: 'current' }, modules: 'commonjs' }],
|
||||
'@babel/preset-typescript',
|
||||
],
|
||||
plugins: [
|
||||
'@babel/plugin-transform-modules-commonjs',
|
||||
],
|
||||
};
|
||||
@@ -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 ""
|
||||
|
||||
Vendored
+4536
File diff suppressed because it is too large
Load Diff
Vendored
-6712
File diff suppressed because it is too large
Load Diff
Vendored
-1
File diff suppressed because one or more lines are too long
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+517
-1036
File diff suppressed because it is too large
Load Diff
Vendored
+3295
-5472
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+3785
-5962
File diff suppressed because it is too large
Load Diff
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+1
-1
File diff suppressed because one or more lines are too long
Vendored
+4617
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
Vendored
+509
@@ -0,0 +1,509 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.5.0
|
||||
*/
|
||||
/** Toast 通知类型(107种内置图标类型) */
|
||||
type ToastType = 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading' | 'check' | 'x' | 'alert' | 'question' | 'star' | 'heart' | 'bell' | 'mail' | 'settings' | 'user' | 'home' | 'search' | 'plus' | 'minus' | 'edit' | 'trash' | 'download' | 'upload' | 'share' | 'link' | 'external' | 'clock' | 'calendar' | 'map' | 'compass' | 'globe' | 'wifi' | 'cloud' | 'sun' | 'moon' | 'zap' | 'activity' | 'cpu' | 'database' | 'server' | 'terminal' | 'code' | 'git' | 'package' | 'layers' | 'grid' | 'list' | 'filter' | 'sort' | 'refresh' | 'sync' | 'power' | 'battery' | 'bluetooth' | 'volume' | 'mic' | 'camera' | 'image' | 'video' | 'music' | 'file' | 'folder' | 'clipboard' | 'save' | 'print' | 'eye' | 'eyeOff' | 'lock' | 'unlock' | 'shield' | 'key' | 'flag' | 'bookmark' | 'tag' | 'gift' | 'award' | 'target' | 'crosshair' | 'move' | 'maximize' | 'minimize' | 'copy' | 'cut' | 'paste' | 'rotateCw' | 'rotateCcw' | 'zoomIn' | 'zoomOut' | 'crop' | 'sliders' | 'toggleLeft' | 'toggleRight' | 'checkCircle' | 'xCircle' | 'alertCircle' | 'infoCircle' | 'helpCircle' | 'alertTriangle' | 'checkSquare' | 'square' | 'circle' | 'triangle' | 'hexagon' | 'octagon' | 'pentagon' | 'diamond';
|
||||
/** Toast 显示位置 */
|
||||
type ToastPosition = 'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
/** Toast 主题 */
|
||||
type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
|
||||
/** Toast 动画类型 */
|
||||
type ToastAnimation = 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | (string & {});
|
||||
/** 进度条方向 */
|
||||
type ToastProgressDirection = 'horizontal' | 'vertical';
|
||||
interface TypeColor {
|
||||
fg: string;
|
||||
bg: string;
|
||||
}
|
||||
interface ToastConfig {
|
||||
position?: ToastPosition;
|
||||
duration?: number;
|
||||
max?: number;
|
||||
gap?: number;
|
||||
offset?: number;
|
||||
pauseOnHover?: boolean;
|
||||
closeOnClick?: boolean;
|
||||
draggable?: boolean;
|
||||
dragThreshold?: number;
|
||||
showProgress?: boolean;
|
||||
progressDirection?: ToastProgressDirection;
|
||||
icon?: boolean;
|
||||
closeButton?: boolean;
|
||||
theme?: ToastTheme;
|
||||
animation?: ToastAnimation;
|
||||
zIndex?: number;
|
||||
width?: number | string;
|
||||
className?: string;
|
||||
style?: Record<string, string>;
|
||||
onShow?: ((toast: ToastInstance) => void) | null;
|
||||
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
|
||||
onClose?: ((toast: ToastInstance) => void) | null;
|
||||
onClick?: ((toast: ToastInstance) => void) | null;
|
||||
onUpdate?: ((toast: ToastInstance) => void) | null;
|
||||
render?: ((toast: ToastInstance) => string) | null;
|
||||
resetTimerOnUpdate?: boolean;
|
||||
notifyWhenHidden?: boolean;
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
group?: string | null;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
interface ErrorInfo {
|
||||
hook?: string;
|
||||
source?: string;
|
||||
error: Error;
|
||||
toast?: ToastInstance;
|
||||
}
|
||||
interface ToastOptions extends Partial<ToastConfig> {
|
||||
type?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
content?: string;
|
||||
html?: string;
|
||||
iconHTML?: string;
|
||||
id?: string;
|
||||
}
|
||||
interface ToastInstance {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
html: string;
|
||||
iconHTML: string;
|
||||
config: ToastConfig;
|
||||
el: HTMLElement | null;
|
||||
barEl: HTMLElement | null;
|
||||
rafId: number | null;
|
||||
remaining: number;
|
||||
startedAt: number;
|
||||
paused: boolean;
|
||||
closing: boolean;
|
||||
group: string | null;
|
||||
_cleanups: Array<() => void>;
|
||||
_palette(): {
|
||||
theme: string;
|
||||
c: TypeColor;
|
||||
t: Partial<ThemeConfig>;
|
||||
};
|
||||
create(): this;
|
||||
update(partial: Partial<ToastOptions>): this;
|
||||
updatePosition(position: string): this;
|
||||
remove(): void;
|
||||
close(immediate?: boolean): void;
|
||||
_pause(): void;
|
||||
_resume(): void;
|
||||
_destroy(): void;
|
||||
}
|
||||
interface LoadingControl {
|
||||
id: string;
|
||||
success(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
error(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
info(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
update(partial: Partial<ToastOptions>): unknown;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface ProgressControl {
|
||||
id: string;
|
||||
setProgress(percent: number): void;
|
||||
complete(message?: string): void;
|
||||
error(message?: string): void;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface CountdownControl {
|
||||
id: string;
|
||||
cancel(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
}
|
||||
interface ActionControl {
|
||||
id: string;
|
||||
toast: ToastInstance;
|
||||
dismiss(): void;
|
||||
}
|
||||
interface QueueControl {
|
||||
then<TResult1 = void, TResult2 = never>(onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>;
|
||||
catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<void | TResult>;
|
||||
cancel(): void;
|
||||
}
|
||||
interface GroupAPI {
|
||||
_group: string;
|
||||
show: (...args: unknown[]) => ToastInstance;
|
||||
success: (...args: unknown[]) => ToastInstance;
|
||||
error: (...args: unknown[]) => ToastInstance;
|
||||
warning: (...args: unknown[]) => ToastInstance;
|
||||
info: (...args: unknown[]) => ToastInstance;
|
||||
loading: (...args: unknown[]) => LoadingControl;
|
||||
action: (...args: unknown[]) => ActionControl;
|
||||
dismiss(): void;
|
||||
count(): number;
|
||||
}
|
||||
interface ActionButton {
|
||||
text: string;
|
||||
onClick: (toast: ToastInstance) => void;
|
||||
color?: string;
|
||||
style?: Record<string, string>;
|
||||
close?: boolean;
|
||||
}
|
||||
interface PromiseOptions extends ToastOptions {
|
||||
loading?: string;
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
interface ConfirmOptions extends ToastOptions {
|
||||
confirmText?: string;
|
||||
confirmColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
interface PromptOptions extends ToastOptions {
|
||||
inputType?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
submitText?: string;
|
||||
submitColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
interface ProgressOptions extends ToastOptions {
|
||||
progressColor?: string;
|
||||
}
|
||||
interface CountdownOptions extends ToastOptions {
|
||||
onComplete?: (() => void) | null;
|
||||
}
|
||||
interface QueueOptions extends ToastOptions {
|
||||
delay?: number;
|
||||
}
|
||||
interface StackOptions extends ToastOptions {
|
||||
stagger?: number;
|
||||
}
|
||||
interface InitOptions {
|
||||
config?: Partial<ToastConfig>;
|
||||
theme?: ToastTheme;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
interface StatusInfo {
|
||||
version: string;
|
||||
toasts: number;
|
||||
theme: string;
|
||||
locale: string;
|
||||
plugins: string[];
|
||||
animations: number;
|
||||
}
|
||||
interface AnimationConfig {
|
||||
enter: Record<string, string | number>;
|
||||
leave: Record<string, string | number>;
|
||||
duration: number;
|
||||
easing: string;
|
||||
name?: string;
|
||||
delay?: number;
|
||||
iterations?: number;
|
||||
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
||||
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
|
||||
}
|
||||
interface AnimationUtils {
|
||||
register(name: string, config: Partial<AnimationConfig>): void;
|
||||
unregister(name: string): void;
|
||||
get(name: string): AnimationConfig | null;
|
||||
getAnimationNames(): string[];
|
||||
getActiveCount(): number;
|
||||
cancelAll(): void;
|
||||
reset(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
interface ThemeConfig {
|
||||
bg: string;
|
||||
text: string;
|
||||
border: string;
|
||||
shadow: string;
|
||||
hoverShadow: string;
|
||||
progressBg: string;
|
||||
closeHoverBg: string;
|
||||
backdropFilter?: string;
|
||||
}
|
||||
interface ThemePreview {
|
||||
name: string;
|
||||
resolved: string;
|
||||
colors: {
|
||||
background: string;
|
||||
text: string;
|
||||
border: string;
|
||||
};
|
||||
isDark: boolean;
|
||||
isLight: boolean;
|
||||
isAuto: boolean;
|
||||
}
|
||||
interface ThemeUtils {
|
||||
getSystemTheme(): 'light' | 'dark';
|
||||
resolveTheme(theme: string): string;
|
||||
getThemeConfig(theme: string): ThemeConfig;
|
||||
applyTheme(theme: string): void;
|
||||
getCurrentTheme(): string;
|
||||
getResolvedTheme(): string;
|
||||
switchTheme(theme: string): void;
|
||||
toggleTheme(): void;
|
||||
resetToAuto(): void;
|
||||
initTheme(): void;
|
||||
watchSystemTheme(): void;
|
||||
unwatchSystemTheme(): void;
|
||||
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
|
||||
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
|
||||
clearThemeListeners(): void;
|
||||
registerTheme(name: string, config: ThemeConfig): void;
|
||||
unregisterTheme(name: string): void;
|
||||
getAllThemes(): Record<string, ThemeConfig>;
|
||||
getThemeNames(): string[];
|
||||
hasTheme(name: string): boolean;
|
||||
getThemePreview(theme: string): ThemePreview;
|
||||
generateThemeCSS(theme: string): string;
|
||||
applyThemeCSS(theme: string): void;
|
||||
removeThemeCSS(): void;
|
||||
saveTheme(theme: string): void;
|
||||
loadTheme(): string;
|
||||
}
|
||||
interface LocaleInfo {
|
||||
code: string;
|
||||
name: string;
|
||||
direction: 'ltr' | 'rtl';
|
||||
isSupported: boolean;
|
||||
isCurrent: boolean;
|
||||
isFallback: boolean;
|
||||
}
|
||||
interface I18nUtils {
|
||||
t(key: string, params?: Record<string, string | number>): string;
|
||||
plural(key: string, count: number, params?: Record<string, string | number>): string;
|
||||
getCurrentLocale(): string;
|
||||
setCurrentLocale(locale: string): void;
|
||||
switchLocale(locale: string): void;
|
||||
getFallbackLocale(): string;
|
||||
setFallbackLocale(locale: string): void;
|
||||
hasTranslation(key: string): boolean;
|
||||
getTranslations(locale: string): Record<string, unknown>;
|
||||
addTranslations(locale: string, translations: Record<string, unknown>): void;
|
||||
removeTranslation(locale: string, key: string): void;
|
||||
clearTranslations(locale: string): void;
|
||||
getSupportedLocales(): string[];
|
||||
isLocaleSupported(locale: string): boolean;
|
||||
getLocaleName(locale: string): string;
|
||||
getLocaleDirection(locale: string): 'ltr' | 'rtl';
|
||||
getLocaleInfo(locale: string): LocaleInfo;
|
||||
getAllLocaleInfo(): LocaleInfo[];
|
||||
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
||||
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
|
||||
formatList(list: string[], options?: Record<string, unknown>): string;
|
||||
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
|
||||
addLocaleListener(listener: (locale: string) => void): () => void;
|
||||
removeLocaleListener(listener: (locale: string) => void): void;
|
||||
clearLocaleListeners(): void;
|
||||
initI18n(): void;
|
||||
saveLocale(locale: string): void;
|
||||
loadLocale(): string;
|
||||
getDefaultLocale(): string;
|
||||
}
|
||||
interface Plugin {
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
hooks?: Record<string, (...args: unknown[]) => void>;
|
||||
middleware?: (...args: unknown[]) => unknown;
|
||||
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
|
||||
uninstall?: (manager: PluginManager) => void;
|
||||
init?: (manager: PluginManager) => void;
|
||||
destroy?: (manager: PluginManager) => void;
|
||||
installed?: boolean;
|
||||
enabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
interface PluginManager {
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
destroy(): void;
|
||||
}
|
||||
interface PluginUtils {
|
||||
createManager(): PluginManager;
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
getPreset(name: string): Plugin | null;
|
||||
getAllPresets(): Record<string, Plugin>;
|
||||
createPlugin(config: Partial<Plugin>): Plugin;
|
||||
validatePlugin(plugin: Partial<Plugin>): {
|
||||
valid: boolean;
|
||||
errors: string[];
|
||||
};
|
||||
}
|
||||
interface MeToast {
|
||||
version: string;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
}
|
||||
declare global {
|
||||
interface Window {
|
||||
MeToast: MeToast;
|
||||
Met: MeToast;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
*/
|
||||
declare const enhancedMeToast: {
|
||||
version: string;
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
};
|
||||
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
/** 支持声明式渲染的 Toast 类型 */
|
||||
type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||
interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||
message: string;
|
||||
type?: ReactToastType;
|
||||
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||
autoClose?: boolean;
|
||||
}
|
||||
/**
|
||||
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||
* 纯函数,便于独立测试与扩展。
|
||||
*/
|
||||
declare const createBoundApi: (cleanup: Set<string>) => Record<string, unknown>;
|
||||
/**
|
||||
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||
*/
|
||||
declare const useToast: () => Record<string, unknown>;
|
||||
/**
|
||||
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||
*/
|
||||
declare const Toast: (props: ToastProps) => null;
|
||||
|
||||
export { enhancedMeToast as MeToast, ReactToastType, Toast, ToastProps, createBoundApi, useToast };
|
||||
Vendored
+4612
File diff suppressed because it is too large
Load Diff
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -0,0 +1,30 @@
|
||||
module.exports = {
|
||||
testEnvironment: 'jsdom',
|
||||
setupFiles: ['<rootDir>/tests/setup.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': 'babel-jest',
|
||||
},
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
transformIgnorePatterns: [
|
||||
'/node_modules/(?!(@rollup)/)',
|
||||
],
|
||||
moduleFileExtensions: ['ts', 'js', 'json'],
|
||||
collectCoverageFrom: [
|
||||
'src/**/*.ts',
|
||||
'!src/index.ts',
|
||||
'!src/types.ts',
|
||||
],
|
||||
coverageDirectory: 'coverage',
|
||||
coverageReporters: ['text', 'lcov'],
|
||||
coverageThreshold: {
|
||||
global: {
|
||||
statements: 85,
|
||||
branches: 65,
|
||||
functions: 85,
|
||||
lines: 95,
|
||||
},
|
||||
},
|
||||
verbose: true,
|
||||
};
|
||||
@@ -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
+524
-5
@@ -1,34 +1,51 @@
|
||||
{
|
||||
"name": "metona-toast",
|
||||
"version": "2.0.0",
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.3.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "metona-toast",
|
||||
"version": "2.0.0",
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "0.3.0",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.22.0",
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
"@babel/preset-typescript": "^7.22.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
"eslint": "^8.40.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup": "^3.20.0",
|
||||
"rollup-plugin-dts": "^5.3.0",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-serve": "^2.0.1",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
@@ -1570,6 +1587,26 @@
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-typescript": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz",
|
||||
"integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-annotate-as-pure": "^7.29.7",
|
||||
"@babel/helper-create-class-features-plugin": "^7.29.7",
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
"@babel/helper-skip-transparent-expression-wrappers": "^7.29.7",
|
||||
"@babel/plugin-syntax-typescript": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/plugin-transform-unicode-escapes": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.29.7.tgz",
|
||||
@@ -1738,6 +1775,26 @@
|
||||
"@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/preset-typescript": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/preset-typescript/-/preset-typescript-7.29.7.tgz",
|
||||
"integrity": "sha512-/Foi8vKY2EVbed/1eZx0gJEEwHAIxogrySI7rULcRIvhZzbvoE/b5qG5Ghc0WKAFKOHA9SD1x7RsFlOYdutIiQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-plugin-utils": "^7.29.7",
|
||||
"@babel/helper-validator-option": "^7.29.7",
|
||||
"@babel/plugin-syntax-jsx": "^7.29.7",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.29.7",
|
||||
"@babel/plugin-transform-typescript": "^7.29.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@babel/core": "^7.0.0-0"
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.29.7",
|
||||
"resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.29.7.tgz",
|
||||
@@ -2468,6 +2525,33 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/plugin-typescript": {
|
||||
"version": "11.1.6",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/plugin-typescript/-/plugin-typescript-11.1.6.tgz",
|
||||
"integrity": "sha512-R92yOmIACgYdJ7dJ97p4K69I8gg6IEHt8M7dUBxN3W6nrO8uUxX5ixl0yU/N3aZTi8WhPuICvOHXQvF6FaykAA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@rollup/pluginutils": "^5.1.0",
|
||||
"resolve": "^1.22.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"rollup": "^2.14.0||^3.0.0||^4.0.0",
|
||||
"tslib": "*",
|
||||
"typescript": ">=3.7.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"rollup": {
|
||||
"optional": true
|
||||
},
|
||||
"tslib": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@rollup/pluginutils": {
|
||||
"version": "5.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.4.0.tgz",
|
||||
@@ -2650,6 +2734,34 @@
|
||||
"undici-types": ">=7.24.0 <7.24.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/prop-types": {
|
||||
"version": "15.7.15",
|
||||
"resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
|
||||
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.3.31",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.31.tgz",
|
||||
"integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.3.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
|
||||
"integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"@types/react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/resolve": {
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmmirror.com/@types/resolve/-/resolve-1.20.2.tgz",
|
||||
@@ -2688,6 +2800,301 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.65.0.tgz",
|
||||
"integrity": "sha512-IEgob78X12rHpUmtcwFsXhZdVGJtwTVP8FiCLZkR6GlYVrl2PcuB+KhCE5BlVC/eQpQnu8WXRtkHZuPar+gCRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/type-utils": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"ignore": "^7.0.5",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.65.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmmirror.com/ignore/-/ignore-7.0.6.tgz",
|
||||
"integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.65.0.tgz",
|
||||
"integrity": "sha512-CZ4nMxWwgu1HEEFNkeaCptra9QCtkmKdgf3sWh1rl1trIhmxLilgTV4cwcbQ4wemnT4sWQN8CaKOmdYx+g2gMA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.65.0.tgz",
|
||||
"integrity": "sha512-SxnPhbTsGahizDgbu7oqFH/xVtzIqMd/s+WtnSxNxJZJpLbdT5IPdzg8EZxO3+PoKahXmwJLeNQOpKJb3/bi7Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.65.0",
|
||||
"@typescript-eslint/types": "^8.65.0",
|
||||
"debug": "^4.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.65.0.tgz",
|
||||
"integrity": "sha512-Esbl8OSYiVxBokYgWPf7VVWg/BE798wXhimnn9ML9Pt5qoDf8bfQlgjlKXR/k98+AcNzlLKYrpCcrcuZ9DZLgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-j6GzGqCiRdA7Qhur2VVmKZAkBLfnHFQfx4TaJGL9RMveZqCo48jSHHO0DTgizEnGhtWnqmbtCUSrqSkdiY/0Hg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.65.0.tgz",
|
||||
"integrity": "sha512-YjaZ7PRI5qY7ax2L3PbvX0rRyGtipAReCWs0mhhDBHjH/vl0g0BonaGXrKdKpMbIIsMIwDgbk/xzkBTyAltS5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0",
|
||||
"@typescript-eslint/utils": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.65.0.tgz",
|
||||
"integrity": "sha512-JSSwWNy+H0E/01jJEM+hrX6N0OFDzFzeIhHFSAS01tlVaevpG8cFyYRPhS5yjGOvBUx3sqQHVMjCL1CAZZMxBg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.65.0.tgz",
|
||||
"integrity": "sha512-JboAE2swaYt4tb1fHhHTABE2K+OLy09XfcTbhnk4Pw96f9dd2e9iYsJ28gBggHlo5z5x1rkyWvcPoTuNTd4oGg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.65.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/visitor-keys": "8.65.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^10.2.2",
|
||||
"semver": "^7.7.3",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.5.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.8.tgz",
|
||||
"integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
|
||||
"version": "10.2.5",
|
||||
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz",
|
||||
"integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
|
||||
"dev": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.8.5",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-7.8.5.tgz",
|
||||
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.65.0.tgz",
|
||||
"integrity": "sha512-gXiwIHsYreboxeJucHKPvgwl7dXt50mF8s1/c00cP/WoVTyWKFdtfhRWwZiXYFU5H2O8vVoSLNrexFZjYS/SGA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.65.0",
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"@typescript-eslint/typescript-estree": "8.65.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
|
||||
"typescript": ">=4.8.4 <6.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.65.0",
|
||||
"resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.65.0.tgz",
|
||||
"integrity": "sha512-8C71BQkGjiMmXtop7pHVJu1l2NNShFdkCyD6a2ezzs5vU/L3LRtb69EtcteFwz0mYMPzIgOw0n6OV4VBUWZd7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.65.0",
|
||||
"eslint-visitor-keys": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/typescript-eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
|
||||
"integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/eslint"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.1.tgz",
|
||||
@@ -3443,6 +3850,13 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/csstype": {
|
||||
"version": "3.2.3",
|
||||
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
|
||||
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-urls": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-3.0.2.tgz",
|
||||
@@ -4073,6 +4487,24 @@
|
||||
"bser": "2.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/fdir": {
|
||||
"version": "6.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
|
||||
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"picomatch": "^3 || ^4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"picomatch": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/file-entry-cache": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
|
||||
@@ -5718,6 +6150,19 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/loose-envify": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
|
||||
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-tokens": "^3.0.0 || ^4.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"loose-envify": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "5.1.1",
|
||||
"resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
|
||||
@@ -6328,6 +6773,33 @@
|
||||
"safe-buffer": "^5.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
|
||||
@@ -6670,6 +7142,16 @@
|
||||
"node": ">=v12.22.7"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.23.2",
|
||||
"resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
|
||||
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "6.3.1",
|
||||
"resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
|
||||
@@ -6980,6 +7462,23 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tinyglobby": {
|
||||
"version": "0.2.17",
|
||||
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.17.tgz",
|
||||
"integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fdir": "^6.5.0",
|
||||
"picomatch": "^4.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/SuperchupuDev"
|
||||
}
|
||||
},
|
||||
"node_modules/tmpl": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmmirror.com/tmpl/-/tmpl-1.0.5.tgz",
|
||||
@@ -7029,6 +7528,26 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ts-api-utils": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
|
||||
"integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": ">=4.8.4"
|
||||
}
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
|
||||
|
||||
+40
-20
@@ -1,39 +1,50 @@
|
||||
{
|
||||
"name": "@metona-team/metona-toast",
|
||||
"version": "2.0.1",
|
||||
"description": "轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。",
|
||||
"main": "dist/metona-toast.js",
|
||||
"module": "src/index.js",
|
||||
"version": "0.5.0",
|
||||
"description": "轻量、零依赖、精致美观的Toast通知库。TypeScript源码,开箱即用。",
|
||||
"type": "module",
|
||||
"main": "dist/metona-toast.cjs",
|
||||
"module": "dist/metona-toast.esm.js",
|
||||
"unpkg": "dist/metona-toast.min.js",
|
||||
"jsdelivr": "dist/metona-toast.min.js",
|
||||
"types": "types/index.d.ts",
|
||||
"types": "dist/metona-toast.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"import": "./src/index.js",
|
||||
"require": "./dist/metona-toast.js",
|
||||
"types": "./types/index.d.ts"
|
||||
"import": "./dist/metona-toast.esm.js",
|
||||
"require": "./dist/metona-toast.cjs",
|
||||
"types": "./dist/metona-toast.d.ts"
|
||||
},
|
||||
"./react": {
|
||||
"import": "./dist/react.js",
|
||||
"require": "./dist/react.cjs",
|
||||
"types": "./dist/react.d.ts"
|
||||
}
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=17.0.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"react": {
|
||||
"optional": true
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"dist/",
|
||||
"src/",
|
||||
"types/",
|
||||
"README.md",
|
||||
"LICENSE"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rollup -c",
|
||||
"build": "tsc --noEmit && rollup -c",
|
||||
"dev": "rollup -c -w",
|
||||
"test": "jest",
|
||||
"test": "jest --coverage",
|
||||
"test:watch": "jest --watch",
|
||||
"test:coverage": "jest --coverage",
|
||||
"lint": "eslint src/",
|
||||
"lint:fix": "eslint src/ --fix",
|
||||
"format": "prettier --write src/",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"format": "prettier --write src/**/*.ts tests/**/*.ts",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"prepublishOnly": "npm run build",
|
||||
"docs": "jsdoc src/ -d docs",
|
||||
"example": "serve examples/"
|
||||
"prepublishOnly": "npm run typecheck && npm run lint && npm test && npm run build",
|
||||
"serve": "bash serve.sh"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -58,25 +69,34 @@
|
||||
"homepage": "https://git.metona.cn/MetonaTeam/MetonaToast#readme",
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.22.0",
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
"@babel/plugin-transform-modules-commonjs": "^7.22.0",
|
||||
"@babel/preset-env": "^7.22.0",
|
||||
"@babel/preset-typescript": "^7.22.0",
|
||||
"@rollup/plugin-commonjs": "^25.0.0",
|
||||
"@rollup/plugin-node-resolve": "^15.0.0",
|
||||
"@rollup/plugin-terser": "^0.4.0",
|
||||
"@rollup/plugin-typescript": "^11.1.6",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/react": "^18.3.31",
|
||||
"@types/react-dom": "^18.3.7",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"babel-jest": "^29.5.0",
|
||||
"eslint": "^8.40.0",
|
||||
"jest": "^29.5.0",
|
||||
"jest-environment-jsdom": "^29.5.0",
|
||||
"prettier": "^2.8.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"rollup": "^3.20.0",
|
||||
"rollup-plugin-dts": "^5.3.0",
|
||||
"rollup-plugin-livereload": "^2.0.5",
|
||||
"rollup-plugin-serve": "^2.0.1",
|
||||
"tslib": "^2.6.0",
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"> 1%",
|
||||
|
||||
+99
-96
@@ -1,5 +1,6 @@
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs';
|
||||
import typescript from '@rollup/plugin-typescript';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
import dts from 'rollup-plugin-dts';
|
||||
import serve from 'rollup-plugin-serve';
|
||||
@@ -8,128 +9,130 @@ import livereload from 'rollup-plugin-livereload';
|
||||
const isDev = process.env.ROLLUP_WATCH;
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
// 基础配置
|
||||
const baseConfig = {
|
||||
input: 'src/index.js',
|
||||
plugins: [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
],
|
||||
};
|
||||
// 基础插件
|
||||
const basePlugins = [
|
||||
resolve(),
|
||||
commonjs(),
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
declaration: false,
|
||||
sourceMap: !isProd,
|
||||
}),
|
||||
];
|
||||
|
||||
// 开发服务器配置
|
||||
const devPlugins = isDev ? [
|
||||
serve({
|
||||
open: true,
|
||||
contentBase: ['site', 'dist'],
|
||||
port: 3000,
|
||||
port: 3001,
|
||||
}),
|
||||
livereload({
|
||||
watch: ['src', 'site'],
|
||||
}),
|
||||
] : [];
|
||||
|
||||
// 生产环境配置
|
||||
const prodPlugins = isProd ? [
|
||||
terser({
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log', 'console.warn'],
|
||||
},
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
}),
|
||||
] : [];
|
||||
|
||||
// 输出配置
|
||||
export default [
|
||||
// UMD格式(浏览器)
|
||||
// UMD(开发模式)
|
||||
{
|
||||
...baseConfig,
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.js',
|
||||
format: 'umd',
|
||||
name: 'MeToast',
|
||||
exports: 'named',
|
||||
sourcemap: !isProd,
|
||||
globals: {},
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
...devPlugins,
|
||||
...prodPlugins,
|
||||
],
|
||||
plugins: [...basePlugins, ...devPlugins],
|
||||
},
|
||||
|
||||
// ESM格式(现代浏览器/打包工具)
|
||||
{
|
||||
...baseConfig,
|
||||
output: {
|
||||
file: 'dist/metona-toast.esm.js',
|
||||
format: 'es',
|
||||
exports: 'named',
|
||||
sourcemap: !isProd,
|
||||
// 生产构建(非 dev 模式时输出全格式)
|
||||
...(isDev ? [] : [
|
||||
// ESM
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.esm.js',
|
||||
format: 'es',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
...prodPlugins,
|
||||
],
|
||||
},
|
||||
|
||||
// CommonJS格式(Node.js)
|
||||
{
|
||||
...baseConfig,
|
||||
output: {
|
||||
file: 'dist/metona-toast.cjs.js',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: !isProd,
|
||||
// CommonJS(.cjs 扩展名:包声明 type:module,.js 会被 Node 按 ESM 解析)
|
||||
// footer 将 default 提升为 module.exports,保证 require() 直接返回 MeToast
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.cjs',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
footer: 'module.exports = Object.assign(exports.default, exports);',
|
||||
},
|
||||
plugins: basePlugins,
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
...prodPlugins,
|
||||
],
|
||||
},
|
||||
|
||||
// 压缩版本(UMD)
|
||||
{
|
||||
...baseConfig,
|
||||
output: {
|
||||
file: 'dist/metona-toast.min.js',
|
||||
format: 'umd',
|
||||
name: 'MeToast',
|
||||
exports: 'named',
|
||||
sourcemap: false,
|
||||
globals: {},
|
||||
// UMD 压缩版
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.min.js',
|
||||
format: 'umd',
|
||||
name: 'MeToast',
|
||||
exports: 'named',
|
||||
sourcemap: false,
|
||||
},
|
||||
plugins: [
|
||||
...basePlugins,
|
||||
terser({
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log', 'console.warn'],
|
||||
passes: 2,
|
||||
},
|
||||
format: { comments: false },
|
||||
mangle: { toplevel: true },
|
||||
}),
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
...baseConfig.plugins,
|
||||
terser({
|
||||
compress: {
|
||||
drop_console: true,
|
||||
drop_debugger: true,
|
||||
pure_funcs: ['console.log', 'console.warn'],
|
||||
passes: 2,
|
||||
// TypeScript 声明文件合并
|
||||
{
|
||||
input: 'src/index.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
// React 适配器(ESM + CJS),react 为 optional peer 外部依赖
|
||||
{
|
||||
input: 'src/react.ts',
|
||||
external: ['react'],
|
||||
output: [
|
||||
{
|
||||
file: 'dist/react.js',
|
||||
format: 'es',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
format: {
|
||||
comments: false,
|
||||
{
|
||||
file: 'dist/react.cjs',
|
||||
format: 'cjs',
|
||||
exports: 'named',
|
||||
sourcemap: true,
|
||||
},
|
||||
mangle: {
|
||||
toplevel: true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
// TypeScript声明文件
|
||||
{
|
||||
input: 'types/index.d.ts',
|
||||
output: {
|
||||
file: 'dist/metona-toast.d.ts',
|
||||
format: 'es',
|
||||
],
|
||||
plugins: basePlugins,
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
{
|
||||
input: 'src/react.ts',
|
||||
external: ['react'],
|
||||
output: {
|
||||
file: 'dist/react.d.ts',
|
||||
format: 'es',
|
||||
},
|
||||
plugins: [dts()],
|
||||
},
|
||||
]),
|
||||
];
|
||||
|
||||
@@ -15,7 +15,7 @@ else
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PORT=${1:-3000}
|
||||
PORT=${1:-3001}
|
||||
|
||||
echo "✅ Python: $($PYTHON --version 2>&1)"
|
||||
echo "🌐 启动服务器..."
|
||||
|
||||
+227
-98
@@ -7,51 +7,78 @@
|
||||
<title>在线演示 — MetonaToast</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1121; --surface: #151d33; --border: #1e2d4d;
|
||||
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
|
||||
--bg: #070b14; --surface: rgba(255,255,255,0.03); --surface2: rgba(255,255,255,0.05);
|
||||
--border: rgba(255,255,255,0.08); --text: #e8ecf4; --muted: #8b94a7;
|
||||
--accent: #a5b4fc; --grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||
--radius: 14px;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.6; min-height:100vh; }
|
||||
a { color:var(--accent2); text-decoration:none; }
|
||||
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.85em; }
|
||||
.nav { position:sticky; top:0; z-index:100; background:rgba(11,17,33,0.88); backdrop-filter:blur(14px); border-bottom:1px solid var(--border); }
|
||||
.nav-inner { max-width:1300px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 20px; height:56px; }
|
||||
.nav-logo { font-size:18px; font-weight:700; background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
||||
.nav-links { display:flex; gap:20px; }
|
||||
.nav-links a { color:var(--muted); font-size:13px; font-weight:500; transition:.2s; }
|
||||
.nav-links a:hover { color:var(--accent2); }
|
||||
.hero { text-align:center; padding:40px 20px 30px; border-bottom:1px solid var(--border); background:radial-gradient(ellipse at 50% 0%,rgba(99,102,241,0.12) 0%,transparent 50%); }
|
||||
.hero h1 { font-size:36px; font-weight:800; }
|
||||
.hero h1 span { background:linear-gradient(135deg,#818cf8,#c084fc,#f472b6); -webkit-background-clip:text; -webkit-text-fill-color:transparent; background-clip:text; }
|
||||
body {
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||
background:var(--bg); color:var(--text); line-height:1.65; min-height:100vh;
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 45% at 50% -10%, rgba(99,102,241,0.12), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 90% 20%, rgba(168,85,247,0.07), transparent);
|
||||
}
|
||||
a { color:var(--accent); text-decoration:none; }
|
||||
code { background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px; border-radius:5px; font-size:.85em; }
|
||||
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
|
||||
|
||||
.nav { position:sticky; top:0; z-index:100; background:rgba(7,11,20,0.72); backdrop-filter:blur(16px); border-bottom:1px solid var(--border); }
|
||||
.nav-inner { max-width:1320px; margin:0 auto; display:flex; align-items:center; justify-content:space-between; padding:0 22px; height:58px; }
|
||||
.nav-logo { font-size:18px; font-weight:800; }
|
||||
.nav-links { display:flex; gap:22px; }
|
||||
.nav-links a { color:var(--muted); font-size:13.5px; font-weight:500; transition:.2s; }
|
||||
.nav-links a:hover { color:var(--accent); }
|
||||
|
||||
.hero { text-align:center; padding:38px 20px 28px; }
|
||||
.hero h1 { font-size:34px; font-weight:800; }
|
||||
.hero p { color:var(--muted); font-size:14px; margin-top:6px; }
|
||||
|
||||
.container { max-width:1300px; margin:0 auto; padding:24px 20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:18px; }
|
||||
.card { background:var(--surface); border:1px solid var(--border); border-radius:var(--radius); padding:20px; }
|
||||
.card h2 { font-size:15px; margin-bottom:2px; }
|
||||
.card > p { color:var(--muted); font-size:11px; margin-bottom:12px; }
|
||||
.container { max-width:1320px; margin:0 auto; padding:20px; display:grid; grid-template-columns:repeat(auto-fill,minmax(400px,1fr)); gap:16px; }
|
||||
.card {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||
padding:20px; transition:border-color .2s, transform .2s; position:relative; overflow:hidden;
|
||||
}
|
||||
.card:hover { border-color:rgba(129,140,248,.35); }
|
||||
.card::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||
background:var(--grad); opacity:0; transition:opacity .25s;
|
||||
}
|
||||
.card:hover::before { opacity:.8; }
|
||||
.card h2 { font-size:14.5px; margin-bottom:2px; font-weight:700; }
|
||||
.card > p { color:var(--muted); font-size:11.5px; margin-bottom:12px; }
|
||||
.btn-row { display:flex; flex-wrap:wrap; gap:6px; }
|
||||
.btn { padding:8px 14px; border:none; border-radius:7px; font-size:12px; font-weight:500; cursor:pointer; transition:.15s; color:#fff; }
|
||||
.btn:hover { transform:translateY(-1px); filter:brightness(1.15); }
|
||||
.btn-sm { padding:5px 10px; font-size:11px; border-radius:5px; }
|
||||
.btn {
|
||||
padding:8px 14px; border:none; border-radius:8px; font-size:12px; font-weight:600;
|
||||
cursor:pointer; transition:transform .15s, filter .15s, box-shadow .15s; color:#fff;
|
||||
}
|
||||
.btn:hover { transform:translateY(-1.5px); filter:brightness(1.15); box-shadow:0 6px 18px -6px rgba(0,0,0,.5); }
|
||||
.btn-sm { padding:5.5px 11px; font-size:11px; border-radius:6px; }
|
||||
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:5px; }
|
||||
.btn-outline { background:transparent; border:1px solid var(--border); color:var(--text); }
|
||||
.btn-outline:hover { border-color:var(--accent); background:rgba(99,102,241,0.06); }
|
||||
.btn-tiny { padding:4px 9px; font-size:10px; border-radius:4px; }
|
||||
.btn-outline:hover { border-color:rgba(129,140,248,.5); background:rgba(129,140,248,.08); }
|
||||
.c-s { background:#10b981; } .c-e { background:#ef4444; } .c-w { background:#f59e0b; color:#1e293b; }
|
||||
.c-i { background:#3b82f6; } .c-p { background:#6366f1; } .c-pp { background:#8b5cf6; }
|
||||
.c-pk { background:#ec4899; } .c-t { background:#14b8a6; } .c-o { background:#f97316; }
|
||||
.c-sl { background:#475569; } .c-r { background:#f43f5e; } .c-g { background:#22c55e; }
|
||||
.label { font-size:10px; text-transform:uppercase; letter-spacing:0.8px; color:var(--muted); margin-bottom:10px; font-weight:600; }
|
||||
.demo-section { border-top:1px solid var(--border); padding-top:8px; margin-top:8px; }
|
||||
.c-grad { background:var(--grad); }
|
||||
.label {
|
||||
font-size:10px; text-transform:uppercase; letter-spacing:1px; color:var(--muted);
|
||||
margin-bottom:10px; font-weight:700;
|
||||
}
|
||||
.demo-section { border-top:1px solid var(--border); padding-top:10px; margin-top:10px; }
|
||||
.status-line { font-size:11.5px; color:#34d399; margin-top:10px; font-family:monospace; min-height:18px; }
|
||||
footer { text-align:center; padding:30px 20px; color:var(--muted); font-size:12px; border-top:1px solid var(--border); }
|
||||
@media (max-width:768px) { .container { grid-template-columns:1fr; } }
|
||||
|
||||
@media (max-width:820px) { .container { grid-template-columns:1fr; } }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<div class="nav-logo">MetonaToast</div>
|
||||
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">首页</a>
|
||||
<a href="docs.html">文档</a>
|
||||
@@ -62,7 +89,7 @@
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<h1><span>在线演示</span></h1>
|
||||
<h1><span class="grad-text">在线演示</span></h1>
|
||||
<p>覆盖项目全部功能的交互演示 — 点击即可体验</p>
|
||||
</header>
|
||||
|
||||
@@ -81,10 +108,10 @@
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.success({title:'保存成功',message:'文件已同步到云端'})">带标题</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续10秒',{duration:10000})">长时10s</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('持续 10 秒',{duration:10000})">长时 10s</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.warning('手动关闭',{duration:0,closeButton:true})">手动关闭</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('',{icon:false})">无图标</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无按钮',{closeButton:false})">无关闭按钮</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.info('无关闭按钮',{closeButton:false})">无按钮</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoCustomIcon()">自定义图标</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -92,16 +119,17 @@
|
||||
|
||||
<!-- 2. Loading 链式 -->
|
||||
<div class="card">
|
||||
<div class="label">🔄 Loading 链式转换</div>
|
||||
<div class="label">🔄 Loading 链式转换 <span style="color:#34d399">id 稳定</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-s" onclick="demoLoading()">提交成功</button>
|
||||
<button class="btn c-e" onclick="demoLoadingFail()">提交失败</button>
|
||||
<button class="btn c-w" onclick="demoLoadingWarn()">转为警告</button>
|
||||
<button class="btn c-i" onclick="demoLoadingUpdate()">原地更新</button>
|
||||
<button class="btn c-s" onclick="demoLoading('success')">提交成功</button>
|
||||
<button class="btn c-e" onclick="demoLoading('error')">提交失败</button>
|
||||
<button class="btn c-w" onclick="demoLoading('warning')">转为警告</button>
|
||||
<button class="btn c-i" onclick="demoLoading('update')">原地更新</button>
|
||||
</div>
|
||||
<p class="status-line" id="loading-status">转换后返回的 toast 与 loading 是同一个实例(id 不变)</p>
|
||||
</div>
|
||||
|
||||
<!-- 3. Promise + confirm + prompt -->
|
||||
<!-- 3. 对话框 -->
|
||||
<div class="card">
|
||||
<div class="label">💬 对话框</div>
|
||||
<div class="btn-row">
|
||||
@@ -116,8 +144,8 @@
|
||||
<div class="card">
|
||||
<div class="label">📊 进度 & 倒计时</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-o" onclick="demoProgress()">进度条(水平)</button>
|
||||
<button class="btn c-g" onclick="demoProgressV()">进度条(垂直)</button>
|
||||
<button class="btn c-o" onclick="demoProgress('horizontal')">进度条(水平)</button>
|
||||
<button class="btn c-g" onclick="demoProgress('vertical')">进度条(垂直)</button>
|
||||
<button class="btn c-pk" onclick="demoCountdown()">倒计时 5s</button>
|
||||
<button class="btn c-r" onclick="demoCountdownPause()">倒计时(可暂停)</button>
|
||||
</div>
|
||||
@@ -136,7 +164,7 @@
|
||||
|
||||
<!-- 6. Action Toast -->
|
||||
<div class="card">
|
||||
<div class="label">⚡ Action Toast <span style="color:#34d399;font-size:10px;">v2.0</span></div>
|
||||
<div class="label">⚡ Action Toast <span style="color:#34d399">close:false 不关闭</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-e" onclick="demoActionDelete()">删除+撤销</button>
|
||||
<button class="btn c-i" onclick="demoActionMulti()">多按钮</button>
|
||||
@@ -146,7 +174,7 @@
|
||||
|
||||
<!-- 7. 分组管理 -->
|
||||
<div class="card">
|
||||
<div class="label">📁 分组管理 <span style="color:#34d399;font-size:10px;">v2.0</span></div>
|
||||
<div class="label">📁 分组管理</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-p" onclick="demoGroup()">创建分组消息</button>
|
||||
<button class="btn c-e" onclick="dismissGroup()">关闭分组</button>
|
||||
@@ -157,7 +185,7 @@
|
||||
|
||||
<!-- 8. 动画效果 -->
|
||||
<div class="card">
|
||||
<div class="label">🎭 11种动画效果</div>
|
||||
<div class="label">🎭 11 种动画效果</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('slide')">slide</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('fade')">fade</button>
|
||||
@@ -171,9 +199,14 @@
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('slideLeft')">slideLeft</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="anim('slideRight')">slideRight</button>
|
||||
</div>
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
<button class="btn c-grad" onclick="demoCustomAnim()">✨ 注册自定义动画</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 9. 6种位置 -->
|
||||
<!-- 9. 位置 -->
|
||||
<div class="card">
|
||||
<div class="label">📍 全部位置</div>
|
||||
<div class="btn-row">
|
||||
@@ -186,7 +219,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 10. 主题切换 + 自定义 -->
|
||||
<!-- 10. 主题 -->
|
||||
<div class="card">
|
||||
<div class="label">🎨 主题切换 & 自定义</div>
|
||||
<div class="btn-row">
|
||||
@@ -194,24 +227,43 @@
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('dark')">🌙 Dark</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('auto')">🔄 Auto</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="switchTheme('warm')">🔥 Warm</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义主题</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoRegisterTheme()">注册自定义</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 11. 插件系统 -->
|
||||
<!-- 11. 插件 -->
|
||||
<div class="card">
|
||||
<div class="label">🔌 插件系统</div>
|
||||
<div class="label">🔌 插件系统 <span style="color:#34d399">4 款预设</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard ESC关闭</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置将持久化到 localStorage')">persistence 持久化</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器可用')">accessibility 无障碍</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('keyboard');MeToast.info('已安装 — 按 ESC 关闭所有 Toast')">keyboard</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('persistence');MeToast.info('已安装 — 配置持久化')">persistence</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('accessibility');MeToast.info('已安装 — 屏幕阅读器')">accessibility</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="MeToast.use('dedupe');MeToast.info('已安装 — 同消息去重')">dedupe</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoCustomPlugin()">自定义插件</button>
|
||||
</div>
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
<button class="btn c-t btn-sm" onclick="demoDedupe()">去重演示</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 12. v2.0 新功能 -->
|
||||
<!-- 12. 钩子系统 -->
|
||||
<div class="card">
|
||||
<div class="label">🆕 v2.0 新功能 <span style="color:#34d399;font-size:10px;">resetTimer · onUpdate · render · notify</span></div>
|
||||
<div class="label">🔗 钩子系统 <span style="color:#34d399">beforeShow 拦截演示</span></div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-p" onclick="demoHookBlock()">开启拦截 error</button>
|
||||
<button class="btn c-e" onclick="MeToast.error('这条会被拦截')">触发被拦 error</button>
|
||||
<button class="btn c-s" onclick="MeToast.success('这条不受影响')">正常 success</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoHookUnblock()">关闭拦截</button>
|
||||
<button class="btn btn-outline btn-sm" onclick="demoHookCount()">钩子统计</button>
|
||||
</div>
|
||||
<p class="status-line" id="hook-status">钩子:未注册</p>
|
||||
</div>
|
||||
|
||||
<!-- 13. 高级特性 -->
|
||||
<div class="card">
|
||||
<div class="label">🔥 高级特性</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-i" onclick="demoResetTimer()">resetTimerOnUpdate</button>
|
||||
<button class="btn c-p" onclick="demoOnUpdate()">onUpdate 回调</button>
|
||||
@@ -220,7 +272,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 13. Toast 管理 -->
|
||||
<!-- 14. Toast 管理 -->
|
||||
<div class="card">
|
||||
<div class="label">🔧 Toast 管理</div>
|
||||
<div class="btn-row">
|
||||
@@ -230,7 +282,7 @@
|
||||
<button class="btn c-sl" onclick="MeToast.pauseAll();MeToast.info('已暂停全部')">暂停全部</button>
|
||||
<button class="btn c-sl" onclick="MeToast.resumeAll();MeToast.info('已恢复全部')">恢复全部</button>
|
||||
<button class="btn c-e" onclick="MeToast.dismiss()">关闭全部</button>
|
||||
<button class="btn c-r" onclick="MeToast.destroy();MeToast.info('已销毁')">销毁</button>
|
||||
<button class="btn c-r" onclick="MeToast.destroy();MeToast.init({});MeToast.info('已销毁并重建')">销毁重建</button>
|
||||
</div>
|
||||
<div class="demo-section">
|
||||
<div class="btn-row">
|
||||
@@ -238,23 +290,34 @@
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoGetAll()">getAll</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoClearByPos()">clear(位置)</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoGetConfig()">查看配置</button>
|
||||
<button class="btn btn-outline btn-tiny" onclick="demoRemove()">removeToast 立即移除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 14. 交互特性 -->
|
||||
<!-- 15. onError -->
|
||||
<div class="card">
|
||||
<div class="label">🛡️ onError 错误捕获</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-e" onclick="demoOnError()">演示 onError</button>
|
||||
<button class="btn c-w" onclick="demoOnErrorReset()">重置回调</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 16. 交互特性 -->
|
||||
<div class="card">
|
||||
<div class="label">✋ 交互特性</div>
|
||||
<div class="btn-row">
|
||||
<button class="btn c-i" onclick="demoDraggable()">拖拽关闭</button>
|
||||
<button class="btn c-p" onclick="demoPauseHover()">悬停暂停</button>
|
||||
<button class="btn c-w" onclick="demoCloseOnClick()">点击关闭</button>
|
||||
<button class="btn c-pp" onclick="demoWidth()">自定义宽度</button>
|
||||
<button class="btn c-t" onclick="demoClassName()">自定义className</button>
|
||||
<button class="btn c-p" onclick="demoDragThreshold()">拖拽阈值 300px</button>
|
||||
<button class="btn c-w" onclick="demoPauseHover()">悬停暂停</button>
|
||||
<button class="btn c-pp" onclick="demoCloseOnClick()">点击关闭</button>
|
||||
<button class="btn c-t" onclick="demoWidth()">自定义宽度</button>
|
||||
<button class="btn c-sl" onclick="demoClassName()">自定义 className</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 15. 国际化切换 -->
|
||||
<!-- 17. 国际化 -->
|
||||
<div class="card">
|
||||
<div class="label">🌍 国际化 & 格式化</div>
|
||||
<div class="btn-row">
|
||||
@@ -264,16 +327,19 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 16. 107种预设图标 -->
|
||||
<!-- 18. 图标网格 -->
|
||||
<div class="card" style="grid-column:1/-1;">
|
||||
<div class="label">🎨 107种预设图标 — 点击任意图标发送Toast</div>
|
||||
<div class="btn-row" id="icon-grid" style="max-height:300px;overflow-y:auto;padding:4px 0;"></div>
|
||||
<div class="label">🎨 107 种预设图标 — 点击任意图标发送 Toast</div>
|
||||
<div class="btn-row" id="icon-grid" style="max-height:320px;overflow-y:auto;padding:4px 0;"></div>
|
||||
</div>
|
||||
|
||||
</main>
|
||||
|
||||
<footer>
|
||||
MetonaToast v2.0.1 · MIT · <a href="index.html">首页</a> · <a href="docs.html">文档</a> · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
MetonaToast v0.5.0 · MIT ·
|
||||
<a href="index.html">首页</a> ·
|
||||
<a href="docs.html">文档</a> ·
|
||||
<a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
@@ -281,27 +347,41 @@
|
||||
MeToast.configure({ position:'top-right', duration:4000, theme:'dark', animation:'slide', pauseOnHover:true, showProgress:true });
|
||||
|
||||
// === Loading ===
|
||||
function demoLoading() { const l=MeToast.loading('正在提交...'); setTimeout(()=>l.success('提交成功!'),2000); }
|
||||
function demoLoadingFail() { const l=MeToast.loading('正在处理...'); setTimeout(()=>l.error('处理失败'),2000); }
|
||||
function demoLoadingWarn() { const l=MeToast.loading('检查中...'); setTimeout(()=>l.warning('发现潜在问题'),2000); }
|
||||
function demoLoadingUpdate() { const l=MeToast.loading('处理中 0%'); let i=1; const t=setInterval(()=>{if(i>3){clearInterval(t);l.success('完成!');return;} l.update({message:'处理中 '+i*33+'%'}); i++;},600); }
|
||||
function demoLoading(mode) {
|
||||
const l = MeToast.loading('正在处理...');
|
||||
const showId = () => document.getElementById('loading-status').textContent = 'loading id: ' + l.id;
|
||||
showId();
|
||||
setTimeout(() => {
|
||||
const map = { success:['提交成功!','success'], error:['处理失败','error'], warning:['发现潜在问题','warning'], update:null };
|
||||
if (mode === 'update') {
|
||||
l.update({ message: '进度 50%' });
|
||||
setTimeout(() => l.success('完成!'), 1000);
|
||||
return;
|
||||
}
|
||||
const [msg, type] = map[mode];
|
||||
const t = l[type](msg);
|
||||
if (t) document.getElementById('loading-status').textContent = '转换后 id: ' + t.id + '(' + (t.id === l.id ? '同一实例 ✓' : '不同实例 ✗') + ')';
|
||||
}, 1600);
|
||||
}
|
||||
|
||||
// === Promise / confirm / prompt ===
|
||||
async function demoPromise() {
|
||||
const p=new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
|
||||
try{await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'});}catch(_){}
|
||||
const p = new Promise((r,e)=>setTimeout(()=>Math.random()>0.3?r('ok'):e('fail'),2000));
|
||||
try{ await MeToast.promise(p,{loading:'处理中...',success:'成功!',error:'失败'}); }catch(_){}
|
||||
}
|
||||
async function demoConfirm() { const ok=await MeToast.confirm('确定删除?',{confirmText:'删除',confirmColor:'#ef4444'}); if(ok)MeToast.success('已删除'); }
|
||||
async function demoPrompt() { const v=await MeToast.prompt('请输入姓名',{placeholder:'请输入...'}); if(v) MeToast.info('你好,'+v+'!'); }
|
||||
async function demoPromptPassword() { const v=await MeToast.prompt('请输入密码',{placeholder:'密码',inputType:'password',submitText:'登录'}); if(v) MeToast.info('密码已输入'); }
|
||||
|
||||
// === Progress & Countdown ===
|
||||
function demoProgress() { const p=MeToast.progress('上传中...',{progressColor:'#3b82f6'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400); }
|
||||
function demoProgressV() { const p=MeToast.progress('上传中...',{progressColor:'#10b981',progressDirection:'vertical'}); let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('完成!');}else p.setProgress(v);},400); }
|
||||
function demoProgress(dir) {
|
||||
const p=MeToast.progress('上传中...',{progressColor: dir==='vertical'?'#10b981':'#3b82f6', progressDirection: dir});
|
||||
let v=0; const t=setInterval(()=>{v+=Math.random()*25; if(v>=100){clearInterval(t);p.complete('上传完成!');}else p.setProgress(v);},400);
|
||||
}
|
||||
function demoCountdown() { MeToast.countdown('{seconds} 秒后执行',5,{onComplete:()=>MeToast.success('已执行')}); }
|
||||
function demoCountdownPause() {
|
||||
const c=MeToast.countdown('{seconds}s 可暂停',10,{type:'info',onComplete:()=>MeToast.success('倒计时结束')});
|
||||
setTimeout(()=>{c.pause();MeToast.warning('已暂停3秒后恢复',{duration:3000})},4000);
|
||||
setTimeout(()=>{c.pause();MeToast.warning('已暂停 3 秒后恢复',{duration:3000})},4000);
|
||||
setTimeout(()=>{c.resume();MeToast.info('已恢复倒计时')},7000);
|
||||
}
|
||||
|
||||
@@ -312,14 +392,15 @@
|
||||
setTimeout(()=>{q.cancel();MeToast.warning('队列已取消',{duration:2000});},3500);
|
||||
}
|
||||
function demoStack() { MeToast.stack(['消息1','消息2','消息3','消息4','消息5'],{stagger:120,type:'info'}); }
|
||||
function demoStackMixed() {
|
||||
MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150});
|
||||
}
|
||||
function demoStackMixed() { MeToast.stack(['普通消息',{message:'警告消息',type:'warning',duration:5000},{message:'错误消息',type:'error'}],{stagger:150}); }
|
||||
|
||||
// === Action ===
|
||||
function demoActionDelete() { MeToast.action('文件 data.json 已删除',[{text:'撤销',onClick:()=>MeToast.success('已撤销'),color:'#3b82f6'}]); }
|
||||
function demoActionMulti() { MeToast.action('邮件已发送',[{text:'查看',onClick:()=>MeToast.info('打开邮件详情'),color:'#3b82f6'},{text:'撤回',onClick:()=>MeToast.warning('已撤回'),color:'#f59e0b'}]); }
|
||||
function demoActionNoClose() { MeToast.action('完成后点击关闭',[{text:'我知道了',onClick:()=>{},color:'#10b981'}]); }
|
||||
function demoActionNoClose() {
|
||||
let n=0;
|
||||
MeToast.action('点「+1」不会关闭,可连续点击',[{text:'+1',onClick:(t)=>{n++;t.update({message:'已点击 '+n+' 次(close:false 不关闭)'});},color:'#10b981',close:false}],{duration:8000});
|
||||
}
|
||||
|
||||
// === Group ===
|
||||
const group = MeToast.group('demo-group');
|
||||
@@ -333,32 +414,71 @@
|
||||
|
||||
// === Animation ===
|
||||
function anim(name) { MeToast.success('动画: '+name,{animation:name,duration:3000,showProgress:false}); }
|
||||
function demoCustomAnim() {
|
||||
MeToast.animations.register('swing', {
|
||||
enter: { transform: 'rotate(-40deg) scale(0.3)', opacity: 0 },
|
||||
leave: { transform: 'rotate(20deg) scale(0.5)', opacity: 0 },
|
||||
duration: 600,
|
||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
});
|
||||
MeToast.success('自定义 swing 动画!', { animation: 'swing', duration: 3000 });
|
||||
}
|
||||
|
||||
// === Theme ===
|
||||
function switchTheme(t) { MeToast.themes.switchTheme(t); MeToast.configure({theme:t}); MeToast.info('主题: '+t,{theme:t}); }
|
||||
function demoRegisterTheme() {
|
||||
try {
|
||||
MeToast.themes.registerTheme('sunset',{bg:'rgba(255,248,240,0.96)',text:'#92400e',border:'rgba(249,115,22,0.2)',shadow:'0 10px 36px -10px rgba(249,115,22,0.2)',hoverShadow:'0 14px 48px -10px rgba(249,115,22,0.3)',progressBg:'rgba(249,115,22,0.1)',closeHoverBg:'rgba(249,115,22,0.1)'});
|
||||
MeToast.themes.switchTheme('sunset');
|
||||
MeToast.configure({theme:'sunset'});
|
||||
MeToast.themes.switchTheme('sunset'); MeToast.configure({theme:'sunset'});
|
||||
MeToast.success('已切换到 sunset 主题',{theme:'sunset'});
|
||||
} catch(_) { MeToast.warning('主题已注册,已切换'); }
|
||||
}
|
||||
|
||||
// === Plugins ===
|
||||
function demoCustomPlugin() {
|
||||
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\\(\\d+\\)/,'')+' (总通知#'+c+')'}),1);});}});
|
||||
MeToast.use({name:'click-counter',version:'1.0',install(){let c=0;Toast.on('afterShow',t=>{c++;setTimeout(()=>t.update({message:t.message.replace(/\(\d+\)/,'')+' (总通知#'+c+')'}),1);});}});
|
||||
MeToast.info('自定义插件已安装(消息后追加计数)');
|
||||
}
|
||||
|
||||
// === v2.0 new features ===
|
||||
function demoResetTimer() {
|
||||
const t=MeToast.info('这条消息不会自动消失(resetTimer)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
||||
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n+' — 每次更新重置3秒'});},1000);
|
||||
function demoDedupe() {
|
||||
if (!MeToast.plugins.has('dedupe')) MeToast.use('dedupe');
|
||||
MeToast.info('同一条消息');
|
||||
MeToast.info('同一条消息');
|
||||
MeToast.info('同一条消息');
|
||||
MeToast.info('当前仅 ' + MeToast.count() + ' 条 — 相同消息自动合并');
|
||||
}
|
||||
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); setTimeout(()=>t.update({message:'再次更新!'}),2000); }
|
||||
|
||||
// === Hooks ===
|
||||
let hookOff = null;
|
||||
function demoHookBlock() {
|
||||
if (hookOff) hookOff();
|
||||
hookOff = Toast.on('beforeShow', (toast) => {
|
||||
if (toast.type === 'error') { document.getElementById('hook-status').textContent = '钩子:error 已被拦截 ✋'; return false; }
|
||||
});
|
||||
document.getElementById('hook-status').textContent = '钩子:已注册(error 类型将被拦截)';
|
||||
MeToast.info('拦截器已开启 — 试试触发 error');
|
||||
}
|
||||
function demoHookUnblock() {
|
||||
if (hookOff) { hookOff(); hookOff = null; }
|
||||
document.getElementById('hook-status').textContent = '钩子:已移除';
|
||||
MeToast.info('拦截器已关闭');
|
||||
}
|
||||
function demoHookCount() {
|
||||
const stats = [];
|
||||
['afterShow','afterClose','configChange','themeChange'].forEach(name => {
|
||||
const list = Toast._hooks.get(name);
|
||||
stats.push(name + ':' + (list ? list.length : 0));
|
||||
});
|
||||
MeToast.info('钩子数量 — ' + stats.join(' '), { duration: 5000 });
|
||||
}
|
||||
|
||||
// === 高级特性 ===
|
||||
function demoResetTimer() {
|
||||
const t=MeToast.info('这条不会自动消失(每次更新重置 3 秒)',{duration:3000,resetTimerOnUpdate:true,closeButton:true});
|
||||
let n=0; const iv=setInterval(()=>{n++;if(n>8){clearInterval(iv);t.close();return;}t.update({message:'重置计时器 #'+n});},1000);
|
||||
}
|
||||
function demoOnUpdate() { const t=MeToast.success('onUpdate 演示',{onUpdate:t=>{console.log('更新了:',t.message);}}); setTimeout(()=>t.update({message:'已更新!'}),1000); }
|
||||
function demoCustomRender() {
|
||||
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render by thzxx</span></div>',message:'自定义渲染内容',duration:5000});
|
||||
MeToast.show({render:toast=>'<div style="background:#111;color:#0f0;padding:16px 20px;border-radius:10px;border:1px solid #0f0;font-family:monospace">🟢 <b>'+toast.message+'</b><br><span style="font-size:11px;opacity:0.6"># custom render</span></div>',message:'自定义渲染内容',duration:5000});
|
||||
}
|
||||
function demoNotify() {
|
||||
if(typeof Notification==='undefined'){MeToast.error('浏览器不支持 Notification');return;}
|
||||
@@ -373,29 +493,38 @@
|
||||
}
|
||||
function demoGetAll() { const all=MeToast.getAll(); MeToast.info('当前共 '+all.size+' 条 toast'); }
|
||||
function demoClearByPos() { MeToast.error('右下角临时消息',{position:'bottom-right',duration:3000}); setTimeout(()=>{MeToast.clear('bottom-right');MeToast.info('右下角已清除');},500); }
|
||||
function demoGetConfig() { try{const c=MeToast.getConfig();MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000});}catch(e){MeToast.error('无 getConfig 方法');} }
|
||||
function demoGetConfig() { const c=MeToast.getConfig(); MeToast.info('配置: '+JSON.stringify({position:c.position,duration:c.duration,theme:c.theme,animation:c.animation,locale:c.locale}).replace(/"/g,''),{duration:5000}); }
|
||||
function demoRemove() { const t=MeToast.warning('这条将被立即移除(无动画)'); setTimeout(()=>{MeToast.removeToast(t.id);MeToast.info('已立即移除');},800); }
|
||||
|
||||
// === onError ===
|
||||
function demoOnError() {
|
||||
MeToast.configure({ onError: function(info) { MeToast.error('⚠️ 捕获: ' + (info.hook || info.source), { duration: 3000, position: 'bottom-center' }); } });
|
||||
MeToast.info('已启用 onError 监控', { duration: 3000 });
|
||||
var badFn = function() { throw new Error('demo-error'); };
|
||||
Toast.on('afterShow', badFn);
|
||||
MeToast.success('这条会触发错误回调', { duration: 2000 });
|
||||
setTimeout(function() { Toast.off('afterShow', badFn); }, 3000);
|
||||
}
|
||||
function demoOnErrorReset() { MeToast.configure({ onError: null }); MeToast.info('onError 已重置'); }
|
||||
|
||||
// === Interaction ===
|
||||
function demoDraggable() { MeToast.info('← 试试拖动我来关闭',{draggable:true,duration:8000,closeButton:true}); }
|
||||
function demoPauseHover() { MeToast.info('鼠标悬停在我上面,倒计时会暂停',{pauseOnHover:true,duration:8000}); }
|
||||
function demoDraggable() { MeToast.info('← 拖动我超过 120px 关闭',{draggable:true,duration:8000,closeButton:true}); }
|
||||
function demoDragThreshold() { MeToast.info('← 拖动阈值 300px(要拖更远)',{draggable:true,dragThreshold:300,duration:8000,closeButton:true}); }
|
||||
function demoPauseHover() { MeToast.info('悬停时倒计时会暂停',{pauseOnHover:true,duration:8000}); }
|
||||
function demoCloseOnClick() { MeToast.warning('点击我即可关闭',{closeOnClick:true,duration:10000}); }
|
||||
function demoWidth() { MeToast.info('宽550px',{width:550}); setTimeout(()=>MeToast.info('宽280px(窄)',{width:280}),500); }
|
||||
function demoWidth() { MeToast.info('宽 550px',{width:550}); setTimeout(()=>MeToast.info('宽 280px(窄)',{width:280}),500); }
|
||||
function demoClassName() { MeToast.success('带自定义 className',{className:'demo-custom-class'}); }
|
||||
function demoCustomIcon() {
|
||||
MeToast.info('通用通知', {
|
||||
iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>'
|
||||
});
|
||||
MeToast.info('通用通知', { iconHTML: '<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/></svg>' });
|
||||
}
|
||||
|
||||
// === i18n ===
|
||||
function switchLocale(loc) {
|
||||
try{MeToast.i18n.switchLocale(loc);MeToast.configure({locale:loc});MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English',{theme:'dark'});}
|
||||
catch(e){MeToast.info('已切换语言');}
|
||||
MeToast.i18n.switchLocale(loc); MeToast.configure({locale:loc});
|
||||
MeToast.success(loc==='zh-CN'?'已切换到中文':'Switched to English');
|
||||
}
|
||||
function demoFormat() {
|
||||
try{
|
||||
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
|
||||
}catch(e){MeToast.info('格式化: 1234567 → 1,234,567');}
|
||||
MeToast.info('数字: '+MeToast.i18n.formatNumber(1234567)+' | 货币: '+MeToast.i18n.formatCurrency(99.5,'CNY')+' | 日期: '+MeToast.i18n.formatDate(new Date()),{duration:8000});
|
||||
}
|
||||
|
||||
// === 图标网格 ===
|
||||
|
||||
+356
-348
@@ -7,446 +7,442 @@
|
||||
<title>API 文档 — MetonaToast</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1121; --surface: #1a2332; --border: #1e2d4d;
|
||||
--text: #e2e8f0; --muted: #64748b; --accent: #6366f1; --accent2: #818cf8;
|
||||
--radius: 10px;
|
||||
--bg: #070b14; --surface: rgba(255,255,255,0.03); --border: rgba(255,255,255,0.08);
|
||||
--text: #e8ecf4; --muted: #8b94a7; --accent: #a5b4fc; --accent2: #c7d2fe;
|
||||
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||
--radius: 12px;
|
||||
}
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
body { font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif; background:var(--bg); color:var(--text); line-height:1.7; display:flex; min-height:100vh; }
|
||||
code { background:rgba(99,102,241,0.12); color:#a5b4fc; padding:1px 6px; border-radius:4px; font-size:0.9em; white-space:nowrap; }
|
||||
pre { background:#0a0f1a; border:1px solid var(--border); border-radius:8px; padding:14px 18px; overflow-x:auto; font:13px/1.7 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; margin:10px 0; }
|
||||
pre .kw { color:#c084fc; } pre .fn { color:#60a5fa; } pre .s { color:#34d399; } pre .cm { color:#475569; }
|
||||
a { color:var(--accent2); text-decoration:none; }
|
||||
html { scroll-behavior:smooth; }
|
||||
body {
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||
background:var(--bg); color:var(--text); line-height:1.75; display:flex; min-height:100vh;
|
||||
background-image:
|
||||
radial-gradient(ellipse 70% 40% at 80% -10%, rgba(99,102,241,0.12), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 0% 20%, rgba(236,72,153,0.05), transparent);
|
||||
}
|
||||
code {
|
||||
background:rgba(99,102,241,0.15); color:#a5b4fc; padding:1px 6px;
|
||||
border-radius:5px; font-size:.88em; white-space:nowrap;
|
||||
}
|
||||
a { color:var(--accent); text-decoration:none; }
|
||||
a:hover { color:var(--accent2); }
|
||||
|
||||
.sidebar { width:260px; min-width:260px; background:var(--surface); border-right:1px solid var(--border); padding:24px 0; position:sticky; top:0; height:100vh; overflow-y:auto; }
|
||||
.sidebar h2 { font-size:15px; padding:0 24px; margin-bottom:16px; color:var(--accent2); }
|
||||
.sidebar a { display:block; padding:7px 24px; font-size:13px; color:var(--muted); transition:.15s; border-left:2px solid transparent; }
|
||||
.sidebar a:hover, .sidebar a.active { color:var(--accent2); border-left-color:var(--accent); background:rgba(99,102,241,0.06); }
|
||||
.sidebar .section { font-size:11px; text-transform:uppercase; letter-spacing:1px; padding:16px 24px 6px; color:var(--muted); font-weight:600; }
|
||||
/* ===== 侧边栏 ===== */
|
||||
.sidebar {
|
||||
width:264px; min-width:264px; background:rgba(10,15,26,.8);
|
||||
border-right:1px solid var(--border); padding:22px 0;
|
||||
position:sticky; top:0; height:100vh; overflow-y:auto;
|
||||
backdrop-filter:blur(14px);
|
||||
}
|
||||
.sidebar::-webkit-scrollbar { width:5px; }
|
||||
.sidebar::-webkit-scrollbar-thumb { background:rgba(255,255,255,.12); border-radius:3px; }
|
||||
.sidebar-logo { font-size:16px; font-weight:800; padding:0 24px; margin-bottom:4px; }
|
||||
.grad-text { background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; color:transparent; }
|
||||
.sidebar .back { display:block; font-size:12.5px; padding:6px 24px; color:var(--muted); }
|
||||
.sidebar .back:hover { color:var(--accent2); }
|
||||
.sidebar a.snav {
|
||||
display:block; padding:6.5px 24px; font-size:13px; color:var(--muted);
|
||||
transition:.15s; border-left:2px solid transparent;
|
||||
}
|
||||
.sidebar a.snav:hover { color:var(--accent2); }
|
||||
.sidebar a.snav.active { color:var(--accent2); border-left-color:#a855f7; background:rgba(139,92,246,.08); }
|
||||
.sidebar .section {
|
||||
font-size:11px; text-transform:uppercase; letter-spacing:1.2px;
|
||||
padding:18px 24px 6px; color:var(--muted); font-weight:700; opacity:.75;
|
||||
}
|
||||
|
||||
.main { flex:1; padding:40px 48px; max-width:950px; }
|
||||
.main h1 { font-size:30px; font-weight:700; margin-bottom:8px; }
|
||||
.main h2 { font-size:22px; font-weight:700; margin:44px 0 14px; padding-top:22px; border-top:1px solid var(--border); }
|
||||
.main h3 { font-size:17px; font-weight:600; margin:28px 0 10px; color:var(--accent2); }
|
||||
/* ===== 主内容 ===== */
|
||||
.main { flex:1; padding:44px 52px; max-width:980px; min-width:0; }
|
||||
.main h1 { font-size:32px; font-weight:800; margin-bottom:6px; letter-spacing:-.5px; }
|
||||
.main .h1-desc { color:var(--muted); font-size:14px; margin-bottom:8px; }
|
||||
.main h2 {
|
||||
font-size:22px; font-weight:700; margin:52px 0 12px; padding-top:22px;
|
||||
border-top:1px solid var(--border); scroll-margin-top:24px;
|
||||
}
|
||||
.main h2::before { content:'#'; margin-right:8px; background:var(--grad); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
|
||||
.main h3 { font-size:16.5px; font-weight:700; margin:26px 0 10px; color:var(--accent2); }
|
||||
.main p { margin:8px 0; color:var(--muted); font-size:14px; }
|
||||
.main table { width:100%; border-collapse:collapse; margin:14px 0 20px; font-size:13px; }
|
||||
.main p code { font-size:.9em; }
|
||||
.main table { width:100%; border-collapse:collapse; margin:14px 0 22px; font-size:13px; }
|
||||
.main th, .main td { text-align:left; padding:9px 14px; border-bottom:1px solid var(--border); vertical-align:top; }
|
||||
.main th { color:var(--muted); font-weight:500; font-size:11px; text-transform:uppercase; letter-spacing:0.5px; }
|
||||
.main td:first-child { color:var(--accent2); font-family:monospace; font-size:12px; white-space:nowrap; }
|
||||
.main td:nth-child(3) { font-family:monospace; font-size:11px; color:var(--muted); }
|
||||
.badge { display:inline-block; padding:1px 7px; border-radius:3px; font-size:10px; font-weight:700; margin-left:6px; vertical-align:middle; }
|
||||
.badge-new { background:rgba(16,185,129,0.2); color:#34d399; }
|
||||
.hint { font-size:12px; color:var(--muted); margin-top:2px; }
|
||||
.return-box { background:rgba(16,185,129,0.08); border:1px solid rgba(16,185,129,0.2); border-radius:8px; padding:14px 18px; margin:12px 0; }
|
||||
.return-box h4 { font-size:12px; color:#34d399; margin-bottom:6px; text-transform:uppercase; letter-spacing:0.5px; }
|
||||
.main th { color:var(--muted); font-weight:600; font-size:11px; text-transform:uppercase; letter-spacing:.8px; }
|
||||
.main td:first-child { color:var(--accent); font-family:monospace; font-size:12px; white-space:nowrap; }
|
||||
.main td:nth-child(3) { font-family:monospace; font-size:11.5px; color:var(--muted); }
|
||||
.badge {
|
||||
display:inline-block; padding:1px 8px; border-radius:4px; font-size:10px;
|
||||
font-weight:700; margin-left:6px; vertical-align:middle;
|
||||
}
|
||||
.badge-new { background:rgba(16,185,129,.18); color:#34d399; border:1px solid rgba(16,185,129,.3); }
|
||||
.badge-v5 { background:rgba(168,85,247,.2); color:#d8b4fe; border:1px solid rgba(168,85,247,.4); }
|
||||
.hint { font-size:12.5px; color:var(--muted); margin-top:2px; }
|
||||
.return-box {
|
||||
background:rgba(16,185,129,.06); border:1px solid rgba(16,185,129,.2);
|
||||
border-radius:10px; padding:14px 18px; margin:14px 0;
|
||||
}
|
||||
.return-box h4 { font-size:11.5px; color:#34d399; margin-bottom:8px; text-transform:uppercase; letter-spacing:.8px; }
|
||||
|
||||
@media (max-width:768px) {
|
||||
.terminal {
|
||||
background:#0a0f1c; border:1px solid var(--border); border-radius:10px;
|
||||
overflow:hidden; margin:12px 0 20px;
|
||||
}
|
||||
.terminal-bar { display:flex; align-items:center; gap:7px; padding:9px 14px; background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border); }
|
||||
.terminal-bar .t-dot { width:10px; height:10px; border-radius:50%; }
|
||||
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
|
||||
.terminal-bar .t-title { margin-left:10px; font-size:11px; color:var(--muted); }
|
||||
.terminal pre { padding:16px 18px; overflow-x:auto; font:12.5px/1.75 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color:#cbd5e1; }
|
||||
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; } .terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
|
||||
|
||||
@media (max-width:860px) {
|
||||
body { flex-direction:column; }
|
||||
.sidebar { width:100%; min-width:0; height:auto; position:static; border-right:none; border-bottom:1px solid var(--border); padding:12px 16px; }
|
||||
.sidebar a { display:inline-block; padding:4px 10px; border-left:none; }
|
||||
.main { padding:24px 20px; }
|
||||
.sidebar .section, .sidebar .back { padding:4px 8px; }
|
||||
.sidebar a.snav { display:inline-block; padding:4px 10px; border-left:none; font-size:12.5px; }
|
||||
.sidebar a.snav.active { border-left:none; border-radius:6px; }
|
||||
.main { padding:26px 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<nav class="sidebar">
|
||||
<h2>📘 MetonaToast 文档</h2>
|
||||
<a href="index.html">← 返回首页</a>
|
||||
<a href="demo.html">🎮 在线演示</a>
|
||||
<div class="sidebar-logo"><span class="grad-text">MetonaToast 文档</span></div>
|
||||
<a class="back" href="index.html">← 返回首页</a>
|
||||
<a class="back" href="demo.html">🎮 在线演示</a>
|
||||
<div class="section">基础通知</div>
|
||||
<a href="#show">show()</a>
|
||||
<a href="#success">success()</a>
|
||||
<a href="#error">error()</a>
|
||||
<a href="#warning">warning()</a>
|
||||
<a href="#info">info()</a>
|
||||
<a href="#loading">loading()</a>
|
||||
<a class="snav" href="#show">show()</a>
|
||||
<a class="snav" href="#success">success()</a>
|
||||
<a class="snav" href="#error">error()</a>
|
||||
<a class="snav" href="#warning">warning()</a>
|
||||
<a class="snav" href="#info">info()</a>
|
||||
<a class="snav" href="#loading">loading()</a>
|
||||
<div class="section">高级功能</div>
|
||||
<a href="#promise">promise()</a>
|
||||
<a href="#confirm">confirm()</a>
|
||||
<a href="#prompt">prompt()</a>
|
||||
<a href="#progress">progress()</a>
|
||||
<a href="#countdown">countdown()</a>
|
||||
<a href="#queue">queue()</a>
|
||||
<a href="#stack">stack()</a>
|
||||
<a href="#action">action() <span class="badge badge-new">NEW</span></a>
|
||||
<a class="snav" href="#promise">promise()</a>
|
||||
<a class="snav" href="#confirm">confirm()</a>
|
||||
<a class="snav" href="#prompt">prompt()</a>
|
||||
<a class="snav" href="#progress">progress()</a>
|
||||
<a class="snav" href="#countdown">countdown()</a>
|
||||
<a class="snav" href="#queue">queue()</a>
|
||||
<a class="snav" href="#stack">stack()</a>
|
||||
<a class="snav" href="#action">action()</a>
|
||||
<div class="section">管理方法</div>
|
||||
<a href="#group">group() <span class="badge badge-new">NEW</span></a>
|
||||
<a href="#dismiss">dismiss()</a>
|
||||
<a href="#clear">clear()</a>
|
||||
<a href="#configure">configure()</a>
|
||||
<a href="#use">use()</a>
|
||||
<a href="#destroy">destroy()</a>
|
||||
<div class="section">配置参考</div>
|
||||
<a href="#config">全部配置项</a>
|
||||
<a href="#callbacks">回调函数</a>
|
||||
<a href="#animations">动画列表</a>
|
||||
<a href="#themes">主题参考</a>
|
||||
<a class="snav" href="#group">group()</a>
|
||||
<a class="snav" href="#dismiss">dismiss()</a>
|
||||
<a class="snav" href="#clear">clear()</a>
|
||||
<a class="snav" href="#updatePosition">updatePosition()</a>
|
||||
<a class="snav" href="#remove">remove() / removeToast()</a>
|
||||
<a class="snav" href="#configure">configure()</a>
|
||||
<a class="snav" href="#use">use()</a>
|
||||
<a class="snav" href="#destroy">destroy()</a>
|
||||
<div class="section">扩展</div>
|
||||
<a class="snav" href="#hooks">钩子系统</a>
|
||||
<a class="snav" href="#react">React 适配器</a>
|
||||
<a class="snav" href="#config">全部配置项</a>
|
||||
<a class="snav" href="#callbacks">回调函数</a>
|
||||
<a class="snav" href="#animations">动画列表</a>
|
||||
<a class="snav" href="#themes">主题参考</a>
|
||||
</nav>
|
||||
|
||||
<main class="main">
|
||||
|
||||
<h1>API 文档</h1>
|
||||
<p>MetonaToast v2.0.1 完整 API 参考。所有基础通知方法(show/success/error/warning/info/loading)支持两种调用形式,均可传入任何 <a href="#config">配置项</a> 作为可选第二参数。</p>
|
||||
<p class="h1-desc">MetonaToast v0.5.0 完整 API 参考。所有基础通知方法支持字符串与对象两种调用形式,均可传入任何<a href="#config">配置项</a>作为可选参数。</p>
|
||||
|
||||
<!-- ===== 基础通知 ===== -->
|
||||
<h2 id="show">show(message, opts?)</h2>
|
||||
<p>显示默认类型的 Toast 通知,无特定颜色和图标。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串,或包含 title/message 等属性的配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置对象,覆盖 <a href="#config">全部配置项</a>。仅当 message 为字符串时有效</td></tr>
|
||||
</table>
|
||||
<p>显示默认类型 Toast,无特定颜色与图标。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">show.js</span></div>
|
||||
<pre><span class="cm">// 字符串形式</span>
|
||||
MeToast.<span class="fn">show</span>(<span class="s">'默认消息'</span>);
|
||||
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: 2000, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
|
||||
<span class="cm">// 对象形式(所有 opts 作为一级属性)</span>
|
||||
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: 3000 });</pre>
|
||||
MeToast.<span class="fn">show</span>(<span class="s">'自定义'</span>, { <span class="fn">duration</span>: <span class="num">2000</span>, <span class="fn">position</span>: <span class="s">'bottom-center'</span> });
|
||||
<span class="cm">// 对象形式</span>
|
||||
MeToast.<span class="fn">show</span>({ <span class="fn">title</span>: <span class="s">'标题'</span>, <span class="fn">message</span>: <span class="s">'内容'</span>, <span class="fn">duration</span>: <span class="num">3000</span> });</pre></div>
|
||||
|
||||
<h2 id="success">success(message, opts?)</h2>
|
||||
<p>显示成功通知。绿色对勾图标 <code>#10b981</code>,type = success。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 success</td></tr>
|
||||
</table>
|
||||
<p>绿色对勾图标 <code>#10b981</code>,type = success。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">success.js</span></div>
|
||||
<pre>MeToast.<span class="fn">success</span>(<span class="s">'保存成功!'</span>);
|
||||
MeToast.<span class="fn">success</span>({ <span class="fn">title</span>: <span class="s">'已保存'</span>, <span class="fn">message</span>: <span class="s">'数据已同步'</span> });
|
||||
Met.<span class="fn">success</span>(<span class="s">'MeToast和Met等价'</span>);</pre>
|
||||
Met.<span class="fn">success</span>(<span class="s">'Met 与 MeToast 等价'</span>);</pre></div>
|
||||
|
||||
<h2 id="error">error(message, opts?)</h2>
|
||||
<p>显示错误通知。红色叉号图标 <code>#ef4444</code>,type = error。aria-live 设为 assertive。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 error</td></tr>
|
||||
</table>
|
||||
<p>红色叉号图标 <code>#ef4444</code>,type = error。aria-live = assertive。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">error.js</span></div>
|
||||
<pre>MeToast.<span class="fn">error</span>(<span class="s">'网络错误,请重试'</span>);
|
||||
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre>
|
||||
MeToast.<span class="fn">error</span>({ <span class="fn">title</span>: <span class="s">'提交失败'</span>, <span class="fn">message</span>: <span class="s">'服务器不可达'</span> });</pre></div>
|
||||
|
||||
<h2 id="warning">warning(message, opts?)</h2>
|
||||
<p>显示警告通知。黄色三角图标 <code>#f59e0b</code>,type = warning。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 warning</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre>
|
||||
<p>黄色三角图标 <code>#f59e0b</code>,type = warning。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">warning.js</span></div>
|
||||
<pre>MeToast.<span class="fn">warning</span>(<span class="s">'请注意检查输入内容'</span>);</pre></div>
|
||||
|
||||
<h2 id="info">info(message, opts?)</h2>
|
||||
<p>显示信息通知。蓝色圆形图标 <code>#3b82f6</code>,type = info。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖。type 固定为 info</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre>
|
||||
<p>蓝色圆形图标 <code>#3b82f6</code>,type = info。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">info.js</span></div>
|
||||
<pre>MeToast.<span class="fn">info</span>(<span class="s">'系统将于 22:00 维护'</span>);</pre></div>
|
||||
|
||||
<h2 id="loading">loading(message, opts?)</h2>
|
||||
<p>显示加载状态。type = loading,duration 强制为 0(不自动关闭),closeButton 和 showProgress 强制为 false。返回 LoadingControl 对象。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>加载提示文本或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置覆盖</td></tr>
|
||||
</table>
|
||||
<p>type = loading,duration 强制 0(不自动关闭)。返回 LoadingControl。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">loading.js</span></div>
|
||||
<pre><span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), 2000);</pre>
|
||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre></div>
|
||||
<div class="return-box">
|
||||
<h4>返回 LoadingControl</h4>
|
||||
<h4>LoadingControl(链式转换 id 稳定)</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
|
||||
<tr><td>success</td><td>(msg, opts?) => ToastInstance</td><td>关闭加载 toast,原地替换为 success 类型</td></tr>
|
||||
<tr><td>error</td><td>(msg, opts?) => ToastInstance</td><td>替换为 error 类型</td></tr>
|
||||
<tr><td>info</td><td>(msg, opts?) => ToastInstance</td><td>替换为 info 类型</td></tr>
|
||||
<tr><td>warning</td><td>(msg, opts?) => ToastInstance</td><td>替换为 warning 类型</td></tr>
|
||||
<tr><td>update</td><td>(partial) => MeToast</td><td>不关闭加载 toast,原地更新其内容(支持 resetTimerOnUpdate)</td></tr>
|
||||
<tr><td>dismiss</td><td>()</td><td>直接关闭加载 toast 不替换</td></tr>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>success/error/info/warning</td><td>原地更新为对应类型(同一实例,id 不变),duration 自动恢复默认值</td></tr>
|
||||
<tr><td>update(partial)</td><td>原地更新内容,不改变类型</td></tr>
|
||||
<tr><td>dismiss()</td><td>直接关闭,不转换</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- ===== 高级功能 ===== -->
|
||||
<h2 id="promise">promise(promise, opts)</h2>
|
||||
<p>监听 Promise 生命周期。自动显示 loading → 根据 resolve/reject 自动切换 success/error。返回原 Promise,支持 await 获取结果。</p>
|
||||
<p>监听 Promise 生命周期,自动 loading → resolve/reject 切换 success/error,返回原 Promise。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>promise</td><td>Promise</td><td>—</td><td>要监听的 Promise 对象。非 Promise 会打印错误并返回 rejected Promise</td></tr>
|
||||
<tr><td>opts.loading</td><td>string</td><td>"加载中..."</td><td>加载中显示的文本</td></tr>
|
||||
<tr><td>opts.success</td><td>string</td><td>"操作成功"</td><td>resolve 后显示的文本</td></tr>
|
||||
<tr><td>opts.error</td><td>string</td><td>"操作失败"</td><td>reject 后显示的文本</td></tr>
|
||||
<tr><th>参数</th><th>类型</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>promise</td><td>Promise</td><td>—</td><td>要监听的 Promise。非 Promise 打印错误并返回 rejected Promise</td></tr>
|
||||
<tr><td>opts.loading / success / error</td><td>string</td><td>加载中... / 操作成功 / 操作失败</td><td>各阶段文本</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">promise.js</span></div>
|
||||
<pre><span class="kw">try</span> {
|
||||
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
|
||||
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
|
||||
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成!'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>,
|
||||
});
|
||||
} <span class="kw">catch</span> (<span class="fn">e</span>) { <span class="cm">/* promise reject 会继续抛出 */</span> }</pre>
|
||||
} <span class="kw">catch</span> (e) { <span class="cm">/* reject 会继续抛出 */</span> }</pre></div>
|
||||
|
||||
<h2 id="confirm">confirm(message, opts?)</h2>
|
||||
<p>确认对话框。返回 <code>Promise<boolean></code>。内置 10 秒安全超时,超时自动 resolve(false)。</p>
|
||||
<p>确认对话框,返回 <code>Promise<boolean></code>,内置 10 秒安全超时自动 resolve(false)。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string</td><td>—</td><td>对话框消息。非字符串会打印错误并 resolve(false)</td></tr>
|
||||
<tr><td>opts.confirmText</td><td>string</td><td>"确认"</td><td>确认按钮文字</td></tr>
|
||||
<tr><td>opts.confirmColor</td><td>string</td><td>"#10b981"</td><td>确认按钮背景色</td></tr>
|
||||
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
|
||||
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型(影响图标和颜色)</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>confirmText / confirmColor</td><td>确认 / #10b981</td><td>确认按钮文字与颜色</td></tr>
|
||||
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮文字与颜色</td></tr>
|
||||
<tr><td>type</td><td>warning</td><td>Toast 类型</td></tr>
|
||||
</table>
|
||||
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, {
|
||||
<span class="fn">confirmText</span>: <span class="s">'删除'</span>,
|
||||
<span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span>,
|
||||
<span class="fn">cancelText</span>: <span class="s">'保留'</span>,
|
||||
});
|
||||
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">confirm.js</span></div>
|
||||
<pre><span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span>, <span class="fn">confirmColor</span>: <span class="s">'#ef4444'</span> });
|
||||
<span class="kw">if</span> (ok) MeToast.<span class="fn">success</span>(<span class="s">'已删除'</span>);</pre></div>
|
||||
|
||||
<h2 id="prompt">prompt(message, opts?)</h2>
|
||||
<p>输入对话框。返回 <code>Promise<string|null></code>。按 Enter 或点击提交按钮返回输入值,取消返回 null。内置 10 秒安全超时。</p>
|
||||
<p>输入对话框,返回 <code>Promise<string|null></code>。Enter / 提交返回输入值,取消返回 null,10 秒超时。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string</td><td>—</td><td>对话框消息。非字符串会打印错误并 resolve(null)</td></tr>
|
||||
<tr><td>opts.placeholder</td><td>string</td><td>""</td><td>输入框占位文字</td></tr>
|
||||
<tr><td>opts.defaultValue</td><td>string</td><td>""</td><td>输入框默认值</td></tr>
|
||||
<tr><td>opts.inputType</td><td>string</td><td>"text"</td><td>input 标签 type 属性(如 password/email/number)</td></tr>
|
||||
<tr><td>opts.submitText</td><td>string</td><td>"确认"</td><td>提交按钮文字</td></tr>
|
||||
<tr><td>opts.submitColor</td><td>string</td><td>"#3b82f6"</td><td>提交按钮背景色</td></tr>
|
||||
<tr><td>opts.cancelText</td><td>string</td><td>"取消"</td><td>取消按钮文字</td></tr>
|
||||
<tr><td>opts.cancelColor</td><td>string</td><td>"#6b7280"</td><td>取消按钮背景色</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>"info"</td><td>Toast 类型</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>placeholder / defaultValue</td><td>""</td><td>输入框占位与默认值</td></tr>
|
||||
<tr><td>inputType</td><td>text</td><td>input 的 type(password/email/number 等)</td></tr>
|
||||
<tr><td>submitText / submitColor</td><td>确认 / #3b82f6</td><td>提交按钮</td></tr>
|
||||
<tr><td>cancelText / cancelColor</td><td>取消 / #6b7280</td><td>取消按钮</td></tr>
|
||||
</table>
|
||||
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, {
|
||||
<span class="fn">placeholder</span>: <span class="s">'请输入...'</span>,
|
||||
<span class="fn">defaultValue</span>: <span class="s">'张三'</span>,
|
||||
<span class="fn">submitText</span>: <span class="s">'确定'</span>,
|
||||
});
|
||||
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">prompt.js</span></div>
|
||||
<pre><span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>, { <span class="fn">placeholder</span>: <span class="s">'请输入...'</span> });
|
||||
<span class="kw">if</span> (name) MeToast.<span class="fn">info</span>(<span class="s">'你好,'</span> + name);</pre></div>
|
||||
|
||||
<h2 id="progress">progress(message, opts?)</h2>
|
||||
<p>进度条通知。不自动关闭。返回 ProgressControl 对象以手动更新进度。</p>
|
||||
<p>进度条通知,不自动关闭。返回 ProgressControl。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>进度提示文本或配置对象</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。type 默认 info</td></tr>
|
||||
<tr><td>opts.progressColor</td><td>string</td><td>"#3b82f6"</td><td>进度条填充颜色</td></tr>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>setProgress(percent)</td><td>设置进度 0~100,自动 clamp</td></tr>
|
||||
<tr><td>complete(message?)</td><td>跳 100%,300ms 后转 success,1s 后自动关闭</td></tr>
|
||||
<tr><td>error(message?)</td><td>转 error,2s 后自动关闭</td></tr>
|
||||
<tr><td>dismiss()</td><td>直接关闭</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">progress.js</span></div>
|
||||
<pre><span class="kw">const</span> p = MeToast.<span class="fn">progress</span>(<span class="s">'上传中...'</span>, { <span class="fn">progressColor</span>: <span class="s">'#10b981'</span> });
|
||||
p.<span class="fn">setProgress</span>(45); <span class="cm">// → 45%</span>
|
||||
p.<span class="fn">setProgress</span>(90); <span class="cm">// → 90%</span>
|
||||
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>); <span class="cm">// → 100% → success</span>
|
||||
<span class="cm">// 或</span>
|
||||
p.<span class="fn">error</span>(<span class="s">'上传失败'</span>);
|
||||
p.<span class="fn">dismiss</span>();</pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 ProgressControl</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>签名</th><th>说明</th></tr>
|
||||
<tr><td>setProgress</td><td>(percent: number)</td><td>设置进度 0~100,自动 clamp。更新进度条宽度和百分比文字</td></tr>
|
||||
<tr><td>complete</td><td>(message?: string)</td><td>跳到 100%,300ms 后替换为 success toast,1s 后自动关闭</td></tr>
|
||||
<tr><td>error</td><td>(message?: string)</td><td>替换为 error toast,2s 后自动关闭</td></tr>
|
||||
<tr><td>dismiss</td><td>()</td><td>直接关闭</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
p.<span class="fn">setProgress</span>(<span class="num">45</span>);
|
||||
p.<span class="fn">complete</span>(<span class="s">'上传完成!'</span>);</pre></div>
|
||||
|
||||
<h2 id="countdown">countdown(message, seconds, opts?)</h2>
|
||||
<p>倒计时 Toast。<code>{seconds}</code> 占位符每秒自动替换为剩余秒数。</p>
|
||||
<p>倒计时 Toast,<code>{seconds}</code> 占位符每秒自动替换。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string</td><td>—</td><td>消息文本。支持 <code>{seconds}</code> 占位符。非字符串打印错误并返回空控制对象</td></tr>
|
||||
<tr><td>seconds</td><td>number</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
|
||||
<tr><td>opts.onComplete</td><td>function</td><td>—</td><td>倒计时归零时的回调</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>"warning"</td><td>Toast 类型</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>seconds</td><td>10</td><td>倒计时秒数,最小 1</td></tr>
|
||||
<tr><td>opts.onComplete</td><td>—</td><td>归零回调</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, 5, {
|
||||
<span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>),
|
||||
});</pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 CountdownControl</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>cancel()</td><td>清除计时器并关闭 toast</td></tr>
|
||||
<tr><td>pause()</td><td>暂停倒计时</td></tr>
|
||||
<tr><td>resume()</td><td>恢复倒计时</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">countdown.js</span></div>
|
||||
<pre>MeToast.<span class="fn">countdown</span>(<span class="s">'{seconds} 秒后执行'</span>, <span class="num">5</span>, { <span class="fn">onComplete</span>: () => MeToast.<span class="fn">success</span>(<span class="s">'已执行'</span>) });
|
||||
<span class="cm">// 返回 { cancel(), pause(), resume() }</span></pre></div>
|
||||
|
||||
<h2 id="queue">queue(messages, opts?)</h2>
|
||||
<p>顺序逐个显示消息队列。前一条关闭后延时显示下一条。返回 QueueControl(thenable + cancel)。</p>
|
||||
<p>顺序逐个显示消息队列,返回 thenable + cancel。支持 await。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>messages</td><td>Array<string|object></td><td>—</td><td>消息数组。可为字符串或带 message/type/duration/onClose 的对象。非数组打印错误并返回空控制对象</td></tr>
|
||||
<tr><td>opts.delay</td><td>number</td><td>1000</td><td>每条消息关闭后到显示下一条的间隔(ms)</td></tr>
|
||||
<tr><td>opts.duration</td><td>number</td><td>3000</td><td>每条消息显示时长(ms),可被消息级 duration 覆盖</td></tr>
|
||||
<tr><td>opts.onClose</td><td>function</td><td>—</td><td>全部队列完成后的回调。消息级 onClose 和队列级 onClose 都会依次调用</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>—</td><td>默认类型</td></tr>
|
||||
<tr><th>参数</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>opts.delay</td><td>1000</td><td>每条关闭后到下一条的间隔(ms)</td></tr>
|
||||
<tr><td>opts.duration</td><td>3000</td><td>每条显示时长,可被消息级 duration 覆盖</td></tr>
|
||||
</table>
|
||||
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([
|
||||
<span class="s">'步骤一'</span>,
|
||||
{ <span class="fn">message</span>: <span class="s">'步骤二'</span>, <span class="fn">duration</span>: 5000, <span class="fn">type</span>: <span class="s">'warning'</span> },
|
||||
<span class="s">'步骤三'</span>,
|
||||
], { <span class="fn">delay</span>: 800, <span class="fn">duration</span>: 2000, <span class="fn">type</span>: <span class="s">'info'</span> });
|
||||
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
||||
<span class="kw">await</span> q; <span class="cm">// 等待完成(thenable 支持 await)</span></pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 QueueControl (thenable)</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>.then(fn, rj)</td><td>Promise.then 代理,支持 await</td></tr>
|
||||
<tr><td>.catch(rj)</td><td>Promise.catch 代理</td></tr>
|
||||
<tr><td>.cancel()</td><td>设置取消标志,不再显示下一条消息</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">queue.js</span></div>
|
||||
<pre><span class="kw">const</span> q = MeToast.<span class="fn">queue</span>([<span class="s">'步骤一'</span>, <span class="s">'步骤二'</span>, <span class="s">'步骤三'</span>], { <span class="fn">delay</span>: <span class="num">800</span>, <span class="fn">duration</span>: <span class="num">2000</span> });
|
||||
q.<span class="fn">cancel</span>(); <span class="cm">// 中途取消</span>
|
||||
<span class="kw">await</span> q; <span class="cm">// 等待完成</span></pre></div>
|
||||
|
||||
<h2 id="stack">stack(messages, opts?)</h2>
|
||||
<p>同时错峰显示多条消息。每条间隔 stagger 毫秒依次出现,全部叠加在屏幕上。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>messages</td><td>Array<string|object></td><td>—</td><td>消息数组。可为字符串或带 message/type/duration 的对象。非数组打印错误</td></tr>
|
||||
<tr><td>opts.stagger</td><td>number</td><td>100</td><td>每条消息之间的显示间隔(ms)</td></tr>
|
||||
<tr><td>opts.type</td><td>string</td><td>—</td><td>默认类型</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], {
|
||||
<span class="fn">stagger</span>: 150,
|
||||
<span class="fn">type</span>: <span class="s">'info'</span>,
|
||||
});</pre>
|
||||
<p>同时错峰显示多条消息。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">stack.js</span></div>
|
||||
<pre>MeToast.<span class="fn">stack</span>([<span class="s">'消息1'</span>, <span class="s">'消息2'</span>, { <span class="fn">message</span>: <span class="s">'警告'</span>, <span class="fn">type</span>: <span class="s">'warning'</span> }], { <span class="fn">stagger</span>: <span class="num">150</span> });</pre></div>
|
||||
|
||||
<h2 id="action">action(message, actions, opts?) <span class="badge badge-new">v2.0</span></h2>
|
||||
<p>Action Toast:内嵌操作按钮。默认 duration=0 不自动关闭,closeButton=true。</p>
|
||||
<h2 id="action">action(message, actions, opts?)</h2>
|
||||
<p>内嵌操作按钮。默认 duration=0 不自动关闭。点击按钮不会误触 closeOnClick(已隔离冒泡)。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>message</td><td>string | object</td><td>—</td><td>消息字符串或配置对象</td></tr>
|
||||
<tr><td>actions</td><td>ActionButton[]</td><td>[]</td><td>按钮数组。每个按钮可配 text/onClick/color/style/close</td></tr>
|
||||
<tr><td>opts</td><td>object</td><td>{}</td><td>可选配置。duration 默认 0,closeButton 默认 true</td></tr>
|
||||
</table>
|
||||
<table>
|
||||
<tr><th>ActionButton 字段</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>text</td><td>string</td><td>—(必填)</td><td>按钮显示文字</td></tr>
|
||||
<tr><td>onClick</td><td>(toast) => void</td><td>—(必填)</td><td>点击回调函数,参数为当前 toast 实例</td></tr>
|
||||
<tr><td>color</td><td>string</td><td>"#6366f1"</td><td>按钮背景色</td></tr>
|
||||
<tr><td>style</td><td>object</td><td>{}</td><td>按钮内联样式,可覆盖 color(style.background 优先)</td></tr>
|
||||
<tr><td>close</td><td>boolean</td><td>true</td><td>点击后是否自动关闭 toast。设为 false 可多次点击</td></tr>
|
||||
<tr><th>ActionButton</th><th>默认</th><th>说明</th></tr>
|
||||
<tr><td>text</td><td>—(必填)</td><td>按钮文字</td></tr>
|
||||
<tr><td>onClick</td><td>—(必填)</td><td>点击回调,参数为当前 toast</td></tr>
|
||||
<tr><td>color / style</td><td>#6366f1</td><td>背景色 / 内联样式(style.background 优先)</td></tr>
|
||||
<tr><td>close</td><td>true</td><td>点击后是否关闭。false 可多次点击</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">action.js</span></div>
|
||||
<pre>MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
||||
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>(), <span class="fn">color</span>: <span class="s">'#3b82f6'</span> },
|
||||
{ <span class="fn">text</span>: <span class="s">'查看详情'</span>, <span class="fn">onClick</span>: (t) => <span class="fn">openFile</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: false },
|
||||
]);</pre>
|
||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">color</span>: <span class="s">'#10b981'</span>, <span class="fn">close</span>: <span class="kw">false</span> },
|
||||
]);</pre></div>
|
||||
|
||||
<!-- ===== 管理方法 ===== -->
|
||||
<h2 id="group">group(name) <span class="badge badge-new">v2.0</span></h2>
|
||||
<p>创建 Toast 分组,返回 GroupAPI 对象。该对象所有方法自动传入 <code>group: name</code>,按组管理。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>name</td><td>string</td><td>分组名称。GroupAPI 和 dismissGroup 通过此名称关联</td></tr>
|
||||
</table>
|
||||
<h2 id="group">group(name)</h2>
|
||||
<p>创建分组,返回 GroupAPI。所有方法自动注入 <code>group: name</code>。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">group.js</span></div>
|
||||
<pre><span class="kw">const</span> orders = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
||||
orders.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
||||
orders.<span class="fn">error</span>(<span class="s">'支付失败'</span>, { <span class="fn">duration</span>: 5000 });
|
||||
orders.<span class="fn">count</span>(); <span class="cm">// 该组当前 toast 数量</span>
|
||||
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭该组全部 toast</span>
|
||||
<span class="cm">// 也支持主对象关闭</span>
|
||||
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre>
|
||||
<div class="return-box">
|
||||
<h4>返回 GroupAPI</h4>
|
||||
<table>
|
||||
<tr><th>方法</th><th>说明</th></tr>
|
||||
<tr><td>show(msg, opts?)</td><td>等价 MeToast.show,自动注入 group</td></tr>
|
||||
<tr><td>success(msg, opts?)</td><td>等价 MeToast.success</td></tr>
|
||||
<tr><td>error(msg, opts?)</td><td>等价 MeToast.error</td></tr>
|
||||
<tr><td>warning(msg, opts?)</td><td>等价 MeToast.warning</td></tr>
|
||||
<tr><td>info(msg, opts?)</td><td>等价 MeToast.info</td></tr>
|
||||
<tr><td>loading(msg, opts?)</td><td>等价 MeToast.loading</td></tr>
|
||||
<tr><td>action(msg, actions, opts?)</td><td>等价 MeToast.action</td></tr>
|
||||
<tr><td>dismiss()</td><td>关闭该组所有 toast</td></tr>
|
||||
<tr><td>count()</td><td>返回该组当前 toast 数量</td></tr>
|
||||
</table>
|
||||
</div>
|
||||
orders.<span class="fn">count</span>(); <span class="cm">// 该组数量</span>
|
||||
orders.<span class="fn">dismiss</span>(); <span class="cm">// 关闭整组</span>
|
||||
MeToast.<span class="fn">dismissGroup</span>(<span class="s">'orders'</span>);</pre></div>
|
||||
|
||||
<h2 id="dismiss">dismiss(id?)</h2>
|
||||
<p>关闭 Toast。无参数则关闭全部。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>id</td><td>string</td><td>可选。Toast 的 id,不传则关闭所有</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 关闭所有</span>
|
||||
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 关闭指定</span></pre>
|
||||
<p>关闭 Toast。无参数关闭全部,传入 id 关闭指定。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">dismiss.js</span></div>
|
||||
<pre>MeToast.<span class="fn">dismiss</span>(); <span class="cm">// 全部</span>
|
||||
MeToast.<span class="fn">dismiss</span>(toast.id); <span class="cm">// 指定</span></pre></div>
|
||||
|
||||
<h2 id="clear">clear(position?)</h2>
|
||||
<p>按位置清除 Toast。不传参数清除所有位置。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>position</td><td>string</td><td>可选。位置如 'top-right',不传则清除全部</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
|
||||
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre>
|
||||
<p>按位置清除。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">clear.js</span></div>
|
||||
<pre>MeToast.<span class="fn">clear</span>(); <span class="cm">// 全部</span>
|
||||
MeToast.<span class="fn">clear</span>(<span class="s">'bottom-right'</span>); <span class="cm">// 仅右下角</span></pre></div>
|
||||
|
||||
<h2 id="updatePosition">updatePosition(position) <span class="badge badge-v5">v0.3</span></h2>
|
||||
<p>运行时将 Toast 移动到新位置容器,立即生效。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">position.js</span></div>
|
||||
<pre><span class="kw">const</span> t = MeToast.<span class="fn">info</span>(<span class="s">'可移动的 Toast'</span>);
|
||||
t.<span class="fn">updatePosition</span>(<span class="s">'bottom-left'</span>);</pre></div>
|
||||
|
||||
<h2 id="remove">remove() / removeToast(id) <span class="badge badge-new">立即移除</span></h2>
|
||||
<p>立即从 DOM 和内存移除,<b>不触发离场动画</b>。适用于无动画快速清除。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">remove.js</span></div>
|
||||
<pre>t.<span class="fn">remove</span>(); <span class="cm">// 实例方法</span>
|
||||
MeToast.<span class="fn">removeToast</span>(t.id); <span class="cm">// 按 id(等价)</span></pre></div>
|
||||
|
||||
<h2 id="configure">configure(opts)</h2>
|
||||
<p>全局配置,影响后续所有 Toast。theme 和 locale 变化会触发相应副作用(应用主题 CSS / 切换语言)。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>opts</td><td>object</td><td>包含任意 <a href="#config">配置项</a> 的对象</td></tr>
|
||||
</table>
|
||||
<p>全局配置,影响后续所有 Toast。theme/locale 变化触发副作用。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">configure.js</span></div>
|
||||
<pre>MeToast.<span class="fn">configure</span>({
|
||||
<span class="fn">position</span>: <span class="s">'top-right'</span>,
|
||||
<span class="fn">duration</span>: 4000,
|
||||
<span class="fn">theme</span>: <span class="s">'dark'</span>,
|
||||
<span class="fn">animation</span>: <span class="s">'slide'</span>,
|
||||
<span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
|
||||
});</pre>
|
||||
<span class="fn">position</span>: <span class="s">'top-right'</span>, <span class="fn">duration</span>: <span class="num">4000</span>, <span class="fn">theme</span>: <span class="s">'dark'</span>,
|
||||
<span class="fn">animation</span>: <span class="s">'slide'</span>, <span class="fn">locale</span>: <span class="s">'zh-CN'</span>,
|
||||
});</pre></div>
|
||||
|
||||
<h2 id="use">use(plugin)</h2>
|
||||
<p>安装插件。支持字符串(内置预设名)或插件对象。已被拒绝注册的无效插件会打印错误。</p>
|
||||
<table>
|
||||
<tr><th>参数</th><th>类型</th><th>说明</th></tr>
|
||||
<tr><td>plugin</td><td>string | object</td><td>预设名 'keyboard'/'persistence'/'accessibility' 或自定义插件对象 { name, version?, install, uninstall? }</td></tr>
|
||||
</table>
|
||||
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有 Toast</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置自动保存到 localStorage</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器实时朗读 toast 内容</span></pre>
|
||||
<p class="hint">内置插件详情:keyboard 监听 keydown Escape 键;persistence 将配置写入 localStorage 并在页面加载时恢复;accessibility 在 afterShow/afterUpdate 钩子中通过 aria-live 区域朗读 toast 内容。</p>
|
||||
<p>安装插件:字符串预设名或插件对象。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">use.js</span></div>
|
||||
<pre>MeToast.<span class="fn">use</span>(<span class="s">'keyboard'</span>); <span class="cm">// ESC 关闭所有</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'persistence'</span>); <span class="cm">// 配置持久化 localStorage</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <span class="cm">// 屏幕阅读器朗读</span>
|
||||
MeToast.<span class="fn">use</span>(<span class="s">'dedupe'</span>); <span class="cm">// 相同 type+message 自动去重</span></pre></div>
|
||||
<p class="hint">dedupe:检测已有相同 toast,更新它并阻止重复弹出(beforeShow 钩子实现,支持 uninstall 卸载钩子)。</p>
|
||||
|
||||
<h2 id="destroy">destroy()</h2>
|
||||
<p>完全销毁。关闭所有 Toast、移除所有 DOM 容器和注入的样式标签、清空内存缓存。</p>
|
||||
<pre>MeToast.<span class="fn">destroy</span>();</pre>
|
||||
<p>完全销毁:关闭所有 Toast、移除全部 DOM 容器与注入样式、清空内存缓存与钩子。支持重复调用,<code>init()</code> 可恢复。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">destroy.js</span></div>
|
||||
<pre>MeToast.<span class="fn">destroy</span>();
|
||||
MeToast.<span class="fn">init</span>({ <span class="fn">config</span>: { <span class="fn">duration</span>: <span class="num">3000</span> } }); <span class="cm">// 恢复</span></pre></div>
|
||||
|
||||
<!-- ===== 钩子系统 ===== -->
|
||||
<h2 id="hooks">钩子系统 <span class="badge badge-v5">v0.3</span></h2>
|
||||
<p><code>Toast.on(name, handler)</code> 注册钩子并返回取消函数。<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 的 handler 返回 <code>false</code> 可拦截对应操作。</p>
|
||||
<table>
|
||||
<tr><th>钩子</th><th>触发时机</th><th>拦截</th></tr>
|
||||
<tr><td>beforeInit / afterInit</td><td>init() 前后</td><td>—</td></tr>
|
||||
<tr><td>beforeDestroy / afterDestroy</td><td>destroy() 前后</td><td>—</td></tr>
|
||||
<tr><td>beforeShow / afterShow</td><td>显示前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||
<tr><td>beforeClose / afterClose</td><td>关闭前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||
<tr><td>beforeUpdate / afterUpdate</td><td>update() 前后</td><td>✅ 返回 false 阻止</td></tr>
|
||||
<tr><td>configChange</td><td>configure / updateConfig / resetConfig</td><td>—</td></tr>
|
||||
<tr><td>themeChange / localeChange</td><td>主题 / 语言切换</td><td>—</td></tr>
|
||||
<tr><td>click / hover</td><td>点击 / 悬停进出</td><td>—</td></tr>
|
||||
<tr><td>dragStart / dragEnd</td><td>拖拽开始 / 结束</td><td>—</td></tr>
|
||||
<tr><td>animationStart / animationEnd</td><td>入场动画开始 / 结束</td><td>—</td></tr>
|
||||
<tr><td>progressStart / progressEnd</td><td>倒计时开始 / 归零</td><td>—</td></tr>
|
||||
</table>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">hooks.js</span></div>
|
||||
<pre><span class="kw">import</span> MeToast, { <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast'</span>;
|
||||
|
||||
<span class="cm">// 拦截:非允许时段禁止错误提示</span>
|
||||
<span class="kw">const</span> off = <span class="fn">Toast</span>.<span class="fn">on</span>(<span class="s">'beforeShow'</span>, (toast) => {
|
||||
<span class="kw">if</span> (toast.type === <span class="s">'error'</span> && !isAllowed) <span class="kw">return</span> <span class="kw">false</span>;
|
||||
});
|
||||
<span class="cm">// ...</span>
|
||||
off(); <span class="cm">// 取消注册</span></pre></div>
|
||||
|
||||
<!-- ===== React ===== -->
|
||||
<h2 id="react">React 适配器 <span class="badge badge-v5">v0.4</span></h2>
|
||||
<p>主包保持零依赖。通过子路径 <code>@metona-team/metona-toast/react</code> 导入,<code>react</code> 为 optional peerDependency。</p>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">react.tsx</span></div>
|
||||
<pre><span class="kw">import</span> { <span class="fn">useToast</span>, <span class="fn">Toast</span> } <span class="kw">from</span> <span class="s">'@metona-team/metona-toast/react'</span>;
|
||||
|
||||
<span class="kw">function</span> <span class="fn">SubmitButton</span>() {
|
||||
<span class="kw">const</span> toast = <span class="fn">useToast</span>(); <span class="cm">// 组件卸载自动清理本组件创建的 Toast</span>
|
||||
<span class="kw">const</span> submit = <span class="kw">async</span> () => {
|
||||
<span class="kw">const</span> loading = toast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||
<span class="kw">try</span> { <span class="kw">await</span> api(); loading.<span class="fn">success</span>(<span class="s">'完成!'</span>); }
|
||||
<span class="kw">catch</span> (e) { loading.<span class="fn">error</span>(<span class="s">'失败'</span>); }
|
||||
};
|
||||
<span class="kw">return</span> <button onClick={submit}>提交</button>;
|
||||
}
|
||||
|
||||
<span class="cm">// 声明式 Toast:props 变化更新,卸载自动移除(autoClose 默认 true)</span>
|
||||
<span class="kw">function</span> <span class="fn">SaveIndicator</span>({ saving }) {
|
||||
<span class="kw">return</span> saving ? <<span class="fn">Toast</span> type=<span class="s">"info"</span> message=<span class="s">"正在保存..."</span> /> : null;
|
||||
}</pre></div>
|
||||
|
||||
<!-- ===== 配置参考 ===== -->
|
||||
<h2 id="config">全部配置项</h2>
|
||||
<p>以下配置可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts 参数。带 <span class="badge badge-new">v2.0</span> 标识的为 v2.0 新增。</p>
|
||||
<p>可用于 <code>configure()</code>、<code>init()</code> 或单个 Toast 方法的 opts。</p>
|
||||
<table>
|
||||
<tr><th>配置项</th><th>类型</th><th>默认值</th><th>说明</th></tr>
|
||||
<tr><td>position</td><td>string</td><td>'top-right'</td><td>位置:top-left / top-center / top-right / bottom-left / bottom-center / bottom-right</td></tr>
|
||||
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms),0=不自动关闭</td></tr>
|
||||
<tr><td>max</td><td>number</td><td>6</td><td>同一位置最多同时显示条数,超出则关闭最早的</td></tr>
|
||||
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 之间的间距(px)</td></tr>
|
||||
<tr><td>offset</td><td>number</td><td>24</td><td>容器到屏幕边缘的距离(px)</td></tr>
|
||||
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>鼠标悬停时暂停 duration 倒计时和进度条</td></tr>
|
||||
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击 Toast 任意位置关闭。关闭按钮始终触发关闭</td></tr>
|
||||
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭。拖拽超过 120px 触发关闭</td></tr>
|
||||
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示 duration 倒计时进度条</td></tr>
|
||||
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>进度条方向:horizontal(底部水平) / vertical(右侧垂直)</td></tr>
|
||||
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型对应图标(success→对勾等)</td></tr>
|
||||
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示右上角关闭 × 按钮</td></tr>
|
||||
<tr><td>theme</td><td>string</td><td>'auto'</td><td>主题:light / dark / auto(跟随系统)/ warm / 自定义注册名</td></tr>
|
||||
<tr><td>animation</td><td>string</td><td>'slide'</td><td>入场动画名称,支持 CSS 动画列表见下</td></tr>
|
||||
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 CSS z-index</td></tr>
|
||||
<tr><td>width</td><td>number|string</td><td>360</td><td>Toast 宽度。数字表示 px</td></tr>
|
||||
<tr><td>className</td><td>string</td><td>''</td><td>附加到 Toast 元素上的 CSS 类名</td></tr>
|
||||
<tr><td>style</td><td>object</td><td>{}</td><td>附加到 Toast 元素上的内联样式对象</td></tr>
|
||||
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码(zh-CN / en-US 等)</td></tr>
|
||||
<tr><td>resetTimerOnUpdate <span class="badge badge-new">v2.0</span></td><td>boolean</td><td>false</td><td>调用 update() 时重置 duration 倒计时</td></tr>
|
||||
<tr><td>notifyWhenHidden <span class="badge badge-new">v2.0</span></td><td>boolean</td><td>false</td><td>页面不可见时自动通过 Notification API 发送系统通知</td></tr>
|
||||
<tr><td>render <span class="badge badge-new">v2.0</span></td><td>function</td><td>—</td><td>自定义渲染函数 <code>(toast) => htmlString</code>,完全接管 DOM 构建</td></tr>
|
||||
<tr><td>position</td><td>string</td><td>'top-right'</td><td>6 个位置之一(RTL 自动翻转)</td></tr>
|
||||
<tr><td>duration</td><td>number</td><td>4000</td><td>显示时长(ms),0 = 不自动关闭</td></tr>
|
||||
<tr><td>max</td><td>number</td><td>6</td><td>同位置最多条数,超出真正关闭最早的</td></tr>
|
||||
<tr><td>gap</td><td>number</td><td>12</td><td>Toast 间距(px)</td></tr>
|
||||
<tr><td>offset</td><td>number</td><td>24</td><td>容器距屏幕边缘(px)</td></tr>
|
||||
<tr><td>pauseOnHover</td><td>boolean</td><td>true</td><td>悬停暂停倒计时</td></tr>
|
||||
<tr><td>closeOnClick</td><td>boolean</td><td>true</td><td>点击关闭</td></tr>
|
||||
<tr><td>draggable</td><td>boolean</td><td>true</td><td>允许拖拽关闭</td></tr>
|
||||
<tr><td>dragThreshold</td><td>number</td><td>120</td><td>拖拽关闭阈值(px)</td></tr>
|
||||
<tr><td>showProgress</td><td>boolean</td><td>true</td><td>显示倒计时进度条</td></tr>
|
||||
<tr><td>progressDirection</td><td>string</td><td>'horizontal'</td><td>horizontal / vertical</td></tr>
|
||||
<tr><td>icon</td><td>boolean</td><td>true</td><td>显示类型图标</td></tr>
|
||||
<tr><td>closeButton</td><td>boolean</td><td>true</td><td>显示关闭 × 按钮</td></tr>
|
||||
<tr><td>theme</td><td>string</td><td>'auto'</td><td>light / dark / auto / warm / 自定义名</td></tr>
|
||||
<tr><td>animation</td><td>string</td><td>'slide'</td><td>11 种内置或自定义动画名</td></tr>
|
||||
<tr><td>zIndex</td><td>number</td><td>9999</td><td>容器 z-index</td></tr>
|
||||
<tr><td>width</td><td>number|string</td><td>360</td><td>宽度,数字表示 px</td></tr>
|
||||
<tr><td>className</td><td>string</td><td>''</td><td>附加 CSS 类名</td></tr>
|
||||
<tr><td>style</td><td>object</td><td>{}</td><td>附加内联样式</td></tr>
|
||||
<tr><td>locale</td><td>string</td><td>'zh-CN'</td><td>语言代码</td></tr>
|
||||
<tr><td>resetTimerOnUpdate</td><td>boolean</td><td>false</td><td>update() 时重置倒计时</td></tr>
|
||||
<tr><td>notifyWhenHidden</td><td>boolean</td><td>false</td><td>页面不可见时发系统通知</td></tr>
|
||||
<tr><td>render</td><td>function</td><td>—</td><td>自定义渲染函数,完全接管 DOM</td></tr>
|
||||
<tr><td>onBeforeShow</td><td>function</td><td>—</td><td>返回 false 阻止显示</td></tr>
|
||||
<tr><td>onError</td><td>function</td><td>—</td><td>钩子/定时器异常全局回调</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="callbacks">回调函数</h2>
|
||||
<table>
|
||||
<tr><th>回调</th><th>签名</th><th>触发时机</th></tr>
|
||||
<tr><td>onShow</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 创建并播放入场动画后</td></tr>
|
||||
<tr><td>onClose</td><td>(toast: ToastInstance) => void</td><td>Toast DOM 被移除后(离场动画完成时)</td></tr>
|
||||
<tr><td>onClick</td><td>(toast: ToastInstance) => void</td><td>Toast 被点击时(closeOnClick 为 true 时还会自动关闭)</td></tr>
|
||||
<tr><td>onUpdate <span class="badge badge-new">v2.0</span></td><td>(toast: ToastInstance) => void</td><td>Toast 内容通过 update() 更新后</td></tr>
|
||||
<tr><td>onBeforeShow</td><td>(toast) => boolean | void</td><td>DOM 创建前,返回 false 阻止显示</td></tr>
|
||||
<tr><td>onShow</td><td>(toast) => void</td><td>创建并播放入场动画后</td></tr>
|
||||
<tr><td>onClose</td><td>(toast) => void</td><td>DOM 移除后(离场完成)</td></tr>
|
||||
<tr><td>onClick</td><td>(toast) => void</td><td>点击时(closeOnClick=true 时还会关闭)</td></tr>
|
||||
<tr><td>onUpdate</td><td>(toast) => void</td><td>update() 后</td></tr>
|
||||
<tr><td>onError</td><td>({ hook, source, error, toast }) => void</td><td>钩子/定时器异常时</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="animations">动画列表</h2>
|
||||
<p>配置 <code>animation</code> 的值,支持以下 CSS 动画。未在此列表中的值将 fallback 到 slide。</p>
|
||||
<p>未注册的动画名自动 fallback 到 slide。</p>
|
||||
<table>
|
||||
<tr><th>名称</th><th>效果</th><th>时长</th></tr>
|
||||
<tr><td>slide</td><td>从右侧滑入 + 回弹</td><td>400ms</td></tr>
|
||||
@@ -456,20 +452,13 @@ MeToast.<span class="fn">use</span>(<span class="s">'accessibility'</span>); <sp
|
||||
<tr><td>flip</td><td>3D 翻转入场 + 回摆</td><td>500ms</td></tr>
|
||||
<tr><td>rotate</td><td>旋转摇摆进入</td><td>500ms</td></tr>
|
||||
<tr><td>zoom</td><td>从中心爆发式弹出</td><td>500ms</td></tr>
|
||||
<tr><td>slideUp</td><td>从下方弹入</td><td>400ms</td></tr>
|
||||
<tr><td>slideDown</td><td>从上方弹入</td><td>400ms</td></tr>
|
||||
<tr><td>slideLeft</td><td>从左侧滑入</td><td>400ms</td></tr>
|
||||
<tr><td>slideRight</td><td>从右侧滑入</td><td>400ms</td></tr>
|
||||
<tr><td>slideUp / slideDown</td><td>从下方 / 上方弹入</td><td>400ms</td></tr>
|
||||
<tr><td>slideLeft / slideRight</td><td>从左侧 / 右侧滑入</td><td>400ms</td></tr>
|
||||
</table>
|
||||
|
||||
<h2 id="themes">主题参考</h2>
|
||||
<pre><span class="cm">// 内置主题</span>
|
||||
light <span class="cm">— 白色玻璃质感</span>
|
||||
dark <span class="cm">— 深色玻璃质感</span>
|
||||
auto <span class="cm">— 跟随系统主题设置</span>
|
||||
warm <span class="cm">— 暖色调</span>
|
||||
|
||||
<span class="cm">// 注册自定义主题</span>
|
||||
<div class="terminal"><div class="terminal-bar"><span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span><span class="t-title">theme.js</span></div>
|
||||
<pre><span class="cm">// 内置主题:light · dark · auto(跟随系统)· warm</span>
|
||||
MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</span>, {
|
||||
<span class="fn">bg</span>: <span class="s">'rgba(240,249,255,0.96)'</span>,
|
||||
<span class="fn">text</span>: <span class="s">'#0c4a6e'</span>,
|
||||
@@ -479,19 +468,38 @@ MeToast.themes.<span class="fn">registerTheme</span>(<span class="s">'ocean'</sp
|
||||
<span class="fn">progressBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
||||
<span class="fn">closeHoverBg</span>: <span class="s">'rgba(14,165,233,0.1)'</span>,
|
||||
});
|
||||
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre>
|
||||
MeToast.themes.<span class="fn">switchTheme</span>(<span class="s">'ocean'</span>);</pre></div>
|
||||
|
||||
</main>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
<script>
|
||||
MeToast.configure({ position: 'top-right', duration: 4000, theme: 'dark', animation: 'slide' });
|
||||
document.querySelectorAll('.sidebar a[href^="#"]').forEach(a => {
|
||||
a.addEventListener('click', () => {
|
||||
document.querySelectorAll('.sidebar a').forEach(x => x.classList.remove('active'));
|
||||
a.classList.add('active');
|
||||
});
|
||||
|
||||
// 侧边栏滚动高亮
|
||||
const links = document.querySelectorAll('.sidebar a.snav');
|
||||
const sections = [];
|
||||
links.forEach(l => {
|
||||
const id = l.getAttribute('href').slice(1);
|
||||
const el = document.getElementById(id);
|
||||
if (el) sections.push({ id, el, link: l });
|
||||
});
|
||||
const spy = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
links.forEach(l => l.classList.remove('active'));
|
||||
const hit = sections.find(s => s.el === e.target);
|
||||
if (hit) hit.link.classList.add('active');
|
||||
}
|
||||
});
|
||||
}, { rootMargin: '-15% 0px -70% 0px' });
|
||||
sections.forEach(s => spy.observe(s.el));
|
||||
|
||||
// 点击关闭高亮
|
||||
links.forEach(l => l.addEventListener('click', () => {
|
||||
links.forEach(x => x.classList.remove('active'));
|
||||
l.classList.add('active');
|
||||
}));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
+375
-156
@@ -5,62 +5,231 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<title>MetonaToast — 轻量精致的 Toast 通知库</title>
|
||||
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。80+ 图标类型、11 种动画、主题系统、国际化、插件架构。">
|
||||
<meta name="description" content="MetonaToast 是轻量、零依赖、精致美观的 Toast 通知库。107 种图标、11 种动画、主题系统、国际化、插件架构、React 适配器。">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0b1121; --bg2: #111827; --surface: #1a2332;
|
||||
--border: #1e2d4d; --text: #e2e8f0; --muted: #64748b;
|
||||
--accent: #6366f1; --accent2: #818cf8; --radius: 14px;
|
||||
--bg: #070b14;
|
||||
--surface: rgba(255,255,255,0.03);
|
||||
--surface-hover: rgba(255,255,255,0.06);
|
||||
--border: rgba(255,255,255,0.08);
|
||||
--text: #e8ecf4;
|
||||
--muted: #8b94a7;
|
||||
--accent1: #6366f1;
|
||||
--accent2: #a855f7;
|
||||
--accent3: #ec4899;
|
||||
--grad: linear-gradient(135deg, #818cf8, #c084fc, #f472b6);
|
||||
--radius: 16px;
|
||||
}
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif; background: var(--bg); color: var(--text); line-height: 1.7; }
|
||||
a { color: var(--accent2); text-decoration: none; }
|
||||
code { background: rgba(99,102,241,0.12); color: #a5b4fc; padding: 2px 7px; border-radius: 5px; font-size: 0.9em; }
|
||||
pre { background: #0a0f1a; border: 1px solid var(--border); border-radius: 10px; padding: 18px 22px; overflow-x: auto; font: 13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace; color: #cbd5e1; }
|
||||
pre .kw { color: #c084fc; } pre .fn { color: #60a5fa; } pre .s { color: #34d399; } pre .cm { color: #475569; }
|
||||
* { margin:0; padding:0; box-sizing:border-box; }
|
||||
html { scroll-behavior:smooth; }
|
||||
body {
|
||||
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','PingFang SC','Microsoft YaHei',sans-serif;
|
||||
background:var(--bg); color:var(--text); line-height:1.7; overflow-x:hidden;
|
||||
background-image:
|
||||
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(99,102,241,0.15), transparent),
|
||||
radial-gradient(ellipse 60% 40% at 90% 10%, rgba(168,85,247,0.08), transparent),
|
||||
radial-gradient(ellipse 50% 40% at 10% 30%, rgba(236,72,153,0.06), transparent);
|
||||
}
|
||||
/* 背景网格 */
|
||||
body::before {
|
||||
content:''; position:fixed; inset:0; pointer-events:none; z-index:0;
|
||||
background-image:
|
||||
linear-gradient(rgba(255,255,255,0.025) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(255,255,255,0.025) 1px, transparent 1px);
|
||||
background-size:56px 56px;
|
||||
mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
|
||||
-webkit-mask-image: radial-gradient(ellipse 90% 70% at 50% 0%, black 40%, transparent 100%);
|
||||
}
|
||||
a { color:#a5b4fc; text-decoration:none; transition:color .2s; }
|
||||
a:hover { color:#c7d2fe; }
|
||||
::selection { background:rgba(139,92,246,.4); }
|
||||
|
||||
.nav { position: sticky; top: 0; z-index: 100; background: rgba(11,17,33,0.88); backdrop-filter: blur(14px); border-bottom: 1px solid var(--border); }
|
||||
.nav-inner { max-width: 1200px; margin: 0 auto; display: flex; align-items: center; justify-content: space-between; padding: 0 24px; height: 56px; }
|
||||
.nav-logo { font-size: 18px; font-weight: 700; background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
||||
.nav-links { display: flex; gap: 24px; }
|
||||
.nav-links a { color: var(--muted); font-size: 14px; font-weight: 500; transition: .2s; }
|
||||
.nav-links a:hover { color: var(--accent2); }
|
||||
.grad-text {
|
||||
background:var(--grad);
|
||||
-webkit-background-clip:text; background-clip:text;
|
||||
-webkit-text-fill-color:transparent; color:transparent;
|
||||
}
|
||||
|
||||
.hero { text-align: center; padding: 100px 20px 70px; background: radial-gradient(ellipse at 50% 0%, rgba(99,102,241,0.18) 0%, transparent 60%), linear-gradient(180deg, #0f172a 0%, var(--bg) 100%); border-bottom: 1px solid var(--border); }
|
||||
.hero h1 { font-size: 52px; font-weight: 800; }
|
||||
.hero h1 span { background: linear-gradient(135deg, #818cf8, #c084fc, #f472b6); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
||||
.hero p { color: var(--muted); font-size: 17px; margin-top: 12px; max-width: 600px; margin-left: auto; margin-right: auto; }
|
||||
.hero .badges { display: flex; gap: 12px; justify-content: center; margin-top: 24px; flex-wrap: wrap; }
|
||||
.hero .badges span { background: var(--surface); border: 1px solid var(--border); border-radius: 20px; padding: 6px 18px; font-size: 13px; color: var(--accent2); }
|
||||
.hero-actions { margin-top: 32px; display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }
|
||||
.btn { padding: 11px 28px; border-radius: 10px; font-size: 14px; font-weight: 600; cursor: pointer; transition: .2s; border: none; }
|
||||
.btn-primary { background: var(--accent); color: #fff; }
|
||||
.btn-primary:hover { background: var(--accent2); transform: translateY(-1px); }
|
||||
.btn-outline { background: transparent; border: 1px solid var(--border); color: var(--text); }
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent2); }
|
||||
/* ===== Nav ===== */
|
||||
.nav {
|
||||
position:sticky; top:0; z-index:100;
|
||||
background:rgba(7,11,20,0.7);
|
||||
backdrop-filter:blur(16px); -webkit-backdrop-filter:blur(16px);
|
||||
border-bottom:1px solid var(--border);
|
||||
}
|
||||
.nav-inner {
|
||||
max-width:1180px; margin:0 auto; display:flex; align-items:center;
|
||||
justify-content:space-between; padding:0 28px; height:60px;
|
||||
}
|
||||
.nav-logo { font-size:19px; font-weight:800; letter-spacing:.2px; cursor:pointer; }
|
||||
.nav-links { display:flex; gap:26px; }
|
||||
.nav-links a { color:var(--muted); font-size:14px; font-weight:500; position:relative; }
|
||||
.nav-links a::after {
|
||||
content:''; position:absolute; left:0; right:100%; bottom:-6px; height:2px;
|
||||
background:var(--grad); border-radius:2px; transition:right .25s;
|
||||
}
|
||||
.nav-links a:hover::after { right:0; }
|
||||
.nav-links a:hover { color:var(--text); }
|
||||
|
||||
.container { max-width: 1100px; margin: 0 auto; padding: 60px 24px; }
|
||||
.section-title { font-size: 28px; font-weight: 700; text-align: center; margin-bottom: 8px; }
|
||||
.section-desc { color: var(--muted); text-align: center; margin-bottom: 40px; font-size: 15px; }
|
||||
/* ===== Hero ===== */
|
||||
.hero { text-align:center; padding:110px 24px 60px; position:relative; z-index:1; }
|
||||
.hero-badge {
|
||||
display:inline-flex; align-items:center; gap:8px;
|
||||
background:rgba(99,102,241,0.1); border:1px solid rgba(99,102,241,0.3);
|
||||
color:#a5b4fc; font-size:12px; font-weight:600; letter-spacing:1px;
|
||||
padding:6px 16px; border-radius:999px; margin-bottom:26px;
|
||||
}
|
||||
.hero-badge .dot {
|
||||
width:7px; height:7px; border-radius:50%; background:#34d399;
|
||||
box-shadow:0 0 12px #34d399; animation:pulse 2s infinite;
|
||||
}
|
||||
@keyframes pulse { 50% { opacity:.4; } }
|
||||
.hero h1 { font-size:clamp(42px, 7vw, 76px); font-weight:800; letter-spacing:-1px; line-height:1.1; }
|
||||
.hero h1 .grad-text { animation:shimmer 6s linear infinite; }
|
||||
@keyframes shimmer {
|
||||
0%,100% { filter:hue-rotate(0deg); }
|
||||
50% { filter:hue-rotate(30deg); }
|
||||
}
|
||||
.hero-sub {
|
||||
color:var(--muted); font-size:clamp(15px, 2.2vw, 18px);
|
||||
max-width:640px; margin:18px auto 0;
|
||||
}
|
||||
.hero-badges { display:flex; gap:10px; justify-content:center; margin-top:26px; flex-wrap:wrap; }
|
||||
.hero-badges span {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:999px;
|
||||
padding:5px 14px; font-size:12.5px; color:#cbd5e1;
|
||||
transition:border-color .2s, transform .2s;
|
||||
}
|
||||
.hero-badges span:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
|
||||
.hero-actions { margin-top:36px; display:flex; gap:14px; justify-content:center; flex-wrap:wrap; }
|
||||
.btn {
|
||||
padding:12px 30px; border-radius:12px; font-size:14.5px; font-weight:600;
|
||||
cursor:pointer; transition:transform .2s, box-shadow .2s, border-color .2s;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.btn-primary {
|
||||
background:var(--grad); color:#fff;
|
||||
box-shadow:0 8px 30px -8px rgba(139,92,246,.6);
|
||||
}
|
||||
.btn-primary:hover { transform:translateY(-2px); box-shadow:0 12px 40px -8px rgba(139,92,246,.8); }
|
||||
.btn-ghost { background:var(--surface); border-color:var(--border); color:var(--text); }
|
||||
.btn-ghost:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); background:var(--surface-hover); }
|
||||
|
||||
.features { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 18px; }
|
||||
.feature { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 28px; transition: .2s; }
|
||||
.feature:hover { border-color: var(--accent); }
|
||||
.feature-icon { font-size: 28px; margin-bottom: 12px; }
|
||||
.feature h3 { font-size: 16px; margin-bottom: 6px; }
|
||||
.feature p { color: var(--muted); font-size: 13px; line-height: 1.6; }
|
||||
/* 浮动光球 */
|
||||
.orb { position:absolute; border-radius:50%; filter:blur(70px); pointer-events:none; z-index:0; }
|
||||
.orb-1 { width:340px; height:340px; background:rgba(99,102,241,.25); top:-80px; left:12%; animation:float 12s ease-in-out infinite; }
|
||||
.orb-2 { width:260px; height:260px; background:rgba(236,72,153,.18); top:40px; right:10%; animation:float 15s ease-in-out infinite reverse; }
|
||||
.orb-3 { width:200px; height:200px; background:rgba(168,85,247,.2); bottom:-40px; left:40%; animation:float 18s ease-in-out infinite 2s; }
|
||||
@keyframes float { 0%,100%{transform:translateY(0) translateX(0);} 50%{transform:translateY(-30px) translateX(20px);} }
|
||||
|
||||
.code-block { max-width: 750px; margin: 0 auto; }
|
||||
/* ===== 数据统计 ===== */
|
||||
.stats { position:relative; z-index:1; max-width:1000px; margin:10px auto 0; padding:0 24px; }
|
||||
.stats-grid {
|
||||
display:grid; grid-template-columns:repeat(auto-fit, minmax(150px, 1fr)); gap:1px;
|
||||
background:var(--border); border:1px solid var(--border); border-radius:var(--radius); overflow:hidden;
|
||||
}
|
||||
.stat { background:rgba(10,15,26,.85); padding:22px 12px; text-align:center; backdrop-filter:blur(8px); }
|
||||
.stat-num { font-size:26px; font-weight:800; }
|
||||
.stat-label { font-size:12px; color:var(--muted); margin-top:2px; letter-spacing:.5px; }
|
||||
|
||||
.theme-demo { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin: 24px 0; }
|
||||
.theme-btn { padding: 10px 20px; border-radius: 8px; border: 1px solid var(--border); cursor: pointer; background: var(--surface); color: var(--text); font-size: 13px; font-weight: 500; transition: .2s; }
|
||||
.theme-btn:hover { border-color: var(--accent); }
|
||||
/* ===== 通用 Section ===== */
|
||||
.container { position:relative; z-index:1; max-width:1100px; margin:0 auto; padding:80px 24px; }
|
||||
.section-title { font-size:30px; font-weight:800; text-align:center; letter-spacing:-.5px; }
|
||||
.section-desc { color:var(--muted); text-align:center; margin-top:8px; font-size:15px; }
|
||||
|
||||
footer { text-align: center; padding: 40px 24px; color: var(--muted); font-size: 13px; border-top: 1px solid var(--border); }
|
||||
/* ===== 特性卡片 ===== */
|
||||
.features { display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:16px; margin-top:44px; }
|
||||
.feature {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:var(--radius);
|
||||
padding:26px 24px; transition:transform .25s, border-color .25s, background .25s;
|
||||
position:relative; overflow:hidden;
|
||||
}
|
||||
.feature::before {
|
||||
content:''; position:absolute; top:0; left:0; right:0; height:2px;
|
||||
background:var(--grad); opacity:0; transition:opacity .25s;
|
||||
}
|
||||
.feature:hover { transform:translateY(-4px); border-color:rgba(129,140,248,.4); background:var(--surface-hover); }
|
||||
.feature:hover::before { opacity:1; }
|
||||
.feature-icon { font-size:26px; margin-bottom:12px; display:inline-block; }
|
||||
.feature h3 { font-size:15.5px; margin-bottom:6px; font-weight:700; }
|
||||
.feature p { color:var(--muted); font-size:13px; line-height:1.65; }
|
||||
.feature code {
|
||||
background:rgba(99,102,241,0.15); color:#a5b4fc;
|
||||
padding:1px 6px; border-radius:5px; font-size:12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.hero h1 { font-size: 32px; }
|
||||
.nav-links { gap: 14px; }
|
||||
/* ===== 代码块 ===== */
|
||||
.code-block { max-width:760px; margin:36px auto 0; }
|
||||
.terminal {
|
||||
background:#0a0f1c; border:1px solid var(--border); border-radius:12px; overflow:hidden;
|
||||
box-shadow:0 20px 60px -20px rgba(0,0,0,.6);
|
||||
}
|
||||
.terminal-bar {
|
||||
display:flex; align-items:center; gap:7px; padding:11px 16px;
|
||||
background:rgba(255,255,255,0.03); border-bottom:1px solid var(--border);
|
||||
}
|
||||
.terminal-bar .t-dot { width:11px; height:11px; border-radius:50%; }
|
||||
.terminal-bar .t-dot:nth-child(1){ background:#ff5f57; } .terminal-bar .t-dot:nth-child(2){ background:#febc2e; } .terminal-bar .t-dot:nth-child(3){ background:#28c840; }
|
||||
.terminal-bar .t-title { margin-left:10px; font-size:11.5px; color:var(--muted); letter-spacing:.5px; }
|
||||
.terminal pre {
|
||||
padding:20px 22px; overflow-x:auto;
|
||||
font:13px/1.8 'SF Mono','Cascadia Code','Fira Code',Consolas,monospace;
|
||||
color:#cbd5e1;
|
||||
}
|
||||
.terminal pre .kw { color:#c084fc; } .terminal pre .fn { color:#60a5fa; }
|
||||
.terminal pre .s { color:#34d399; } .terminal pre .cm { color:#475569; }
|
||||
.terminal pre .num { color:#f472b6; }
|
||||
|
||||
/* ===== 主题预览 ===== */
|
||||
.theme-demo { display:flex; gap:12px; justify-content:center; flex-wrap:wrap; margin-top:36px; }
|
||||
.theme-btn {
|
||||
padding:11px 22px; border-radius:10px; border:1px solid var(--border);
|
||||
cursor:pointer; background:var(--surface); color:var(--text);
|
||||
font-size:13.5px; font-weight:500; transition:all .2s;
|
||||
}
|
||||
.theme-btn:hover { border-color:rgba(129,140,248,.5); transform:translateY(-2px); }
|
||||
.theme-btn.active {
|
||||
background:var(--grad); color:#fff; border-color:transparent;
|
||||
box-shadow:0 6px 24px -6px rgba(139,92,246,.6);
|
||||
}
|
||||
|
||||
/* ===== 环境支持 ===== */
|
||||
.env-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(160px, 1fr)); gap:12px; margin-top:40px; }
|
||||
.env-card {
|
||||
background:var(--surface); border:1px solid var(--border); border-radius:12px;
|
||||
padding:20px 14px; text-align:center; transition:border-color .2s, transform .2s;
|
||||
}
|
||||
.env-card:hover { border-color:rgba(129,140,248,.4); transform:translateY(-3px); }
|
||||
.env-card .e-ico { font-size:26px; }
|
||||
.env-card h3 { font-size:14px; margin:8px 0 2px; }
|
||||
.env-card p { font-size:12.5px; color:var(--muted); }
|
||||
|
||||
/* ===== CTA ===== */
|
||||
.cta {
|
||||
max-width:760px; margin:0 auto; text-align:center;
|
||||
background:var(--grad); border-radius:20px; padding:56px 32px;
|
||||
position:relative; overflow:hidden;
|
||||
}
|
||||
.cta::before {
|
||||
content:''; position:absolute; inset:0;
|
||||
background:radial-gradient(ellipse 60% 60% at 50% 120%, rgba(255,255,255,.25), transparent);
|
||||
}
|
||||
.cta h2 { font-size:30px; font-weight:800; color:#fff; position:relative; }
|
||||
.cta p { color:rgba(255,255,255,.85); margin-top:10px; position:relative; }
|
||||
.cta .btn { margin-top:26px; background:#fff; color:#6d28d9; position:relative; }
|
||||
.cta .btn:hover { transform:translateY(-2px); }
|
||||
|
||||
footer { text-align:center; padding:44px 24px; color:var(--muted); font-size:13px; border-top:1px solid var(--border); position:relative; z-index:1; }
|
||||
footer a { color:#a5b4fc; }
|
||||
|
||||
/* 滚动淡入 */
|
||||
.reveal { opacity:0; transform:translateY(24px); transition:opacity .6s ease, transform .6s ease; }
|
||||
.reveal.visible { opacity:1; transform:none; }
|
||||
|
||||
@media (max-width:768px) {
|
||||
.hero { padding:80px 20px 44px; }
|
||||
.nav-links { gap:16px; }
|
||||
.container { padding:56px 20px; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
@@ -68,7 +237,7 @@
|
||||
|
||||
<nav class="nav">
|
||||
<div class="nav-inner">
|
||||
<div class="nav-logo">MetonaToast</div>
|
||||
<div class="nav-logo"><span class="grad-text">MetonaToast</span></div>
|
||||
<div class="nav-links">
|
||||
<a href="index.html">首页</a>
|
||||
<a href="docs.html">文档</a>
|
||||
@@ -79,107 +248,117 @@
|
||||
</nav>
|
||||
|
||||
<header class="hero">
|
||||
<h1><span>MetonaToast</span></h1>
|
||||
<p>轻量 · 零依赖 · 精致美观的 Toast 通知库。纯原生 JavaScript,gzip 不到 10KB。</p>
|
||||
<div class="badges">
|
||||
<span>📦 <10KB gzip</span><span>🎨 107 图标</span><span>🎭 11 动画</span><span>🌍 国际化</span><span>🔌 插件</span><span>📘 TypeScript</span>
|
||||
<div class="orb orb-1"></div>
|
||||
<div class="orb orb-2"></div>
|
||||
<div class="orb orb-3"></div>
|
||||
<div class="hero-badge"><span class="dot"></span> v0.5.0 · 385 测试 · 覆盖率 96%</div>
|
||||
<h1><span class="grad-text">MetonaToast</span></h1>
|
||||
<p class="hero-sub">轻量 · 零依赖 · 精致美观的 Toast 通知库。TypeScript 严格模式,gzip 不到 10KB,一切开箱即用。</p>
|
||||
<div class="hero-badges">
|
||||
<span>📦 <10KB gzip</span>
|
||||
<span>🎨 107 图标</span>
|
||||
<span>🎭 11 动画</span>
|
||||
<span>🧩 4 插件</span>
|
||||
<span>🔗 17 钩子</span>
|
||||
<span>⚛️ React 适配器</span>
|
||||
<span>🌍 国际化</span>
|
||||
</div>
|
||||
<div class="hero-actions">
|
||||
<button class="btn btn-primary" onclick="demoQuickStart()">快速预览</button>
|
||||
<button class="btn btn-outline" onclick="location.href='docs.html'">查看文档</button>
|
||||
<button class="btn btn-primary" onclick="demoQuickStart()">✨ 快速预览</button>
|
||||
<button class="btn btn-ghost" onclick="location.href='docs.html'">📖 查看文档</button>
|
||||
<button class="btn btn-ghost" onclick="location.href='demo.html'">🎮 在线演示</button>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="stats reveal">
|
||||
<div class="stats-grid">
|
||||
<div class="stat"><div class="stat-num grad-text"><10KB</div><div class="stat-label">gzip 体积</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">107</div><div class="stat-label">内置图标</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">11</div><div class="stat-label">动画效果</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">17</div><div class="stat-label">生命周期钩子</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">385</div><div class="stat-label">测试用例</div></div>
|
||||
<div class="stat"><div class="stat-num grad-text">96%</div><div class="stat-label">行覆盖率</div></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<main class="container">
|
||||
<h2 class="section-title">核心特性</h2>
|
||||
<p class="section-desc">一切你需要的,都在一个不到 10KB 的包中</p>
|
||||
<h2 class="section-title reveal">核心特性</h2>
|
||||
<p class="section-desc reveal">你需要的一切,都在一个不到 10KB 的包中</p>
|
||||
|
||||
<div class="features">
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>107 种图标类型</h3>
|
||||
<p>success / error / warning / info / loading 以及 100+ 扩展类型,覆盖所有常见场景。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🎭</div>
|
||||
<h3>11 种动画效果</h3>
|
||||
<p>slide / fade / scale / bounce / flip / rotate / zoom / slideUp / slideDown / slideLeft / slideRight,每种感官差异明显。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🖌️</div>
|
||||
<h3>主题系统</h3>
|
||||
<p>内置 light / dark / auto / warm 四种主题,支持 <code>registerTheme()</code> 自定义注册任意主题。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🌍</div>
|
||||
<h3>国际化</h3>
|
||||
<p>内置 zh-CN 和 en-US 完整翻译,400+ 词汇。支持 <code>addTranslations()</code> 扩展任意语言。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">🔌</div>
|
||||
<h3>插件系统</h3>
|
||||
<p>3 款预设插件 + 自定义插件架构。keyboard / persistence / accessibility 开箱即用。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature-icon">✋</div>
|
||||
<h3>拖拽关闭</h3>
|
||||
<p>任意方向拖动 Toast 即可关闭。支持 Pointer Events,移动端和桌面端体验一致。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">⚡</div>
|
||||
<h3>零依赖</h3>
|
||||
<p>纯原生 JavaScript 实现,无需任何运行时依赖。兼容所有现代浏览器。</p>
|
||||
<p>TypeScript 严格模式,纯原生实现,无任何运行时依赖。兼容所有现代浏览器。</p>
|
||||
</div>
|
||||
<div class="feature">
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🎨</div>
|
||||
<h3>107 种图标类型</h3>
|
||||
<p>success / error / warning / info / loading 及 100+ 扩展类型,覆盖所有常见场景。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🎭</div>
|
||||
<h3>11 种动画 + 自定义</h3>
|
||||
<p>每种感官差异明显,<code>animations.register()</code> 注册自定义动画,动态注入 keyframes 真实生效。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🖌️</div>
|
||||
<h3>主题系统</h3>
|
||||
<p>light / dark / auto / warm 内置主题,<code>registerTheme()</code> 自定义,跟随系统自动切换。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🌍</div>
|
||||
<h3>国际化</h3>
|
||||
<p>zh-CN / en-US 完整翻译,<code>addTranslations()</code> 扩展任意语言,RTL 完整适配。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🧩</div>
|
||||
<h3>插件系统</h3>
|
||||
<p>4 款预设:keyboard / persistence / accessibility / dedupe,支持自定义插件架构。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🔗</div>
|
||||
<h3>17 个钩子</h3>
|
||||
<p>完整生命周期钩子,<code>beforeShow</code> / <code>beforeClose</code> / <code>beforeUpdate</code> 支持拦截。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">✋</div>
|
||||
<h3>拖拽关闭</h3>
|
||||
<p>任意方向拖动即关,阈值 <code>dragThreshold</code> 可配置。Pointer Events 全端一致。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">⚛️</div>
|
||||
<h3>React 适配器</h3>
|
||||
<p><code>@metona-team/metona-toast/react</code> — useToast hook + 声明式 Toast 组件,主包保持零依赖。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">🛡️</div>
|
||||
<h3>错误捕获</h3>
|
||||
<p>全局 <code>onError</code> 回调捕获钩子与定时器异常,方便接入 Sentry、DataDog。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">⏱️</div>
|
||||
<h3>暂停恢复</h3>
|
||||
<p>悬停暂停、拖拽暂停、手动 pause/resume,剩余时间精确保持不丢失。</p>
|
||||
</div>
|
||||
<div class="feature reveal">
|
||||
<div class="feature-icon">📘</div>
|
||||
<h3>TypeScript</h3>
|
||||
<p>完整类型定义,智能提示全覆盖。<code>MeToast.success()</code>、<code>.confirm()</code>、<code>.action()</code> 全部有类型。</p>
|
||||
<h3>类型安全</h3>
|
||||
<p>严格模式源码,类型自动生成零漂移。加载态转换 id 稳定,原地更新。</p>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">环境支持</h2>
|
||||
<p class="section-desc">所有现代浏览器原生支持,无需 polyfill</p>
|
||||
<div class="env-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(165px,1fr)); gap:12px;">
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🌐</div>
|
||||
<h3>Chrome</h3>
|
||||
<p>64+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🦊</div>
|
||||
<h3>Firefox</h3>
|
||||
<p>63+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🧭</div>
|
||||
<h3>Safari</h3>
|
||||
<p>11+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">🪟</div>
|
||||
<h3>Edge</h3>
|
||||
<p>79+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">📦</div>
|
||||
<h3>Node.js</h3>
|
||||
<p>18+</p>
|
||||
</div>
|
||||
<div class="feature" style="text-align:center;padding:20px 16px;">
|
||||
<div style="font-size:28px;margin-bottom:6px;">📘</div>
|
||||
<h3>TypeScript</h3>
|
||||
<p>4.5+</p>
|
||||
</div>
|
||||
</div>
|
||||
<p style="text-align:center;color:var(--muted);font-size:13px;margin-top:16px">核心依赖:Web Animations API · Pointer Events · CSS Grid · CSS Custom Properties</p>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">快速开始</h2>
|
||||
<p class="section-desc">浏览器 window 上仅注册两个全局变量:<code>MeToast</code> 和 <code>Met</code>,完全等价</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="cm"># npm 安装</span>
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">快速开始</h2>
|
||||
<p class="section-desc reveal">浏览器上仅注册两个全局变量:<code>MeToast</code> 和 <code>Met</code>,完全等价</p>
|
||||
<div class="code-block reveal">
|
||||
<div class="terminal">
|
||||
<div class="terminal-bar">
|
||||
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
|
||||
<span class="t-title">quickstart.js</span>
|
||||
</div>
|
||||
<pre><span class="cm"># 安装</span>
|
||||
npm install @metona-team/metona-toast
|
||||
|
||||
<span class="cm">// 导入</span>
|
||||
@@ -192,43 +371,45 @@ MeToast.<span class="fn">loading</span>(<span class="s">'加载中...'</span>);
|
||||
|
||||
<span class="cm">// CDN 直接引用</span>
|
||||
<span class="kw"><script</span> <span class="fn">src</span>=<span class="s">"https://git.metona.cn/MetonaTeam/MetonaToast/raw/branch/master/dist/metona-toast.min.js"</span><span class="kw">></script></span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">高级功能速览</h2>
|
||||
<p class="section-desc">不仅仅是一个 Toast——确认框、输入框、进度条、倒计时、队列,应有尽有</p>
|
||||
<div class="code-block">
|
||||
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">高级功能速览</h2>
|
||||
<p class="section-desc reveal">不仅仅是 Toast — 确认框、输入框、进度条、倒计时、队列、Action,应有尽有</p>
|
||||
<div class="code-block reveal">
|
||||
<div class="terminal">
|
||||
<div class="terminal-bar">
|
||||
<span class="t-dot"></span><span class="t-dot"></span><span class="t-dot"></span>
|
||||
<span class="t-title">advanced.js</span>
|
||||
</div>
|
||||
<pre><span class="cm">// Promise 风格 — 自动 loading → success/error</span>
|
||||
<span class="kw">await</span> MeToast.<span class="fn">promise</span>(<span class="fn">fetch</span>(<span class="s">'/api/data'</span>), {
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>,
|
||||
<span class="fn">success</span>: <span class="s">'加载完成!'</span>,
|
||||
<span class="fn">error</span>: <span class="s">'加载失败'</span>,
|
||||
<span class="fn">loading</span>: <span class="s">'加载中...'</span>, <span class="fn">success</span>: <span class="s">'完成'</span>, <span class="fn">error</span>: <span class="s">'失败'</span>
|
||||
});
|
||||
|
||||
<span class="cm">// 确认对话框 → Promise<boolean></span>
|
||||
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除吗?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
|
||||
|
||||
<span class="cm">// 输入对话框 → Promise<string|null></span>
|
||||
<span class="cm">// 确认 / 输入对话框</span>
|
||||
<span class="kw">const</span> ok = <span class="kw">await</span> MeToast.<span class="fn">confirm</span>(<span class="s">'确定删除?'</span>, { <span class="fn">confirmText</span>: <span class="s">'删除'</span> });
|
||||
<span class="kw">const</span> name = <span class="kw">await</span> MeToast.<span class="fn">prompt</span>(<span class="s">'请输入姓名'</span>);
|
||||
|
||||
<span class="cm">// Action Toast — 内嵌操作按钮</span>
|
||||
MeToast.<span class="fn">action</span>(<span class="s">'文件已删除'</span>, [
|
||||
{ <span class="fn">text</span>: <span class="s">'撤销'</span>, <span class="fn">onClick</span>: () => <span class="fn">restore</span>() },
|
||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>() },
|
||||
{ <span class="fn">text</span>: <span class="s">'查看'</span>, <span class="fn">onClick</span>: () => <span class="fn">open</span>(), <span class="fn">close</span>: <span class="kw">false</span> },
|
||||
]);
|
||||
|
||||
<span class="cm">// Toast 分组管理</span>
|
||||
<span class="kw">const</span> orderGroup = MeToast.<span class="fn">group</span>(<span class="s">'orders'</span>);
|
||||
orderGroup.<span class="fn">success</span>(<span class="s">'订单已创建'</span>);
|
||||
orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭整组</span></pre>
|
||||
<span class="cm">// 加载 → 链式转换(id 稳定)</span>
|
||||
<span class="kw">const</span> loading = MeToast.<span class="fn">loading</span>(<span class="s">'正在提交...'</span>);
|
||||
<span class="fn">setTimeout</span>(() => loading.<span class="fn">success</span>(<span class="s">'提交成功!'</span>), <span class="num">2000</span>);</pre>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container">
|
||||
<h2 class="section-title">主题切换</h2>
|
||||
<p class="section-desc">点击按钮实时预览四种内置主题</p>
|
||||
<div class="theme-demo">
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">主题切换</h2>
|
||||
<p class="section-desc reveal">点击按钮实时预览内置主题,切换结果自动持久化</p>
|
||||
<div class="theme-demo reveal">
|
||||
<button class="theme-btn" onclick="switchTheme('light')">☀️ 浅色 Light</button>
|
||||
<button class="theme-btn" onclick="switchTheme('dark')">🌙 深色 Dark</button>
|
||||
<button class="theme-btn" onclick="switchTheme('auto')">🔄 自动 Auto</button>
|
||||
@@ -236,8 +417,34 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container" style="padding-top:0">
|
||||
<h2 class="section-title reveal">环境支持</h2>
|
||||
<p class="section-desc reveal">所有现代浏览器原生支持,无需 polyfill</p>
|
||||
<div class="env-grid reveal">
|
||||
<div class="env-card"><div class="e-ico">🌐</div><h3>Chrome</h3><p>64+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🦊</div><h3>Firefox</h3><p>63+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🧭</div><h3>Safari</h3><p>11+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🪟</div><h3>Edge</h3><p>79+</p></div>
|
||||
<div class="env-card"><div class="e-ico">📱</div><h3>iOS Safari</h3><p>11+</p></div>
|
||||
<div class="env-card"><div class="e-ico">🤖</div><h3>Android</h3><p>64+</p></div>
|
||||
<div class="env-card"><div class="e-ico">📦</div><h3>Node.js</h3><p>16+</p></div>
|
||||
<div class="env-card"><div class="e-ico">📘</div><h3>TypeScript</h3><p>4.5+</p></div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="container" style="padding-top:0">
|
||||
<div class="cta reveal">
|
||||
<h2>开始使用 MetonaToast</h2>
|
||||
<p>一条命令安装,立即体验精致优雅的 Toast 通知</p>
|
||||
<button class="btn" onclick="location.href='demo.html'">🎮 打开在线演示</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>MetonaToast v2.0.1 · MIT License · <a href="https://git.metona.cn/MetonaTeam/MetonaToast">Gitea</a></p>
|
||||
MetonaToast v0.5.0 · MIT License ·
|
||||
<a href="https://git.metona.cn/MetonaTeam/MetonaToast" target="_blank">Gitea</a> ·
|
||||
<a href="docs.html">文档</a> ·
|
||||
<a href="demo.html">演示</a>
|
||||
</footer>
|
||||
|
||||
<script src="../dist/metona-toast.js"></script>
|
||||
@@ -246,16 +453,28 @@ orderGroup.<span class="fn">dismiss</span>(); <span class="cm">// 一键关闭
|
||||
|
||||
function demoQuickStart() {
|
||||
MeToast.success('✨ 欢迎使用 MetonaToast!');
|
||||
setTimeout(() => MeToast.info('💡 试试点击右上角的 Toast 来关闭它'), 800);
|
||||
setTimeout(() => MeToast.warning('⚠️ 你也可以拖动 Toast 来关闭'), 1600);
|
||||
setTimeout(() => MeToast.info('💡 点击 Toast 或点击 × 即可关闭'), 800);
|
||||
setTimeout(() => MeToast.warning('⚠️ 拖动 Toast 也能关闭', { draggable: true }), 1600);
|
||||
setTimeout(() => MeToast.success('🎭 试试 bounce 动画', { animation: 'bounce' }), 2400);
|
||||
}
|
||||
|
||||
function switchTheme(t) {
|
||||
MeToast.themes.switchTheme(t);
|
||||
MeToast.configure({ theme: t });
|
||||
document.querySelectorAll('.theme-btn').forEach(b => b.classList.remove('active'));
|
||||
event.target.classList.add('active');
|
||||
const names = { light: '浅色', dark: '深色', auto: '自动', warm: '暖色' };
|
||||
MeToast.info('已切换到「' + names[t] + '」主题', { theme: t });
|
||||
}
|
||||
|
||||
// 滚动淡入
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
entries.forEach(e => { if (e.isIntersecting) { e.target.classList.add('visible'); observer.unobserve(e.target); } });
|
||||
}, { threshold: 0.12 });
|
||||
document.querySelectorAll('.reveal').forEach(el => observer.observe(el));
|
||||
|
||||
// 页面加载自动演示
|
||||
setTimeout(demoQuickStart, 600);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
/**
|
||||
* MetonaToast Animations - 动画管理(精简版 v2.0.1)
|
||||
* @module animations
|
||||
* @description 动画注册与管理,移除未使用的 Web Animations API dead code
|
||||
*/
|
||||
|
||||
import { ANIMATIONS } from './constants.js';
|
||||
|
||||
// 动画缓存
|
||||
const animationMap = new Map();
|
||||
|
||||
/**
|
||||
* 注册默认动画
|
||||
*/
|
||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||
animationMap.set(name, config);
|
||||
});
|
||||
|
||||
/**
|
||||
* 动画工具函数
|
||||
*/
|
||||
export const animationUtils = {
|
||||
/**
|
||||
* 注册自定义动画
|
||||
* @param {string} name - 动画名称
|
||||
* @param {Object} config - 动画配置 { enter, leave, duration, easing }
|
||||
*/
|
||||
register(name, config) {
|
||||
animationMap.set(name, {
|
||||
name,
|
||||
enter: config.enter || {},
|
||||
leave: config.leave || {},
|
||||
duration: config.duration || 300,
|
||||
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 注销动画
|
||||
* @param {string} name - 动画名称
|
||||
*/
|
||||
unregister(name) {
|
||||
animationMap.delete(name);
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取动画配置
|
||||
* @param {string} name - 动画名称
|
||||
* @returns {Object|null} 动画配置
|
||||
*/
|
||||
get(name) {
|
||||
return animationMap.get(name) || ANIMATIONS[name] || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取所有动画名称
|
||||
* @returns {Array} 动画名称数组
|
||||
*/
|
||||
getAnimationNames() {
|
||||
return Array.from(animationMap.keys());
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取活动动画数量(兼容API,当前CSS动画不跟踪活动实例)
|
||||
* @returns {number} 动画数量
|
||||
*/
|
||||
getActiveCount() {
|
||||
return 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 取消所有动画(兼容API,当前CSS动画由浏览器管理)
|
||||
*/
|
||||
cancelAll() {
|
||||
// CSS动画由浏览器原生管理,无需手动取消
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置动画管理器
|
||||
*/
|
||||
reset() {
|
||||
animationMap.clear();
|
||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||
animationMap.set(name, config);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 销毁动画管理器
|
||||
*/
|
||||
destroy() {
|
||||
animationMap.clear();
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 动画预设
|
||||
*/
|
||||
export const animationPresets = {};
|
||||
|
||||
/**
|
||||
* 创建自定义动画配置
|
||||
* @param {Object} config - 动画配置
|
||||
* @returns {Object} 动画配置
|
||||
*/
|
||||
export const createAnimation = (config) => ({
|
||||
enter: config.enter || {},
|
||||
leave: config.leave || {},
|
||||
duration: config.duration || 300,
|
||||
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
});
|
||||
@@ -0,0 +1,150 @@
|
||||
/**
|
||||
* MetonaToast Animations — 动画管理
|
||||
* @module animations
|
||||
* @version 0.2.1
|
||||
*/
|
||||
|
||||
import { ANIMATIONS, ANIMATION_TYPES } from './constants.js';
|
||||
import type { AnimationConfig, AnimationUtils } from './types.js';
|
||||
|
||||
// 动画缓存
|
||||
const animationMap: Map<string, AnimationConfig> = new Map();
|
||||
|
||||
// 内置动画(keyframes 由 styles.ts 注入,无需动态生成)
|
||||
const BUILTIN_ANIMATIONS: Set<string> = new Set(ANIMATION_TYPES);
|
||||
|
||||
// 动态注入的自定义动画样式(惰性创建)
|
||||
let animStyleElement: HTMLStyleElement | null = null;
|
||||
const injectedAnimations: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* 将 CSS 属性对象转换为内联 CSS 字符串
|
||||
*/
|
||||
const toCss = (props: Record<string, string | number>): string =>
|
||||
Object.entries(props)
|
||||
.map(([key, value]) => `${key}:${value}`)
|
||||
.join(';');
|
||||
|
||||
/**
|
||||
* 为自定义动画生成入场 keyframes + 类规则
|
||||
*/
|
||||
const buildAnimationCSS = (name: string, config: AnimationConfig): string => {
|
||||
const enter = toCss(config.enter);
|
||||
return `
|
||||
@keyframes met-${name}-in {
|
||||
from { ${enter}; }
|
||||
}
|
||||
.met-anim-${name}.met-toast { opacity: 0; }
|
||||
.met-anim-${name}.met-toast.met-show {
|
||||
animation: met-${name}-in ${config.duration}ms ${config.easing} forwards;
|
||||
}
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 确保自定义动画的 CSS 已注入文档(幂等,内置动画自动跳过)
|
||||
*/
|
||||
export const ensureAnimationCSS = (name: string): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (BUILTIN_ANIMATIONS.has(name)) return;
|
||||
|
||||
const config = animationMap.get(name);
|
||||
if (!config || injectedAnimations.has(name)) return;
|
||||
|
||||
if (!animStyleElement) {
|
||||
animStyleElement = document.getElementById('metona-toast-anim-styles') as HTMLStyleElement | null;
|
||||
if (!animStyleElement) {
|
||||
animStyleElement = document.createElement('style');
|
||||
animStyleElement.id = 'metona-toast-anim-styles';
|
||||
document.head.appendChild(animStyleElement);
|
||||
}
|
||||
}
|
||||
|
||||
animStyleElement.textContent += buildAnimationCSS(name, config);
|
||||
injectedAnimations.add(name);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查动画是否存在(内置或自定义)
|
||||
*/
|
||||
export const hasAnimation = (name: string): boolean => animationMap.has(name);
|
||||
|
||||
// 注册默认动画
|
||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||
animationMap.set(name, {
|
||||
name,
|
||||
enter: config.enter,
|
||||
leave: config.leave,
|
||||
duration: config.duration,
|
||||
easing: config.easing,
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* 动画工具函数
|
||||
*/
|
||||
export const animationUtils: AnimationUtils = {
|
||||
register(name: string, config: Partial<AnimationConfig>): void {
|
||||
animationMap.set(name, {
|
||||
name,
|
||||
enter: config.enter || {},
|
||||
leave: config.leave || {},
|
||||
duration: config.duration || 300,
|
||||
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
});
|
||||
},
|
||||
|
||||
unregister(name: string): void {
|
||||
animationMap.delete(name);
|
||||
},
|
||||
|
||||
get(name: string): AnimationConfig | null {
|
||||
return animationMap.get(name) || null;
|
||||
},
|
||||
|
||||
getAnimationNames(): string[] {
|
||||
return Array.from(animationMap.keys());
|
||||
},
|
||||
|
||||
getActiveCount(): number {
|
||||
return animationMap.size;
|
||||
},
|
||||
|
||||
cancelAll(): void {
|
||||
// CSS动画由浏览器原生管理,无需手动取消
|
||||
},
|
||||
|
||||
reset(): void {
|
||||
animationMap.clear();
|
||||
Object.entries(ANIMATIONS).forEach(([name, config]) => {
|
||||
animationMap.set(name, {
|
||||
name,
|
||||
enter: config.enter,
|
||||
leave: config.leave,
|
||||
duration: config.duration,
|
||||
easing: config.easing,
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
destroy(): void {
|
||||
animationMap.clear();
|
||||
injectedAnimations.clear();
|
||||
animStyleElement = null;
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 动画预设(占位,兼容旧API)
|
||||
*/
|
||||
export const animationPresets: Record<string, AnimationConfig> = {};
|
||||
|
||||
/**
|
||||
* 创建自定义动画配置
|
||||
*/
|
||||
export const createAnimation = (config: Partial<AnimationConfig>): AnimationConfig => ({
|
||||
enter: config.enter || {},
|
||||
leave: config.leave || {},
|
||||
duration: config.duration || 300,
|
||||
easing: config.easing || 'cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
});
|
||||
+761
@@ -0,0 +1,761 @@
|
||||
/**
|
||||
* MetonaToast API — meToast 核心 API 对象
|
||||
* @module api
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { Toast, _containerCache } from './toast.js';
|
||||
import { DEFAULTS, VERSION } from './constants.js';
|
||||
import { t, setCurrentLocale } from './i18n.js';
|
||||
import { applyTheme } from './themes.js';
|
||||
import { confirmHTML, promptHTML, progressHTML, actionHTML } from './templates.js';
|
||||
import { presetPlugins } from './plugins.js';
|
||||
import type {
|
||||
ToastConfig, ToastOptions, ToastInstance,
|
||||
LoadingControl, ProgressControl, CountdownControl,
|
||||
ActionControl, QueueControl, GroupAPI,
|
||||
ActionButton, PromiseOptions, ConfirmOptions,
|
||||
PromptOptions, ProgressOptions, CountdownOptions,
|
||||
QueueOptions, StackOptions, MeToast, ErrorInfo, Plugin,
|
||||
StatusInfo, InitOptions,
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* 参数标准化工具
|
||||
*/
|
||||
const normalizeArgs = (args: unknown[], defaultType = 'default'): ToastOptions => {
|
||||
const [first, second] = args;
|
||||
|
||||
if (typeof first === 'string') {
|
||||
return {
|
||||
...((second as Record<string, unknown>) || {}),
|
||||
type: (second as Record<string, unknown>)?.type as string || defaultType,
|
||||
message: first,
|
||||
};
|
||||
}
|
||||
|
||||
if (first && typeof first === 'object') {
|
||||
return {
|
||||
...(first as Record<string, unknown>),
|
||||
type: (first as Record<string, unknown>).type as string || defaultType,
|
||||
} as ToastOptions;
|
||||
}
|
||||
|
||||
return {
|
||||
type: defaultType,
|
||||
message: '',
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* meToast API 对象
|
||||
*/
|
||||
const meToast: MeToast = {
|
||||
_toasts: new Map<string, ToastInstance>(),
|
||||
_config: { ...DEFAULTS } as unknown as ToastConfig,
|
||||
version: VERSION,
|
||||
|
||||
configure(opts: Partial<ToastConfig>): MeToast {
|
||||
if (!opts || typeof opts !== 'object') return this;
|
||||
Object.assign(this._config, opts);
|
||||
|
||||
if (opts.theme) {
|
||||
applyTheme(opts.theme);
|
||||
}
|
||||
|
||||
if (opts.locale) {
|
||||
setCurrentLocale(opts.locale);
|
||||
}
|
||||
|
||||
Toast.trigger('configChange');
|
||||
return this;
|
||||
},
|
||||
|
||||
_emit(opts: ToastOptions): Toast {
|
||||
const merged: ToastOptions = { ...this._config, ...opts };
|
||||
if (merged.content && !merged.message) merged.message = merged.content;
|
||||
|
||||
const t = new Toast(merged);
|
||||
t.create();
|
||||
// beforeShow/onBeforeShow 拦截(返回 false)时 el 为 null,不注册幽灵实例
|
||||
if (t.el !== null) {
|
||||
this._toasts.set(t.id, t);
|
||||
}
|
||||
|
||||
return t;
|
||||
},
|
||||
|
||||
_remove(id: string): void {
|
||||
this._toasts.delete(id);
|
||||
},
|
||||
|
||||
find(id: string): ToastInstance | undefined {
|
||||
if (!id) return undefined;
|
||||
return this._toasts.get(id);
|
||||
},
|
||||
|
||||
show(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'default');
|
||||
return this._emit(normalized);
|
||||
},
|
||||
|
||||
success(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'success');
|
||||
return this._emit(normalized);
|
||||
},
|
||||
|
||||
error(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'error');
|
||||
return this._emit(normalized);
|
||||
},
|
||||
|
||||
warning(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'warning');
|
||||
return this._emit(normalized);
|
||||
},
|
||||
|
||||
info(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): ToastInstance {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||||
return this._emit(normalized);
|
||||
},
|
||||
|
||||
loading(messageOrOpts: string | ToastOptions, opts: ToastOptions = {}): LoadingControl {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'loading');
|
||||
normalized.duration = 0;
|
||||
normalized.closeButton = false;
|
||||
normalized.showProgress = false;
|
||||
|
||||
const toast = this._emit(normalized);
|
||||
|
||||
return {
|
||||
id: toast.id,
|
||||
success: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'success', msg || t('success'), o),
|
||||
error: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'error', msg || t('error'), o),
|
||||
info: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'info', msg || t('info'), o),
|
||||
warning: (msg?: string, o: ToastOptions = {}) => this._resolve(toast, 'warning', msg || t('warning'), o),
|
||||
update: (p: Partial<ToastOptions>) => { toast.update(p); return this; },
|
||||
dismiss: () => toast.close(),
|
||||
};
|
||||
},
|
||||
|
||||
promise<T>(promise: Promise<T>, opts: PromiseOptions = {}): Promise<T> {
|
||||
if (!promise || typeof (promise as unknown as { then?: unknown }).then !== 'function') {
|
||||
console.error('MeToast.promise: first argument must be a Promise');
|
||||
return Promise.reject(new Error('Invalid promise'));
|
||||
}
|
||||
|
||||
const loadingMsg = opts.loading || t('loading');
|
||||
const successMsg = opts.success || t('success');
|
||||
const errorMsg = opts.error || t('error');
|
||||
|
||||
const ctrl = this.loading(loadingMsg);
|
||||
|
||||
return Promise.resolve(promise)
|
||||
.then((data: T) => {
|
||||
ctrl.success(successMsg);
|
||||
return data;
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
ctrl.error(errorMsg);
|
||||
throw err;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* loading 链式转换 — 原地 update 同一实例(id 稳定,不重建 DOM)
|
||||
* duration 从 0 恢复为默认值,使转换后的 toast 自动关闭
|
||||
*/
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null {
|
||||
const old = this._toasts.get(loadingToast.id);
|
||||
if (!old) return null;
|
||||
|
||||
old.update({
|
||||
...opts,
|
||||
type,
|
||||
message,
|
||||
duration: opts.duration ?? (old.config.duration || this._config.duration),
|
||||
} as ToastOptions);
|
||||
|
||||
return old;
|
||||
},
|
||||
|
||||
confirm(message: string, opts: ConfirmOptions = {}): Promise<boolean> {
|
||||
if (typeof message !== 'string') {
|
||||
console.error('MeToast.confirm: message must be a string');
|
||||
return Promise.resolve(false);
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
let resolved = false;
|
||||
const safeResolve = (val: boolean) => {
|
||||
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
|
||||
};
|
||||
const safetyTimeout = setTimeout(() => safeResolve(false), 10000);
|
||||
|
||||
const toast = this._emit({
|
||||
...opts,
|
||||
type: opts.type || 'warning',
|
||||
message,
|
||||
duration: 0,
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
draggable: false,
|
||||
html: confirmHTML(opts),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const confirmBtn = toast.el?.querySelector('.met-confirm-btn') as HTMLElement | null;
|
||||
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
|
||||
|
||||
if (confirmBtn) {
|
||||
confirmBtn.addEventListener('click', () => {
|
||||
toast.close();
|
||||
safeResolve(true);
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
toast.close();
|
||||
safeResolve(false);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
},
|
||||
|
||||
prompt(message: string, opts: PromptOptions = {}): Promise<string | null> {
|
||||
if (typeof message !== 'string') {
|
||||
console.error('MeToast.prompt: message must be a string');
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
return new Promise<string | null>((resolve) => {
|
||||
let resolved = false;
|
||||
const safeResolve = (val: string | null) => {
|
||||
if (!resolved) { resolved = true; clearTimeout(safetyTimeout); resolve(val); }
|
||||
};
|
||||
const safetyTimeout = setTimeout(() => safeResolve(null), 10000);
|
||||
|
||||
const toast = this._emit({
|
||||
...opts,
|
||||
type: opts.type || 'info',
|
||||
message,
|
||||
duration: 0,
|
||||
closeButton: false,
|
||||
closeOnClick: false,
|
||||
draggable: false,
|
||||
html: promptHTML(opts),
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
const input = toast.el?.querySelector('.met-input') as HTMLInputElement | null;
|
||||
const submitBtn = toast.el?.querySelector('.met-submit-btn') as HTMLElement | null;
|
||||
const cancelBtn = toast.el?.querySelector('.met-cancel-btn') as HTMLElement | null;
|
||||
|
||||
if (input) {
|
||||
input.focus();
|
||||
input.addEventListener('keydown', (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
toast.close();
|
||||
safeResolve(input.value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (submitBtn) {
|
||||
submitBtn.addEventListener('click', () => {
|
||||
toast.close();
|
||||
safeResolve(input?.value || null);
|
||||
});
|
||||
}
|
||||
|
||||
if (cancelBtn) {
|
||||
cancelBtn.addEventListener('click', () => {
|
||||
toast.close();
|
||||
safeResolve(null);
|
||||
});
|
||||
}
|
||||
}, 0);
|
||||
});
|
||||
},
|
||||
|
||||
progress(messageOrOpts: string | ToastOptions, opts: ProgressOptions = {}): ProgressControl {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||||
normalized.duration = 0;
|
||||
normalized.closeButton = false;
|
||||
normalized.showProgress = false;
|
||||
|
||||
const toast = this._emit({
|
||||
...normalized,
|
||||
html: progressHTML(opts),
|
||||
});
|
||||
|
||||
return {
|
||||
id: toast.id,
|
||||
|
||||
setProgress(percent: number) {
|
||||
const fill = toast.el?.querySelector('.met-progress-fill') as HTMLElement | null;
|
||||
const text = toast.el?.querySelector('.met-progress-text') as HTMLElement | null;
|
||||
|
||||
if (fill) {
|
||||
fill.style.width = `${Math.min(100, Math.max(0, percent))}%`;
|
||||
}
|
||||
if (text) {
|
||||
text.textContent = `${Math.round(percent)}%`;
|
||||
}
|
||||
},
|
||||
|
||||
complete(message?: string) {
|
||||
this.setProgress(100);
|
||||
setTimeout(() => {
|
||||
toast.update({
|
||||
type: 'success',
|
||||
message: message || t('success'),
|
||||
html: '',
|
||||
});
|
||||
setTimeout(() => toast.close(), 1000);
|
||||
}, 300);
|
||||
},
|
||||
|
||||
error(message?: string) {
|
||||
toast.update({
|
||||
type: 'error',
|
||||
message: message || t('error'),
|
||||
html: '',
|
||||
});
|
||||
setTimeout(() => toast.close(), 2000);
|
||||
},
|
||||
|
||||
dismiss() {
|
||||
toast.close();
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
countdown(message: string, seconds = 10, opts: CountdownOptions = {}): CountdownControl {
|
||||
if (typeof message !== 'string') {
|
||||
console.error('MeToast.countdown: message must be a string');
|
||||
return { id: '', cancel: () => {}, pause: () => {}, resume: () => {} };
|
||||
}
|
||||
|
||||
let remaining = Math.max(1, parseInt(String(seconds)) || 10);
|
||||
let timer: ReturnType<typeof setInterval> | null = null;
|
||||
|
||||
const toast = this._emit({
|
||||
...opts,
|
||||
type: opts.type || 'warning',
|
||||
message: message.replace(/\{seconds\}/g, String(remaining)),
|
||||
duration: 0,
|
||||
closeButton: true,
|
||||
showProgress: false,
|
||||
});
|
||||
|
||||
const tick = (): void => {
|
||||
remaining--;
|
||||
|
||||
if (remaining <= 0) {
|
||||
if (timer !== null) clearInterval(timer);
|
||||
toast.close();
|
||||
|
||||
if (typeof opts.onComplete === 'function') {
|
||||
try { opts.onComplete(); } catch (e) { console.error('countdown onComplete error:', e); }
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
toast.update({
|
||||
message: message.replace(/\{seconds\}/g, String(remaining)),
|
||||
});
|
||||
};
|
||||
|
||||
timer = setInterval(tick, 1000);
|
||||
|
||||
return {
|
||||
id: toast.id,
|
||||
cancel() { if (timer !== null) clearInterval(timer); toast.close(); },
|
||||
pause() { if (timer !== null) clearInterval(timer); },
|
||||
resume() { if (timer !== null) clearInterval(timer); timer = setInterval(tick, 1000); },
|
||||
};
|
||||
},
|
||||
|
||||
action(messageOrOpts: string | ToastOptions, actions: ActionButton[] = [], opts: ToastOptions = {}): ActionControl {
|
||||
const normalized = normalizeArgs([messageOrOpts, opts], 'info');
|
||||
normalized.duration = opts.duration ?? 0;
|
||||
normalized.closeButton = opts.closeButton ?? true;
|
||||
|
||||
const toast = this._emit({
|
||||
...normalized,
|
||||
html: (normalized.html || '') + actionHTML(actions),
|
||||
});
|
||||
|
||||
if (Array.isArray(actions)) {
|
||||
setTimeout(() => {
|
||||
actions.forEach((a, i) => {
|
||||
const btn = toast.el?.querySelector(`.met-action-btn-${i}`) as HTMLElement | null;
|
||||
if (btn && typeof a.onClick === 'function') {
|
||||
btn.addEventListener('click', (e: Event) => {
|
||||
// 阻止冒泡到 toast 的 closeOnClick 处理器,由 close 选项控制是否关闭
|
||||
e.stopPropagation();
|
||||
try { a.onClick(toast); } catch (err) { console.error('Action onClick error:', err); }
|
||||
if (a.close !== false) toast.close();
|
||||
});
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}
|
||||
|
||||
return { id: toast.id, toast, dismiss: () => toast.close() };
|
||||
},
|
||||
|
||||
queue(messages: Array<string | ToastOptions>, opts: QueueOptions = {}): QueueControl {
|
||||
if (!Array.isArray(messages)) {
|
||||
console.error('MeToast.queue: messages must be an array');
|
||||
const p = Promise.resolve();
|
||||
return { then: (fn) => p.then(fn), catch: (rj) => p.catch(rj), cancel: () => {} };
|
||||
}
|
||||
|
||||
let cancelled = false;
|
||||
const promise = new Promise<void>((resolve) => {
|
||||
let index = 0;
|
||||
const delay = opts.delay || 1000;
|
||||
const userOnClose = opts.onClose;
|
||||
|
||||
const showNext = (): void => {
|
||||
if (cancelled || index >= messages.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const message = messages[index];
|
||||
index++;
|
||||
|
||||
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
|
||||
const msgObj: ToastOptions = (typeof message === 'object' && message !== null) ? message as ToastOptions : {};
|
||||
const msgOnClose = msgObj.onClose;
|
||||
|
||||
this._emit({
|
||||
...opts,
|
||||
...msgObj,
|
||||
message: msg,
|
||||
duration: msgObj.duration || opts.duration || 3000,
|
||||
onClose: (toast: ToastInstance) => {
|
||||
if (typeof msgOnClose === 'function') {
|
||||
try { msgOnClose(toast); } catch (_e) { /* noop */ }
|
||||
}
|
||||
if (typeof userOnClose === 'function') {
|
||||
try { userOnClose(toast); } catch (_e) { /* noop */ }
|
||||
}
|
||||
setTimeout(showNext, delay);
|
||||
},
|
||||
} as ToastOptions);
|
||||
};
|
||||
|
||||
showNext();
|
||||
});
|
||||
|
||||
return {
|
||||
then: (fn, rj) => promise.then(fn, rj),
|
||||
catch: (rj) => promise.catch(rj),
|
||||
cancel: () => { cancelled = true; },
|
||||
};
|
||||
},
|
||||
|
||||
stack(messages: Array<string | ToastOptions>, opts: StackOptions = {}): void {
|
||||
if (!Array.isArray(messages)) {
|
||||
console.error('MeToast.stack: messages must be an array');
|
||||
return;
|
||||
}
|
||||
|
||||
messages.forEach((message, index) => {
|
||||
setTimeout(() => {
|
||||
const msg = typeof message === 'string' ? message : ((message as ToastOptions)?.message || '');
|
||||
|
||||
this._emit({
|
||||
...opts,
|
||||
...((typeof message === 'object' && message !== null) ? message as ToastOptions : {}),
|
||||
message: msg,
|
||||
} as ToastOptions);
|
||||
}, index * (opts.stagger || 100));
|
||||
});
|
||||
},
|
||||
|
||||
dismiss(id?: string): void {
|
||||
if (id) {
|
||||
const t = this._toasts.get(id);
|
||||
if (t) t.close();
|
||||
return;
|
||||
}
|
||||
this._toasts.forEach(t => t.close());
|
||||
},
|
||||
|
||||
/**
|
||||
* 立即移除指定 Toast(不触发离场动画)
|
||||
*/
|
||||
removeToast(id: string): void {
|
||||
if (!id) return;
|
||||
const t = this._toasts.get(id);
|
||||
if (t) t.remove();
|
||||
},
|
||||
|
||||
clear(position?: string): void {
|
||||
this._toasts.forEach(t => {
|
||||
if (!position || t.config.position === position) t.close();
|
||||
});
|
||||
},
|
||||
|
||||
getAll(): Map<string, ToastInstance> {
|
||||
return new Map(this._toasts);
|
||||
},
|
||||
|
||||
count(): number {
|
||||
return this._toasts.size;
|
||||
},
|
||||
|
||||
group(name: string): GroupAPI {
|
||||
const self = this;
|
||||
const methods = ['show', 'success', 'error', 'warning', 'info', 'loading', 'action'];
|
||||
const g: GroupAPI = { _group: name } as unknown as GroupAPI;
|
||||
methods.forEach(m => {
|
||||
(g as unknown as Record<string, (...args: unknown[]) => unknown>)[m] = (...args: unknown[]) => {
|
||||
const lastArg = args[args.length - 1];
|
||||
const isObj = typeof lastArg === 'object' && lastArg !== null && !Array.isArray(lastArg);
|
||||
const selfMethods = self as unknown as Record<string, (...a: unknown[]) => unknown>;
|
||||
if (isObj && args.length === 1) {
|
||||
return selfMethods[m]({ ...(lastArg as Record<string, unknown>), group: name });
|
||||
}
|
||||
if (isObj) {
|
||||
args.pop();
|
||||
return selfMethods[m](...args, { ...(lastArg as Record<string, unknown>), group: name });
|
||||
}
|
||||
return selfMethods[m](...args, { group: name });
|
||||
};
|
||||
});
|
||||
g.dismiss = () => self.dismissGroup(name);
|
||||
g.count = () => self._groupCount(name);
|
||||
return g;
|
||||
},
|
||||
|
||||
dismissGroup(name: string): void {
|
||||
this._toasts.forEach(t => { if (t.group === name) t.close(); });
|
||||
},
|
||||
|
||||
_groupCount(name: string): number {
|
||||
let c = 0;
|
||||
this._toasts.forEach(t => { if (t.group === name) c++; });
|
||||
return c;
|
||||
},
|
||||
|
||||
getToasts(): ToastInstance[] {
|
||||
return Array.from(this._toasts.values());
|
||||
},
|
||||
|
||||
hasToasts(): boolean {
|
||||
return this._toasts.size > 0;
|
||||
},
|
||||
|
||||
getToast(id: string): ToastInstance | null {
|
||||
if (!id) return null;
|
||||
return this._toasts.get(id) || null;
|
||||
},
|
||||
|
||||
closeAll(): void {
|
||||
this._toasts.forEach(t => t.close());
|
||||
},
|
||||
|
||||
clearAll(): void {
|
||||
this._toasts.forEach(t => t.close());
|
||||
},
|
||||
|
||||
pauseAll(): void {
|
||||
this._toasts.forEach(t => t._pause());
|
||||
},
|
||||
|
||||
resumeAll(): void {
|
||||
this._toasts.forEach(t => t._resume());
|
||||
},
|
||||
|
||||
updateAll(partial: Partial<ToastOptions>): void {
|
||||
if (partial && typeof partial === 'object') {
|
||||
this._toasts.forEach(t => t.update(partial));
|
||||
}
|
||||
},
|
||||
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[] {
|
||||
if (typeof predicate !== 'function') return [];
|
||||
return Array.from(this._toasts.values()).filter(predicate);
|
||||
},
|
||||
|
||||
findByType(type: string): ToastInstance[] {
|
||||
return this.findToasts(t => t.type === type);
|
||||
},
|
||||
|
||||
findByPosition(position: string): ToastInstance[] {
|
||||
return this.findToasts(t => t.config.position === position);
|
||||
},
|
||||
|
||||
// ====== 以下方法依赖子模块(themes/i18n/plugins/animations),由 index.ts 注入 ======
|
||||
|
||||
init(options: InitOptions = {}): MeToast {
|
||||
Toast.trigger('beforeInit');
|
||||
this._destroyed = false;
|
||||
|
||||
if (options.config) {
|
||||
this.configure(options.config);
|
||||
}
|
||||
if (options.theme && this.themes) {
|
||||
this.themes.switchTheme(options.theme);
|
||||
}
|
||||
if (options.locale && this.i18n) {
|
||||
this.i18n.switchLocale(options.locale);
|
||||
}
|
||||
if (options.plugins && Array.isArray(options.plugins)) {
|
||||
options.plugins.forEach((p) => {
|
||||
this.use(p);
|
||||
});
|
||||
}
|
||||
|
||||
Toast.trigger('afterInit');
|
||||
return this;
|
||||
},
|
||||
|
||||
getStatus(): StatusInfo {
|
||||
return {
|
||||
version: VERSION,
|
||||
toasts: this._toasts.size,
|
||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||
plugins: this.plugins?.getNames?.() || [],
|
||||
animations: this.animations?.getActiveCount?.() || 0,
|
||||
};
|
||||
},
|
||||
|
||||
getConfig(): ToastConfig {
|
||||
return { ...this._config };
|
||||
},
|
||||
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast {
|
||||
if (config && typeof config === 'object') {
|
||||
Object.assign(this._config, config);
|
||||
Toast.trigger('configChange');
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
resetConfig(): MeToast {
|
||||
const keys = Object.keys(this._config);
|
||||
keys.forEach(k => delete (this._config as Record<string, unknown>)[k]);
|
||||
Object.assign(this._config, DEFAULTS);
|
||||
Toast.trigger('configChange');
|
||||
return this;
|
||||
},
|
||||
|
||||
use(plugin: string | Plugin, options: Record<string, unknown> = {}): MeToast {
|
||||
if (typeof plugin === 'string') {
|
||||
const preset = presetPlugins[plugin];
|
||||
if (!preset) {
|
||||
console.warn(`Preset plugin "${plugin}" not found`);
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
|
||||
// 连接插件钩子(重复 use 前先卸载旧钩子,防止重复注册)
|
||||
// 注意:off 引用存在已注册的插件对象上(stored),uninstall 时 this 即该对象
|
||||
const stored = this.plugins.get(plugin) as (Record<string, unknown> & Plugin) | null;
|
||||
if (plugin === 'accessibility') {
|
||||
if (stored && typeof stored._off === 'function') { (stored._off as () => void)(); }
|
||||
const acc = preset as Record<string, (t: ToastInstance) => void>;
|
||||
if (stored) {
|
||||
stored._off = Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
if (typeof acc.announce === 'function') acc.announce(toast);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
if (stored && typeof stored._saveOff === 'function') { (stored._saveOff as () => void)(); }
|
||||
const saved = typeof preset.install === 'function'
|
||||
? preset.install(this.plugins as unknown as import('./types.js').PluginManager)
|
||||
: null;
|
||||
if (saved) this.configure(saved as Partial<ToastConfig>);
|
||||
const p = preset as Record<string, (c: unknown) => void>;
|
||||
if (stored) {
|
||||
stored._saveOff = Toast.on('afterClose', () => {
|
||||
if (typeof p.save === 'function') p.save(this.getConfig());
|
||||
});
|
||||
}
|
||||
}
|
||||
} else if (plugin && typeof plugin === 'object') {
|
||||
this.plugins.register(plugin.name || 'custom', { ...plugin, ...options } as Plugin);
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
destroy(): void {
|
||||
if (this._destroyed) return;
|
||||
this._destroyed = true;
|
||||
|
||||
Toast.trigger('beforeDestroy');
|
||||
this.dismiss();
|
||||
|
||||
// 清理子模块
|
||||
if (this.plugins && typeof (this.plugins as unknown as Record<string, unknown>).destroy === 'function') {
|
||||
(this.plugins as unknown as Record<string, () => void>).destroy();
|
||||
}
|
||||
if (this.themes) {
|
||||
if (typeof this.themes.clearThemeListeners === 'function') {
|
||||
this.themes.clearThemeListeners();
|
||||
}
|
||||
if (typeof this.themes.unwatchSystemTheme === 'function') {
|
||||
this.themes.unwatchSystemTheme();
|
||||
}
|
||||
}
|
||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||
this.i18n.clearLocaleListeners();
|
||||
}
|
||||
if (this.animations && typeof this.animations.destroy === 'function') {
|
||||
this.animations.destroy();
|
||||
}
|
||||
|
||||
// 清理 DOM 与静态资源
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach((c) => { if (c.parentNode) c.parentNode.removeChild(c); });
|
||||
|
||||
['metona-toast-styles', 'metona-toast-theme-css', 'metona-toast-custom-styles', 'metona-toast-anim-styles'].forEach((id) => {
|
||||
const el = document.getElementById(id);
|
||||
if (el && el.parentNode) el.parentNode.removeChild(el);
|
||||
});
|
||||
}
|
||||
|
||||
this._toasts.clear();
|
||||
_containerCache.clear();
|
||||
Toast._registry.clear();
|
||||
|
||||
// afterDestroy 必须在清理 hooks 之前触发
|
||||
Toast.trigger('afterDestroy');
|
||||
Toast._hooks.clear();
|
||||
},
|
||||
|
||||
// 子模块引用(由 index.ts 注入实际实现)
|
||||
animations: null as unknown as MeToast['animations'],
|
||||
themes: null as unknown as MeToast['themes'],
|
||||
i18n: null as unknown as MeToast['i18n'],
|
||||
plugins: null as unknown as MeToast['plugins'],
|
||||
presetPlugins: {} as MeToast['presetPlugins'],
|
||||
};
|
||||
|
||||
// 连接 Toast 静态回调到 meToast 实例
|
||||
Toast.setCallbacks({
|
||||
onError: (info: ErrorInfo) => {
|
||||
const onError = meToast._config.onError;
|
||||
if (typeof onError === 'function') {
|
||||
try { onError(info); } catch (_e) { /* noop */ }
|
||||
}
|
||||
},
|
||||
removeToast: (id: string) => {
|
||||
meToast._toasts.delete(id);
|
||||
},
|
||||
});
|
||||
|
||||
export { meToast as default, meToast };
|
||||
@@ -1,69 +1,58 @@
|
||||
/**
|
||||
* MetonaToast Constants - 常量定义
|
||||
* MetonaToast Constants — 常量定义
|
||||
* @module constants
|
||||
* @version 2.0.1
|
||||
* @description 默认配置、颜色、动画、主题等常量
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { ICONS } from './icons.js';
|
||||
import { LOCALES } from './locales.js';
|
||||
|
||||
export { ICONS, LOCALES };
|
||||
|
||||
/**
|
||||
* 版本号 — 唯一来源,发布时只需修改此处
|
||||
*/
|
||||
export const VERSION = '0.5.0';
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
export const DEFAULTS = Object.freeze({
|
||||
// 基础配置
|
||||
position: 'top-right',
|
||||
duration: 4000,
|
||||
max: 6,
|
||||
gap: 12,
|
||||
offset: 24,
|
||||
|
||||
// 交互配置
|
||||
pauseOnHover: true,
|
||||
closeOnClick: true,
|
||||
draggable: true,
|
||||
|
||||
// 显示配置
|
||||
dragThreshold: 120,
|
||||
showProgress: true,
|
||||
progressDirection: 'horizontal',
|
||||
progressDirection: 'horizontal' as const,
|
||||
icon: true,
|
||||
closeButton: true,
|
||||
|
||||
// 主题配置
|
||||
theme: 'auto',
|
||||
animation: 'slide',
|
||||
|
||||
// 布局配置
|
||||
zIndex: 9999,
|
||||
width: 360,
|
||||
|
||||
// 自定义配置
|
||||
className: '',
|
||||
style: {},
|
||||
|
||||
// 回调函数
|
||||
onShow: null,
|
||||
onClose: null,
|
||||
onClick: null,
|
||||
onUpdate: null,
|
||||
|
||||
// 高级
|
||||
style: {} as Record<string, string>,
|
||||
onShow: null as ((...args: unknown[]) => void) | null,
|
||||
onBeforeShow: null as ((...args: unknown[]) => boolean | void) | null,
|
||||
onClose: null as ((...args: unknown[]) => void) | null,
|
||||
onClick: null as ((...args: unknown[]) => void) | null,
|
||||
onUpdate: null as ((...args: unknown[]) => void) | null,
|
||||
resetTimerOnUpdate: false,
|
||||
notifyWhenHidden: false,
|
||||
|
||||
// 国际化
|
||||
onError: null as ((...args: unknown[]) => void) | null,
|
||||
locale: 'zh-CN',
|
||||
|
||||
// 插件
|
||||
plugins: [],
|
||||
plugins: [] as Array<string | Record<string, unknown>>,
|
||||
});
|
||||
|
||||
// Icons — 从 icons.js 导入并重新导出(保持向后兼容)
|
||||
import { ICONS } from './icons.js';
|
||||
export { ICONS };
|
||||
|
||||
/**
|
||||
* 类型颜色配置
|
||||
*/
|
||||
export const TYPE_COLORS = {
|
||||
export const TYPE_COLORS: Record<string, { fg: string; bg: string }> = {
|
||||
success: { fg: '#10b981', bg: 'rgba(16, 185, 129, 0.1)' },
|
||||
error: { fg: '#ef4444', bg: 'rgba(239, 68, 68, 0.1)' },
|
||||
warning: { fg: '#f59e0b', bg: 'rgba(245, 158, 11, 0.1)' },
|
||||
@@ -176,70 +165,75 @@ export const TYPE_COLORS = {
|
||||
/**
|
||||
* 动画配置
|
||||
*/
|
||||
export const ANIMATIONS = {
|
||||
export const ANIMATIONS: Record<string, {
|
||||
enter: Record<string, string>;
|
||||
leave: Record<string, string>;
|
||||
duration: number;
|
||||
easing: string;
|
||||
}> = {
|
||||
slide: {
|
||||
enter: { transform: 'translateX(80px)', opacity: 0 },
|
||||
leave: { transform: 'translateX(120%)', opacity: 0 },
|
||||
enter: { transform: 'translateX(80px)', opacity: '0' },
|
||||
leave: { transform: 'translateX(120%)', opacity: '0' },
|
||||
duration: 400,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
fade: {
|
||||
enter: { opacity: 0, filter: 'blur(3px)' },
|
||||
leave: { opacity: 0, filter: 'blur(3px)' },
|
||||
enter: { opacity: '0', filter: 'blur(3px)' },
|
||||
leave: { opacity: '0', filter: 'blur(3px)' },
|
||||
duration: 500,
|
||||
easing: 'ease',
|
||||
},
|
||||
scale: {
|
||||
enter: { transform: 'scale(0.55)', opacity: 0 },
|
||||
leave: { transform: 'scale(0.55)', opacity: 0 },
|
||||
enter: { transform: 'scale(0.55)', opacity: '0' },
|
||||
leave: { transform: 'scale(0.55)', opacity: '0' },
|
||||
duration: 450,
|
||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
},
|
||||
bounce: {
|
||||
enter: { transform: 'translateY(-80px)', opacity: 0 },
|
||||
leave: { transform: 'translateY(20px)', opacity: 0 },
|
||||
enter: { transform: 'translateY(-80px)', opacity: '0' },
|
||||
leave: { transform: 'translateY(20px)', opacity: '0' },
|
||||
duration: 650,
|
||||
easing: 'ease',
|
||||
},
|
||||
flip: {
|
||||
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: 0 },
|
||||
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: 0 },
|
||||
enter: { transform: 'perspective(500px) rotateX(-90deg)', opacity: '0' },
|
||||
leave: { transform: 'perspective(500px) rotateX(90deg)', opacity: '0' },
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
rotate: {
|
||||
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: 0 },
|
||||
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: 0 },
|
||||
enter: { transform: 'rotate(-25deg) scale(0.6)', opacity: '0' },
|
||||
leave: { transform: 'rotate(25deg) scale(0.6)', opacity: '0' },
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
zoom: {
|
||||
enter: { transform: 'scale(0.1)', opacity: 0 },
|
||||
leave: { transform: 'scale(0.1)', opacity: 0 },
|
||||
enter: { transform: 'scale(0.1)', opacity: '0' },
|
||||
leave: { transform: 'scale(0.1)', opacity: '0' },
|
||||
duration: 500,
|
||||
easing: 'cubic-bezier(0.34, 1.56, 0.64, 1)',
|
||||
},
|
||||
slideUp: {
|
||||
enter: { transform: 'translateY(60px)', opacity: 0 },
|
||||
leave: { transform: 'translateY(-120%)', opacity: 0 },
|
||||
enter: { transform: 'translateY(60px)', opacity: '0' },
|
||||
leave: { transform: 'translateY(-120%)', opacity: '0' },
|
||||
duration: 400,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
slideDown: {
|
||||
enter: { transform: 'translateY(-60px)', opacity: 0 },
|
||||
leave: { transform: 'translateY(120%)', opacity: 0 },
|
||||
enter: { transform: 'translateY(-60px)', opacity: '0' },
|
||||
leave: { transform: 'translateY(120%)', opacity: '0' },
|
||||
duration: 400,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
slideLeft: {
|
||||
enter: { transform: 'translateX(-90px)', opacity: 0 },
|
||||
leave: { transform: 'translateX(120%)', opacity: 0 },
|
||||
enter: { transform: 'translateX(-90px)', opacity: '0' },
|
||||
leave: { transform: 'translateX(120%)', opacity: '0' },
|
||||
duration: 400,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
slideRight: {
|
||||
enter: { transform: 'translateX(90px)', opacity: 0 },
|
||||
leave: { transform: 'translateX(-120%)', opacity: 0 },
|
||||
enter: { transform: 'translateX(90px)', opacity: '0' },
|
||||
leave: { transform: 'translateX(-120%)', opacity: '0' },
|
||||
duration: 400,
|
||||
easing: 'cubic-bezier(0.25, 0.46, 0.45, 0.94)',
|
||||
},
|
||||
@@ -248,7 +242,15 @@ export const ANIMATIONS = {
|
||||
/**
|
||||
* 主题配置
|
||||
*/
|
||||
export const THEMES = {
|
||||
export const THEMES: Record<string, {
|
||||
bg: string;
|
||||
text: string;
|
||||
border: string;
|
||||
shadow: string;
|
||||
hoverShadow: string;
|
||||
progressBg: string;
|
||||
closeHoverBg: string;
|
||||
}> = {
|
||||
light: {
|
||||
bg: 'rgba(255, 255, 255, 0.96)',
|
||||
text: '#1f2937',
|
||||
@@ -267,7 +269,15 @@ export const THEMES = {
|
||||
progressBg: 'rgba(255, 255, 255, 0.08)',
|
||||
closeHoverBg: 'rgba(255, 255, 255, 0.08)',
|
||||
},
|
||||
auto: 'auto',
|
||||
auto: {
|
||||
bg: 'rgba(255, 255, 255, 0.96)',
|
||||
text: '#1f2937',
|
||||
border: 'rgba(0, 0, 0, 0.06)',
|
||||
shadow: '0 10px 36px -10px rgba(0, 0, 0, 0.18), 0 4px 14px -4px rgba(0, 0, 0, 0.08)',
|
||||
hoverShadow: '0 14px 48px -10px rgba(0, 0, 0, 0.22), 0 6px 18px -4px rgba(0, 0, 0, 0.10)',
|
||||
progressBg: 'rgba(0, 0, 0, 0.06)',
|
||||
closeHoverBg: 'rgba(0, 0, 0, 0.06)',
|
||||
},
|
||||
warm: {
|
||||
bg: 'rgba(255, 251, 235, 0.96)',
|
||||
text: '#78350f',
|
||||
@@ -280,7 +290,7 @@ export const THEMES = {
|
||||
};
|
||||
|
||||
/**
|
||||
* 位置配置
|
||||
* 位置列表
|
||||
*/
|
||||
export const POSITIONS = [
|
||||
'top-left',
|
||||
@@ -289,10 +299,10 @@ export const POSITIONS = [
|
||||
'bottom-left',
|
||||
'bottom-center',
|
||||
'bottom-right',
|
||||
];
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 类型配置
|
||||
* 类型列表
|
||||
*/
|
||||
export const TYPES = [
|
||||
'default', 'success', 'error', 'warning', 'info', 'loading',
|
||||
@@ -311,23 +321,19 @@ export const TYPES = [
|
||||
'checkCircle', 'xCircle', 'alertCircle', 'infoCircle', 'helpCircle',
|
||||
'alertTriangle', 'checkSquare', 'square', 'circle', 'triangle',
|
||||
'hexagon', 'octagon', 'pentagon', 'diamond',
|
||||
];
|
||||
|
||||
// Locales — 从 locales.js 导入并重新导出(保持向后兼容)
|
||||
import { LOCALES } from './locales.js';
|
||||
export { LOCALES };
|
||||
] as const;
|
||||
|
||||
/**
|
||||
* 进度条方向
|
||||
*/
|
||||
export const PROGRESS_DIRECTIONS = ['horizontal', 'vertical'];
|
||||
export const PROGRESS_DIRECTIONS = ['horizontal', 'vertical'] as const;
|
||||
|
||||
/**
|
||||
* 动画类型
|
||||
*/
|
||||
export const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'];
|
||||
export const ANIMATION_TYPES = ['slide', 'fade', 'scale', 'bounce', 'flip', 'rotate', 'zoom', 'slideUp', 'slideDown', 'slideLeft', 'slideRight'] as const;
|
||||
|
||||
/**
|
||||
* 主题类型
|
||||
*/
|
||||
export const THEME_TYPES = ['light', 'dark', 'auto'];
|
||||
export const THEME_TYPES = ['light', 'dark', 'auto'] as const;
|
||||
-1206
File diff suppressed because it is too large
Load Diff
+115
-274
@@ -1,35 +1,33 @@
|
||||
/**
|
||||
* MetonaToast i18n - 国际化管理
|
||||
* MetonaToast i18n — 国际化管理
|
||||
* @module i18n
|
||||
* @version 2.0.1
|
||||
* @description 多语言支持、语言切换和翻译管理
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { LOCALES } from './constants.js';
|
||||
import type { I18nUtils, LocaleInfo } from './types.js';
|
||||
|
||||
// 当前语言状态
|
||||
let currentLocale = 'zh-CN';
|
||||
let localeListeners = new Set();
|
||||
const localeListeners: Set<(locale: string) => void> = new Set();
|
||||
let fallbackLocale = 'zh-CN';
|
||||
|
||||
/**
|
||||
* 获取当前语言
|
||||
* @returns {string} 当前语言
|
||||
*/
|
||||
export const getCurrentLocale = () => {
|
||||
export const getCurrentLocale = (): string => {
|
||||
return currentLocale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置当前语言
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const setCurrentLocale = (locale) => {
|
||||
export const setCurrentLocale = (locale: string): void => {
|
||||
if (!LOCALES[locale]) {
|
||||
console.warn(`Locale "${locale}" not found, falling back to "${fallbackLocale}"`);
|
||||
locale = fallbackLocale;
|
||||
}
|
||||
|
||||
|
||||
currentLocale = locale;
|
||||
notifyLocaleListeners(locale);
|
||||
saveLocale(locale);
|
||||
@@ -37,130 +35,104 @@ export const setCurrentLocale = (locale) => {
|
||||
|
||||
/**
|
||||
* 获取回退语言
|
||||
* @returns {string} 回退语言
|
||||
*/
|
||||
export const getFallbackLocale = () => {
|
||||
export const getFallbackLocale = (): string => {
|
||||
return fallbackLocale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置回退语言
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const setFallbackLocale = (locale) => {
|
||||
export const setFallbackLocale = (locale: string): void => {
|
||||
if (!LOCALES[locale]) {
|
||||
console.warn(`Fallback locale "${locale}" not found`);
|
||||
return;
|
||||
}
|
||||
|
||||
fallbackLocale = locale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 翻译函数
|
||||
* @param {string} key - 翻译键
|
||||
* @param {Object} params - 插值参数
|
||||
* @returns {string} 翻译后的字符串
|
||||
*/
|
||||
export const t = (key, params = {}) => {
|
||||
// 尝试当前语言
|
||||
export const t = (key: string, params: Record<string, string | number> = {}): string => {
|
||||
const currentTranslation = getTranslation(currentLocale, key);
|
||||
if (currentTranslation !== undefined) {
|
||||
return interpolate(currentTranslation, params);
|
||||
}
|
||||
|
||||
// 尝试回退语言
|
||||
|
||||
if (currentLocale !== fallbackLocale) {
|
||||
const fallbackTranslation = getTranslation(fallbackLocale, key);
|
||||
if (fallbackTranslation !== undefined) {
|
||||
return interpolate(fallbackTranslation, params);
|
||||
}
|
||||
}
|
||||
|
||||
// 返回键名
|
||||
|
||||
console.warn(`Translation missing for key "${key}" in locale "${currentLocale}"`);
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @param {string} key - 翻译键
|
||||
* @returns {string|undefined} 翻译字符串
|
||||
*/
|
||||
const getTranslation = (locale, key) => {
|
||||
const getTranslation = (locale: string, key: string): string | undefined => {
|
||||
const localeData = LOCALES[locale];
|
||||
if (!localeData) return undefined;
|
||||
|
||||
// 支持嵌套键 (如 'common.close')
|
||||
|
||||
const keys = key.split('.');
|
||||
let result = localeData;
|
||||
|
||||
let result: unknown = localeData;
|
||||
|
||||
for (const k of keys) {
|
||||
if (result && typeof result === 'object' && k in result) {
|
||||
result = result[k];
|
||||
if (result && typeof result === 'object' && k in (result as Record<string, unknown>)) {
|
||||
result = (result as Record<string, unknown>)[k];
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return typeof result === 'string' ? result : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 插值函数
|
||||
* @param {string} str - 包含占位符的字符串
|
||||
* @param {Object} params - 插值参数
|
||||
* @returns {string} 插值后的字符串
|
||||
*/
|
||||
const interpolate = (str, params) => {
|
||||
return str.replace(/\{(\w+)\}/g, (match, key) => {
|
||||
return params[key] !== undefined ? params[key] : match;
|
||||
const interpolate = (str: string, params: Record<string, string | number>): string => {
|
||||
return str.replace(/\{(\w+)\}/g, (_match, key: string) => {
|
||||
return params[key] !== undefined ? String(params[key]) : _match;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查翻译是否存在
|
||||
* @param {string} key - 翻译键
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
export const hasTranslation = (key) => {
|
||||
export const hasTranslation = (key: string): boolean => {
|
||||
return getTranslation(currentLocale, key) !== undefined ||
|
||||
getTranslation(fallbackLocale, key) !== undefined;
|
||||
getTranslation(fallbackLocale, key) !== undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {Object} 翻译对象
|
||||
*/
|
||||
export const getTranslations = (locale) => {
|
||||
export const getTranslations = (locale: string): Record<string, unknown> => {
|
||||
return LOCALES[locale] || {};
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @param {Object} translations - 翻译对象
|
||||
*/
|
||||
export const addTranslations = (locale, translations) => {
|
||||
export const addTranslations = (locale: string, translations: Record<string, unknown>): void => {
|
||||
if (!LOCALES[locale]) {
|
||||
LOCALES[locale] = {};
|
||||
}
|
||||
|
||||
deepMerge(LOCALES[locale], translations);
|
||||
deepMerge(LOCALES[locale], translations as Record<string, string>);
|
||||
};
|
||||
|
||||
/**
|
||||
* 深度合并对象
|
||||
* @param {Object} target - 目标对象
|
||||
* @param {Object} source - 源对象
|
||||
* @returns {Object} 合并后的对象
|
||||
*/
|
||||
const deepMerge = (target, source) => {
|
||||
const deepMerge = (target: Record<string, unknown>, source: Record<string, unknown>): Record<string, unknown> => {
|
||||
for (const key in source) {
|
||||
if (source[key] instanceof Object && key in target && target[key] instanceof Object) {
|
||||
deepMerge(target[key], source[key]);
|
||||
deepMerge(target[key] as Record<string, unknown>, source[key] as Record<string, unknown>);
|
||||
} else {
|
||||
target[key] = source[key];
|
||||
}
|
||||
@@ -170,32 +142,29 @@ const deepMerge = (target, source) => {
|
||||
|
||||
/**
|
||||
* 移除翻译
|
||||
* @param {string} locale - 语言代码
|
||||
* @param {string} key - 翻译键
|
||||
*/
|
||||
export const removeTranslation = (locale, key) => {
|
||||
export const removeTranslation = (locale: string, key: string): void => {
|
||||
const localeData = LOCALES[locale];
|
||||
if (!localeData) return;
|
||||
|
||||
|
||||
const keys = key.split('.');
|
||||
let current = localeData;
|
||||
|
||||
let current: Record<string, unknown> = localeData;
|
||||
|
||||
for (let i = 0; i < keys.length - 1; i++) {
|
||||
if (current[keys[i]] && typeof current[keys[i]] === 'object') {
|
||||
current = current[keys[i]];
|
||||
current = current[keys[i]] as Record<string, unknown>;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
delete current[keys[keys.length - 1]];
|
||||
};
|
||||
|
||||
/**
|
||||
* 清除翻译
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const clearTranslations = (locale) => {
|
||||
export const clearTranslations = (locale: string): void => {
|
||||
if (LOCALES[locale]) {
|
||||
LOCALES[locale] = {};
|
||||
}
|
||||
@@ -203,28 +172,23 @@ export const clearTranslations = (locale) => {
|
||||
|
||||
/**
|
||||
* 获取支持的语言列表
|
||||
* @returns {Array} 语言代码数组
|
||||
*/
|
||||
export const getSupportedLocales = () => {
|
||||
export const getSupportedLocales = (): string[] => {
|
||||
return Object.keys(LOCALES);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查语言是否支持
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {boolean} 是否支持
|
||||
*/
|
||||
export const isLocaleSupported = (locale) => {
|
||||
export const isLocaleSupported = (locale: string): boolean => {
|
||||
return locale in LOCALES;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言名称
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {string} 语言名称
|
||||
*/
|
||||
export const getLocaleName = (locale) => {
|
||||
const names = {
|
||||
export const getLocaleName = (locale: string): string => {
|
||||
const names: Record<string, string> = {
|
||||
'zh-CN': '简体中文',
|
||||
'zh-TW': '繁體中文',
|
||||
'en-US': 'English (US)',
|
||||
@@ -304,26 +268,22 @@ export const getLocaleName = (locale) => {
|
||||
've': 'Tshivenda',
|
||||
'nr': 'isiNdebele',
|
||||
};
|
||||
|
||||
|
||||
return names[locale] || locale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言方向
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {string} 语言方向 ('ltr' 或 'rtl')
|
||||
*/
|
||||
export const getLocaleDirection = (locale) => {
|
||||
export const getLocaleDirection = (locale: string): 'ltr' | 'rtl' => {
|
||||
const rtlLocales = ['ar', 'he', 'fa', 'ur', 'yi', 'ps', 'sd', 'ug'];
|
||||
return rtlLocales.includes(locale) ? 'rtl' : 'ltr';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取语言信息
|
||||
* @param {string} locale - 语言代码
|
||||
* @returns {Object} 语言信息
|
||||
*/
|
||||
export const getLocaleInfo = (locale) => {
|
||||
export const getLocaleInfo = (locale: string): LocaleInfo => {
|
||||
return {
|
||||
code: locale,
|
||||
name: getLocaleName(locale),
|
||||
@@ -336,17 +296,15 @@ export const getLocaleInfo = (locale) => {
|
||||
|
||||
/**
|
||||
* 获取所有语言信息
|
||||
* @returns {Array} 语言信息数组
|
||||
*/
|
||||
export const getAllLocaleInfo = () => {
|
||||
export const getAllLocaleInfo = (): LocaleInfo[] => {
|
||||
return getSupportedLocales().map(getLocaleInfo);
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存语言到本地存储
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
export const saveLocale = (locale) => {
|
||||
export const saveLocale = (locale: string): void => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('metona-toast-locale', locale);
|
||||
@@ -358,9 +316,8 @@ export const saveLocale = (locale) => {
|
||||
|
||||
/**
|
||||
* 从本地存储加载语言
|
||||
* @returns {string} 语言代码
|
||||
*/
|
||||
export const loadLocale = () => {
|
||||
export const loadLocale = (): string => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
return localStorage.getItem('metona-toast-locale') || getDefaultLocale();
|
||||
@@ -373,50 +330,43 @@ export const loadLocale = () => {
|
||||
|
||||
/**
|
||||
* 获取默认语言
|
||||
* @returns {string} 语言代码
|
||||
*/
|
||||
export const getDefaultLocale = () => {
|
||||
export const getDefaultLocale = (): string => {
|
||||
if (typeof navigator !== 'undefined') {
|
||||
// 尝试从浏览器获取语言
|
||||
const browserLocale = navigator.language || navigator.userLanguage;
|
||||
const browserLocale = navigator.language || (navigator as unknown as { userLanguage?: string }).userLanguage;
|
||||
if (browserLocale && isLocaleSupported(browserLocale)) {
|
||||
return browserLocale;
|
||||
}
|
||||
|
||||
// 尝试语言代码的前两位
|
||||
|
||||
const shortLocale = browserLocale?.split('-')[0];
|
||||
if (shortLocale && isLocaleSupported(shortLocale)) {
|
||||
return shortLocale;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return fallbackLocale;
|
||||
};
|
||||
|
||||
/**
|
||||
* 初始化国际化系统
|
||||
*/
|
||||
export const initI18n = () => {
|
||||
export const initI18n = (): void => {
|
||||
const savedLocale = loadLocale();
|
||||
setCurrentLocale(savedLocale);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换语言
|
||||
* @param {string} locale - 新语言
|
||||
*/
|
||||
export const switchLocale = (locale) => {
|
||||
export const switchLocale = (locale: string): void => {
|
||||
setCurrentLocale(locale);
|
||||
};
|
||||
|
||||
/**
|
||||
* 添加语言监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
* @returns {Function} 移除监听器函数
|
||||
*/
|
||||
export const addLocaleListener = (listener) => {
|
||||
export const addLocaleListener = (listener: (locale: string) => void): () => void => {
|
||||
localeListeners.add(listener);
|
||||
|
||||
return () => {
|
||||
localeListeners.delete(listener);
|
||||
};
|
||||
@@ -424,17 +374,15 @@ export const addLocaleListener = (listener) => {
|
||||
|
||||
/**
|
||||
* 移除语言监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
*/
|
||||
export const removeLocaleListener = (listener) => {
|
||||
export const removeLocaleListener = (listener: (locale: string) => void): void => {
|
||||
localeListeners.delete(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知语言监听器
|
||||
* @param {string} locale - 语言代码
|
||||
*/
|
||||
const notifyLocaleListeners = (locale) => {
|
||||
const notifyLocaleListeners = (locale: string): void => {
|
||||
localeListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(locale);
|
||||
@@ -447,50 +395,40 @@ const notifyLocaleListeners = (locale) => {
|
||||
/**
|
||||
* 清除所有语言监听器
|
||||
*/
|
||||
export const clearLocaleListeners = () => {
|
||||
export const clearLocaleListeners = (): void => {
|
||||
localeListeners.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化数字
|
||||
* @param {number} number - 数字
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatNumber = (number, options = {}) => {
|
||||
export const formatNumber = (number: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, options).format(number);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return number.toString();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化货币
|
||||
* @param {number} amount - 金额
|
||||
* @param {string} currency - 货币代码
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatCurrency = (amount, currency = 'USD', options = {}) => {
|
||||
export const formatCurrency = (amount: number, currency = 'USD', options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, {
|
||||
style: 'currency',
|
||||
currency,
|
||||
...options,
|
||||
}).format(amount);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return amount.toString();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化百分比
|
||||
* @param {number} value - 值
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatPercent = (value, options = {}) => {
|
||||
export const formatPercent = (value: number, options: Intl.NumberFormatOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.NumberFormat(currentLocale, {
|
||||
style: 'percent',
|
||||
@@ -498,33 +436,27 @@ export const formatPercent = (value, options = {}) => {
|
||||
maximumFractionDigits: 2,
|
||||
...options,
|
||||
}).format(value / 100);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return `${value}%`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
* @param {Date|number|string} date - 日期
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatDate = (date, options = {}) => {
|
||||
export const formatDate = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
|
||||
try {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat(currentLocale, options).format(dateObj);
|
||||
} catch (e) {
|
||||
return date.toString();
|
||||
} catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
* @param {Date|number|string} date - 日期
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatTime = (date, options = {}) => {
|
||||
export const formatTime = (date: Date | number | string, options: Intl.DateTimeFormatOptions = {}): string => {
|
||||
return formatDate(date, {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
@@ -535,22 +467,19 @@ export const formatTime = (date, options = {}) => {
|
||||
|
||||
/**
|
||||
* 格式化相对时间
|
||||
* @param {Date|number|string} date - 日期
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatRelativeTime = (date, options = {}) => {
|
||||
export const formatRelativeTime = (date: Date | number | string, options: Intl.RelativeTimeFormatOptions = {}): string => {
|
||||
try {
|
||||
const dateObj = date instanceof Date ? date : new Date(date);
|
||||
const now = new Date();
|
||||
const diff = dateObj - now;
|
||||
|
||||
const diff = dateObj.getTime() - now.getTime();
|
||||
|
||||
const rtf = new Intl.RelativeTimeFormat(currentLocale, {
|
||||
numeric: 'auto',
|
||||
...options,
|
||||
});
|
||||
|
||||
const units = [
|
||||
|
||||
const units: Array<{ unit: Intl.RelativeTimeFormatUnit; ms: number }> = [
|
||||
{ unit: 'year', ms: 365 * 24 * 60 * 60 * 1000 },
|
||||
{ unit: 'month', ms: 30 * 24 * 60 * 60 * 1000 },
|
||||
{ unit: 'week', ms: 7 * 24 * 60 * 60 * 1000 },
|
||||
@@ -559,143 +488,93 @@ export const formatRelativeTime = (date, options = {}) => {
|
||||
{ unit: 'minute', ms: 60 * 1000 },
|
||||
{ unit: 'second', ms: 1000 },
|
||||
];
|
||||
|
||||
|
||||
for (const { unit, ms } of units) {
|
||||
if (Math.abs(diff) >= ms || unit === 'second') {
|
||||
const value = Math.round(diff / ms);
|
||||
return rtf.format(value, unit);
|
||||
}
|
||||
}
|
||||
|
||||
return date.toString();
|
||||
} catch (e) {
|
||||
return date.toString();
|
||||
|
||||
return String(date);
|
||||
} catch {
|
||||
return String(date);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化列表
|
||||
* @param {Array} list - 列表
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
* 格式化列表 — 优先使用 Intl.ListFormat,降级到逗号拼接
|
||||
*/
|
||||
export const formatList = (list, options = {}) => {
|
||||
try {
|
||||
return new Intl.ListFormat(currentLocale, options).format(list);
|
||||
} catch (e) {
|
||||
return list.join(', ');
|
||||
export const formatList = (list: string[], _options: Record<string, unknown> = {}): string => {
|
||||
const intl = Intl as typeof Intl & { ListFormat?: new (locale: string, options?: Record<string, unknown>) => { format: (items: string[]) => string } };
|
||||
if (typeof intl.ListFormat === 'function') {
|
||||
try {
|
||||
return new intl.ListFormat(currentLocale, {
|
||||
style: 'long',
|
||||
type: 'conjunction',
|
||||
}).format(list);
|
||||
} catch (_e) { /* fallback below */ }
|
||||
}
|
||||
return list.join(', ');
|
||||
};
|
||||
|
||||
/**
|
||||
* 格式化复数
|
||||
* @param {number} count - 数量
|
||||
* @param {Object} options - 格式化选项
|
||||
* @returns {string} 格式化后的字符串
|
||||
*/
|
||||
export const formatPlural = (count, options = {}) => {
|
||||
export const formatPlural = (count: number, options: Intl.PluralRulesOptions = {}): string => {
|
||||
try {
|
||||
return new Intl.PluralRules(currentLocale, options).select(count);
|
||||
} catch (e) {
|
||||
} catch {
|
||||
return count === 1 ? 'one' : 'other';
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 复数翻译
|
||||
* @param {string} key - 翻译键
|
||||
* @param {number} count - 数量
|
||||
* @param {Object} params - 插值参数
|
||||
* @returns {string} 翻译后的字符串
|
||||
*/
|
||||
export const plural = (key, count, params = {}) => {
|
||||
export const plural = (key: string, count: number, params: Record<string, string | number> = {}): string => {
|
||||
const pluralForm = formatPlural(count);
|
||||
const pluralKey = `${key}.${pluralForm}`;
|
||||
|
||||
|
||||
if (hasTranslation(pluralKey)) {
|
||||
return t(pluralKey, { ...params, count });
|
||||
}
|
||||
|
||||
// 尝试通用形式
|
||||
|
||||
if (hasTranslation(key)) {
|
||||
return t(key, { ...params, count });
|
||||
}
|
||||
|
||||
|
||||
return key;
|
||||
};
|
||||
|
||||
/**
|
||||
* 日期时间格式化选项
|
||||
*/
|
||||
export const dateTimeFormats = {
|
||||
short: {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
},
|
||||
medium: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
},
|
||||
long: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
weekday: 'long',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
},
|
||||
time: {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric',
|
||||
},
|
||||
date: {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
},
|
||||
weekday: {
|
||||
weekday: 'long',
|
||||
},
|
||||
month: {
|
||||
month: 'long',
|
||||
},
|
||||
year: {
|
||||
year: 'numeric',
|
||||
},
|
||||
export const dateTimeFormats: Record<string, Intl.DateTimeFormatOptions> = {
|
||||
short: { year: 'numeric', month: 'short', day: 'numeric' },
|
||||
medium: { year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' },
|
||||
long: { year: 'numeric', month: 'long', day: 'numeric', weekday: 'long', hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
time: { hour: 'numeric', minute: 'numeric', second: 'numeric' },
|
||||
date: { year: 'numeric', month: 'long', day: 'numeric' },
|
||||
weekday: { weekday: 'long' },
|
||||
month: { month: 'long' },
|
||||
year: { year: 'numeric' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 数字格式化选项
|
||||
*/
|
||||
export const numberFormats = {
|
||||
integer: {
|
||||
maximumFractionDigits: 0,
|
||||
},
|
||||
decimal: {
|
||||
minimumFractionDigits: 2,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
percent: {
|
||||
style: 'percent',
|
||||
minimumFractionDigits: 0,
|
||||
maximumFractionDigits: 2,
|
||||
},
|
||||
currency: {
|
||||
style: 'currency',
|
||||
currency: 'USD',
|
||||
},
|
||||
export const numberFormats: Record<string, Intl.NumberFormatOptions> = {
|
||||
integer: { maximumFractionDigits: 0 },
|
||||
decimal: { minimumFractionDigits: 2, maximumFractionDigits: 2 },
|
||||
percent: { style: 'percent', minimumFractionDigits: 0, maximumFractionDigits: 2 },
|
||||
currency: { style: 'currency', currency: 'USD' },
|
||||
};
|
||||
|
||||
/**
|
||||
* 国际化工具 — 代理层,直接引用模块函数
|
||||
* 国际化工具 — 代理层
|
||||
*/
|
||||
export const i18nUtils = {
|
||||
export const i18nUtils: I18nUtils = {
|
||||
t,
|
||||
plural,
|
||||
getCurrentLocale,
|
||||
@@ -734,14 +613,18 @@ export const i18nUtils = {
|
||||
/**
|
||||
* 预设语言包
|
||||
*/
|
||||
export const presetLocales = {
|
||||
export const presetLocales: Record<string, {
|
||||
name: string;
|
||||
nativeName: string;
|
||||
direction: 'ltr' | 'rtl';
|
||||
translations: Record<string, string>;
|
||||
}> = {
|
||||
'zh-CN': {
|
||||
name: '简体中文',
|
||||
nativeName: '简体中文',
|
||||
direction: 'ltr',
|
||||
translations: LOCALES['zh-CN'],
|
||||
},
|
||||
|
||||
'en-US': {
|
||||
name: 'English (US)',
|
||||
nativeName: 'English (US)',
|
||||
@@ -750,46 +633,4 @@ export const presetLocales = {
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建国际化管理器
|
||||
* @returns {Object} 国际化管理器
|
||||
*/
|
||||
export const createI18nManager = () => {
|
||||
return {
|
||||
t,
|
||||
plural,
|
||||
getCurrentLocale,
|
||||
setCurrentLocale,
|
||||
switchLocale,
|
||||
getFallbackLocale,
|
||||
setFallbackLocale,
|
||||
hasTranslation,
|
||||
getTranslations,
|
||||
addTranslations,
|
||||
removeTranslation,
|
||||
clearTranslations,
|
||||
getSupportedLocales,
|
||||
isLocaleSupported,
|
||||
getLocaleName,
|
||||
getLocaleDirection,
|
||||
getLocaleInfo,
|
||||
getAllLocaleInfo,
|
||||
formatNumber,
|
||||
formatCurrency,
|
||||
formatPercent,
|
||||
formatDate,
|
||||
formatTime,
|
||||
formatRelativeTime,
|
||||
formatList,
|
||||
formatPlural,
|
||||
addLocaleListener,
|
||||
removeLocaleListener,
|
||||
clearLocaleListeners,
|
||||
initI18n,
|
||||
saveLocale,
|
||||
loadLocale,
|
||||
getDefaultLocale,
|
||||
};
|
||||
};
|
||||
|
||||
export { i18nUtils as default };
|
||||
+109
-109
@@ -1,134 +1,134 @@
|
||||
/**
|
||||
* MetonaToast Icons — 图标SVG定义
|
||||
* @module icons
|
||||
* @version 2.0.1
|
||||
* @description 80+ 内置SVG图标
|
||||
* @version 0.5.0
|
||||
* @description 107 个内置 SVG 图标
|
||||
*/
|
||||
|
||||
export const ICONS = {
|
||||
export const ICONS: Record<string, string> = {
|
||||
success: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
error: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
warning: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
info: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
loading: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12a9 9 0 1 1-6.219-8.56" class="met-spin"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
close: `<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
check: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 6 9 17 4 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
x: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
alert: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
question: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
star: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
heart: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
bell: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"/>
|
||||
<path d="M13.73 21a2 2 0 0 1-3.46 0"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
mail: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z"/>
|
||||
<polyline points="22,6 12,13 2,6"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
settings: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
user: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
|
||||
<circle cx="12" cy="7" r="4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
home: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
|
||||
<polyline points="9 22 9 12 15 12 15 22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
search: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
plus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
minus: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="5" y1="12" x2="19" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
edit: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
|
||||
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
trash: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="3 6 5 6 21 6"/>
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
|
||||
<line x1="10" y1="11" x2="10" y2="17"/>
|
||||
<line x1="14" y1="11" x2="14" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
download: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="7 10 12 15 17 10"/>
|
||||
<line x1="12" y1="15" x2="12" y2="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
upload: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
||||
<polyline points="17 8 12 3 7 8"/>
|
||||
<line x1="12" y1="3" x2="12" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
share: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="5" r="3"/>
|
||||
<circle cx="6" cy="12" r="3"/>
|
||||
@@ -136,58 +136,58 @@ export const ICONS = {
|
||||
<line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/>
|
||||
<line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
link: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71"/>
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
external: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/>
|
||||
<polyline points="15 3 21 3 21 9"/>
|
||||
<line x1="10" y1="14" x2="21" y2="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
clock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polyline points="12 6 12 12 16 14"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
calendar: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="4" width="18" height="18" rx="2" ry="2"/>
|
||||
<line x1="16" y1="2" x2="16" y2="6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="6"/>
|
||||
<line x1="3" y1="10" x2="21" y2="10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
map: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="1 6 1 22 8 18 16 22 23 18 23 2 16 6 8 2 1 6"/>
|
||||
<line x1="8" y1="2" x2="8" y2="18"/>
|
||||
<line x1="16" y1="6" x2="16" y2="22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
compass: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
globe: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
wifi: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M5 12.55a11 11 0 0 1 14.08 0"/>
|
||||
<path d="M1.42 9a16 16 0 0 1 21.16 0"/>
|
||||
<path d="M8.53 16.11a6 6 0 0 1 6.95 0"/>
|
||||
<line x1="12" y1="20" x2="12.01" y2="20"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
cloud: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18 10h-1.26A8 8 0 1 0 9 20h9a5 5 0 0 0 0-10z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sun: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="5"/>
|
||||
<line x1="12" y1="1" x2="12" y2="3"/>
|
||||
@@ -199,19 +199,19 @@ export const ICONS = {
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/>
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
moon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
zap: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
activity: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
cpu: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4" y="4" width="16" height="16" rx="2" ry="2"/>
|
||||
<rect x="9" y="9" width="6" height="6"/>
|
||||
@@ -224,57 +224,57 @@ export const ICONS = {
|
||||
<line x1="1" y1="9" x2="4" y2="9"/>
|
||||
<line x1="1" y1="14" x2="4" y2="14"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
database: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<ellipse cx="12" cy="5" rx="9" ry="3"/>
|
||||
<path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3"/>
|
||||
<path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
server: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="2" y="2" width="20" height="8" rx="2" ry="2"/>
|
||||
<rect x="2" y="14" width="20" height="8" rx="2" ry="2"/>
|
||||
<line x1="6" y1="6" x2="6.01" y2="6"/>
|
||||
<line x1="6" y1="18" x2="6.01" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
terminal: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="4 17 10 11 4 5"/>
|
||||
<line x1="12" y1="19" x2="20" y2="19"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
code: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="16 18 22 12 16 6"/>
|
||||
<polyline points="8 6 2 12 8 18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
git: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="18" cy="18" r="3"/>
|
||||
<circle cx="6" cy="6" r="3"/>
|
||||
<path d="M13 6h3a2 2 0 0 1 2 2v7"/>
|
||||
<line x1="6" y1="9" x2="6" y2="21"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
package: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="16.5" y1="9.4" x2="7.5" y2="4.21"/>
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
|
||||
<line x1="12" y1="22.08" x2="12" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
layers: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
|
||||
<polyline points="2 17 12 22 22 17"/>
|
||||
<polyline points="2 12 12 17 22 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
grid: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="3" width="7" height="7"/>
|
||||
<rect x="14" y="14" width="7" height="7"/>
|
||||
<rect x="3" y="14" width="7" height="7"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
list: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="8" y1="6" x2="21" y2="6"/>
|
||||
<line x1="8" y1="12" x2="21" y2="12"/>
|
||||
@@ -283,144 +283,144 @@ export const ICONS = {
|
||||
<line x1="3" y1="12" x2="3.01" y2="12"/>
|
||||
<line x1="3" y1="18" x2="3.01" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
filter: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sort: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="12" y1="5" x2="12" y2="19"/>
|
||||
<polyline points="19 12 12 19 5 12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
refresh: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<polyline points="1 20 1 14 7 14"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sync: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<polyline points="1 20 1 14 7 14"/>
|
||||
<path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
power: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M18.36 6.64a9 9 0 1 1-12.73 0"/>
|
||||
<line x1="12" y1="2" x2="12" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
battery: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="6" width="18" height="12" rx="2" ry="2"/>
|
||||
<line x1="23" y1="13" x2="23" y2="11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
bluetooth: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6.5 6.5 17.5 17.5 12 23 12 1 17.5 6.5 6.5 17.5"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
volume: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="11 5 6 9 2 9 2 15 6 15 11 19 11 5"/>
|
||||
<path d="M19.07 4.93a10 10 0 0 1 0 14.14M15.54 8.46a5 5 0 0 1 0 7.07"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
mic: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 1a3 3 0 0 0-3 3v8a3 3 0 0 0 6 0V4a3 3 0 0 0-3-3z"/>
|
||||
<path d="M19 10v2a7 7 0 0 1-14 0v-2"/>
|
||||
<line x1="12" y1="19" x2="12" y2="23"/>
|
||||
<line x1="8" y1="23" x2="16" y2="23"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
camera: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
||||
<circle cx="12" cy="13" r="4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
image: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
<circle cx="8.5" cy="8.5" r="1.5"/>
|
||||
<polyline points="21 15 16 10 5 21"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
video: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="23 7 16 12 23 17 23 7"/>
|
||||
<rect x="1" y="5" width="15" height="14" rx="2" ry="2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
music: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 18V5l12-2v13"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
<circle cx="18" cy="16" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
file: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/>
|
||||
<polyline points="13 2 13 9 20 9"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
folder: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
clipboard: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
save: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11l5 5v11a2 2 0 0 1-2 2z"/>
|
||||
<polyline points="17 21 17 13 7 13 7 21"/>
|
||||
<polyline points="7 3 7 8 15 8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
print: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 6 2 18 2 18 9"/>
|
||||
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/>
|
||||
<rect x="6" y="14" width="12" height="8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
eye: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"/>
|
||||
<circle cx="12" cy="12" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
eyeOff: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/>
|
||||
<line x1="1" y1="1" x2="23" y2="23"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
lock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
unlock: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"/>
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
shield: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
key: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
flag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M4 15s1-1 4-1 5 2 8 2 4-1 4-1V3s-1 1-4 1-5-2-8-2-4 1-4 1z"/>
|
||||
<line x1="4" y1="22" x2="4" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
bookmark: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
tag: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M20.59 13.41l-7.17 7.17a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/>
|
||||
<line x1="7" y1="7" x2="7.01" y2="7"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
gift: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="20 12 20 22 4 22 4 12"/>
|
||||
<rect x="2" y="7" width="20" height="5"/>
|
||||
@@ -428,18 +428,18 @@ export const ICONS = {
|
||||
<path d="M12 7H7.5a2.5 2.5 0 0 1 0-5C11 2 12 7 12 7z"/>
|
||||
<path d="M12 7h4.5a2.5 2.5 0 0 0 0-5C13 2 12 7 12 7z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
award: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="8" r="7"/>
|
||||
<polyline points="8.21 13.89 7 23 12 20 17 23 15.79 13.88"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
target: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<circle cx="12" cy="12" r="6"/>
|
||||
<circle cx="12" cy="12" r="2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
crosshair: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="22" y1="12" x2="18" y2="12"/>
|
||||
@@ -447,7 +447,7 @@ export const ICONS = {
|
||||
<line x1="12" y1="6" x2="12" y2="2"/>
|
||||
<line x1="12" y1="22" x2="12" y2="18"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
move: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="5 9 2 12 5 15"/>
|
||||
<polyline points="9 5 12 2 15 5"/>
|
||||
@@ -456,20 +456,20 @@ export const ICONS = {
|
||||
<line x1="2" y1="12" x2="22" y2="12"/>
|
||||
<line x1="12" y1="2" x2="12" y2="22"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
maximize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
minimize: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M8 3v3a2 2 0 0 1-2 2H3m18 0h-3a2 2 0 0 1-2-2V3m0 18v-3a2 2 0 0 1 2-2h3M3 16h3a2 2 0 0 1 2 2v3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
copy: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"/>
|
||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
cut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="6" cy="6" r="3"/>
|
||||
<circle cx="6" cy="18" r="3"/>
|
||||
@@ -477,40 +477,40 @@ export const ICONS = {
|
||||
<line x1="14.47" y1="14.48" x2="20" y2="20"/>
|
||||
<line x1="8.12" y1="8.12" x2="12" y2="12"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
paste: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M16 4h2a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V6a2 2 0 0 1 2-2h2"/>
|
||||
<rect x="8" y="2" width="8" height="4" rx="1" ry="1"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
rotateCw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="23 4 23 10 17 10"/>
|
||||
<path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
rotateCcw: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="1 4 1 10 7 10"/>
|
||||
<path d="M3.51 15a9 9 0 1 0 2.13-9.36L1 10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
zoomIn: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
<line x1="11" y1="8" x2="11" y2="14"/>
|
||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
zoomOut: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="11" cy="11" r="8"/>
|
||||
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
|
||||
<line x1="8" y1="11" x2="14" y2="11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
crop: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M6.13 1L6 16a2 2 0 0 0 2 2h15"/>
|
||||
<path d="M1 6.13L16 6a2 2 0 0 1 2 2v15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
sliders: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="4" y1="21" x2="4" y2="14"/>
|
||||
<line x1="4" y1="10" x2="4" y2="3"/>
|
||||
@@ -522,81 +522,81 @@ export const ICONS = {
|
||||
<line x1="9" y1="8" x2="15" y2="8"/>
|
||||
<line x1="17" y1="16" x2="23" y2="16"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
toggleLeft: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
|
||||
<circle cx="8" cy="12" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
toggleRight: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="1" y="5" width="22" height="14" rx="7" ry="7"/>
|
||||
<circle cx="16" cy="12" r="3"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
checkCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
|
||||
<polyline points="22 4 12 14.01 9 11.01"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
xCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="15" y1="9" x2="9" y2="15"/>
|
||||
<line x1="9" y1="9" x2="15" y2="15"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
alertCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="8" x2="12" y2="12"/>
|
||||
<line x1="12" y1="16" x2="12.01" y2="16"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
infoCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<line x1="12" y1="16" x2="12" y2="12"/>
|
||||
<line x1="12" y1="8" x2="12.01" y2="8"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
helpCircle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
alertTriangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
<line x1="12" y1="9" x2="12" y2="13"/>
|
||||
<line x1="12" y1="17" x2="12.01" y2="17"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
checkSquare: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="9 11 12 14 22 4"/>
|
||||
<path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
square: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
circle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<circle cx="12" cy="12" r="10"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
triangle: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
hexagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
octagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="7.86 2 16.14 2 22 7.86 22 16.14 16.14 22 7.86 22 2 16.14 2 7.86 7.86 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
pentagon: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polygon points="12 2 22 8.5 19 20 5 20 2 8.5 12 2"/>
|
||||
</svg>`,
|
||||
|
||||
|
||||
diamond: `<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect x="4.5" y="4.5" width="15" height="15" rx="1" transform="rotate(45 12 12)"/>
|
||||
</svg>`,
|
||||
-283
@@ -1,283 +0,0 @@
|
||||
/**
|
||||
* MetonaToast - 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 2.0.1
|
||||
* @author thzxx
|
||||
* @description 轻量、零依赖、精致美观的Toast通知库。单文件,开箱即用。
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { meToast, Toast } from './core.js';
|
||||
import { animationUtils } from './animations.js';
|
||||
import { themeUtils } from './themes.js';
|
||||
import { i18nUtils } from './i18n.js';
|
||||
import { pluginUtils, presetPlugins } from './plugins.js';
|
||||
import { DEFAULTS } from './constants.js';
|
||||
|
||||
// 版本信息
|
||||
const VERSION = '2.0.1';
|
||||
|
||||
/**
|
||||
* 主对象增强
|
||||
*/
|
||||
const enhancedMeToast = {
|
||||
...meToast,
|
||||
|
||||
version: VERSION,
|
||||
|
||||
animations: animationUtils,
|
||||
themes: themeUtils,
|
||||
i18n: i18nUtils,
|
||||
plugins: pluginUtils,
|
||||
presetPlugins,
|
||||
|
||||
/**
|
||||
* 安装插件,并连接必要的生命周期钩子
|
||||
*/
|
||||
use(plugin, options = {}) {
|
||||
if (typeof plugin === 'string') {
|
||||
const preset = presetPlugins[plugin];
|
||||
if (!preset) {
|
||||
console.warn(`Preset plugin "${plugin}" not found`);
|
||||
return this;
|
||||
}
|
||||
this.plugins.register(plugin, { ...preset, ...options });
|
||||
|
||||
// 连接插件钩子
|
||||
if (plugin === 'accessibility') {
|
||||
Toast.on('afterShow', (toast) => preset.announce(toast));
|
||||
}
|
||||
if (plugin === 'persistence') {
|
||||
const saved = preset.install();
|
||||
if (saved) this.configure(saved);
|
||||
Toast.on('afterClose', () => { preset.save(this.getConfig()); });
|
||||
}
|
||||
} else if (plugin && typeof plugin === 'object') {
|
||||
const name = plugin.name || 'custom';
|
||||
this.plugins.register(name, { ...plugin, ...options });
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
init(options = {}) {
|
||||
if (options.config) {
|
||||
this.configure(options.config);
|
||||
}
|
||||
|
||||
if (options.theme) {
|
||||
this.themes.switchTheme(options.theme);
|
||||
}
|
||||
|
||||
if (options.locale) {
|
||||
this.i18n.switchLocale(options.locale);
|
||||
}
|
||||
|
||||
if (options.plugins && Array.isArray(options.plugins)) {
|
||||
options.plugins.forEach((plugin) => {
|
||||
this.use(plugin);
|
||||
});
|
||||
}
|
||||
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 销毁
|
||||
*/
|
||||
destroy() {
|
||||
this.dismiss();
|
||||
|
||||
if (this.plugins && typeof this.plugins.destroy === 'function') {
|
||||
this.plugins.destroy();
|
||||
}
|
||||
|
||||
if (this.themes) {
|
||||
if (typeof this.themes.clearThemeListeners === 'function') {
|
||||
this.themes.clearThemeListeners();
|
||||
}
|
||||
if (typeof this.themes.unwatchSystemTheme === 'function') {
|
||||
this.themes.unwatchSystemTheme();
|
||||
}
|
||||
}
|
||||
|
||||
if (this.i18n && typeof this.i18n.clearLocaleListeners === 'function') {
|
||||
this.i18n.clearLocaleListeners();
|
||||
}
|
||||
|
||||
if (this.animations && typeof this.animations.cancelAll === 'function') {
|
||||
this.animations.cancelAll();
|
||||
}
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
const containers = document.querySelectorAll('.met-container');
|
||||
containers.forEach((c) => c.parentNode && c.parentNode.removeChild(c));
|
||||
|
||||
const style = document.getElementById('metona-toast-styles');
|
||||
if (style) style.parentNode.removeChild(style);
|
||||
}
|
||||
|
||||
this._toasts.clear();
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取状态
|
||||
*/
|
||||
getStatus() {
|
||||
return {
|
||||
version: VERSION,
|
||||
toasts: this._toasts.size,
|
||||
theme: this.themes?.getCurrentTheme?.() || 'auto',
|
||||
locale: this.i18n?.getCurrentLocale?.() || 'zh-CN',
|
||||
plugins: this.plugins?.getNames?.() || [],
|
||||
animations: this.animations?.getActiveCount?.() || 0,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
*/
|
||||
getConfig() {
|
||||
return { ...this._config };
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新配置
|
||||
*/
|
||||
updateConfig(config) {
|
||||
if (config && typeof config === 'object') {
|
||||
this._config = { ...this._config, ...config };
|
||||
}
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 重置配置
|
||||
*/
|
||||
resetConfig() {
|
||||
this._config = { ...DEFAULTS };
|
||||
return this;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取Toast列表
|
||||
*/
|
||||
getToasts() {
|
||||
return Array.from(this._toasts.values());
|
||||
},
|
||||
|
||||
/**
|
||||
* 检查是否有Toast
|
||||
*/
|
||||
hasToasts() {
|
||||
return this._toasts.size > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* 获取Toast
|
||||
*/
|
||||
getToast(id) {
|
||||
if (!id) return null;
|
||||
return this._toasts.get(id) || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* 关闭所有Toast
|
||||
*/
|
||||
closeAll() {
|
||||
this._toasts.forEach((t) => t.close());
|
||||
},
|
||||
|
||||
/**
|
||||
* 清除所有Toast
|
||||
*/
|
||||
clearAll() {
|
||||
this._toasts.forEach((t) => t.close());
|
||||
},
|
||||
|
||||
/**
|
||||
* 暂停所有Toast
|
||||
*/
|
||||
pauseAll() {
|
||||
this._toasts.forEach((t) => t._pause());
|
||||
},
|
||||
|
||||
/**
|
||||
* 恢复所有Toast
|
||||
*/
|
||||
resumeAll() {
|
||||
this._toasts.forEach((t) => t._resume());
|
||||
},
|
||||
|
||||
/**
|
||||
* 更新所有Toast
|
||||
*/
|
||||
updateAll(partial) {
|
||||
if (partial && typeof partial === 'object') {
|
||||
this._toasts.forEach((t) => t.update(partial));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 查找Toast
|
||||
*/
|
||||
findToasts(predicate) {
|
||||
if (typeof predicate !== 'function') return [];
|
||||
return Array.from(this._toasts.values()).filter(predicate);
|
||||
},
|
||||
|
||||
/**
|
||||
* 按类型查找Toast
|
||||
*/
|
||||
findByType(type) {
|
||||
return this.findToasts((t) => t.type === type);
|
||||
},
|
||||
|
||||
/**
|
||||
* 按位置查找Toast
|
||||
*/
|
||||
findByPosition(position) {
|
||||
return this.findToasts((t) => t.config.position === position);
|
||||
},
|
||||
|
||||
};
|
||||
// 初始化主题和国际化
|
||||
if (typeof themeUtils.initTheme === 'function') {
|
||||
themeUtils.initTheme();
|
||||
}
|
||||
if (typeof i18nUtils.initI18n === 'function') {
|
||||
i18nUtils.initI18n();
|
||||
}
|
||||
|
||||
// 浏览器环境全局注册
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MeToast = enhancedMeToast;
|
||||
window.Met = enhancedMeToast;
|
||||
|
||||
// Notification API — 页面不可见时自动发送系统通知
|
||||
enhancedMeToast._notify = (toast) => {
|
||||
if (typeof Notification === 'undefined') return;
|
||||
if (Notification.permission === 'denied') return;
|
||||
if (!document.hidden) return;
|
||||
if (Notification.permission === 'default') {
|
||||
Notification.requestPermission().then(p => { if (p === 'granted') sendNotification(toast); });
|
||||
return;
|
||||
}
|
||||
sendNotification(toast);
|
||||
};
|
||||
const sendNotification = (toast) => {
|
||||
try {
|
||||
new Notification(toast.title || toast.type, { body: toast.message });
|
||||
} catch (_) {}
|
||||
};
|
||||
Toast.on('afterShow', (toast) => {
|
||||
if (toast.config.notifyWhenHidden) enhancedMeToast._notify(toast);
|
||||
});
|
||||
}
|
||||
|
||||
// 导出
|
||||
export default enhancedMeToast;
|
||||
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* MetonaToast — 轻量级Toast通知库
|
||||
* @module metona-toast
|
||||
* @version 0.5.0
|
||||
* @author thzxx
|
||||
* @license MIT
|
||||
*/
|
||||
|
||||
import { meToast } from './api.js';
|
||||
import { Toast } from './toast.js';
|
||||
import { animationUtils } from './animations.js';
|
||||
import { themeUtils } from './themes.js';
|
||||
import { i18nUtils } from './i18n.js';
|
||||
import { pluginUtils, presetPlugins } from './plugins.js';
|
||||
import { VERSION } from './constants.js';
|
||||
import type { ToastInstance } from './types.js';
|
||||
|
||||
/**
|
||||
* 增强的 MeToast 对象 — 在 meToast 基础上注入子模块
|
||||
*/
|
||||
const enhancedMeToast = {
|
||||
...meToast,
|
||||
|
||||
version: VERSION,
|
||||
|
||||
animations: animationUtils,
|
||||
themes: themeUtils,
|
||||
i18n: i18nUtils,
|
||||
plugins: pluginUtils,
|
||||
presetPlugins,
|
||||
};
|
||||
|
||||
// 初始化主题和国际化
|
||||
if (typeof themeUtils.initTheme === 'function') {
|
||||
themeUtils.initTheme();
|
||||
}
|
||||
if (typeof i18nUtils.initI18n === 'function') {
|
||||
i18nUtils.initI18n();
|
||||
}
|
||||
|
||||
// 全局钩子 — 主题/语言变化转发到 Toast 钩子系统
|
||||
if (typeof themeUtils.addThemeListener === 'function') {
|
||||
themeUtils.addThemeListener(() => Toast.trigger('themeChange'));
|
||||
}
|
||||
if (typeof i18nUtils.addLocaleListener === 'function') {
|
||||
i18nUtils.addLocaleListener(() => Toast.trigger('localeChange'));
|
||||
}
|
||||
|
||||
// 浏览器环境全局注册
|
||||
if (typeof window !== 'undefined') {
|
||||
window.MeToast = enhancedMeToast;
|
||||
window.Met = enhancedMeToast;
|
||||
|
||||
// Notification API — 页面不可见时自动发送系统通知
|
||||
const sendNotification = (toast: ToastInstance): void => {
|
||||
try {
|
||||
new Notification(toast.title || toast.type, { body: toast.message });
|
||||
} catch (_e) { /* noop */ }
|
||||
};
|
||||
|
||||
(enhancedMeToast as unknown as Record<string, unknown>)._notify = (toast: ToastInstance) => {
|
||||
if (typeof Notification === 'undefined') return;
|
||||
if (Notification.permission === 'denied') return;
|
||||
if (!document.hidden) return;
|
||||
if (Notification.permission === 'default') {
|
||||
Notification.requestPermission().then(p => { if (p === 'granted') sendNotification(toast); });
|
||||
return;
|
||||
}
|
||||
sendNotification(toast);
|
||||
};
|
||||
|
||||
Toast.on('afterShow', (toast: ToastInstance) => {
|
||||
const config = toast.config;
|
||||
if (config.notifyWhenHidden) {
|
||||
((enhancedMeToast as unknown as Record<string, (t: ToastInstance) => void>)._notify)(toast);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 导出
|
||||
export default enhancedMeToast;
|
||||
export { enhancedMeToast as meToast, enhancedMeToast as Met, enhancedMeToast as MeToast, Toast, VERSION };
|
||||
|
||||
// 类型重导出(供 TypeScript 消费者 import type 使用)
|
||||
export type {
|
||||
ToastType,
|
||||
ToastPosition,
|
||||
ToastTheme,
|
||||
ToastAnimation,
|
||||
ToastProgressDirection,
|
||||
ToastConfig,
|
||||
ToastOptions,
|
||||
ToastInstance,
|
||||
TypeColor,
|
||||
ErrorInfo,
|
||||
LoadingControl,
|
||||
ProgressControl,
|
||||
CountdownControl,
|
||||
ActionControl,
|
||||
QueueControl,
|
||||
GroupAPI,
|
||||
ActionButton,
|
||||
PromiseOptions,
|
||||
ConfirmOptions,
|
||||
PromptOptions,
|
||||
ProgressOptions,
|
||||
CountdownOptions,
|
||||
QueueOptions,
|
||||
StackOptions,
|
||||
InitOptions,
|
||||
StatusInfo,
|
||||
AnimationConfig,
|
||||
AnimationUtils,
|
||||
ThemeConfig,
|
||||
ThemePreview,
|
||||
ThemeUtils,
|
||||
LocaleInfo,
|
||||
I18nUtils,
|
||||
Plugin,
|
||||
PluginManager,
|
||||
PluginUtils,
|
||||
} from './types.js';
|
||||
+21
-277
@@ -1,12 +1,13 @@
|
||||
/**
|
||||
* MetonaToast Locales — 国际化翻译数据
|
||||
* @module locales
|
||||
* @version 2.0.1
|
||||
* @version 0.5.0
|
||||
* @description 内置 zh-CN / en-US 完整翻译
|
||||
*/
|
||||
|
||||
export const LOCALES = {
|
||||
export const LOCALES: Record<string, Record<string, string>> = {
|
||||
'zh-CN': {
|
||||
// === 操作 ===
|
||||
close: '关闭',
|
||||
loading: '加载中...',
|
||||
success: '操作成功',
|
||||
@@ -87,6 +88,8 @@ export const LOCALES = {
|
||||
hide: '隐藏',
|
||||
visible: '可见',
|
||||
hidden: '隐藏',
|
||||
|
||||
// === 状态 ===
|
||||
enabled: '已启用',
|
||||
disabled: '已禁用',
|
||||
active: '活跃',
|
||||
@@ -95,7 +98,6 @@ export const LOCALES = {
|
||||
offline: '离线',
|
||||
connected: '已连接',
|
||||
disconnected: '已断开',
|
||||
loading: '加载中',
|
||||
loaded: '已加载',
|
||||
saving: '保存中',
|
||||
saved: '已保存',
|
||||
@@ -114,12 +116,9 @@ export const LOCALES = {
|
||||
receiving: '接收中',
|
||||
received: '已接收',
|
||||
connecting: '连接中',
|
||||
error: '错误',
|
||||
warning: '警告',
|
||||
info: '信息',
|
||||
success: '成功',
|
||||
|
||||
// === 交互 ===
|
||||
question: '问题',
|
||||
help: '帮助',
|
||||
feedback: '反馈',
|
||||
report: '报告',
|
||||
bug: '错误',
|
||||
@@ -133,7 +132,6 @@ export const LOCALES = {
|
||||
complete: '完成',
|
||||
incomplete: '未完成',
|
||||
pending: '待处理',
|
||||
processing: '处理中',
|
||||
failed: '失败',
|
||||
cancelled: '已取消',
|
||||
timeout: '超时',
|
||||
@@ -142,8 +140,6 @@ export const LOCALES = {
|
||||
valid: '有效',
|
||||
required: '必填',
|
||||
optional: '可选',
|
||||
enabled: '启用',
|
||||
disabled: '禁用',
|
||||
allowed: '允许',
|
||||
denied: '拒绝',
|
||||
approved: '已批准',
|
||||
@@ -158,6 +154,8 @@ export const LOCALES = {
|
||||
unauthenticated: '未认证',
|
||||
authorized: '已授权',
|
||||
unauthorized: '未授权',
|
||||
|
||||
// === 范围 / 角色 ===
|
||||
public: '公开',
|
||||
private: '私有',
|
||||
protected: '受保护',
|
||||
@@ -181,6 +179,8 @@ export const LOCALES = {
|
||||
following: '关注中',
|
||||
friend: '朋友',
|
||||
contact: '联系人',
|
||||
|
||||
// === 组织 ===
|
||||
group: '群组',
|
||||
team: '团队',
|
||||
organization: '组织',
|
||||
@@ -190,8 +190,6 @@ export const LOCALES = {
|
||||
task: '任务',
|
||||
issue: '问题',
|
||||
ticket: '工单',
|
||||
request: '请求',
|
||||
response: '响应',
|
||||
message: '消息',
|
||||
notification: '通知',
|
||||
alert: '警报',
|
||||
@@ -199,7 +197,6 @@ export const LOCALES = {
|
||||
event: '事件',
|
||||
activity: '活动',
|
||||
log: '日志',
|
||||
report: '报告',
|
||||
analytics: '分析',
|
||||
statistics: '统计',
|
||||
metrics: '指标',
|
||||
@@ -251,134 +248,10 @@ export const LOCALES = {
|
||||
which: '哪个',
|
||||
whose: '谁的',
|
||||
whom: '谁',
|
||||
myself: '我自己',
|
||||
yourself: '你自己',
|
||||
himself: '他自己',
|
||||
herself: '她自己',
|
||||
itself: '它自己',
|
||||
ourselves: '我们自己',
|
||||
yourselves: '你们自己',
|
||||
themselves: '他们自己',
|
||||
mine: '我的',
|
||||
yours: '你的',
|
||||
his: '他的',
|
||||
hers: '她的',
|
||||
its: '它的',
|
||||
ours: '我们的',
|
||||
yours: '你们的',
|
||||
theirs: '他们的',
|
||||
me: '我',
|
||||
you: '你',
|
||||
him: '他',
|
||||
her: '她',
|
||||
it: '它',
|
||||
us: '我们',
|
||||
them: '他们',
|
||||
my: '我的',
|
||||
your: '你的',
|
||||
his: '他的',
|
||||
her: '她的',
|
||||
its: '它的',
|
||||
our: '我们的',
|
||||
your: '你们的',
|
||||
their: '他们的',
|
||||
i: '我',
|
||||
you: '你',
|
||||
he: '他',
|
||||
she: '她',
|
||||
it: '它',
|
||||
we: '我们',
|
||||
they: '他们',
|
||||
am: '是',
|
||||
is: '是',
|
||||
are: '是',
|
||||
was: '是',
|
||||
were: '是',
|
||||
be: '是',
|
||||
been: '是',
|
||||
being: '是',
|
||||
have: '有',
|
||||
has: '有',
|
||||
had: '有',
|
||||
having: '有',
|
||||
do: '做',
|
||||
does: '做',
|
||||
did: '做',
|
||||
doing: '做',
|
||||
will: '会',
|
||||
would: '会',
|
||||
shall: '将',
|
||||
should: '应该',
|
||||
may: '可以',
|
||||
might: '可能',
|
||||
can: '能',
|
||||
could: '能',
|
||||
must: '必须',
|
||||
need: '需要',
|
||||
dare: '敢',
|
||||
ought: '应该',
|
||||
used: '过去常常',
|
||||
to: '到',
|
||||
of: '的',
|
||||
in: '在',
|
||||
for: '为了',
|
||||
on: '在',
|
||||
with: '和',
|
||||
at: '在',
|
||||
by: '通过',
|
||||
from: '从',
|
||||
into: '进入',
|
||||
during: '在...期间',
|
||||
before: '在...之前',
|
||||
after: '在...之后',
|
||||
above: '在...上面',
|
||||
below: '在...下面',
|
||||
between: '在...之间',
|
||||
under: '在...下面',
|
||||
over: '在...上面',
|
||||
across: '穿过',
|
||||
through: '通过',
|
||||
into: '进入',
|
||||
towards: '朝向',
|
||||
upon: '在...上面',
|
||||
about: '关于',
|
||||
against: '反对',
|
||||
among: '在...之中',
|
||||
along: '沿着',
|
||||
around: '周围',
|
||||
beyond: '超出',
|
||||
but: '但是',
|
||||
despite: '尽管',
|
||||
except: '除了',
|
||||
inside: '里面',
|
||||
outside: '外面',
|
||||
since: '自从',
|
||||
until: '直到',
|
||||
unless: '除非',
|
||||
whether: '是否',
|
||||
while: '当...时候',
|
||||
although: '虽然',
|
||||
because: '因为',
|
||||
if: '如果',
|
||||
once: '一旦',
|
||||
since: '自从',
|
||||
so: '所以',
|
||||
that: '那个',
|
||||
though: '虽然',
|
||||
till: '直到',
|
||||
unless: '除非',
|
||||
until: '直到',
|
||||
when: '当...时候',
|
||||
whenever: '每当',
|
||||
where: '哪里',
|
||||
wherever: '无论哪里',
|
||||
whereas: '然而',
|
||||
wherever: '无论哪里',
|
||||
while: '当...时候',
|
||||
why: '为什么',
|
||||
},
|
||||
|
||||
|
||||
'en-US': {
|
||||
// === Actions ===
|
||||
close: 'Close',
|
||||
loading: 'Loading...',
|
||||
success: 'Success',
|
||||
@@ -459,6 +332,8 @@ export const LOCALES = {
|
||||
hide: 'Hide',
|
||||
visible: 'Visible',
|
||||
hidden: 'Hidden',
|
||||
|
||||
// === Status ===
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
active: 'Active',
|
||||
@@ -467,7 +342,6 @@ export const LOCALES = {
|
||||
offline: 'Offline',
|
||||
connected: 'Connected',
|
||||
disconnected: 'Disconnected',
|
||||
loading: 'Loading',
|
||||
loaded: 'Loaded',
|
||||
saving: 'Saving',
|
||||
saved: 'Saved',
|
||||
@@ -486,12 +360,9 @@ export const LOCALES = {
|
||||
receiving: 'Receiving',
|
||||
received: 'Received',
|
||||
connecting: 'Connecting',
|
||||
error: 'Error',
|
||||
warning: 'Warning',
|
||||
info: 'Info',
|
||||
success: 'Success',
|
||||
|
||||
// === Interaction ===
|
||||
question: 'Question',
|
||||
help: 'Help',
|
||||
feedback: 'Feedback',
|
||||
report: 'Report',
|
||||
bug: 'Bug',
|
||||
@@ -505,7 +376,6 @@ export const LOCALES = {
|
||||
complete: 'Complete',
|
||||
incomplete: 'Incomplete',
|
||||
pending: 'Pending',
|
||||
processing: 'Processing',
|
||||
failed: 'Failed',
|
||||
cancelled: 'Cancelled',
|
||||
timeout: 'Timeout',
|
||||
@@ -514,8 +384,6 @@ export const LOCALES = {
|
||||
valid: 'Valid',
|
||||
required: 'Required',
|
||||
optional: 'Optional',
|
||||
enabled: 'Enabled',
|
||||
disabled: 'Disabled',
|
||||
allowed: 'Allowed',
|
||||
denied: 'Denied',
|
||||
approved: 'Approved',
|
||||
@@ -530,6 +398,8 @@ export const LOCALES = {
|
||||
unauthenticated: 'Unauthenticated',
|
||||
authorized: 'Authorized',
|
||||
unauthorized: 'Unauthorized',
|
||||
|
||||
// === Scope / Role ===
|
||||
public: 'Public',
|
||||
private: 'Private',
|
||||
protected: 'Protected',
|
||||
@@ -553,6 +423,8 @@ export const LOCALES = {
|
||||
following: 'Following',
|
||||
friend: 'Friend',
|
||||
contact: 'Contact',
|
||||
|
||||
// === Organization ===
|
||||
group: 'Group',
|
||||
team: 'Team',
|
||||
organization: 'Organization',
|
||||
@@ -562,8 +434,6 @@ export const LOCALES = {
|
||||
task: 'Task',
|
||||
issue: 'Issue',
|
||||
ticket: 'Ticket',
|
||||
request: 'Request',
|
||||
response: 'Response',
|
||||
message: 'Message',
|
||||
notification: 'Notification',
|
||||
alert: 'Alert',
|
||||
@@ -571,7 +441,6 @@ export const LOCALES = {
|
||||
event: 'Event',
|
||||
activity: 'Activity',
|
||||
log: 'Log',
|
||||
report: 'Report',
|
||||
analytics: 'Analytics',
|
||||
statistics: 'Statistics',
|
||||
metrics: 'Metrics',
|
||||
@@ -623,130 +492,5 @@ export const LOCALES = {
|
||||
which: 'Which',
|
||||
whose: 'Whose',
|
||||
whom: 'Whom',
|
||||
myself: 'Myself',
|
||||
yourself: 'Yourself',
|
||||
himself: 'Himself',
|
||||
herself: 'Herself',
|
||||
itself: 'Itself',
|
||||
ourselves: 'Ourselves',
|
||||
yourselves: 'Yourselves',
|
||||
themselves: 'Themselves',
|
||||
mine: 'Mine',
|
||||
yours: 'Yours',
|
||||
his: 'His',
|
||||
hers: 'Hers',
|
||||
its: 'Its',
|
||||
ours: 'Ours',
|
||||
yours: 'Yours',
|
||||
theirs: 'Theirs',
|
||||
me: 'Me',
|
||||
you: 'You',
|
||||
him: 'Him',
|
||||
her: 'Her',
|
||||
it: 'It',
|
||||
us: 'Us',
|
||||
them: 'Them',
|
||||
my: 'My',
|
||||
your: 'Your',
|
||||
his: 'His',
|
||||
her: 'Her',
|
||||
its: 'Its',
|
||||
our: 'Our',
|
||||
your: 'Your',
|
||||
their: 'Their',
|
||||
i: 'I',
|
||||
you: 'You',
|
||||
he: 'He',
|
||||
she: 'She',
|
||||
it: 'It',
|
||||
we: 'We',
|
||||
they: 'They',
|
||||
am: 'Am',
|
||||
is: 'Is',
|
||||
are: 'Are',
|
||||
was: 'Was',
|
||||
were: 'Were',
|
||||
be: 'Be',
|
||||
been: 'Been',
|
||||
being: 'Being',
|
||||
have: 'Have',
|
||||
has: 'Has',
|
||||
had: 'Had',
|
||||
having: 'Having',
|
||||
do: 'Do',
|
||||
does: 'Does',
|
||||
did: 'Did',
|
||||
doing: 'Doing',
|
||||
will: 'Will',
|
||||
would: 'Would',
|
||||
shall: 'Shall',
|
||||
should: 'Should',
|
||||
may: 'May',
|
||||
might: 'Might',
|
||||
can: 'Can',
|
||||
could: 'Could',
|
||||
must: 'Must',
|
||||
need: 'Need',
|
||||
dare: 'Dare',
|
||||
ought: 'Ought',
|
||||
used: 'Used',
|
||||
to: 'To',
|
||||
of: 'Of',
|
||||
in: 'In',
|
||||
for: 'For',
|
||||
on: 'On',
|
||||
with: 'With',
|
||||
at: 'At',
|
||||
by: 'By',
|
||||
from: 'From',
|
||||
into: 'Into',
|
||||
during: 'During',
|
||||
before: 'Before',
|
||||
after: 'After',
|
||||
above: 'Above',
|
||||
below: 'Below',
|
||||
between: 'Between',
|
||||
under: 'Under',
|
||||
over: 'Over',
|
||||
across: 'Across',
|
||||
through: 'Through',
|
||||
into: 'Into',
|
||||
towards: 'Towards',
|
||||
upon: 'Upon',
|
||||
about: 'About',
|
||||
against: 'Against',
|
||||
among: 'Among',
|
||||
along: 'Along',
|
||||
around: 'Around',
|
||||
beyond: 'Beyond',
|
||||
but: 'But',
|
||||
despite: 'Despite',
|
||||
except: 'Except',
|
||||
inside: 'Inside',
|
||||
outside: 'Outside',
|
||||
since: 'Since',
|
||||
until: 'Until',
|
||||
unless: 'Unless',
|
||||
whether: 'Whether',
|
||||
while: 'While',
|
||||
although: 'Although',
|
||||
because: 'Because',
|
||||
if: 'If',
|
||||
once: 'Once',
|
||||
since: 'Since',
|
||||
so: 'So',
|
||||
that: 'That',
|
||||
though: 'Though',
|
||||
till: 'Till',
|
||||
unless: 'Unless',
|
||||
until: 'Until',
|
||||
when: 'When',
|
||||
whenever: 'Whenever',
|
||||
where: 'Where',
|
||||
wherever: 'Wherever',
|
||||
whereas: 'Whereas',
|
||||
wherever: 'Wherever',
|
||||
while: 'While',
|
||||
why: 'Why',
|
||||
},
|
||||
};
|
||||
-206
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* MetonaToast Plugins - 插件系统
|
||||
* @module plugins
|
||||
* @version 2.0.1
|
||||
* @description 插件管理器 + 3 款预设插件 (keyboard / persistence / accessibility)
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*/
|
||||
class PluginManager {
|
||||
constructor() {
|
||||
this.plugins = new Map();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
register(name, plugin) {
|
||||
if (this.plugins.has(name)) {
|
||||
console.warn(`Plugin "${name}" is already registered`);
|
||||
return this;
|
||||
}
|
||||
if (!plugin || typeof plugin !== 'object' || (!plugin.name && !plugin.version)) {
|
||||
console.error(`Invalid plugin "${name}"`);
|
||||
return this;
|
||||
}
|
||||
|
||||
this.plugins.set(name, { name, ...plugin, installed: false, enabled: true });
|
||||
|
||||
if (plugin.install) {
|
||||
try { plugin.install(this); this.plugins.get(name).installed = true; }
|
||||
catch (e) { console.error(`Failed to install plugin "${name}":`, e); }
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unregister(name) {
|
||||
const plugin = this.plugins.get(name);
|
||||
if (!plugin) return this;
|
||||
if (plugin.uninstall) {
|
||||
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
|
||||
}
|
||||
this.plugins.delete(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
get(name) { return this.plugins.get(name) || null; }
|
||||
has(name) { return this.plugins.has(name); }
|
||||
getAll() { return Array.from(this.plugins.values()); }
|
||||
getNames() { return Array.from(this.plugins.keys()); }
|
||||
|
||||
enable(name) {
|
||||
const p = this.plugins.get(name);
|
||||
if (p) p.enabled = true;
|
||||
return this;
|
||||
}
|
||||
disable(name) {
|
||||
const p = this.plugins.get(name);
|
||||
if (p) p.enabled = false;
|
||||
return this;
|
||||
}
|
||||
isEnabled(name) {
|
||||
const p = this.plugins.get(name);
|
||||
return p ? p.enabled : false;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.plugins.forEach((p, name) => {
|
||||
if (p.destroy) { try { p.destroy(this); } catch (e) {} }
|
||||
});
|
||||
this.plugins.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预设插件
|
||||
*/
|
||||
const presetPlugins = {
|
||||
|
||||
/**
|
||||
* 键盘快捷键插件 — ESC 关闭所有 Toast
|
||||
*/
|
||||
keyboard: {
|
||||
name: 'keyboard',
|
||||
version: '1.0.0',
|
||||
description: 'ESC 关闭所有 Toast',
|
||||
|
||||
_handler: null,
|
||||
|
||||
install() {
|
||||
if (typeof document === 'undefined') return;
|
||||
this._handler = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
// 点击所有关闭按钮
|
||||
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
|
||||
try { btn.click(); } catch (_) {}
|
||||
});
|
||||
}
|
||||
};
|
||||
document.addEventListener('keydown', this._handler);
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
if (this._handler && typeof document !== 'undefined') {
|
||||
document.removeEventListener('keydown', this._handler);
|
||||
this._handler = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 持久化插件 — 自动保存/加载配置到 localStorage
|
||||
*/
|
||||
persistence: {
|
||||
name: 'persistence',
|
||||
version: '1.0.0',
|
||||
description: '自动持久化配置到 localStorage',
|
||||
storageKey: 'metona-toast-config',
|
||||
|
||||
install() {
|
||||
// 加载已保存的配置
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
const saved = localStorage.getItem(this.storageKey);
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
} catch (_) { return null; }
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
save(config) {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try { localStorage.setItem(this.storageKey, JSON.stringify(config)); } catch (_) {}
|
||||
}
|
||||
},
|
||||
|
||||
uninstall() {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try { localStorage.removeItem(this.storageKey); } catch (_) {}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 无障碍插件 — 屏幕阅读器公告
|
||||
*/
|
||||
accessibility: {
|
||||
name: 'accessibility',
|
||||
version: '1.0.0',
|
||||
description: '通过屏幕阅读器朗读 Toast 内容',
|
||||
|
||||
announce(toast) {
|
||||
if (typeof document === 'undefined') return;
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('aria-live', 'assertive');
|
||||
el.setAttribute('aria-atomic', 'true');
|
||||
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
||||
const typeName = t(toast.type) || toast.type;
|
||||
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 插件工具
|
||||
*/
|
||||
const pluginUtils = {
|
||||
createManager() { return new PluginManager(); },
|
||||
register(name, plugin) { return defaultPluginManager.register(name, plugin); },
|
||||
unregister(name) { return defaultPluginManager.unregister(name); },
|
||||
get(name) { return defaultPluginManager.get(name); },
|
||||
has(name) { return defaultPluginManager.has(name); },
|
||||
getAll() { return defaultPluginManager.getAll(); },
|
||||
getNames() { return defaultPluginManager.getNames(); },
|
||||
enable(name) { return defaultPluginManager.enable(name); },
|
||||
disable(name) { return defaultPluginManager.disable(name); },
|
||||
isEnabled(name) { return defaultPluginManager.isEnabled(name); },
|
||||
getPreset(name) { return presetPlugins[name] || null; },
|
||||
getAllPresets() { return { ...presetPlugins }; },
|
||||
|
||||
createPlugin(config) {
|
||||
return {
|
||||
name: config.name || 'custom',
|
||||
version: config.version || '1.0.0',
|
||||
description: config.description || '',
|
||||
hooks: config.hooks || {},
|
||||
install: config.install || null,
|
||||
uninstall: config.uninstall || null,
|
||||
...config,
|
||||
};
|
||||
},
|
||||
|
||||
validatePlugin(plugin) {
|
||||
const errors = [];
|
||||
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
|
||||
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
|
||||
return { valid: errors.length === 0, errors };
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPluginManager = new PluginManager();
|
||||
|
||||
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
|
||||
+266
@@ -0,0 +1,266 @@
|
||||
/**
|
||||
* MetonaToast Plugins — 插件系统
|
||||
* @module plugins
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { t } from './i18n.js';
|
||||
import { Toast } from './toast.js';
|
||||
import type { Plugin, PluginManager as IPluginManager, PluginUtils, ToastInstance } from './types.js';
|
||||
|
||||
/**
|
||||
* 插件管理器
|
||||
*/
|
||||
class PluginManager implements IPluginManager {
|
||||
plugins: Map<string, Plugin>;
|
||||
initialized: boolean;
|
||||
|
||||
constructor() {
|
||||
this.plugins = new Map();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
register(name: string, plugin: Plugin): PluginManager {
|
||||
if (this.plugins.has(name)) {
|
||||
console.warn(`Plugin "${name}" is already registered`);
|
||||
return this;
|
||||
}
|
||||
if (!plugin || typeof plugin !== 'object' || !plugin.name) {
|
||||
console.error(`Invalid plugin "${name}": must be an object with a "name" property`);
|
||||
return this;
|
||||
}
|
||||
|
||||
const stored: Plugin = { ...plugin, installed: false, enabled: true };
|
||||
if (!stored.name) stored.name = name;
|
||||
this.plugins.set(name, stored);
|
||||
|
||||
if (stored.install) {
|
||||
try {
|
||||
stored.install(this);
|
||||
stored.installed = true;
|
||||
} catch (e) {
|
||||
console.error(`Failed to install plugin "${name}":`, e);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
unregister(name: string): PluginManager {
|
||||
const plugin = this.plugins.get(name);
|
||||
if (!plugin) return this;
|
||||
if (plugin.uninstall) {
|
||||
try { plugin.uninstall(this); } catch (e) { console.error(`Failed to uninstall plugin "${name}":`, e); }
|
||||
}
|
||||
this.plugins.delete(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
get(name: string): Plugin | null { return this.plugins.get(name) || null; }
|
||||
has(name: string): boolean { return this.plugins.has(name); }
|
||||
getAll(): Plugin[] { return Array.from(this.plugins.values()); }
|
||||
getNames(): string[] { return Array.from(this.plugins.keys()); }
|
||||
|
||||
enable(name: string): PluginManager {
|
||||
const p = this.plugins.get(name);
|
||||
if (p) p.enabled = true;
|
||||
return this;
|
||||
}
|
||||
disable(name: string): PluginManager {
|
||||
const p = this.plugins.get(name);
|
||||
if (p) p.enabled = false;
|
||||
return this;
|
||||
}
|
||||
isEnabled(name: string): boolean {
|
||||
const p = this.plugins.get(name);
|
||||
return p ? !!p.enabled : false;
|
||||
}
|
||||
|
||||
destroy(): void {
|
||||
this.plugins.forEach((p) => {
|
||||
if (p.destroy) { try { p.destroy(this); } catch (_e) { /* noop */ } }
|
||||
});
|
||||
this.plugins.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 预设插件
|
||||
*/
|
||||
const presetPlugins: Record<string, Plugin> = {
|
||||
/**
|
||||
* 键盘快捷键插件 — ESC 关闭所有 Toast
|
||||
*/
|
||||
keyboard: {
|
||||
name: 'keyboard',
|
||||
version: '1.0.0',
|
||||
description: 'ESC 关闭所有 Toast',
|
||||
_handler: null as ((e: KeyboardEvent) => void) | null,
|
||||
|
||||
install(this: Plugin) {
|
||||
if (typeof document === 'undefined') return;
|
||||
const handler = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
document.querySelectorAll('.met-toast .met-close').forEach(btn => {
|
||||
try { (btn as HTMLElement).click(); } catch (_e) { /* noop */ }
|
||||
});
|
||||
}
|
||||
};
|
||||
(this as Record<string, unknown>)._handler = handler;
|
||||
document.addEventListener('keydown', handler);
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
const handler = (this as Record<string, unknown>)._handler as ((e: KeyboardEvent) => void) | null;
|
||||
if (handler && typeof document !== 'undefined') {
|
||||
document.removeEventListener('keydown', handler);
|
||||
(this as Record<string, unknown>)._handler = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 持久化插件 — 自动保存/加载配置到 localStorage
|
||||
*/
|
||||
persistence: {
|
||||
name: 'persistence',
|
||||
version: '1.0.0',
|
||||
description: '自动持久化配置到 localStorage',
|
||||
storageKey: 'metona-toast-config',
|
||||
|
||||
install(this: Plugin) {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
const key = (this as Record<string, unknown>).storageKey as string;
|
||||
const saved = localStorage.getItem(key);
|
||||
return saved ? JSON.parse(saved) : null;
|
||||
} catch (_e) { return null; }
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
save(this: Plugin, config: Record<string, unknown>) {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
const key = (this as Record<string, unknown>).storageKey as string;
|
||||
localStorage.setItem(key, JSON.stringify(config));
|
||||
} catch (_e) { /* noop */ }
|
||||
}
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
const key = (this as Record<string, unknown>).storageKey as string;
|
||||
localStorage.removeItem(key);
|
||||
} catch (_e) { /* noop */ }
|
||||
}
|
||||
// 卸载 use() 注册的配置保存钩子
|
||||
const meta = this as Record<string, unknown>;
|
||||
if (typeof meta._saveOff === 'function') {
|
||||
(meta._saveOff as () => void)();
|
||||
meta._saveOff = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 无障碍插件 — 屏幕阅读器公告
|
||||
*/
|
||||
accessibility: {
|
||||
name: 'accessibility',
|
||||
version: '1.0.0',
|
||||
description: '通过屏幕阅读器朗读 Toast 内容',
|
||||
|
||||
announce(toast: { type: string; title?: string; message?: string }) {
|
||||
if (typeof document === 'undefined') return;
|
||||
const el = document.createElement('div');
|
||||
el.setAttribute('aria-live', 'assertive');
|
||||
el.setAttribute('aria-atomic', 'true');
|
||||
el.style.cssText = 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
|
||||
const typeName = t(toast.type) || toast.type;
|
||||
el.textContent = `${typeName}: ${toast.title || ''} ${toast.message || ''}`;
|
||||
document.body.appendChild(el);
|
||||
setTimeout(() => { if (el.parentNode) el.parentNode.removeChild(el); }, 3000);
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
// 卸载 use() 注册的朗读钩子
|
||||
const meta = this as Record<string, unknown>;
|
||||
if (typeof meta._off === 'function') {
|
||||
(meta._off as () => void)();
|
||||
meta._off = null;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 去重插件 — 相同 type + message 的 toast 自动合并(更新已有的一条)
|
||||
*/
|
||||
dedupe: {
|
||||
name: 'dedupe',
|
||||
version: '1.0.0',
|
||||
description: '相同类型和消息的 Toast 自动去重,更新已有实例而非重复弹出',
|
||||
_off: null as (() => void) | null,
|
||||
|
||||
install(this: Plugin) {
|
||||
const off = Toast.on('beforeShow', (toast: ToastInstance) => {
|
||||
const existing = Array.from(Toast._registry.values()).find(
|
||||
t => !t.closing && t.type === toast.type && t.message === toast.message
|
||||
);
|
||||
if (existing) {
|
||||
existing.update({ title: toast.title || existing.title });
|
||||
return false;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
(this as Record<string, unknown>)._off = off;
|
||||
},
|
||||
|
||||
uninstall(this: Plugin) {
|
||||
const off = (this as Record<string, unknown>)._off as (() => void) | null;
|
||||
if (off) off();
|
||||
(this as Record<string, unknown>)._off = null;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* 插件工具
|
||||
*/
|
||||
const pluginUtils: PluginUtils = {
|
||||
createManager(): PluginManager { return new PluginManager(); },
|
||||
register(name: string, plugin: Plugin): PluginManager { return defaultPluginManager.register(name, plugin); },
|
||||
unregister(name: string): PluginManager { return defaultPluginManager.unregister(name); },
|
||||
get(name: string): Plugin | null { return defaultPluginManager.get(name); },
|
||||
has(name: string): boolean { return defaultPluginManager.has(name); },
|
||||
getAll(): Plugin[] { return defaultPluginManager.getAll(); },
|
||||
getNames(): string[] { return defaultPluginManager.getNames(); },
|
||||
enable(name: string): PluginManager { return defaultPluginManager.enable(name); },
|
||||
disable(name: string): PluginManager { return defaultPluginManager.disable(name); },
|
||||
isEnabled(name: string): boolean { return defaultPluginManager.isEnabled(name); },
|
||||
getPreset(name: string): Plugin | null { return presetPlugins[name] || null; },
|
||||
getAllPresets(): Record<string, Plugin> { return { ...presetPlugins }; },
|
||||
|
||||
createPlugin(config: Partial<Plugin>): Plugin {
|
||||
return {
|
||||
name: config.name || 'custom',
|
||||
version: config.version || '1.0.0',
|
||||
description: config.description || '',
|
||||
hooks: config.hooks || {},
|
||||
install: config.install || undefined,
|
||||
uninstall: config.uninstall || undefined,
|
||||
...config,
|
||||
};
|
||||
},
|
||||
|
||||
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = [];
|
||||
if (!plugin || typeof plugin !== 'object') errors.push('Plugin must be an object');
|
||||
if (!plugin.name && !plugin.version) errors.push('Plugin must have a name or version');
|
||||
return { valid: errors.length === 0, errors };
|
||||
},
|
||||
};
|
||||
|
||||
const defaultPluginManager = new PluginManager();
|
||||
|
||||
export { presetPlugins, pluginUtils, defaultPluginManager, PluginManager };
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* MetonaToast React — React 适配器
|
||||
* @module react
|
||||
* @version 0.5.0
|
||||
* @description useToast hook + 声明式 <Toast /> 组件。主包保持零依赖,React 为 optional peerDependency
|
||||
*/
|
||||
|
||||
import { useEffect, useMemo, useRef } from 'react';
|
||||
import MeToast from './index.js';
|
||||
import type { ToastInstance, ToastOptions } from './types.js';
|
||||
|
||||
/** 支持声明式渲染的 Toast 类型 */
|
||||
export type ReactToastType = 'success' | 'error' | 'warning' | 'info' | 'show' | (string & {});
|
||||
|
||||
export interface ToastProps extends Omit<ToastOptions, 'message' | 'type'> {
|
||||
message: string;
|
||||
type?: ReactToastType;
|
||||
/** 组件卸载时自动移除该 Toast(默认 true)。设为 false 则 Toast 独立于组件生命周期 */
|
||||
autoClose?: boolean;
|
||||
}
|
||||
|
||||
const SHOW_METHODS = ['success', 'error', 'warning', 'info', 'show'] as const;
|
||||
type ShowMethod = (typeof SHOW_METHODS)[number];
|
||||
|
||||
/** 有 id 的可追踪对象(ToastInstance / LoadingControl) */
|
||||
interface Tracked {
|
||||
id: string;
|
||||
}
|
||||
|
||||
const showToast = (method: ShowMethod, message: string, opts: Omit<ToastProps, 'message' | 'type' | 'autoClose'>): ToastInstance => {
|
||||
switch (method) {
|
||||
case 'success': return MeToast.success(message, opts);
|
||||
case 'error': return MeToast.error(message, opts);
|
||||
case 'warning': return MeToast.warning(message, opts);
|
||||
case 'info': return MeToast.info(message, opts);
|
||||
default: return MeToast.show(message, opts);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 创建绑定清理集合的 API — 组件卸载时自动移除本组件创建的 Toast。
|
||||
* 纯函数,便于独立测试与扩展。
|
||||
*/
|
||||
export const createBoundApi = (cleanup: Set<string>): Record<string, unknown> => {
|
||||
const track = <T extends Tracked | null>(fn: () => T): T => {
|
||||
const result = fn();
|
||||
if (result && result.id) cleanup.add(result.id);
|
||||
return result;
|
||||
};
|
||||
|
||||
const bound: Record<string, unknown> = {
|
||||
...MeToast,
|
||||
|
||||
success: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.success(messageOrOpts, opts)),
|
||||
error: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.error(messageOrOpts, opts)),
|
||||
warning: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.warning(messageOrOpts, opts)),
|
||||
info: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.info(messageOrOpts, opts)),
|
||||
show: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.show(messageOrOpts, opts)),
|
||||
loading: (messageOrOpts: string | ToastOptions, opts?: ToastOptions) =>
|
||||
track(() => MeToast.loading(messageOrOpts, opts)),
|
||||
|
||||
// 通过 id 关闭时同步清理集合,避免卸载时重复移除
|
||||
dismiss: (id?: string) => {
|
||||
if (id) cleanup.delete(id);
|
||||
return MeToast.dismiss(id);
|
||||
},
|
||||
removeToast: (id: string) => {
|
||||
cleanup.delete(id);
|
||||
return MeToast.removeToast(id);
|
||||
},
|
||||
};
|
||||
|
||||
return bound;
|
||||
};
|
||||
|
||||
/**
|
||||
* useToast — 返回 MeToast API,但本组件创建的 Toast 会在组件卸载时自动移除。
|
||||
* 适合"通知随组件生命周期"的场景(如表单提交、异步任务页)。
|
||||
*/
|
||||
export const useToast = (): Record<string, unknown> => {
|
||||
const cleanupRef = useRef<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
const set = cleanupRef.current;
|
||||
return () => {
|
||||
set.forEach(id => MeToast.removeToast(id));
|
||||
set.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return useMemo(() => createBoundApi(cleanupRef.current), []);
|
||||
};
|
||||
|
||||
/**
|
||||
* Toast — 声明式 Toast 组件。props 变化时更新内容(重新创建实例),
|
||||
* 组件卸载时自动移除(autoClose 默认 true)。
|
||||
*/
|
||||
export const Toast = (props: ToastProps): null => {
|
||||
const { message, type, autoClose = true, ...rest } = props;
|
||||
const toastRef = useRef<ToastInstance | null>(null);
|
||||
const restKey = JSON.stringify(rest);
|
||||
|
||||
useEffect(() => {
|
||||
const method: ShowMethod = SHOW_METHODS.includes(type as ShowMethod) ? (type as ShowMethod) : 'show';
|
||||
toastRef.current = showToast(method, message, rest);
|
||||
|
||||
return () => {
|
||||
if (autoClose) toastRef.current?.remove();
|
||||
toastRef.current = null;
|
||||
};
|
||||
// autoClose 不参与重建依赖:它只影响卸载时的清理行为
|
||||
}, [message, type, restKey]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export { MeToast };
|
||||
@@ -1,20 +1,19 @@
|
||||
/**
|
||||
* MetonaToast Styles - 样式管理(精简版 v2.0.1)
|
||||
* MetonaToast Styles — 样式管理
|
||||
* @module styles
|
||||
* @description 样式注入与主题管理,移除未使用的组件样式
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
import type { ThemeConfig } from './types.js';
|
||||
|
||||
// 样式缓存
|
||||
let styleElement = null;
|
||||
let styleElement: HTMLStyleElement | null = null;
|
||||
|
||||
/**
|
||||
* 生成CSS样式
|
||||
*/
|
||||
const generateCSS = (theme) => {
|
||||
const generateCSS = (): string => {
|
||||
return `
|
||||
/* MetonaToast 基础样式 */
|
||||
.met-container {
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
@@ -27,7 +26,6 @@ const generateCSS = (theme) => {
|
||||
z-index: 9999;
|
||||
}
|
||||
|
||||
/* 位置样式 */
|
||||
.met-container.top-left { top: 0; left: 0; align-items: flex-start; }
|
||||
.met-container.top-center { top: 0; left: 0; right: 0; align-items: center; }
|
||||
.met-container.top-right { top: 0; right: 0; align-items: flex-end; }
|
||||
@@ -35,7 +33,6 @@ const generateCSS = (theme) => {
|
||||
.met-container.bottom-center{ bottom: 0; left: 0; right: 0; align-items: center; }
|
||||
.met-container.bottom-right { bottom: 0; right: 0; align-items: flex-end; }
|
||||
|
||||
/* Toast基础样式 */
|
||||
.met-toast {
|
||||
position: relative;
|
||||
display: flex;
|
||||
@@ -69,17 +66,14 @@ const generateCSS = (theme) => {
|
||||
perspective: 1000;
|
||||
}
|
||||
|
||||
/* 悬停效果 */
|
||||
.met-toast:hover {
|
||||
box-shadow: var(--met-hover-shadow, 0 14px 48px -10px rgba(0,0,0,0.22), 0 6px 18px -4px rgba(0,0,0,0.10)) !important;
|
||||
}
|
||||
|
||||
/* 可点击状态 */
|
||||
.met-toast.met-clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 图标样式 */
|
||||
.met-icon {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
@@ -90,7 +84,6 @@ const generateCSS = (theme) => {
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
/* 内容样式 */
|
||||
.met-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
@@ -98,21 +91,18 @@ const generateCSS = (theme) => {
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* 标题样式 */
|
||||
.met-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
letter-spacing: 0.1px;
|
||||
}
|
||||
|
||||
/* 消息样式 */
|
||||
.met-message {
|
||||
font-size: 13px;
|
||||
opacity: 0.85;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
/* 关闭按钮样式 */
|
||||
.met-close {
|
||||
flex-shrink: 0;
|
||||
background: transparent;
|
||||
@@ -134,7 +124,6 @@ const generateCSS = (theme) => {
|
||||
background: var(--met-close-hover-bg, rgba(0,0,0,0.06));
|
||||
}
|
||||
|
||||
/* 进度条样式 - 水平 */
|
||||
.met-progress {
|
||||
position: absolute;
|
||||
left: 0; right: 0; bottom: 0;
|
||||
@@ -144,7 +133,6 @@ const generateCSS = (theme) => {
|
||||
border-radius: 0 0 12px 12px;
|
||||
}
|
||||
|
||||
/* 进度条样式 - 垂直 */
|
||||
.met-progress-v {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
@@ -154,7 +142,6 @@ const generateCSS = (theme) => {
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
/* 进度条指示器 - 水平 */
|
||||
.met-bar {
|
||||
position: absolute;
|
||||
left: 0; top: 0;
|
||||
@@ -163,7 +150,6 @@ const generateCSS = (theme) => {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
|
||||
/* 进度条指示器 - 垂直 */
|
||||
.met-bar-v {
|
||||
position: absolute;
|
||||
left: 0; bottom: 0;
|
||||
@@ -172,7 +158,6 @@ const generateCSS = (theme) => {
|
||||
transform: scaleY(1);
|
||||
}
|
||||
|
||||
/* 侧边指示器 */
|
||||
.met-side {
|
||||
position: absolute;
|
||||
left: 0; top: 0; bottom: 0;
|
||||
@@ -180,13 +165,11 @@ const generateCSS = (theme) => {
|
||||
border-radius: 12px 0 0 12px;
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.met-spin {
|
||||
animation: met-rot 1s linear infinite;
|
||||
transform-origin: 50% 50%;
|
||||
}
|
||||
|
||||
/* 离开状态 */
|
||||
.met-toast.met-leaving {
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
@@ -196,7 +179,6 @@ const generateCSS = (theme) => {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* === 进入动画:初始隐藏态 === */
|
||||
.met-anim-slide.met-toast,
|
||||
.met-anim-fade.met-toast,
|
||||
.met-anim-scale.met-toast,
|
||||
@@ -211,7 +193,6 @@ const generateCSS = (theme) => {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* === 进入动画:@keyframes 定义 === */
|
||||
@keyframes met-slide-in {
|
||||
0% { transform: translateX(80px); opacity: 0; }
|
||||
65% { transform: translateX(-6px); opacity: 1; }
|
||||
@@ -272,7 +253,6 @@ const generateCSS = (theme) => {
|
||||
100% { transform: translateX(0); opacity: 1; }
|
||||
}
|
||||
|
||||
/* === 进入动画:绑定到 .met-show === */
|
||||
.met-anim-slide.met-toast.met-show { animation: met-slide-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
.met-anim-fade.met-toast.met-show { animation: met-fade-in 0.50s ease forwards; }
|
||||
.met-anim-scale.met-toast.met-show { animation: met-scale-in 0.45s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; }
|
||||
@@ -285,7 +265,6 @@ const generateCSS = (theme) => {
|
||||
.met-anim-slideLeft.met-toast.met-show { animation: met-slideLeft-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
.met-anim-slideRight.met-toast.met-show { animation: met-slideRight-in 0.40s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards; }
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 480px) {
|
||||
.met-container { padding: 12px !important; }
|
||||
.met-toast { width: 100% !important; min-width: 0 !important; }
|
||||
@@ -320,10 +299,6 @@ const generateCSS = (theme) => {
|
||||
.met-toast { width: 100%; min-width: 0; }
|
||||
}
|
||||
|
||||
/* 无障碍支持 */
|
||||
.met-toast[role="alert"] { }
|
||||
.met-toast[role="status"] { }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.met-toast { transition: opacity 0.15s !important; }
|
||||
.met-toast.met-show { animation: none !important; opacity: 1 !important; }
|
||||
@@ -340,7 +315,6 @@ const generateCSS = (theme) => {
|
||||
.met-close { border: 2px solid currentColor; border-radius: 4px; }
|
||||
}
|
||||
|
||||
/* 焦点样式 */
|
||||
.met-toast:focus-visible {
|
||||
outline: 2px solid #3b82f6;
|
||||
outline-offset: 2px;
|
||||
@@ -350,32 +324,22 @@ const generateCSS = (theme) => {
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 触摸设备优化 */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.met-toast:hover { box-shadow: inherit; }
|
||||
.met-toast { min-height: 48px; }
|
||||
.met-close { min-width: 48px; min-height: 48px; }
|
||||
}
|
||||
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.met-toast { min-height: 52px; padding: 16px 18px; }
|
||||
.met-close { min-width: 52px; min-height: 52px; padding: 8px; }
|
||||
.met-icon { width: 28px; height: 28px; }
|
||||
}
|
||||
|
||||
/* 打印样式 */
|
||||
@media print {
|
||||
.met-container { display: none !important; }
|
||||
.met-toast { display: none !important; }
|
||||
}
|
||||
|
||||
/* 全屏/画中画模式 */
|
||||
:fullscreen .met-container,
|
||||
:picture-in-picture .met-container {
|
||||
z-index: 2147483647;
|
||||
}
|
||||
|
||||
/* 安全区域适配 */
|
||||
@supports (padding: max(0px)) {
|
||||
.met-container {
|
||||
padding: max(24px, env(safe-area-inset-top))
|
||||
@@ -385,30 +349,60 @@ const generateCSS = (theme) => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 按下状态 */
|
||||
.met-toast:active {
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
/* 类型左侧边框 */
|
||||
.met-toast.met-success { border-left: 4px solid #10b981; }
|
||||
.met-toast.met-error { border-left: 4px solid #ef4444; }
|
||||
.met-toast.met-warning { border-left: 4px solid #f59e0b; }
|
||||
.met-toast.met-info { border-left: 4px solid #3b82f6; }
|
||||
.met-toast.met-loading { border-left: 4px solid #6366f1; }
|
||||
|
||||
/* 进度条过渡 */
|
||||
.met-bar, .met-bar-v {
|
||||
transition: transform 0.1s linear;
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
/* RTL 镜像:进度条从右向左收缩、side 条移到右侧、类型色条翻转到右边 */
|
||||
[dir="rtl"] .met-bar {
|
||||
transform-origin: right;
|
||||
}
|
||||
[dir="rtl"] .met-side {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
[dir="rtl"] .met-progress-v {
|
||||
left: auto;
|
||||
right: 0;
|
||||
border-radius: 0 12px 12px 0;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-success {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #10b981;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-error {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #ef4444;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-warning {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #f59e0b;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-info {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #3b82f6;
|
||||
}
|
||||
[dir="rtl"] .met-toast.met-loading {
|
||||
border-left-width: 1px;
|
||||
border-right: 4px solid #6366f1;
|
||||
}
|
||||
|
||||
.met-container::-webkit-scrollbar { width: 6px; height: 6px; }
|
||||
.met-container::-webkit-scrollbar-track { background: transparent; }
|
||||
.met-container::-webkit-scrollbar-thumb { background: rgba(0,0,0,0.2); border-radius: 3px; }
|
||||
.met-container::-webkit-scrollbar-thumb:hover { background: rgba(0,0,0,0.3); }
|
||||
|
||||
/* 暗色主题特定样式 */
|
||||
.met-toast.met-theme-dark {
|
||||
background: rgba(28, 32, 40, 0.94);
|
||||
color: #e6e8eb;
|
||||
@@ -426,7 +420,6 @@ const generateCSS = (theme) => {
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
/* 亮色主题特定样式 */
|
||||
.met-toast.met-theme-light {
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
color: #1f2937;
|
||||
@@ -444,7 +437,6 @@ const generateCSS = (theme) => {
|
||||
background: rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
/* 自定义主题支持 */
|
||||
.met-toast[data-theme] {
|
||||
--met-bg: var(--met-theme-bg);
|
||||
--met-text: var(--met-theme-text);
|
||||
@@ -452,7 +444,6 @@ const generateCSS = (theme) => {
|
||||
--met-shadow: var(--met-theme-shadow);
|
||||
}
|
||||
|
||||
/* 暗色模式自动检测 */
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.met-theme-auto .met-toast {
|
||||
background: rgba(28, 32, 40, 0.94);
|
||||
@@ -488,14 +479,14 @@ const generateCSS = (theme) => {
|
||||
/**
|
||||
* 注入样式
|
||||
*/
|
||||
export const injectStyles = (theme = THEMES.light) => {
|
||||
export const injectStyles = (): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
if (styleElement && document.getElementById('metona-toast-styles')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const css = generateCSS(theme);
|
||||
const css = generateCSS();
|
||||
|
||||
styleElement = document.createElement('style');
|
||||
styleElement.id = 'metona-toast-styles';
|
||||
@@ -507,31 +498,30 @@ export const injectStyles = (theme = THEMES.light) => {
|
||||
/**
|
||||
* 更新样式
|
||||
*/
|
||||
export const updateStyles = (theme) => {
|
||||
export const updateStyles = (): void => {
|
||||
if (!styleElement) {
|
||||
injectStyles(theme);
|
||||
injectStyles();
|
||||
return;
|
||||
}
|
||||
|
||||
const css = generateCSS(theme);
|
||||
const css = generateCSS();
|
||||
styleElement.textContent = css.replace(/\s+/g, ' ');
|
||||
};
|
||||
|
||||
/**
|
||||
* 移除样式
|
||||
*/
|
||||
export const removeStyles = () => {
|
||||
export const removeStyles = (): void => {
|
||||
if (styleElement && styleElement.parentNode) {
|
||||
styleElement.parentNode.removeChild(styleElement);
|
||||
}
|
||||
|
||||
styleElement = null;
|
||||
};
|
||||
|
||||
/**
|
||||
* 生成主题CSS变量
|
||||
*/
|
||||
export const generateThemeVariables = (theme) => {
|
||||
export const generateThemeVariables = (theme: ThemeConfig): string => {
|
||||
return `
|
||||
:root {
|
||||
--met-theme-bg: ${theme.bg};
|
||||
@@ -548,9 +538,14 @@ export const generateThemeVariables = (theme) => {
|
||||
/**
|
||||
* 应用主题变量
|
||||
*/
|
||||
export const applyThemeVariables = (theme) => {
|
||||
export const applyThemeVariables = (theme: ThemeConfig): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
const oldStyle = document.getElementById('metona-toast-custom-styles');
|
||||
if (oldStyle && oldStyle.parentNode) {
|
||||
oldStyle.parentNode.removeChild(oldStyle);
|
||||
}
|
||||
|
||||
const css = generateThemeVariables(theme);
|
||||
const customStyle = document.createElement('style');
|
||||
customStyle.id = 'metona-toast-custom-styles';
|
||||
@@ -561,7 +556,7 @@ export const applyThemeVariables = (theme) => {
|
||||
/**
|
||||
* 清除主题变量
|
||||
*/
|
||||
export const clearThemeVariables = () => {
|
||||
export const clearThemeVariables = (): void => {
|
||||
const customStyle = document.getElementById('metona-toast-custom-styles');
|
||||
if (customStyle && customStyle.parentNode) {
|
||||
customStyle.parentNode.removeChild(customStyle);
|
||||
@@ -571,26 +566,26 @@ export const clearThemeVariables = () => {
|
||||
/**
|
||||
* 获取系统主题
|
||||
*/
|
||||
export const getSystemTheme = () => {
|
||||
export const getSystemTheme = (): string => {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
|
||||
return window.matchMedia &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
? 'dark'
|
||||
: 'light';
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听系统主题变化
|
||||
*/
|
||||
export const watchSystemTheme = (callback) => {
|
||||
export const watchSystemTheme = (callback: (theme: string) => void): (() => void) => {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) {
|
||||
return () => {};
|
||||
}
|
||||
|
||||
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
|
||||
const handler = (e) => {
|
||||
const handler = (e: MediaQueryListEvent) => {
|
||||
callback(e.matches ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
@@ -600,17 +595,3 @@ export const watchSystemTheme = (callback) => {
|
||||
mediaQuery.removeEventListener('change', handler);
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 自动应用系统主题
|
||||
*/
|
||||
export const autoApplySystemTheme = () => {
|
||||
const applyTheme = (theme) => {
|
||||
updateStyles(THEMES[theme]);
|
||||
applyThemeVariables(THEMES[theme]);
|
||||
};
|
||||
|
||||
applyTheme(getSystemTheme());
|
||||
|
||||
return watchSystemTheme(applyTheme);
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* MetonaToast Templates — HTML 模板辅助函数
|
||||
* @module templates
|
||||
* @version 0.5.0
|
||||
* @description confirm / prompt / progress / action 的 DOM 模板
|
||||
*/
|
||||
|
||||
import { escapeHTML } from './utils.js';
|
||||
import { t } from './i18n.js';
|
||||
import type { ActionButton, ConfirmOptions, PromptOptions, ProgressOptions } from './types.js';
|
||||
|
||||
/**
|
||||
* 按钮行模板 — confirm/prompt 共用
|
||||
*/
|
||||
export const btnRow = (buttons: string): string => `
|
||||
<div style="display: flex; gap: 8px; margin-top: 12px;">
|
||||
${buttons}
|
||||
</div>`;
|
||||
|
||||
/**
|
||||
* 确认对话框模板
|
||||
*/
|
||||
export const confirmHTML = (opts: ConfirmOptions): string => {
|
||||
const confirmText = opts.confirmText || t('confirm');
|
||||
const cancelText = opts.cancelText || t('cancel');
|
||||
const confirmColor = opts.confirmColor || '#10b981';
|
||||
const cancelColor = opts.cancelColor || '#6b7280';
|
||||
return btnRow(`
|
||||
<button class="met-confirm-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${confirmColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(confirmText)}</button>
|
||||
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
|
||||
`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 输入对话框模板
|
||||
*/
|
||||
export const promptHTML = (opts: PromptOptions): string => {
|
||||
const submitText = opts.submitText || t('confirm');
|
||||
const cancelText = opts.cancelText || t('cancel');
|
||||
const submitColor = opts.submitColor || '#3b82f6';
|
||||
const cancelColor = opts.cancelColor || '#6b7280';
|
||||
return `
|
||||
<input type="${opts.inputType || 'text'}"
|
||||
class="met-input"
|
||||
placeholder="${escapeHTML(opts.placeholder || '')}"
|
||||
value="${escapeHTML(opts.defaultValue || '')}"
|
||||
style="width:100%;padding:8px 12px;border:1px solid rgba(0,0,0,0.1);border-radius:6px;font-size:13px;margin-top:8px;background:transparent;color:inherit;box-sizing:border-box">
|
||||
${btnRow(`
|
||||
<button class="met-submit-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${submitColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(submitText)}</button>
|
||||
<button class="met-cancel-btn" style="flex:1;padding:8px 16px;border:none;border-radius:6px;background:${cancelColor};color:white;cursor:pointer;font-size:13px">${escapeHTML(cancelText)}</button>
|
||||
`)}
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* 进度条模板
|
||||
*/
|
||||
export const progressHTML = (opts: ProgressOptions): string => {
|
||||
const color = opts.progressColor || '#3b82f6';
|
||||
return `
|
||||
<div class="met-progress-bar" style="width:100%;height:8px;background:rgba(0,0,0,0.1);border-radius:4px;margin-top:12px;overflow:hidden">
|
||||
<div class="met-progress-fill" style="width:0%;height:100%;background:${color};border-radius:4px;transition:width 0.3s ease"></div>
|
||||
</div>
|
||||
<div class="met-progress-text" style="font-size:12px;opacity:0.7;margin-top:4px;text-align:right">0%</div>
|
||||
`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Action Toast 模板
|
||||
*/
|
||||
export const actionHTML = (actions: ActionButton[]): string => {
|
||||
if (!Array.isArray(actions) || actions.length === 0) return '';
|
||||
const buttons = actions.map((a, i) => {
|
||||
const bg = a.style?.background || a.color || '#6366f1';
|
||||
const cls = `met-action-btn-${i}`;
|
||||
return `<button class="${cls}" style="flex:1;padding:6px 12px;border:none;border-radius:6px;background:${bg};color:white;cursor:pointer;font-size:12px;font-weight:500">${escapeHTML(a.text || '')}</button>`;
|
||||
}).join('');
|
||||
return btnRow(buttons);
|
||||
};
|
||||
+72
-209
@@ -1,48 +1,39 @@
|
||||
/**
|
||||
* MetonaToast Themes - 主题管理
|
||||
* MetonaToast Themes — 主题管理
|
||||
* @module themes
|
||||
* @version 2.0.1
|
||||
* @description 主题系统、自定义主题和主题切换
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { THEMES } from './constants.js';
|
||||
import { prefersDark } from './utils.js';
|
||||
import type { ThemeConfig, ThemePreview, ThemeUtils } from './types.js';
|
||||
|
||||
// 当前主题状态
|
||||
let currentTheme = 'auto';
|
||||
let themeListeners = new Set();
|
||||
let systemThemeMediaQuery = null;
|
||||
let systemThemeListener = null;
|
||||
const themeListeners: Set<(theme: string, resolved: string) => void> = new Set();
|
||||
let systemThemeMediaQuery: MediaQueryList | null = null;
|
||||
let systemThemeListener: ((e: MediaQueryListEvent) => void) | null = null;
|
||||
|
||||
/**
|
||||
* 获取系统主题
|
||||
* @returns {string} 系统主题 ('light' 或 'dark')
|
||||
*/
|
||||
export const getSystemTheme = () => {
|
||||
export const getSystemTheme = (): 'light' | 'dark' => {
|
||||
if (typeof window === 'undefined') return 'light';
|
||||
return prefersDark() ? 'dark' : 'light';
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题(别名)
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {string} 解析后的主题
|
||||
*/
|
||||
export const getTheme = (theme) => {
|
||||
export const getTheme = (theme: string): string => {
|
||||
return resolveTheme(theme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析主题
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {string} 解析后的主题
|
||||
* 解析主题 — 'auto' 始终按系统主题检测,不依赖当前手动选择的主题
|
||||
*/
|
||||
export const resolveTheme = (theme) => {
|
||||
export const resolveTheme = (theme: string): string => {
|
||||
if (theme === 'auto') {
|
||||
// 优先使用用户设置的全局主题,否则使用系统主题
|
||||
if (currentTheme && currentTheme !== 'auto') {
|
||||
return currentTheme;
|
||||
}
|
||||
return getSystemTheme();
|
||||
}
|
||||
return theme;
|
||||
@@ -50,44 +41,37 @@ export const resolveTheme = (theme) => {
|
||||
|
||||
/**
|
||||
* 获取主题配置
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {Object} 主题配置
|
||||
*/
|
||||
export const getThemeConfig = (theme) => {
|
||||
export const getThemeConfig = (theme: string): ThemeConfig => {
|
||||
const resolved = resolveTheme(theme);
|
||||
return THEMES[resolved] || THEMES.light;
|
||||
return (THEMES[resolved] as ThemeConfig) || (THEMES.light as ThemeConfig);
|
||||
};
|
||||
|
||||
/**
|
||||
* 应用主题
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
export const applyTheme = (theme) => {
|
||||
export const applyTheme = (theme: string): void => {
|
||||
currentTheme = theme;
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
// 应用主题到文档
|
||||
|
||||
if (typeof document !== 'undefined') {
|
||||
document.documentElement.setAttribute('data-met-theme', resolved);
|
||||
document.documentElement.classList.remove('met-theme-light', 'met-theme-dark', 'met-theme-auto');
|
||||
document.documentElement.classList.add(`met-theme-${resolved}`);
|
||||
|
||||
// 设置CSS变量
|
||||
|
||||
const config = getThemeConfig(theme);
|
||||
setThemeVariables(config);
|
||||
}
|
||||
|
||||
// 通知监听器
|
||||
|
||||
notifyThemeListeners(theme, resolved);
|
||||
};
|
||||
|
||||
/**
|
||||
* 设置主题CSS变量
|
||||
* @param {Object} config - 主题配置
|
||||
*/
|
||||
export const setThemeVariables = (config) => {
|
||||
export const setThemeVariables = (config: ThemeConfig): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--met-bg', config.bg);
|
||||
root.style.setProperty('--met-text', config.text);
|
||||
@@ -96,8 +80,7 @@ export const setThemeVariables = (config) => {
|
||||
root.style.setProperty('--met-hover-shadow', config.hoverShadow);
|
||||
root.style.setProperty('--met-progress-bg', config.progressBg);
|
||||
root.style.setProperty('--met-close-hover-bg', config.closeHoverBg);
|
||||
|
||||
// 设置颜色变量
|
||||
|
||||
root.style.setProperty('--met-success', '#10b981');
|
||||
root.style.setProperty('--met-error', '#ef4444');
|
||||
root.style.setProperty('--met-warning', '#f59e0b');
|
||||
@@ -108,51 +91,39 @@ export const setThemeVariables = (config) => {
|
||||
/**
|
||||
* 清除主题CSS变量
|
||||
*/
|
||||
export const clearThemeVariables = () => {
|
||||
export const clearThemeVariables = (): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const root = document.documentElement;
|
||||
const variables = [
|
||||
'--met-bg',
|
||||
'--met-text',
|
||||
'--met-border',
|
||||
'--met-shadow',
|
||||
'--met-hover-shadow',
|
||||
'--met-progress-bg',
|
||||
'--met-close-hover-bg',
|
||||
'--met-success',
|
||||
'--met-error',
|
||||
'--met-warning',
|
||||
'--met-info',
|
||||
'--met-loading',
|
||||
'--met-bg', '--met-text', '--met-border', '--met-shadow',
|
||||
'--met-hover-shadow', '--met-progress-bg', '--met-close-hover-bg',
|
||||
'--met-success', '--met-error', '--met-warning', '--met-info', '--met-loading',
|
||||
];
|
||||
|
||||
variables.forEach((variable) => {
|
||||
root.style.removeProperty(variable);
|
||||
|
||||
variables.forEach((v) => {
|
||||
root.style.removeProperty(v);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取当前主题
|
||||
* @returns {string} 当前主题
|
||||
*/
|
||||
export const getCurrentTheme = () => {
|
||||
export const getCurrentTheme = (): string => {
|
||||
return currentTheme;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取解析后的主题
|
||||
* @returns {string} 解析后的主题
|
||||
*/
|
||||
export const getResolvedTheme = () => {
|
||||
export const getResolvedTheme = (): string => {
|
||||
return resolveTheme(currentTheme);
|
||||
};
|
||||
|
||||
/**
|
||||
* 切换主题
|
||||
* @param {string} theme - 新主题
|
||||
*/
|
||||
export const switchTheme = (theme) => {
|
||||
export const switchTheme = (theme: string): void => {
|
||||
applyTheme(theme);
|
||||
saveTheme(theme);
|
||||
};
|
||||
@@ -160,7 +131,7 @@ export const switchTheme = (theme) => {
|
||||
/**
|
||||
* 切换亮色/暗色主题
|
||||
*/
|
||||
export const toggleTheme = () => {
|
||||
export const toggleTheme = (): void => {
|
||||
const resolved = getResolvedTheme();
|
||||
const newTheme = resolved === 'dark' ? 'light' : 'dark';
|
||||
switchTheme(newTheme);
|
||||
@@ -169,15 +140,14 @@ export const toggleTheme = () => {
|
||||
/**
|
||||
* 重置为自动主题
|
||||
*/
|
||||
export const resetToAuto = () => {
|
||||
export const resetToAuto = (): void => {
|
||||
switchTheme('auto');
|
||||
};
|
||||
|
||||
/**
|
||||
* 保存主题到本地存储
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
export const saveTheme = (theme) => {
|
||||
export const saveTheme = (theme: string): void => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
localStorage.setItem('metona-toast-theme', theme);
|
||||
@@ -189,9 +159,8 @@ export const saveTheme = (theme) => {
|
||||
|
||||
/**
|
||||
* 从本地存储加载主题
|
||||
* @returns {string} 主题名称
|
||||
*/
|
||||
export const loadTheme = () => {
|
||||
export const loadTheme = (): string => {
|
||||
if (typeof localStorage !== 'undefined') {
|
||||
try {
|
||||
return localStorage.getItem('metona-toast-theme') || 'auto';
|
||||
@@ -205,41 +174,36 @@ export const loadTheme = () => {
|
||||
/**
|
||||
* 初始化主题系统
|
||||
*/
|
||||
export const initTheme = () => {
|
||||
// 加载保存的主题
|
||||
export const initTheme = (): void => {
|
||||
const savedTheme = loadTheme();
|
||||
applyTheme(savedTheme);
|
||||
|
||||
// 监听系统主题变化
|
||||
watchSystemTheme();
|
||||
};
|
||||
|
||||
/**
|
||||
* 监听系统主题变化
|
||||
*/
|
||||
export const watchSystemTheme = () => {
|
||||
export const watchSystemTheme = (): void => {
|
||||
if (typeof window === 'undefined' || !window.matchMedia) return;
|
||||
|
||||
// 移除旧的监听器
|
||||
|
||||
if (systemThemeMediaQuery && systemThemeListener) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||
}
|
||||
|
||||
// 创建新的监听器
|
||||
|
||||
systemThemeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
systemThemeListener = (e) => {
|
||||
systemThemeListener = () => {
|
||||
if (currentTheme === 'auto') {
|
||||
applyTheme('auto');
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
systemThemeMediaQuery.addEventListener('change', systemThemeListener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 停止监听系统主题变化
|
||||
*/
|
||||
export const unwatchSystemTheme = () => {
|
||||
export const unwatchSystemTheme = (): void => {
|
||||
if (systemThemeMediaQuery && systemThemeListener) {
|
||||
systemThemeMediaQuery.removeEventListener('change', systemThemeListener);
|
||||
systemThemeMediaQuery = null;
|
||||
@@ -249,12 +213,9 @@ export const unwatchSystemTheme = () => {
|
||||
|
||||
/**
|
||||
* 添加主题监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
* @returns {Function} 移除监听器函数
|
||||
*/
|
||||
export const addThemeListener = (listener) => {
|
||||
export const addThemeListener = (listener: (theme: string, resolved: string) => void): () => void => {
|
||||
themeListeners.add(listener);
|
||||
|
||||
return () => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
@@ -262,18 +223,15 @@ export const addThemeListener = (listener) => {
|
||||
|
||||
/**
|
||||
* 移除主题监听器
|
||||
* @param {Function} listener - 监听器函数
|
||||
*/
|
||||
export const removeThemeListener = (listener) => {
|
||||
export const removeThemeListener = (listener: (theme: string, resolved: string) => void): void => {
|
||||
themeListeners.delete(listener);
|
||||
};
|
||||
|
||||
/**
|
||||
* 通知主题监听器
|
||||
* @param {string} theme - 主题名称
|
||||
* @param {string} resolved - 解析后的主题
|
||||
*/
|
||||
const notifyThemeListeners = (theme, resolved) => {
|
||||
const notifyThemeListeners = (theme: string, resolved: string): void => {
|
||||
themeListeners.forEach((listener) => {
|
||||
try {
|
||||
listener(theme, resolved);
|
||||
@@ -286,74 +244,64 @@ const notifyThemeListeners = (theme, resolved) => {
|
||||
/**
|
||||
* 清除所有主题监听器
|
||||
*/
|
||||
export const clearThemeListeners = () => {
|
||||
export const clearThemeListeners = (): void => {
|
||||
themeListeners.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* 注册自定义主题
|
||||
* @param {string} name - 主题名称
|
||||
* @param {Object} config - 主题配置
|
||||
*/
|
||||
export const registerTheme = (name, config) => {
|
||||
THEMES[name] = {
|
||||
bg: config.bg || THEMES.light.bg,
|
||||
text: config.text || THEMES.light.text,
|
||||
border: config.border || THEMES.light.border,
|
||||
shadow: config.shadow || THEMES.light.shadow,
|
||||
hoverShadow: config.hoverShadow || THEMES.light.hoverShadow,
|
||||
progressBg: config.progressBg || THEMES.light.progressBg,
|
||||
closeHoverBg: config.closeHoverBg || THEMES.light.closeHoverBg,
|
||||
export const registerTheme = (name: string, config: ThemeConfig): void => {
|
||||
(THEMES as Record<string, ThemeConfig>)[name] = {
|
||||
bg: config.bg || (THEMES.light as ThemeConfig).bg,
|
||||
text: config.text || (THEMES.light as ThemeConfig).text,
|
||||
border: config.border || (THEMES.light as ThemeConfig).border,
|
||||
shadow: config.shadow || (THEMES.light as ThemeConfig).shadow,
|
||||
hoverShadow: config.hoverShadow || (THEMES.light as ThemeConfig).hoverShadow,
|
||||
progressBg: config.progressBg || (THEMES.light as ThemeConfig).progressBg,
|
||||
closeHoverBg: config.closeHoverBg || (THEMES.light as ThemeConfig).closeHoverBg,
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* 注销自定义主题
|
||||
* @param {string} name - 主题名称
|
||||
*/
|
||||
export const unregisterTheme = (name) => {
|
||||
export const unregisterTheme = (name: string): void => {
|
||||
if (name === 'light' || name === 'dark' || name === 'auto') {
|
||||
console.warn('Cannot unregister built-in theme:', name);
|
||||
return;
|
||||
}
|
||||
|
||||
delete THEMES[name];
|
||||
delete (THEMES as Record<string, ThemeConfig>)[name];
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取所有主题
|
||||
* @returns {Object} 主题映射
|
||||
*/
|
||||
export const getAllThemes = () => {
|
||||
return { ...THEMES };
|
||||
export const getAllThemes = (): Record<string, ThemeConfig> => {
|
||||
return { ...THEMES } as Record<string, ThemeConfig>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题名称列表
|
||||
* @returns {Array} 主题名称数组
|
||||
*/
|
||||
export const getThemeNames = () => {
|
||||
export const getThemeNames = (): string[] => {
|
||||
return Object.keys(THEMES);
|
||||
};
|
||||
|
||||
/**
|
||||
* 检查主题是否存在
|
||||
* @param {string} name - 主题名称
|
||||
* @returns {boolean} 是否存在
|
||||
*/
|
||||
export const hasTheme = (name) => {
|
||||
export const hasTheme = (name: string): boolean => {
|
||||
return name in THEMES;
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取主题预览
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {Object} 主题预览
|
||||
*/
|
||||
export const getThemePreview = (theme) => {
|
||||
export const getThemePreview = (theme: string): ThemePreview => {
|
||||
const config = getThemeConfig(theme);
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
|
||||
return {
|
||||
name: theme,
|
||||
resolved,
|
||||
@@ -370,13 +318,11 @@ export const getThemePreview = (theme) => {
|
||||
|
||||
/**
|
||||
* 生成主题CSS
|
||||
* @param {string} theme - 主题名称
|
||||
* @returns {string} CSS字符串
|
||||
*/
|
||||
export const generateThemeCSS = (theme) => {
|
||||
export const generateThemeCSS = (theme: string): string => {
|
||||
const config = getThemeConfig(theme);
|
||||
const resolved = resolveTheme(theme);
|
||||
|
||||
|
||||
return `
|
||||
.met-theme-${resolved} {
|
||||
--met-bg: ${config.bg};
|
||||
@@ -392,21 +338,18 @@ export const generateThemeCSS = (theme) => {
|
||||
|
||||
/**
|
||||
* 应用主题CSS
|
||||
* @param {string} theme - 主题名称
|
||||
*/
|
||||
export const applyThemeCSS = (theme) => {
|
||||
export const applyThemeCSS = (theme: string): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const css = generateThemeCSS(theme);
|
||||
const styleId = 'metona-toast-theme-css';
|
||||
|
||||
// 移除旧样式
|
||||
|
||||
const oldStyle = document.getElementById(styleId);
|
||||
if (oldStyle) {
|
||||
oldStyle.remove();
|
||||
}
|
||||
|
||||
// 添加新样式
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = styleId;
|
||||
style.textContent = css;
|
||||
@@ -416,9 +359,9 @@ export const applyThemeCSS = (theme) => {
|
||||
/**
|
||||
* 移除主题CSS
|
||||
*/
|
||||
export const removeThemeCSS = () => {
|
||||
export const removeThemeCSS = (): void => {
|
||||
if (typeof document === 'undefined') return;
|
||||
|
||||
|
||||
const styleId = 'metona-toast-theme-css';
|
||||
const style = document.getElementById(styleId);
|
||||
if (style) {
|
||||
@@ -427,9 +370,9 @@ export const removeThemeCSS = () => {
|
||||
};
|
||||
|
||||
/**
|
||||
* 主题工具 — 代理层,直接引用模块函数
|
||||
* 主题工具 — 代理层
|
||||
*/
|
||||
export const themeUtils = {
|
||||
export const themeUtils: ThemeUtils = {
|
||||
getSystemTheme,
|
||||
resolveTheme,
|
||||
getThemeConfig,
|
||||
@@ -458,84 +401,4 @@ export const themeUtils = {
|
||||
loadTheme,
|
||||
};
|
||||
|
||||
/**
|
||||
* 预设主题
|
||||
*/
|
||||
export const presetThemes = {
|
||||
/**
|
||||
* 浅色主题
|
||||
*/
|
||||
light: {
|
||||
name: '浅色',
|
||||
description: '明亮清晰的主题',
|
||||
config: THEMES.light,
|
||||
},
|
||||
|
||||
/**
|
||||
* 深色主题
|
||||
*/
|
||||
dark: {
|
||||
name: '深色',
|
||||
description: '护眼舒适的暗色主题',
|
||||
config: THEMES.dark,
|
||||
},
|
||||
|
||||
/**
|
||||
* 自动主题
|
||||
*/
|
||||
auto: {
|
||||
name: '自动',
|
||||
description: '跟随系统主题设置',
|
||||
config: 'auto',
|
||||
},
|
||||
|
||||
warm: {
|
||||
name: '暖色',
|
||||
description: '温馨舒适的暖色主题',
|
||||
config: THEMES.warm,
|
||||
},
|
||||
};
|
||||
|
||||
// 注册预设主题
|
||||
Object.entries(presetThemes).forEach(([name, theme]) => {
|
||||
if (name !== 'auto' && theme.config !== 'auto') {
|
||||
THEMES[name] = theme.config;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 创建主题管理器
|
||||
* @returns {Object} 主题管理器
|
||||
*/
|
||||
export const createThemeManager = () => {
|
||||
return {
|
||||
getSystemTheme,
|
||||
resolveTheme,
|
||||
getThemeConfig,
|
||||
applyTheme,
|
||||
getCurrentTheme,
|
||||
getResolvedTheme,
|
||||
switchTheme,
|
||||
toggleTheme,
|
||||
resetToAuto,
|
||||
initTheme,
|
||||
watchSystemTheme,
|
||||
unwatchSystemTheme,
|
||||
addThemeListener,
|
||||
removeThemeListener,
|
||||
clearThemeListeners,
|
||||
registerTheme,
|
||||
unregisterTheme,
|
||||
getAllThemes,
|
||||
getThemeNames,
|
||||
hasTheme,
|
||||
getThemePreview,
|
||||
generateThemeCSS,
|
||||
applyThemeCSS,
|
||||
removeThemeCSS,
|
||||
saveTheme,
|
||||
loadTheme,
|
||||
};
|
||||
};
|
||||
|
||||
export { themeUtils as default };
|
||||
+691
@@ -0,0 +1,691 @@
|
||||
/**
|
||||
* MetonaToast Toast — Toast 类
|
||||
* @module toast
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import { generateId, escapeHTML } from './utils.js';
|
||||
import { DEFAULTS, ICONS, TYPE_COLORS, THEMES } from './constants.js';
|
||||
import { injectStyles } from './styles.js';
|
||||
import { getTheme } from './themes.js';
|
||||
import { t, getLocaleDirection, getCurrentLocale } from './i18n.js';
|
||||
import { hasAnimation, ensureAnimationCSS } from './animations.js';
|
||||
import type { ToastConfig, ToastOptions, ToastInstance, ErrorInfo, TypeColor, ThemeConfig } from './types.js';
|
||||
|
||||
/**
|
||||
* Toast 类 — 核心通知组件
|
||||
*/
|
||||
export class Toast implements ToastInstance {
|
||||
// 静态钩子系统
|
||||
static _hooks: Map<string, Array<(toast: Toast) => boolean | void>> = new Map();
|
||||
|
||||
// 实例注册表 — 用于通过 id 反查实例(max 超限移除、外部接管等)
|
||||
static _registry: Map<string, Toast> = new Map();
|
||||
|
||||
// 由 api.ts 注入的回调,通过 setCallbacks() 设置
|
||||
private static _onError: ((errorInfo: ErrorInfo) => void) | null = null;
|
||||
private static _removeToast: ((id: string) => void) | null = null;
|
||||
|
||||
/**
|
||||
* 设置 Toast 类级别的回调函数
|
||||
*/
|
||||
static setCallbacks(callbacks: {
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
removeToast?: ((id: string) => void) | null;
|
||||
}): void {
|
||||
if (callbacks.onError !== undefined) Toast._onError = callbacks.onError;
|
||||
if (callbacks.removeToast !== undefined) Toast._removeToast = callbacks.removeToast;
|
||||
}
|
||||
|
||||
static on(name: string, fn: (toast: Toast) => boolean | void): () => void {
|
||||
if (!this._hooks.has(name)) this._hooks.set(name, []);
|
||||
this._hooks.get(name)!.push(fn);
|
||||
// 用类名引用而非 this,保证返回的取消函数在任意调用上下文可用
|
||||
return () => Toast.off(name, fn);
|
||||
}
|
||||
|
||||
static off(name: string, fn: (toast: Toast) => boolean | void): void {
|
||||
const list = this._hooks.get(name);
|
||||
if (list) {
|
||||
const next = list.filter(f => f !== fn);
|
||||
// 空数组直接删除 key,避免残留
|
||||
if (next.length === 0) this._hooks.delete(name);
|
||||
else this._hooks.set(name, next);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发钩子 — 如果任意钩子返回 false 则整体返回 false
|
||||
* toast 参数可选:全局事件(configChange/themeChange 等)不关联具体 toast
|
||||
*/
|
||||
static trigger(name: string, toast: Toast | null = null): boolean {
|
||||
const list = this._hooks.get(name);
|
||||
if (!list || list.length === 0) return true;
|
||||
|
||||
let allow = true;
|
||||
list.forEach(fn => {
|
||||
try {
|
||||
const result = fn(toast as Toast);
|
||||
if (result === false) allow = false;
|
||||
}
|
||||
catch (e) {
|
||||
console.error('Hook error:', name, e);
|
||||
if (Toast._onError) {
|
||||
try { Toast._onError({ hook: name, error: e as Error, toast: toast as ToastInstance }); } catch (_e) { /* noop */ }
|
||||
}
|
||||
}
|
||||
});
|
||||
return allow;
|
||||
}
|
||||
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
html: string;
|
||||
iconHTML: string;
|
||||
config: ToastConfig;
|
||||
el: HTMLElement | null = null;
|
||||
barEl: HTMLElement | null = null;
|
||||
rafId: number | null = null;
|
||||
remaining: number;
|
||||
startedAt: number = 0;
|
||||
paused: boolean = false;
|
||||
closing: boolean = false;
|
||||
group: string | null = null;
|
||||
_cleanups: Array<() => void> = [];
|
||||
|
||||
constructor(opts: ToastOptions) {
|
||||
this.id = opts.id || generateId();
|
||||
this.type = opts.type || 'default';
|
||||
this.title = opts.title || '';
|
||||
this.message = opts.message ?? opts.content ?? '';
|
||||
this.html = opts.html || '';
|
||||
this.iconHTML = opts.iconHTML || '';
|
||||
this.config = { ...DEFAULTS as unknown as ToastConfig, ...opts };
|
||||
// 防止嵌套对象被多个 toast 实例共享引用
|
||||
if (opts.style && typeof opts.style === 'object') {
|
||||
this.config.style = { ...opts.style };
|
||||
}
|
||||
this.remaining = this.config.duration || 0;
|
||||
this.group = opts.group || null;
|
||||
}
|
||||
|
||||
_palette(): { theme: string; c: TypeColor; t: Partial<ThemeConfig> } {
|
||||
const theme = getTheme(this.config.theme || 'auto');
|
||||
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
|
||||
const tc = (THEMES[theme] as ThemeConfig) || (THEMES.light as ThemeConfig);
|
||||
const t: Partial<ThemeConfig> = { bg: tc.bg, border: tc.border, shadow: tc.shadow };
|
||||
return { theme, c, t };
|
||||
}
|
||||
|
||||
create(): this {
|
||||
if (typeof window === 'undefined' || typeof document === 'undefined') return this;
|
||||
|
||||
// beforeShow 钩子 — 返回 false 可阻止显示
|
||||
if (!Toast.trigger('beforeShow', this)) return this;
|
||||
|
||||
// onBeforeShow 配置回调 — 返回 false 可阻止显示
|
||||
if (typeof this.config.onBeforeShow === 'function') {
|
||||
try {
|
||||
if (this.config.onBeforeShow(this) === false) return this;
|
||||
} catch (e) {
|
||||
console.error('onBeforeShow callback error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
injectStyles();
|
||||
ensureAnimationCSS(this.config.animation || 'slide');
|
||||
|
||||
const container = this._getContainer();
|
||||
const { theme, c, t } = this._palette();
|
||||
|
||||
this._limitToasts(container);
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = this._buildClassName(theme);
|
||||
el.setAttribute('role', (this.type === 'error' || this.type === 'warning') ? 'alert' : 'status');
|
||||
el.setAttribute('aria-live', this.type === 'error' ? 'assertive' : 'polite');
|
||||
el.dataset.id = this.id;
|
||||
|
||||
this._applyStyles(el, theme, t);
|
||||
this._buildContent(el, c);
|
||||
|
||||
this.el = el;
|
||||
// Chrome bug: column-reverse + appendChild 会导致重叠。改用 column + insertBefore
|
||||
const pos = this.config.position || 'top-right';
|
||||
if (pos.startsWith('top')) {
|
||||
container.insertBefore(el, container.firstChild);
|
||||
} else {
|
||||
container.appendChild(el);
|
||||
}
|
||||
|
||||
Toast._registry.set(this.id, this);
|
||||
|
||||
this._bindEvents(el);
|
||||
this._startTimer();
|
||||
|
||||
// 进入动画
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
el.classList.add('met-show');
|
||||
});
|
||||
});
|
||||
|
||||
if (typeof this.config.onShow === 'function') {
|
||||
try {
|
||||
this.config.onShow(this);
|
||||
} catch (e) {
|
||||
console.error('onShow callback error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
Toast.trigger('afterShow', this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
_getContainer(): HTMLElement {
|
||||
const rawPosition = this.config.position || 'top-right';
|
||||
const zIndex = this.config.zIndex || 9999;
|
||||
|
||||
// RTL 语言时翻转 left/right
|
||||
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
|
||||
const position = isRTL
|
||||
? rawPosition.replace('left', '__TMP__').replace('right', 'left').replace('__TMP__', 'right')
|
||||
: rawPosition;
|
||||
|
||||
// 从模块级缓存获取 (由 api.ts 管理)
|
||||
const cached = _containerCache.get(document.body);
|
||||
if (cached && cached.has(position)) {
|
||||
return cached.get(position)!;
|
||||
}
|
||||
|
||||
// 缓存未命中时创建新容器
|
||||
if (!_containerCache.has(document.body)) {
|
||||
_containerCache.set(document.body, new Map());
|
||||
}
|
||||
|
||||
const el = document.createElement('div');
|
||||
el.className = `met-container ${position}`;
|
||||
el.setAttribute('aria-label', 'Notifications');
|
||||
el.setAttribute('role', 'region');
|
||||
// RTL 语言设置容器文字方向,使内容/关闭按钮/进度条正确镜像
|
||||
if (isRTL) el.setAttribute('dir', 'rtl');
|
||||
|
||||
const posStyles: Record<string, string> = {
|
||||
'top-left': 'top:0;left:0;align-items:flex-start',
|
||||
'top-center': 'top:0;left:0;right:0;align-items:center',
|
||||
'top-right': 'top:0;right:0;align-items:flex-end',
|
||||
'bottom-left': 'bottom:0;left:0;align-items:flex-start',
|
||||
'bottom-center': 'bottom:0;left:0;right:0;align-items:center',
|
||||
'bottom-right': 'bottom:0;right:0;align-items:flex-end',
|
||||
};
|
||||
el.style.cssText = `
|
||||
display:flex;
|
||||
flex-direction:column;
|
||||
gap:${this.config.gap || 12}px;
|
||||
box-sizing:border-box;
|
||||
padding:${this.config.offset || 24}px;
|
||||
max-width:100vw;
|
||||
position:fixed;
|
||||
z-index:${zIndex};
|
||||
pointer-events:none;
|
||||
${posStyles[position] || 'top:0;right:0;align-items:flex-end'}
|
||||
`;
|
||||
|
||||
document.body.appendChild(el);
|
||||
_containerCache.get(document.body)!.set(position, el);
|
||||
|
||||
return el;
|
||||
}
|
||||
|
||||
_limitToasts(container: HTMLElement): void {
|
||||
const max = this.config.max || 6;
|
||||
const list = Array.from(container.querySelectorAll('.met-toast'));
|
||||
while (list.length >= max) {
|
||||
const el = list.shift() as HTMLElement;
|
||||
const id = el?.dataset.id;
|
||||
if (!id) continue;
|
||||
const existing = Toast._registry.get(id);
|
||||
if (existing) {
|
||||
existing.close(true);
|
||||
} else if (Toast._removeToast) {
|
||||
Toast._removeToast(id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_buildClassName(theme: string): string {
|
||||
const anim = hasAnimation(this.config.animation || '') ? (this.config.animation as string) : 'slide';
|
||||
return [
|
||||
'met-toast',
|
||||
`met-${this.type}`,
|
||||
`met-theme-${theme}`,
|
||||
`met-anim-${anim}`,
|
||||
(this.config.closeOnClick || this.config.draggable) ? 'met-clickable' : '',
|
||||
this.config.className || '',
|
||||
].filter(Boolean).join(' ');
|
||||
}
|
||||
|
||||
_applyStyles(el: HTMLElement, _theme: string, _t: Partial<ThemeConfig>): void {
|
||||
const widthValue = typeof this.config.width === 'number'
|
||||
? `${this.config.width}px`
|
||||
: (typeof this.config.width === 'string' ? this.config.width : '360px');
|
||||
|
||||
const set = (p: string, v: string) => el.style.setProperty(p, v, 'important');
|
||||
set('position', 'relative');
|
||||
set('flex-shrink', '0');
|
||||
set('min-width', '240px');
|
||||
set('max-width', 'calc(100vw - 48px)');
|
||||
set('width', widthValue);
|
||||
// 用户自定义样式最后应用
|
||||
if (this.config.style && Object.keys(this.config.style).length > 0) {
|
||||
Object.entries(this.config.style).forEach(([k, v]) => { (el.style as unknown as Record<string, string>)[k] = v; });
|
||||
}
|
||||
}
|
||||
|
||||
_buildContent(el: HTMLElement, c: TypeColor): void {
|
||||
// 自定义渲染函数 — 完全接管 DOM 构建
|
||||
if (typeof this.config.render === 'function') {
|
||||
el.innerHTML = this.config.render(this);
|
||||
this.barEl = el.querySelector('.met-bar, .met-bar-v');
|
||||
return;
|
||||
}
|
||||
|
||||
const showIcon = this.config.icon !== false && (this.iconHTML || ICONS[this.type]);
|
||||
const showClose = this.config.closeButton !== false;
|
||||
const showProgress = this.config.showProgress !== false && (this.config.duration || 0) > 0;
|
||||
const showSide = (!showIcon && this.type !== 'default');
|
||||
|
||||
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
|
||||
const safeMessage = this.html
|
||||
? `<div class="met-message">${this.html}</div>`
|
||||
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||
|
||||
const iconHTML = showIcon
|
||||
? `<div class="met-icon" style="color:${c.fg}">${this.iconHTML || ICONS[this.type]}</div>` : '';
|
||||
const closeHTML = showClose
|
||||
? `<button class="met-close" type="button" aria-label="${t('close')}">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
|
||||
<line x1="18" y1="6" x2="6" y2="18"/>
|
||||
<line x1="6" y1="6" x2="18" y2="18"/>
|
||||
</svg>
|
||||
</button>`
|
||||
: '';
|
||||
const progressHTML = showProgress
|
||||
? (this.config.progressDirection === 'vertical'
|
||||
? `<div class="met-progress-v"><div class="met-bar-v" style="background:${c.fg}"></div></div>`
|
||||
: `<div class="met-progress"><div class="met-bar" style="background:${c.fg}"></div></div>`)
|
||||
: '';
|
||||
const sideHTML = showSide
|
||||
? `<div class="met-side" style="background:${c.fg}"></div>` : '';
|
||||
|
||||
el.innerHTML = `
|
||||
${iconHTML}
|
||||
${sideHTML}
|
||||
<div class="met-content">
|
||||
${safeTitle}
|
||||
${safeMessage}
|
||||
</div>
|
||||
${closeHTML}
|
||||
${progressHTML}
|
||||
`;
|
||||
|
||||
this.barEl = el.querySelector('.met-bar, .met-bar-v');
|
||||
}
|
||||
|
||||
_bindEvents(el: HTMLElement): void {
|
||||
const eventHandler = (e: Event) => {
|
||||
const target = e.target as HTMLElement;
|
||||
|
||||
if (target.closest('.met-close')) {
|
||||
e.stopPropagation();
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
|
||||
Toast.trigger('click', this);
|
||||
|
||||
if (this.config.closeOnClick && !target.closest('.met-close')) {
|
||||
if (typeof this.config.onClick === 'function') {
|
||||
try {
|
||||
this.config.onClick(this);
|
||||
} catch (err) {
|
||||
console.error('onClick callback error:', err);
|
||||
}
|
||||
}
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
el.addEventListener('click', eventHandler);
|
||||
this._cleanups.push(() => el.removeEventListener('click', eventHandler));
|
||||
|
||||
if (this.config.pauseOnHover && (this.config.duration || 0) > 0) {
|
||||
const mouseEnter = () => {
|
||||
Toast.trigger('hover', this);
|
||||
this._pause();
|
||||
};
|
||||
const mouseLeave = () => {
|
||||
Toast.trigger('hover', this);
|
||||
this._resume();
|
||||
};
|
||||
|
||||
el.addEventListener('mouseenter', mouseEnter);
|
||||
el.addEventListener('mouseleave', mouseLeave);
|
||||
|
||||
this._cleanups.push(() => {
|
||||
el.removeEventListener('mouseenter', mouseEnter);
|
||||
el.removeEventListener('mouseleave', mouseLeave);
|
||||
});
|
||||
}
|
||||
|
||||
// 入场动画生命周期钩子(离场为过渡/内联样式,不产生 animation 事件)
|
||||
const animStart = () => Toast.trigger('animationStart', this);
|
||||
const animEnd = () => Toast.trigger('animationEnd', this);
|
||||
el.addEventListener('animationstart', animStart);
|
||||
el.addEventListener('animationend', animEnd);
|
||||
this._cleanups.push(() => {
|
||||
el.removeEventListener('animationstart', animStart);
|
||||
el.removeEventListener('animationend', animEnd);
|
||||
});
|
||||
|
||||
if (this.config.draggable) {
|
||||
this._bindDrag(el);
|
||||
}
|
||||
}
|
||||
|
||||
_bindDrag(el: HTMLElement): void {
|
||||
let sx = 0, sy = 0, dx = 0, dy = 0, dragging = false;
|
||||
|
||||
const down = (e: PointerEvent) => {
|
||||
if ((e.target as HTMLElement).closest('.met-close')) return;
|
||||
dragging = true;
|
||||
sx = e.clientX;
|
||||
sy = e.clientY;
|
||||
el.setPointerCapture(e.pointerId);
|
||||
el.style.transition = 'none';
|
||||
this._pause();
|
||||
Toast.trigger('dragStart', this);
|
||||
};
|
||||
|
||||
const move = (e: PointerEvent) => {
|
||||
if (!dragging) return;
|
||||
dx = e.clientX - sx;
|
||||
dy = e.clientY - sy;
|
||||
el.style.transform = `translate(${dx}px, ${dy}px) rotate(${dx * 0.1}deg)`;
|
||||
el.style.opacity = String(Math.max(0, 1 - Math.abs(dx) / 200));
|
||||
};
|
||||
|
||||
const up = (e: PointerEvent) => {
|
||||
if (!dragging) return;
|
||||
dragging = false;
|
||||
el.releasePointerCapture(e.pointerId);
|
||||
el.style.transition = '';
|
||||
|
||||
const threshold = this.config.dragThreshold ?? 120;
|
||||
if (Math.abs(dx) > threshold) {
|
||||
el.style.transform = `translate(${dx * 2}px, ${dy}px) rotate(${dx * 0.2}deg)`;
|
||||
el.style.opacity = '0';
|
||||
setTimeout(() => this.close(true), 250);
|
||||
} else {
|
||||
el.style.transform = '';
|
||||
el.style.opacity = '';
|
||||
this._resume();
|
||||
}
|
||||
dx = dy = 0;
|
||||
Toast.trigger('dragEnd', this);
|
||||
};
|
||||
|
||||
el.addEventListener('pointerdown', down);
|
||||
el.addEventListener('pointermove', move);
|
||||
el.addEventListener('pointerup', up);
|
||||
el.addEventListener('pointercancel', up);
|
||||
|
||||
this._cleanups.push(() => {
|
||||
el.removeEventListener('pointerdown', down);
|
||||
el.removeEventListener('pointermove', move);
|
||||
el.removeEventListener('pointerup', up);
|
||||
el.removeEventListener('pointercancel', up);
|
||||
});
|
||||
}
|
||||
|
||||
_startTimer(resuming = false): void {
|
||||
if ((this.config.duration || 0) <= 0) return;
|
||||
|
||||
if (!resuming) {
|
||||
this.startedAt = Date.now();
|
||||
this.remaining = this.config.duration || 0;
|
||||
Toast.trigger('progressStart', this);
|
||||
}
|
||||
|
||||
const tick = (): void => {
|
||||
if (this.paused || this.closing) return;
|
||||
|
||||
try {
|
||||
const elapsed = Date.now() - this.startedAt;
|
||||
this.remaining = Math.max(0, (this.config.duration || 0) - elapsed);
|
||||
|
||||
if (this.barEl) {
|
||||
const ratio = this.remaining / (this.config.duration || 1);
|
||||
const t = this.config.progressDirection === 'vertical'
|
||||
? `scaleY(${ratio})` : `scaleX(${ratio})`;
|
||||
this.barEl.style.transform = t;
|
||||
}
|
||||
|
||||
if (this.remaining <= 0) {
|
||||
Toast.trigger('progressEnd', this);
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Timer tick error:', e);
|
||||
if (Toast._onError) {
|
||||
try { Toast._onError({ source: 'timer', error: e as Error, toast: this }); } catch (_e) { /* noop */ }
|
||||
}
|
||||
}
|
||||
|
||||
this.rafId = requestAnimationFrame(tick);
|
||||
};
|
||||
|
||||
this.rafId = requestAnimationFrame(tick);
|
||||
}
|
||||
|
||||
_pause(): void {
|
||||
if (this.paused || (this.config.duration || 0) <= 0) return;
|
||||
this.paused = true;
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this.remaining = Math.max(0, (this.config.duration || 0) - (Date.now() - this.startedAt));
|
||||
}
|
||||
|
||||
_resume(): void {
|
||||
if (!this.paused) return;
|
||||
this.paused = false;
|
||||
this.startedAt = Date.now() - ((this.config.duration || 0) - this.remaining);
|
||||
this._startTimer(true);
|
||||
}
|
||||
|
||||
update(partial: Partial<ToastOptions>): this {
|
||||
// beforeUpdate 钩子 — 返回 false 可阻止更新
|
||||
if (!Toast.trigger('beforeUpdate', this)) return this;
|
||||
|
||||
const typeChanged = partial.type && partial.type !== this.type;
|
||||
if (partial.type) this.type = partial.type;
|
||||
if (partial.title !== undefined) this.title = partial.title;
|
||||
if (partial.message !== undefined) this.message = partial.message;
|
||||
if (partial.html !== undefined) this.html = partial.html;
|
||||
|
||||
if (!this.el) { Toast.trigger('afterUpdate', this); return this; }
|
||||
|
||||
// 类型变更时同步更新 DOM 类名、边框颜色和进度条颜色
|
||||
if (typeChanged) {
|
||||
const typeClasses = ['met-success', 'met-error', 'met-warning', 'met-info', 'met-loading', 'met-default'];
|
||||
typeClasses.forEach(c => this.el!.classList.remove(c));
|
||||
this.el.classList.add(`met-${this.type}`);
|
||||
if (this.barEl) {
|
||||
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
|
||||
this.barEl.style.background = c.fg;
|
||||
}
|
||||
const side = this.el.querySelector('.met-side') as HTMLElement | null;
|
||||
if (side) {
|
||||
const c = (TYPE_COLORS[this.type] as TypeColor) || (TYPE_COLORS.default as TypeColor);
|
||||
side.style.background = c.fg;
|
||||
}
|
||||
}
|
||||
|
||||
const content = this.el.querySelector('.met-content');
|
||||
if (content) {
|
||||
const safeTitle = this.title ? `<div class="met-title">${escapeHTML(this.title)}</div>` : '';
|
||||
const safeMessage = this.html
|
||||
? `<div class="met-message">${this.html}</div>`
|
||||
: (this.message ? `<div class="met-message">${escapeHTML(this.message)}</div>` : '');
|
||||
content.innerHTML = `${safeTitle}${safeMessage}`;
|
||||
}
|
||||
|
||||
// duration 变更:重启或停止计时器(loading 0 → N 转换、动态调整时长等场景)
|
||||
if (partial.duration !== undefined && partial.duration !== this.config.duration) {
|
||||
this.config.duration = partial.duration;
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
this.remaining = Math.max(0, partial.duration);
|
||||
if (partial.duration > 0 && !this.paused) {
|
||||
this.startedAt = Date.now();
|
||||
this._startTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// resetTimerOnUpdate: 更新内容后重置计时器
|
||||
if (this.config.resetTimerOnUpdate && (this.config.duration || 0) > 0) {
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this.startedAt = Date.now();
|
||||
this.remaining = this.config.duration || 0;
|
||||
this._startTimer();
|
||||
}
|
||||
|
||||
if (typeof this.config.onUpdate === 'function') {
|
||||
try { this.config.onUpdate(this); } catch (e) { console.error('onUpdate callback error:', e); }
|
||||
}
|
||||
|
||||
Toast.trigger('afterUpdate', this);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行时移动 Toast 到新位置
|
||||
*/
|
||||
updatePosition(position: string): this {
|
||||
if (!position || !this.el) return this;
|
||||
|
||||
const oldContainer = this.el.parentNode;
|
||||
this.config.position = position as ToastConfig['position'];
|
||||
|
||||
const newContainer = this._getContainer();
|
||||
if (oldContainer === newContainer) return this;
|
||||
|
||||
// 从旧容器移除,加入新容器
|
||||
if (oldContainer && this.el.parentNode === oldContainer) {
|
||||
oldContainer.removeChild(this.el);
|
||||
}
|
||||
const pos = position;
|
||||
if (pos.startsWith('top')) {
|
||||
newContainer.insertBefore(this.el, newContainer.firstChild);
|
||||
} else {
|
||||
newContainer.appendChild(this.el);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 立即从 DOM 和内存中移除(不触发离场动画)
|
||||
*/
|
||||
remove(): void {
|
||||
if (this.rafId !== null) {
|
||||
cancelAnimationFrame(this.rafId);
|
||||
this.rafId = null;
|
||||
}
|
||||
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
||||
this._cleanups = [];
|
||||
|
||||
if (this.el && this.el.parentNode) {
|
||||
this.el.parentNode.removeChild(this.el);
|
||||
}
|
||||
this.el = null;
|
||||
|
||||
Toast._registry.delete(this.id);
|
||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||
}
|
||||
|
||||
close(immediate = false): void {
|
||||
if (this.closing) return;
|
||||
|
||||
// beforeClose 钩子 — 返回 false 可阻止关闭
|
||||
if (!Toast.trigger('beforeClose', this)) return;
|
||||
|
||||
this.closing = true;
|
||||
if (this.rafId !== null) cancelAnimationFrame(this.rafId);
|
||||
this._cleanups.forEach(fn => { try { fn(); } catch (_e) { /* noop */ } });
|
||||
this._cleanups = [];
|
||||
|
||||
const el = this.el;
|
||||
if (!el) {
|
||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||
return;
|
||||
}
|
||||
|
||||
// 立即脱离文档流
|
||||
const container = el.parentNode;
|
||||
if (container && !immediate) {
|
||||
const elRect = el.getBoundingClientRect();
|
||||
const containerRect = (container as HTMLElement).getBoundingClientRect();
|
||||
el.style.position = 'absolute';
|
||||
el.style.top = (elRect.top - containerRect.top) + 'px';
|
||||
el.style.left = (elRect.left - containerRect.left) + 'px';
|
||||
el.style.width = elRect.width + 'px';
|
||||
el.style.margin = '0';
|
||||
}
|
||||
|
||||
el.classList.add('met-leaving');
|
||||
el.classList.remove('met-show');
|
||||
|
||||
const pos = this.config.position || 'top-right';
|
||||
const isRTL = getLocaleDirection(getCurrentLocale()) === 'rtl';
|
||||
let transform = 'scale(.96)';
|
||||
|
||||
if (pos.includes('right')) transform = isRTL ? 'translateX(-120%)' : 'translateX(120%)';
|
||||
else if (pos.includes('left')) transform = isRTL ? 'translateX(120%)' : 'translateX(-120%)';
|
||||
else if (pos.startsWith('top')) transform = 'translateY(-20px)';
|
||||
else transform = 'translateY(20px)';
|
||||
|
||||
el.style.transform = transform;
|
||||
el.style.opacity = '0';
|
||||
|
||||
setTimeout(() => this._destroy(), immediate ? 0 : 300);
|
||||
}
|
||||
|
||||
_destroy(): void {
|
||||
if (this.el && this.el.parentNode) {
|
||||
this.el.parentNode.removeChild(this.el);
|
||||
}
|
||||
this.el = null;
|
||||
|
||||
if (typeof this.config.onClose === 'function') {
|
||||
try {
|
||||
this.config.onClose(this);
|
||||
} catch (e) {
|
||||
console.error('onClose callback error:', e);
|
||||
}
|
||||
}
|
||||
|
||||
Toast.trigger('afterClose', this);
|
||||
Toast._registry.delete(this.id);
|
||||
if (Toast._removeToast) Toast._removeToast(this.id);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 模块级共享容器缓存,所有 Toast 实例共用
|
||||
*/
|
||||
export const _containerCache: Map<Node, Map<string, HTMLElement>> = new Map();
|
||||
+555
@@ -0,0 +1,555 @@
|
||||
/**
|
||||
* MetonaToast — 核心类型定义
|
||||
* @module types
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
// ========== 基础类型 ==========
|
||||
|
||||
/** Toast 通知类型(107种内置图标类型) */
|
||||
export type ToastType =
|
||||
| 'default' | 'success' | 'error' | 'warning' | 'info' | 'loading'
|
||||
| 'check' | 'x' | 'alert' | 'question' | 'star' | 'heart' | 'bell' | 'mail'
|
||||
| 'settings' | 'user' | 'home' | 'search' | 'plus' | 'minus' | 'edit'
|
||||
| 'trash' | 'download' | 'upload' | 'share' | 'link' | 'external'
|
||||
| 'clock' | 'calendar' | 'map' | 'compass' | 'globe' | 'wifi' | 'cloud'
|
||||
| 'sun' | 'moon' | 'zap' | 'activity' | 'cpu' | 'database' | 'server'
|
||||
| 'terminal' | 'code' | 'git' | 'package' | 'layers' | 'grid' | 'list'
|
||||
| 'filter' | 'sort' | 'refresh' | 'sync' | 'power' | 'battery' | 'bluetooth'
|
||||
| 'volume' | 'mic' | 'camera' | 'image' | 'video' | 'music' | 'file'
|
||||
| 'folder' | 'clipboard' | 'save' | 'print' | 'eye' | 'eyeOff' | 'lock'
|
||||
| 'unlock' | 'shield' | 'key' | 'flag' | 'bookmark' | 'tag' | 'gift'
|
||||
| 'award' | 'target' | 'crosshair' | 'move' | 'maximize' | 'minimize'
|
||||
| 'copy' | 'cut' | 'paste' | 'rotateCw' | 'rotateCcw' | 'zoomIn' | 'zoomOut'
|
||||
| 'crop' | 'sliders' | 'toggleLeft' | 'toggleRight'
|
||||
| 'checkCircle' | 'xCircle' | 'alertCircle' | 'infoCircle' | 'helpCircle'
|
||||
| 'alertTriangle' | 'checkSquare' | 'square' | 'circle' | 'triangle'
|
||||
| 'hexagon' | 'octagon' | 'pentagon' | 'diamond';
|
||||
|
||||
/** Toast 显示位置 */
|
||||
export type ToastPosition =
|
||||
| 'top-left' | 'top-center' | 'top-right'
|
||||
| 'bottom-left' | 'bottom-center' | 'bottom-right';
|
||||
|
||||
/** Toast 主题 */
|
||||
export type ToastTheme = 'light' | 'dark' | 'auto' | 'warm' | (string & {});
|
||||
|
||||
/** Toast 动画类型 */
|
||||
export type ToastAnimation =
|
||||
| 'slide' | 'fade' | 'scale' | 'bounce' | 'flip' | 'rotate' | 'zoom'
|
||||
| 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight'
|
||||
| (string & {});
|
||||
|
||||
/** 进度条方向 */
|
||||
export type ToastProgressDirection = 'horizontal' | 'vertical';
|
||||
|
||||
/** 语言代码 */
|
||||
export type ToastLocale =
|
||||
| 'zh-CN' | 'zh-TW' | 'en-US' | 'en-GB' | 'ja' | 'ko' | 'fr' | 'de'
|
||||
| 'es' | 'pt' | 'ru' | 'ar' | 'hi' | 'th' | 'vi' | 'id' | 'ms' | 'tr'
|
||||
| 'it' | 'nl' | 'pl' | 'uk' | 'cs' | 'sv' | 'da' | 'fi' | 'nb' | 'el'
|
||||
| 'he' | 'hu' | 'ro' | 'bg' | 'hr' | 'sk' | 'sl' | 'et' | 'lv' | 'lt'
|
||||
| 'ca' | 'gl' | 'eu' | 'cy' | 'ga' | 'mt' | 'is' | 'mk' | 'sq' | 'sr'
|
||||
| 'bs' | 'me' | 'ka' | 'hy' | 'az' | 'uz' | 'kk' | 'ky' | 'tg' | 'tk'
|
||||
| 'mn' | 'ne' | 'si' | 'my' | 'km' | 'lo' | 'am' | 'sw' | 'yo' | 'ig'
|
||||
| 'ha' | 'zu' | 'af' | 'xh' | 'st' | 'tn' | 'ts' | 'ss' | 've' | 'nr';
|
||||
|
||||
// ========== 颜色配置 ==========
|
||||
|
||||
export interface TypeColor {
|
||||
fg: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
// ========== Toast 配置 ==========
|
||||
|
||||
export interface ToastConfig {
|
||||
position?: ToastPosition;
|
||||
duration?: number;
|
||||
max?: number;
|
||||
gap?: number;
|
||||
offset?: number;
|
||||
pauseOnHover?: boolean;
|
||||
closeOnClick?: boolean;
|
||||
draggable?: boolean;
|
||||
dragThreshold?: number;
|
||||
showProgress?: boolean;
|
||||
progressDirection?: ToastProgressDirection;
|
||||
icon?: boolean;
|
||||
closeButton?: boolean;
|
||||
theme?: ToastTheme;
|
||||
animation?: ToastAnimation;
|
||||
zIndex?: number;
|
||||
width?: number | string;
|
||||
className?: string;
|
||||
style?: Record<string, string>;
|
||||
onShow?: ((toast: ToastInstance) => void) | null;
|
||||
onBeforeShow?: ((toast: ToastInstance) => boolean | void) | null;
|
||||
onClose?: ((toast: ToastInstance) => void) | null;
|
||||
onClick?: ((toast: ToastInstance) => void) | null;
|
||||
onUpdate?: ((toast: ToastInstance) => void) | null;
|
||||
render?: ((toast: ToastInstance) => string) | null;
|
||||
resetTimerOnUpdate?: boolean;
|
||||
notifyWhenHidden?: boolean;
|
||||
onError?: ((errorInfo: ErrorInfo) => void) | null;
|
||||
group?: string | null;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
|
||||
export interface ErrorInfo {
|
||||
hook?: string;
|
||||
source?: string;
|
||||
error: Error;
|
||||
toast?: ToastInstance;
|
||||
}
|
||||
|
||||
// ========== Toast 选项与实例 ==========
|
||||
|
||||
export interface ToastOptions extends Partial<ToastConfig> {
|
||||
type?: string;
|
||||
title?: string;
|
||||
message?: string;
|
||||
content?: string;
|
||||
html?: string;
|
||||
iconHTML?: string;
|
||||
id?: string;
|
||||
}
|
||||
|
||||
export interface ToastInstance {
|
||||
id: string;
|
||||
type: string;
|
||||
title: string;
|
||||
message: string;
|
||||
html: string;
|
||||
iconHTML: string;
|
||||
config: ToastConfig;
|
||||
el: HTMLElement | null;
|
||||
barEl: HTMLElement | null;
|
||||
rafId: number | null;
|
||||
remaining: number;
|
||||
startedAt: number;
|
||||
paused: boolean;
|
||||
closing: boolean;
|
||||
group: string | null;
|
||||
_cleanups: Array<() => void>;
|
||||
|
||||
_palette(): { theme: string; c: TypeColor; t: Partial<ThemeConfig> };
|
||||
create(): this;
|
||||
update(partial: Partial<ToastOptions>): this;
|
||||
updatePosition(position: string): this;
|
||||
remove(): void;
|
||||
close(immediate?: boolean): void;
|
||||
_pause(): void;
|
||||
_resume(): void;
|
||||
_destroy(): void;
|
||||
}
|
||||
|
||||
// ========== 控制对象 ==========
|
||||
|
||||
export interface LoadingControl {
|
||||
id: string;
|
||||
success(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
error(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
info(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
warning(message?: string, opts?: ToastOptions): ToastInstance | null;
|
||||
update(partial: Partial<ToastOptions>): unknown;
|
||||
dismiss(): void;
|
||||
}
|
||||
|
||||
export interface ProgressControl {
|
||||
id: string;
|
||||
setProgress(percent: number): void;
|
||||
complete(message?: string): void;
|
||||
error(message?: string): void;
|
||||
dismiss(): void;
|
||||
}
|
||||
|
||||
export interface CountdownControl {
|
||||
id: string;
|
||||
cancel(): void;
|
||||
pause(): void;
|
||||
resume(): void;
|
||||
}
|
||||
|
||||
export interface ActionControl {
|
||||
id: string;
|
||||
toast: ToastInstance;
|
||||
dismiss(): void;
|
||||
}
|
||||
|
||||
export interface QueueControl {
|
||||
then<TResult1 = void, TResult2 = never>(
|
||||
onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null
|
||||
): Promise<TResult1 | TResult2>;
|
||||
catch<TResult = never>(
|
||||
onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null
|
||||
): Promise<void | TResult>;
|
||||
cancel(): void;
|
||||
}
|
||||
|
||||
export interface GroupAPI {
|
||||
_group: string;
|
||||
show: (...args: unknown[]) => ToastInstance;
|
||||
success: (...args: unknown[]) => ToastInstance;
|
||||
error: (...args: unknown[]) => ToastInstance;
|
||||
warning: (...args: unknown[]) => ToastInstance;
|
||||
info: (...args: unknown[]) => ToastInstance;
|
||||
loading: (...args: unknown[]) => LoadingControl;
|
||||
action: (...args: unknown[]) => ActionControl;
|
||||
dismiss(): void;
|
||||
count(): number;
|
||||
}
|
||||
|
||||
// ========== Action 按钮 ==========
|
||||
|
||||
export interface ActionButton {
|
||||
text: string;
|
||||
onClick: (toast: ToastInstance) => void;
|
||||
color?: string;
|
||||
style?: Record<string, string>;
|
||||
close?: boolean;
|
||||
}
|
||||
|
||||
// ========== Options 接口 ==========
|
||||
|
||||
export interface PromiseOptions extends ToastOptions {
|
||||
loading?: string;
|
||||
success?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface ConfirmOptions extends ToastOptions {
|
||||
confirmText?: string;
|
||||
confirmColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
|
||||
export interface PromptOptions extends ToastOptions {
|
||||
inputType?: string;
|
||||
placeholder?: string;
|
||||
defaultValue?: string;
|
||||
submitText?: string;
|
||||
submitColor?: string;
|
||||
cancelText?: string;
|
||||
cancelColor?: string;
|
||||
}
|
||||
|
||||
export interface ProgressOptions extends ToastOptions {
|
||||
progressColor?: string;
|
||||
}
|
||||
|
||||
export interface CountdownOptions extends ToastOptions {
|
||||
onComplete?: (() => void) | null;
|
||||
}
|
||||
|
||||
export interface QueueOptions extends ToastOptions {
|
||||
delay?: number;
|
||||
}
|
||||
|
||||
export interface StackOptions extends ToastOptions {
|
||||
stagger?: number;
|
||||
}
|
||||
|
||||
export interface InitOptions {
|
||||
config?: Partial<ToastConfig>;
|
||||
theme?: ToastTheme;
|
||||
locale?: string;
|
||||
plugins?: Array<string | Plugin>;
|
||||
}
|
||||
|
||||
export interface StatusInfo {
|
||||
version: string;
|
||||
toasts: number;
|
||||
theme: string;
|
||||
locale: string;
|
||||
plugins: string[];
|
||||
animations: number;
|
||||
}
|
||||
|
||||
// ========== 动画 ==========
|
||||
|
||||
export interface AnimationConfig {
|
||||
enter: Record<string, string | number>;
|
||||
leave: Record<string, string | number>;
|
||||
duration: number;
|
||||
easing: string;
|
||||
name?: string;
|
||||
delay?: number;
|
||||
iterations?: number;
|
||||
direction?: 'normal' | 'reverse' | 'alternate' | 'alternate-reverse';
|
||||
fillMode?: 'none' | 'forwards' | 'backwards' | 'both';
|
||||
}
|
||||
|
||||
export interface AnimationUtils {
|
||||
register(name: string, config: Partial<AnimationConfig>): void;
|
||||
unregister(name: string): void;
|
||||
get(name: string): AnimationConfig | null;
|
||||
getAnimationNames(): string[];
|
||||
getActiveCount(): number;
|
||||
cancelAll(): void;
|
||||
reset(): void;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
// ========== 主题 ==========
|
||||
|
||||
export interface ThemeConfig {
|
||||
bg: string;
|
||||
text: string;
|
||||
border: string;
|
||||
shadow: string;
|
||||
hoverShadow: string;
|
||||
progressBg: string;
|
||||
closeHoverBg: string;
|
||||
backdropFilter?: string;
|
||||
}
|
||||
|
||||
export interface ThemePreview {
|
||||
name: string;
|
||||
resolved: string;
|
||||
colors: {
|
||||
background: string;
|
||||
text: string;
|
||||
border: string;
|
||||
};
|
||||
isDark: boolean;
|
||||
isLight: boolean;
|
||||
isAuto: boolean;
|
||||
}
|
||||
|
||||
export interface ThemeUtils {
|
||||
getSystemTheme(): 'light' | 'dark';
|
||||
resolveTheme(theme: string): string;
|
||||
getThemeConfig(theme: string): ThemeConfig;
|
||||
applyTheme(theme: string): void;
|
||||
getCurrentTheme(): string;
|
||||
getResolvedTheme(): string;
|
||||
switchTheme(theme: string): void;
|
||||
toggleTheme(): void;
|
||||
resetToAuto(): void;
|
||||
initTheme(): void;
|
||||
watchSystemTheme(): void;
|
||||
unwatchSystemTheme(): void;
|
||||
addThemeListener(listener: (theme: string, resolved: string) => void): () => void;
|
||||
removeThemeListener(listener: (theme: string, resolved: string) => void): void;
|
||||
clearThemeListeners(): void;
|
||||
registerTheme(name: string, config: ThemeConfig): void;
|
||||
unregisterTheme(name: string): void;
|
||||
getAllThemes(): Record<string, ThemeConfig>;
|
||||
getThemeNames(): string[];
|
||||
hasTheme(name: string): boolean;
|
||||
getThemePreview(theme: string): ThemePreview;
|
||||
generateThemeCSS(theme: string): string;
|
||||
applyThemeCSS(theme: string): void;
|
||||
removeThemeCSS(): void;
|
||||
saveTheme(theme: string): void;
|
||||
loadTheme(): string;
|
||||
}
|
||||
|
||||
// ========== 国际化 ==========
|
||||
|
||||
export interface LocaleInfo {
|
||||
code: string;
|
||||
name: string;
|
||||
direction: 'ltr' | 'rtl';
|
||||
isSupported: boolean;
|
||||
isCurrent: boolean;
|
||||
isFallback: boolean;
|
||||
}
|
||||
|
||||
export interface I18nUtils {
|
||||
t(key: string, params?: Record<string, string | number>): string;
|
||||
plural(key: string, count: number, params?: Record<string, string | number>): string;
|
||||
getCurrentLocale(): string;
|
||||
setCurrentLocale(locale: string): void;
|
||||
switchLocale(locale: string): void;
|
||||
getFallbackLocale(): string;
|
||||
setFallbackLocale(locale: string): void;
|
||||
hasTranslation(key: string): boolean;
|
||||
getTranslations(locale: string): Record<string, unknown>;
|
||||
addTranslations(locale: string, translations: Record<string, unknown>): void;
|
||||
removeTranslation(locale: string, key: string): void;
|
||||
clearTranslations(locale: string): void;
|
||||
getSupportedLocales(): string[];
|
||||
isLocaleSupported(locale: string): boolean;
|
||||
getLocaleName(locale: string): string;
|
||||
getLocaleDirection(locale: string): 'ltr' | 'rtl';
|
||||
getLocaleInfo(locale: string): LocaleInfo;
|
||||
getAllLocaleInfo(): LocaleInfo[];
|
||||
formatNumber(number: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatCurrency(amount: number, currency?: string, options?: Intl.NumberFormatOptions): string;
|
||||
formatPercent(value: number, options?: Intl.NumberFormatOptions): string;
|
||||
formatDate(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatTime(date: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
|
||||
formatRelativeTime(date: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string;
|
||||
formatList(list: string[], options?: Record<string, unknown>): string;
|
||||
formatPlural(count: number, options?: Intl.PluralRulesOptions): string;
|
||||
addLocaleListener(listener: (locale: string) => void): () => void;
|
||||
removeLocaleListener(listener: (locale: string) => void): void;
|
||||
clearLocaleListeners(): void;
|
||||
initI18n(): void;
|
||||
saveLocale(locale: string): void;
|
||||
loadLocale(): string;
|
||||
getDefaultLocale(): string;
|
||||
}
|
||||
|
||||
// ========== 插件 ==========
|
||||
|
||||
export interface Plugin {
|
||||
name: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
hooks?: Record<string, (...args: unknown[]) => void>;
|
||||
middleware?: (...args: unknown[]) => unknown;
|
||||
install?: (manager: PluginManager) => void | Record<string, unknown> | null;
|
||||
uninstall?: (manager: PluginManager) => void;
|
||||
init?: (manager: PluginManager) => void;
|
||||
destroy?: (manager: PluginManager) => void;
|
||||
installed?: boolean;
|
||||
enabled?: boolean;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface PluginManager {
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
destroy(): void;
|
||||
}
|
||||
|
||||
export interface PluginUtils {
|
||||
createManager(): PluginManager;
|
||||
register(name: string, plugin: Plugin): PluginManager;
|
||||
unregister(name: string): PluginManager;
|
||||
get(name: string): Plugin | null;
|
||||
has(name: string): boolean;
|
||||
getAll(): Plugin[];
|
||||
getNames(): string[];
|
||||
enable(name: string): PluginManager;
|
||||
disable(name: string): PluginManager;
|
||||
isEnabled(name: string): boolean;
|
||||
getPreset(name: string): Plugin | null;
|
||||
getAllPresets(): Record<string, Plugin>;
|
||||
createPlugin(config: Partial<Plugin>): Plugin;
|
||||
validatePlugin(plugin: Partial<Plugin>): { valid: boolean; errors: string[] };
|
||||
}
|
||||
|
||||
// ========== MeToast 主接口 ==========
|
||||
|
||||
export interface MeToast {
|
||||
version: string;
|
||||
_toasts: Map<string, ToastInstance>;
|
||||
_config: ToastConfig;
|
||||
_destroyed?: boolean;
|
||||
|
||||
// 内部方法
|
||||
_emit(opts: ToastOptions): ToastInstance;
|
||||
_remove(id: string): void;
|
||||
_resolve(loadingToast: ToastInstance, type: string, message: string, opts: ToastOptions): ToastInstance | null;
|
||||
_groupCount(name: string): number;
|
||||
|
||||
// 配置
|
||||
configure(opts: Partial<ToastConfig>): MeToast;
|
||||
init(options?: InitOptions): MeToast;
|
||||
destroy(): void;
|
||||
getStatus(): StatusInfo;
|
||||
getConfig(): ToastConfig;
|
||||
updateConfig(config: Partial<ToastConfig>): MeToast;
|
||||
resetConfig(): MeToast;
|
||||
|
||||
// Toast 方法
|
||||
show(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
success(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
error(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
warning(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
info(messageOrOpts: string | ToastOptions, opts?: ToastOptions): ToastInstance;
|
||||
loading(messageOrOpts: string | ToastOptions, opts?: ToastOptions): LoadingControl;
|
||||
promise<T>(promise: Promise<T>, opts?: PromiseOptions): Promise<T>;
|
||||
confirm(message: string, opts?: ConfirmOptions): Promise<boolean>;
|
||||
prompt(message: string, opts?: PromptOptions): Promise<string | null>;
|
||||
progress(messageOrOpts: string | ToastOptions, opts?: ProgressOptions): ProgressControl;
|
||||
countdown(message: string, seconds?: number, opts?: CountdownOptions): CountdownControl;
|
||||
action(messageOrOpts: string | ToastOptions, actions?: ActionButton[], opts?: ToastOptions): ActionControl;
|
||||
queue(messages: Array<string | ToastOptions>, opts?: QueueOptions): QueueControl;
|
||||
stack(messages: Array<string | ToastOptions>, opts?: StackOptions): void;
|
||||
|
||||
// 分组
|
||||
group(name: string): GroupAPI;
|
||||
dismissGroup(name: string): void;
|
||||
|
||||
// Toast 管理
|
||||
find(id: string): ToastInstance | undefined;
|
||||
dismiss(id?: string): void;
|
||||
removeToast(id: string): void;
|
||||
clear(position?: ToastPosition): void;
|
||||
getToasts(): ToastInstance[];
|
||||
count(): number;
|
||||
hasToasts(): boolean;
|
||||
getToast(id: string): ToastInstance | null;
|
||||
closeAll(): void;
|
||||
clearAll(): void;
|
||||
pauseAll(): void;
|
||||
resumeAll(): void;
|
||||
updateAll(partial: Partial<ToastOptions>): void;
|
||||
findToasts(predicate: (toast: ToastInstance) => boolean): ToastInstance[];
|
||||
findByType(type: ToastType): ToastInstance[];
|
||||
findByPosition(position: ToastPosition): ToastInstance[];
|
||||
getAll(): Map<string, ToastInstance>;
|
||||
|
||||
// 插件
|
||||
use(plugin: string | Plugin, options?: Record<string, unknown>): MeToast;
|
||||
|
||||
// 子模块
|
||||
animations: AnimationUtils;
|
||||
themes: ThemeUtils;
|
||||
i18n: I18nUtils;
|
||||
plugins: PluginUtils;
|
||||
presetPlugins: Record<string, Plugin>;
|
||||
}
|
||||
|
||||
// ========== 钩子名称 ==========
|
||||
|
||||
export const HOOK_NAMES = {
|
||||
BEFORE_INIT: 'beforeInit',
|
||||
AFTER_INIT: 'afterInit',
|
||||
BEFORE_DESTROY: 'beforeDestroy',
|
||||
AFTER_DESTROY: 'afterDestroy',
|
||||
BEFORE_SHOW: 'beforeShow',
|
||||
AFTER_SHOW: 'afterShow',
|
||||
BEFORE_CLOSE: 'beforeClose',
|
||||
AFTER_CLOSE: 'afterClose',
|
||||
BEFORE_UPDATE: 'beforeUpdate',
|
||||
AFTER_UPDATE: 'afterUpdate',
|
||||
CONFIG_CHANGE: 'configChange',
|
||||
THEME_CHANGE: 'themeChange',
|
||||
LOCALE_CHANGE: 'localeChange',
|
||||
CLICK: 'click',
|
||||
HOVER: 'hover',
|
||||
DRAG_START: 'dragStart',
|
||||
DRAG_END: 'dragEnd',
|
||||
ANIMATION_START: 'animationStart',
|
||||
ANIMATION_END: 'animationEnd',
|
||||
PROGRESS_START: 'progressStart',
|
||||
PROGRESS_END: 'progressEnd',
|
||||
ERROR: 'error',
|
||||
} as const;
|
||||
|
||||
export type HookName = (typeof HOOK_NAMES)[keyof typeof HOOK_NAMES];
|
||||
|
||||
// ========== 全局声明 ==========
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
MeToast: MeToast;
|
||||
Met: MeToast;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,21 @@
|
||||
/**
|
||||
* MetonaToast Utils - 精简工具函数
|
||||
* MetonaToast Utils — 工具函数
|
||||
* @module utils
|
||||
* @version 2.0.1
|
||||
* @description 仅保留核心模块实际使用的工具函数
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/**
|
||||
* 生成唯一ID
|
||||
* @returns {string} 唯一ID
|
||||
*/
|
||||
export const generateId = () => {
|
||||
export const generateId = (): string => {
|
||||
return 'met-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);
|
||||
};
|
||||
|
||||
/**
|
||||
* HTML转义
|
||||
* @param {string} s - 输入字符串
|
||||
* @returns {string} 转义后的字符串
|
||||
* @param s - 输入字符串
|
||||
*/
|
||||
export const escapeHTML = (s) => {
|
||||
export const escapeHTML = (s: string | null | undefined): string => {
|
||||
if (typeof document === 'undefined') {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&')
|
||||
@@ -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,369 @@
|
||||
/**
|
||||
* MetonaToast DOM 交互测试 — 基于 jsdom 真实 DOM 事件驱动
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import MeToast, { Toast } from '../src/index';
|
||||
import type { ToastInstance } from '../src/types';
|
||||
|
||||
// jsdom polyfill: PointerEvent 相关方法
|
||||
if (typeof Element !== 'undefined') {
|
||||
if (!(Element.prototype as unknown as { setPointerCapture?: unknown }).setPointerCapture) {
|
||||
(Element.prototype as unknown as Record<string, unknown>).setPointerCapture = jest.fn();
|
||||
}
|
||||
if (!(Element.prototype as unknown as { releasePointerCapture?: unknown }).releasePointerCapture) {
|
||||
(Element.prototype as unknown as Record<string, unknown>).releasePointerCapture = jest.fn();
|
||||
}
|
||||
}
|
||||
|
||||
const pointer = (type: string, x: number, y: number): PointerEvent =>
|
||||
new MouseEvent(type, { clientX: x, clientY: y, bubbles: true }) as unknown as PointerEvent;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.destroy();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('toast.ts — DOM 交互', () => {
|
||||
test('onShow 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.success('x', { onShow: () => { throw new Error('show-err'); } });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('onUpdate 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const t = MeToast.success('x', { onUpdate: () => { throw new Error('upd-err'); } });
|
||||
t.update({ message: 'y' });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('_applyStyles 字符串 width 生效', () => {
|
||||
const t = MeToast.info({ message: 'w', width: '80%' });
|
||||
expect(t.el!.style.width).toBe('80%');
|
||||
});
|
||||
|
||||
test('点击 close 按钮关闭 toast 且不触发 click 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('click', fn);
|
||||
const t = MeToast.info('test');
|
||||
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||
closeBtn.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(t.closing).toBe(true);
|
||||
expect(fn).not.toHaveBeenCalled();
|
||||
Toast.off('click', fn);
|
||||
});
|
||||
|
||||
test('点击主体触发 onClick 并关闭', () => {
|
||||
const onClick = jest.fn();
|
||||
const t = MeToast.info('test', { onClick });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(onClick).toHaveBeenCalledWith(t);
|
||||
expect(t.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('onClick 回调异常被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const t = MeToast.info('test', { onClick: () => { throw new Error('click-err'); } });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('closeOnClick false 时点击不关闭但触发 click 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('click', fn);
|
||||
const t = MeToast.info('test', { closeOnClick: false });
|
||||
t.el!.dispatchEvent(new MouseEvent('click', { bubbles: true }));
|
||||
expect(t.closing).toBe(false);
|
||||
expect(fn).toHaveBeenCalled();
|
||||
Toast.off('click', fn);
|
||||
});
|
||||
|
||||
test('hover 触发暂停/恢复与 hover 钩子', () => {
|
||||
const fn = jest.fn();
|
||||
Toast.on('hover', fn);
|
||||
const t = MeToast.info({ message: 'h', duration: 5000 });
|
||||
t.el!.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }));
|
||||
expect(t.paused).toBe(true);
|
||||
t.el!.dispatchEvent(new MouseEvent('mouseleave', { bubbles: true }));
|
||||
expect(t.paused).toBe(false);
|
||||
expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
|
||||
Toast.off('hover', fn);
|
||||
});
|
||||
|
||||
test('animationstart/animationend 触发钩子', () => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('animationStart', start);
|
||||
Toast.on('animationEnd', end);
|
||||
const t = MeToast.info('a');
|
||||
t.el!.dispatchEvent(new Event('animationstart'));
|
||||
t.el!.dispatchEvent(new Event('animationend'));
|
||||
expect(start).toHaveBeenCalled();
|
||||
expect(end).toHaveBeenCalled();
|
||||
Toast.off('animationStart', start);
|
||||
Toast.off('animationEnd', end);
|
||||
});
|
||||
|
||||
test('拖拽 down→move→up 超过阈值触发关闭', (done) => {
|
||||
const t = MeToast.info('drag', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||
el.dispatchEvent(pointer('pointermove', 250, 60));
|
||||
expect(el.style.transform).toContain('translate(150px');
|
||||
expect(el.style.opacity).toBe('0.25');
|
||||
el.dispatchEvent(pointer('pointerup', 250, 60));
|
||||
expect(el.style.opacity).toBe('0');
|
||||
setTimeout(() => {
|
||||
expect(t.closing).toBe(true);
|
||||
done();
|
||||
}, 350);
|
||||
});
|
||||
|
||||
test('拖拽未超阈值松手恢复', () => {
|
||||
const t = MeToast.info('drag2', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 100, 50));
|
||||
el.dispatchEvent(pointer('pointermove', 150, 60));
|
||||
el.dispatchEvent(pointer('pointerup', 150, 60));
|
||||
expect(t.closing).toBe(false);
|
||||
expect(t.paused).toBe(false);
|
||||
expect(el.style.transform).toBe('');
|
||||
});
|
||||
|
||||
test('拖拽触发 dragStart/dragEnd 钩子', () => {
|
||||
const start = jest.fn();
|
||||
const end = jest.fn();
|
||||
Toast.on('dragStart', start);
|
||||
Toast.on('dragEnd', end);
|
||||
const t = MeToast.info('d', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||
el.dispatchEvent(pointer('pointerup', 0, 0));
|
||||
expect(start).toHaveBeenCalled();
|
||||
expect(end).toHaveBeenCalled();
|
||||
Toast.off('dragStart', start);
|
||||
Toast.off('dragEnd', end);
|
||||
});
|
||||
|
||||
test('pointercancel 等价于 pointerup', () => {
|
||||
const t = MeToast.info('pc', { duration: 5000 });
|
||||
const el = t.el!;
|
||||
el.dispatchEvent(pointer('pointerdown', 10, 10));
|
||||
el.dispatchEvent(pointer('pointercancel', 10, 10));
|
||||
expect(t.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('拖拽从 close 按钮按下时早退', () => {
|
||||
const t = MeToast.info('dc', { duration: 5000 });
|
||||
const closeBtn = t.el!.querySelector('.met-close') as HTMLElement;
|
||||
closeBtn.dispatchEvent(pointer('pointerdown', 0, 0));
|
||||
expect(t.paused).toBe(false);
|
||||
});
|
||||
|
||||
test('close 非 immediate 走离场动画并清理', (done) => {
|
||||
const t = MeToast.info('anim-close');
|
||||
t.close();
|
||||
expect(t.el!.classList.contains('met-leaving')).toBe(true);
|
||||
setTimeout(() => {
|
||||
expect(t.el).toBeNull();
|
||||
expect(MeToast.count()).toBe(0);
|
||||
done();
|
||||
}, 400);
|
||||
});
|
||||
|
||||
test('updatePosition 移动到新容器', () => {
|
||||
const t = MeToast.info('move', { position: 'top-left' });
|
||||
t.updatePosition('bottom-right');
|
||||
expect(t.config.position).toBe('bottom-right');
|
||||
expect(t.el!.parentElement!.className).toContain('bottom-right');
|
||||
});
|
||||
|
||||
test('updatePosition 同位置直接返回', () => {
|
||||
const t = MeToast.info('same');
|
||||
const container = t.el!.parentElement;
|
||||
t.updatePosition('top-right');
|
||||
expect(t.el!.parentElement).toBe(container);
|
||||
});
|
||||
|
||||
test('_limitToasts registry 无实例时走 _removeToast 兜底', () => {
|
||||
const el = document.createElement('div');
|
||||
const ghost = document.createElement('div');
|
||||
ghost.dataset.id = 'ghost-id';
|
||||
(el.querySelectorAll as unknown) = () => [ghost];
|
||||
const t = new Toast({ message: 'x', max: 0 });
|
||||
expect(() => t._limitToasts(el)).not.toThrow();
|
||||
});
|
||||
|
||||
test('remove() 清理注册表与内存', () => {
|
||||
const t = MeToast.info('rm');
|
||||
expect(Toast._registry.has(t.id)).toBe(true);
|
||||
t.remove();
|
||||
expect(Toast._registry.has(t.id)).toBe(false);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('api.ts — 对话框与按钮交互', () => {
|
||||
test('confirm 点击确认按钮 resolve true', async () => {
|
||||
const promise = MeToast.confirm('确定删除?');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-confirm-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
test('confirm 点击取消按钮 resolve false', async () => {
|
||||
const promise = MeToast.confirm('确定删除?');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('prompt Enter 提交输入值', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||
input.value = '张三';
|
||||
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
||||
const result = await promise;
|
||||
expect(result).toBe('张三');
|
||||
});
|
||||
|
||||
test('prompt 点击提交按钮', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
const input = toast.el!.querySelector('.met-input') as HTMLInputElement;
|
||||
input.value = '李四';
|
||||
(toast.el!.querySelector('.met-submit-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBe('李四');
|
||||
});
|
||||
|
||||
test('prompt 点击取消按钮 resolve null', async () => {
|
||||
const promise = MeToast.prompt('请输入姓名');
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
const toast = MeToast.getToasts()[0];
|
||||
(toast.el!.querySelector('.met-cancel-btn') as HTMLElement).click();
|
||||
const result = await promise;
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
test('action 按钮点击触发回调并关闭', async () => {
|
||||
const onClick = jest.fn();
|
||||
const a = MeToast.action('msg', [{ text: 'OK', onClick }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(onClick).toHaveBeenCalledWith(a.toast);
|
||||
expect(a.toast.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('action 按钮 close:false 点击不关闭', async () => {
|
||||
const onClick = jest.fn();
|
||||
const a = MeToast.action('msg', [{ text: 'Keep', onClick, close: false }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(a.toast.closing).toBe(false);
|
||||
});
|
||||
|
||||
test('action 按钮 onClick 异常被捕获', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const a = MeToast.action('msg', [{ text: 'Boom', onClick: () => { throw new Error('act-err'); } }]);
|
||||
await new Promise(r => setTimeout(r, 10));
|
||||
(a.toast.el!.querySelector('.met-action-btn-0') as HTMLElement).click();
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('countdown onComplete 异常被捕获', (done) => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.countdown('{seconds} 秒', 1, { onComplete: () => { throw new Error('cd-err'); } });
|
||||
setTimeout(() => {
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
done();
|
||||
}, 1600);
|
||||
});
|
||||
|
||||
test('loading control 支持 update 与 dismiss', () => {
|
||||
const l = MeToast.loading('l1');
|
||||
l.update({ message: 'l2' });
|
||||
const toast = MeToast.find(l.id);
|
||||
expect(toast!.message).toBe('l2');
|
||||
l.dismiss();
|
||||
expect(toast!.closing).toBe(true);
|
||||
});
|
||||
|
||||
test('group 单对象参数分支', () => {
|
||||
const g = MeToast.group('obj-group');
|
||||
const t = g.show({ message: 'obj-arg' });
|
||||
expect(t.group).toBe('obj-group');
|
||||
expect(t.message).toBe('obj-arg');
|
||||
g.dismiss();
|
||||
});
|
||||
|
||||
test('queue 消息级 onClose 异常被静默捕获', async () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const q = MeToast.queue([
|
||||
{ message: 'a', onClose: () => { throw new Error('q-err'); } },
|
||||
], { delay: 5, duration: 10 });
|
||||
await q;
|
||||
expect(consoleError).not.toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('_remove 从内存移除指定 id', () => {
|
||||
const t = MeToast.info('x');
|
||||
MeToast._remove(t.id);
|
||||
expect(MeToast.count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('plugins.ts — 异常与钩子管理', () => {
|
||||
test('插件 install 抛错被捕获', () => {
|
||||
const consoleError = jest.spyOn(console, 'error').mockImplementation(() => {});
|
||||
MeToast.plugins.register('bad-install', { name: 'bad-install', install: () => { throw new Error('install-err'); } });
|
||||
expect(consoleError).toHaveBeenCalled();
|
||||
consoleError.mockRestore();
|
||||
});
|
||||
|
||||
test('keyboard 插件 install/uninstall 移除事件监听', () => {
|
||||
MeToast.use('keyboard');
|
||||
MeToast.plugins.unregister('keyboard');
|
||||
expect(MeToast.plugins.has('keyboard')).toBe(false);
|
||||
});
|
||||
|
||||
test('persistence 插件 save 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||
const preset = pluginUtils.getPreset('persistence');
|
||||
const saveFn = (preset as Record<string, (c: unknown) => void>).save;
|
||||
expect(() => saveFn({ duration: 3000 })).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
|
||||
test('persistence 插件 uninstall removeItem 异常被捕获', () => {
|
||||
const spy = jest.spyOn(Storage.prototype, 'removeItem').mockImplementation(() => { throw new Error('quota'); });
|
||||
const pluginUtils = require('../src/plugins.js').pluginUtils;
|
||||
const preset = pluginUtils.getPreset('persistence');
|
||||
expect(() => preset?.uninstall?.()).not.toThrow();
|
||||
spy.mockRestore();
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* MetonaToast React 适配器测试
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { renderToString } from 'react-dom/server';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { act } from 'react';
|
||||
import MeToast, { Toast } from '../src/index';
|
||||
import { createBoundApi, Toast as ToastComponent, useToast } from '../src/react';
|
||||
import type { ToastInstance } from '../src/types';
|
||||
|
||||
(globalThis as Record<string, unknown>).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
MeToast._toasts.clear();
|
||||
MeToast.resetConfig();
|
||||
(MeToast as unknown as { _destroyed?: boolean })._destroyed = false;
|
||||
Toast._hooks.clear();
|
||||
Toast._registry.clear();
|
||||
const { _containerCache } = require('../src/toast.js');
|
||||
_containerCache.clear();
|
||||
MeToast.animations.reset();
|
||||
});
|
||||
|
||||
describe('React 适配器 — createBoundApi', () => {
|
||||
test('跟踪创建的 toast id', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
(api.info as (m: string) => ToastInstance)('hello');
|
||||
expect(cleanup.size).toBe(1);
|
||||
});
|
||||
|
||||
test('success/error/warning/show 均被跟踪', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
(api.success as (m: string) => ToastInstance)('a');
|
||||
(api.error as (m: string) => ToastInstance)('b');
|
||||
(api.warning as (m: string) => ToastInstance)('c');
|
||||
(api.show as (m: string) => ToastInstance)('d');
|
||||
expect(cleanup.size).toBe(4);
|
||||
});
|
||||
|
||||
test('loading 控制对象也被跟踪', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
(api.loading as (m: string) => { id: string })('loading...');
|
||||
expect(cleanup.size).toBe(1);
|
||||
});
|
||||
|
||||
test('dismiss 同步清理集合', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
const t = (api.success as (m: string) => ToastInstance)('hi');
|
||||
expect(cleanup.has(t.id)).toBe(true);
|
||||
(api.dismiss as (id?: string) => void)(t.id);
|
||||
expect(cleanup.has(t.id)).toBe(false);
|
||||
});
|
||||
|
||||
test('removeToast 同步清理集合', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
const t = (api.info as (m: string) => ToastInstance)('hi');
|
||||
(api.removeToast as (id: string) => void)(t.id);
|
||||
expect(cleanup.size).toBe(0);
|
||||
});
|
||||
|
||||
test('未跟踪的方法直接透传', () => {
|
||||
const cleanup = new Set<string>();
|
||||
const api = createBoundApi(cleanup);
|
||||
expect(typeof (api as Record<string, unknown>).count).toBe('function');
|
||||
expect((api as { count(): number }).count()).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('React 适配器 — Toast 组件', () => {
|
||||
test('SSR 渲染为空(effect 不执行)', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'ssr message' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('SSR 带 type 不抛异常', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'x', type: 'success' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('SSR 未知 type fallback 到 show 不抛异常', () => {
|
||||
const html = renderToString(React.createElement(ToastComponent, { message: 'y', type: 'custom-type' }));
|
||||
expect(html).toBe('');
|
||||
});
|
||||
|
||||
test('组件挂载创建 toast,卸载自动移除', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'mounted', type: 'success' })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('组件 autoClose:false 卸载不移除', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'keep', type: 'error', autoClose: false })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
MeToast.dismiss();
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('组件 message 变化重新创建实例', () => {
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'v1', type: 'info' })); });
|
||||
act(() => { root.render(React.createElement(ToastComponent, { message: 'v2', type: 'warning' })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
const t = MeToast.getToasts()[0];
|
||||
expect(t.message).toBe('v2');
|
||||
expect(t.type).toBe('warning');
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
|
||||
test('useToast 组件内创建的 toast 在卸载时清理', () => {
|
||||
const Harness = (): React.ReactElement => {
|
||||
const toast = useToast() as Record<string, (m: string) => ToastInstance>;
|
||||
return React.createElement('button', { onClick: () => toast.info('tracked') });
|
||||
};
|
||||
const host = document.createElement('div');
|
||||
document.body.appendChild(host);
|
||||
const root = createRoot(host);
|
||||
act(() => { root.render(React.createElement(Harness)); });
|
||||
const btn = host.querySelector('button')!;
|
||||
act(() => { btn.dispatchEvent(new MouseEvent('click', { bubbles: true })); });
|
||||
expect(MeToast.count()).toBe(1);
|
||||
act(() => { root.unmount(); });
|
||||
expect(MeToast.count()).toBe(0);
|
||||
host.remove();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Jest 环境补丁 — jsdom 未暴露 Node 全局对象
|
||||
*/
|
||||
import { TextEncoder, TextDecoder } from 'util';
|
||||
|
||||
const g = globalThis as Record<string, unknown>;
|
||||
|
||||
if (typeof g.TextEncoder === 'undefined') {
|
||||
g.TextEncoder = TextEncoder;
|
||||
}
|
||||
if (typeof g.TextDecoder === 'undefined') {
|
||||
g.TextDecoder = TextDecoder;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* 环境隔离测试 — 需要切换 document/window/localStorage 环境并调用 jest.resetModules()。
|
||||
* 独立成文件:resetModules 会清空模块缓存,若与其他测试同文件会导致
|
||||
* 后续 require 拿到新模块实例、与 import 时绑定的实例状态分裂。
|
||||
* 注:jsdom 以 getter 暴露 document/window,普通赋值无效,须用 defineProperty 覆盖。
|
||||
* @module tests
|
||||
* @version 0.5.0
|
||||
*/
|
||||
|
||||
/** 临时移除全局对象,返回恢复函数 */
|
||||
const hideGlobal = (key: string): (() => void) => {
|
||||
const desc = Object.getOwnPropertyDescriptor(global, key);
|
||||
Object.defineProperty(global, key, { value: undefined, configurable: true, writable: true });
|
||||
return () => {
|
||||
if (desc) Object.defineProperty(global, key, desc);
|
||||
else delete (global as Record<string, unknown>)[key];
|
||||
};
|
||||
};
|
||||
|
||||
describe('环境隔离(SSR / localStorage 异常路径)', () => {
|
||||
test('escapeHTML SSR 路径(无 document)', () => {
|
||||
const restore = hideGlobal('document');
|
||||
jest.resetModules();
|
||||
const { escapeHTML } = require('../src/utils.js');
|
||||
const result = escapeHTML('<script>alert("xss")</script>');
|
||||
expect(result).not.toContain('<script>');
|
||||
expect(result).toContain('<script>');
|
||||
restore();
|
||||
});
|
||||
|
||||
test('prefersDark 无 window 时返回 false', () => {
|
||||
const restore = hideGlobal('window');
|
||||
jest.resetModules();
|
||||
const { prefersDark } = require('../src/utils.js');
|
||||
expect(prefersDark()).toBe(false);
|
||||
restore();
|
||||
});
|
||||
|
||||
test('saveTheme localStorage 异常被捕获', () => {
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = undefined;
|
||||
jest.resetModules();
|
||||
const { saveTheme } = require('../src/themes.js');
|
||||
expect(() => saveTheme('dark')).not.toThrow();
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
});
|
||||
|
||||
test('loadLocale localStorage 异常返回 default', () => {
|
||||
const orig = (global as Record<string, unknown>).localStorage;
|
||||
(global as Record<string, unknown>).localStorage = {
|
||||
getItem: () => { throw new Error('quota'); },
|
||||
};
|
||||
jest.resetModules();
|
||||
const { loadLocale } = require('../src/i18n.js');
|
||||
expect(typeof loadLocale()).toBe('string');
|
||||
(global as Record<string, unknown>).localStorage = orig;
|
||||
});
|
||||
});
|
||||
@@ -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
-1090
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user