1
0

Compare commits

...

4 Commits

Author SHA1 Message Date
8793fbd786 test: 重构测试体系 — 建立组件测试层、补充后端测试、清理低质量测试
- 新增 jsdom + @testing-library/react 组件测试环境
- 新增 12 个组件测试,覆盖所有前端组件
- 补充后端 middleware 和 helpers 单元测试
- 删除伪测试 use-target-detail-logic.test.ts
- 精简过度枚举的 color-threshold.test.ts
- 新增 bunfig.toml 配置测试 preload
- 更新 DEVELOPMENT.md 测试章节
- 安装 @types/jsdom 修复类型声明
2026-05-15 18:31:33 +08:00
2b08f81a0d chore: 依赖版本更新、Vite 构建配置完善、TrendChart 描述修正
- 更新 @commitlint/cli、@commitlint/config-conventional、@types/bun、typescript-eslint 至最新 patch 版本
- vite.config.ts 添加 emptyOutDir: true 消除构建 outDir 警告
- DEVELOPMENT.md TrendChart 描述从"双轴折线图(耗时/可用率)"修正为"趋势折线图(耗时+延迟范围)"
2026-05-15 15:15:57 +08:00
86b8cf1950 refactor: 前端性能优化 — 倒计时组件隔离、React memoization 链路
- 新建 RefreshCountdown 组件,内部持有 timer,消除 App 每秒重渲染
- TargetBoard 分组逻辑 useMemo 化,避免 targets 引用不变时重复计算
- TargetGroup 加 React.memo,阻断无效渲染
- TrendChart 加 React.memo + chartData useMemo,避免 recharts 不必要重绘
- OverviewTab 统计项去掉 Card 包裹,改用纯 CSS 实现视觉效果
- 同步更新 refresh-control 和 target-detail-drawer spec

性能提升:消除每秒全组件树重渲染,减少 DOM 节点数
2026-05-15 12:02:39 +08:00
d6a77b2c6e feat: 迁移前端构建从 Bun fullstack 到 Vite
前端性能问题根因在于 Bun bundler 无法有效 code split、CSS
tree-shake 和产出优化的前端资源。经多轮 Bun 原生优化尝试
均无明显效果后,决定将前端构建迁回 Vite。

主要变更:

- 前端构建:从 Bun HTML import bundling 切换为 Vite build
  (Rolldown code splitting、vendor chunk、CSS 优化)
- 开发模式:从 Bun fullstack 单进程 HMR 切换为 Vite dev
  server + Bun API server 双进程(:5173 + :3000)
- 生产构建:三步流水线(Vite build → code generation →
  Bun compile),通过 `import with { type: "file" }` 嵌入前端资源
- 静态资源服务:从 Bun HTML import manifest 切换为自定义
  serveStaticAsset 函数,支持 SPA fallback 和正确的 Cache-Control
- Server 接口:BootstrapOptions 和 StartServerOptions 增加
  staticAssets? 可选参数
- 文档更新:DEVELOPMENT.md 和 README.md 反映新的开发模式,
  主 specs 同步 delta 变更

新增能力:
- static-asset-embedding: 构建时资源扫描与 code generation、
  运行时静态资源服务
- vite-frontend-bundling: Vite 构建配置、code splitting 策略、
  CSS 处理
2026-05-15 11:26:46 +08:00
48 changed files with 2357 additions and 395 deletions

View File

@@ -22,9 +22,9 @@ src/
server/
bootstrap.ts 后端统一启动引导loadConfig → store → engine → startServer → shutdown
config.ts CLI 参数解析(仅提取配置文件路径)
dev.ts 开发模式启动入口mode: "development"HMR 自动注入
dev.ts 开发模式启动入口mode: "development"仅 API server
main.ts 生产模式启动入口mode: "production",安全头启用)
server.ts HTTP server 启动工厂Bun.serve routes 声明式路由 + HTML import
server.ts HTTP server 启动工厂Bun.serve routes 声明式路由 + fetch fallback 静态资源服务
helpers.ts 共享响应格式化工具(见下方函数清单)
middleware.ts API 参数校验中间件validateTargetId、validateTimeRange、validatePagination
routes/ API 路由 handler按端点拆分
@@ -113,7 +113,7 @@ probe-config.schema.json 用户配置 JSON Schema 导出物(用于 IDE 自动
HTTP 请求:
Request → Bun.serve routes 声明式匹配 → routes/*.ts(handler)
→ middleware.ts(参数校验) → helpers.ts(响应格式化) → Response
前端: "/*": homepage (HTML import) → SPA fallback + HMR(开发模式)
前端: fetch fallback → serveStaticAsset (生产) / Vite proxy (开发)
```
### 1.2 库使用优先级
@@ -691,7 +691,7 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
| `TargetDetailDrawer` | `components/TargetDetailDrawer.tsx` | 目标详情抽屉width=52%、TDesign 生命周期控制、preventScrollThrough、受控 Tabs、记录 TabPanel 懒渲染) |
| `OverviewTab` | `components/OverviewTab.tsx` | 目标详情概览Descriptions 直接展示 + 4×2 统计卡片 + 趋势) |
| `HistoryTab` | `components/HistoryTab.tsx` | 目标历史记录表格和分页 |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 双轴折线图(耗时/可用率) |
| `TrendChart` | `components/TrendChart.tsx` | Recharts 趋势折线图(耗时+延迟范围) |
| `StatusDot` | `components/StatusDot.tsx` | 圆形状态指示点(绿/红) |
| `StatusBar` | `components/StatusBar.tsx` | 最近采样状态条(多色块 + Tooltip 提示时间和状态) |
@@ -748,44 +748,58 @@ export function TargetGroup({ name, targets, onTargetClick }: TargetGroupProps)
bun run dev probes.yaml
```
`bun --watch src/server/dev.ts` 启动单进程 fullstack 开发服务器
`scripts/dev.ts` 同时启动两个进程
- 后端 API + 前端 SPA 在同一端口(默认 3000
- `development` 模式自动注入 HMR前端修改即时热更新
- `--watch` 监听后端文件变更自动重启
- 访问 `http://127.0.0.1:3000` 即可使用完整应用
- **Bun API server**(端口 3000后端 API 服务,`--watch` 监听后端文件变更自动重启
- **Vite dev server**(端口 5173前端 SPA + HMR 热更新
开发时访问 `http://127.0.0.1:5173`Vite 自动将 `/api``/health` 请求代理到后端。
也可以单独启动:
```bash
bun run dev:server probes.yaml # 仅启动后端 API server
bun run dev:web # 仅启动 Vite dev server
```
### 3.2 前后端集成方式
#### 统一进程架构
#### 双进程开发架构
前后端通过 Bun 的 HTML import 机制集成为单进程应用
开发模式下前后端分别由 Vite 和 Bun 服务
- Vite dev server 负责前端 SPA、HMR、模块热替换
- Bun API server 负责后端 API 路由
- Vite 通过 proxy 配置将 `/api/*``/health` 转发到 Bun
#### 生产模式架构
生产模式下前端通过 Vite 构建为静态资源,通过 `import with { type: "file" }` 嵌入 Bun 可执行文件:
```typescript
// server.ts
import homepage from "../web/index.html";
const server = Bun.serve({
development: mode === "development" ? { hmr: true, console: true } : false,
fetch(req) {
// staticAssets 存在时服务嵌入的前端资源
return serveStaticAsset(new URL(req.url).pathname, staticAssets);
},
routes: {
"/*": homepage, // SPA fallback开发模式自动注入 HMR
"/api/*": () => ..., // API 通配符(未匹配路由返回 404
"/api/dashboard": { GET: (req) => handleDashboard(new URL(req.url), store, mode) },
"/api/dashboard": { GET: (req) => handleDashboard(...) },
"/health": { GET: () => handleHealth(mode) },
// ...
},
});
```
- 开发模式(`development: { hmr: true, console: true }`Bun 自动为 HTML import 注入 HMR client前端修改无需手动刷新并将浏览器 console 回显到终端
- 生产模式HTML 及其引用的 JS/CSS 资源在 `bun build --compile` 时自动打包进可执行文件
#### 路由优先级
Bun routes 的匹配规则:具体路径 > 通配符。`/api/dashboard` 优先于 `/api/*``/health` 优先于 `/*`
未匹配 method 的请求(如 POST /api/dashboard会落入 `/api/*` 通配符返回 404。
非 API 路径由 fetch fallback 处理:有文件扩展名的返回对应静态资源或 404无扩展名的返回 SPA index.html。
### 3.3 构建打包
#### 构建命令
@@ -796,26 +810,25 @@ bun run build
#### 构建流程
`scripts/build.ts` 执行单步构建
`scripts/build.ts` 执行三步流水线
```
Bun.build({
compile: { outfile: "dist/dial-server" },
entrypoints: ["src/server/main.ts"],
minify: true,
sourcemap: "linked",
})
1. Vite build → dist/web/ (前端静态资源,含 code splitting)
2. Code generation → .build/static-assets.ts + .build/server-entry.ts
3. Bun compile → dist/dial-server (单可执行文件)
```
- 入口为 `src/server/main.ts``mode: "production"`,启用安全头
- HTML import 的前端资源自动打包进可执行文件Bun 自动生成 manifest
- 无需中间产物目录,一步生成最终 binary
- Vite 构建前端资源到 `dist/web/`,自动 code splittingvendor-react、vendor-tdesign、vendor-chart
- Code generation 扫描 `dist/web/` 生成 `import with { type: "file" }` 声明,将资源嵌入 binary
- Bun compile 以 `.build/server-entry.ts` 为入口编译最终可执行文件
- `.build/` 临时目录在构建完成后自动清理
#### 产物
| 产物 | 用途 |
| ------------------ | ---------------------------------------- |
| `dist/dial-server` | 生产可执行文件(含前端资源,单文件部署) |
| `dist/web/` | Vite 构建的前端资源(构建中间产物) |
#### 构建参数
@@ -839,7 +852,7 @@ Bun.build({
```bash
bun run clean
# 清理 dist/ 构建产物和 *.bun-build 临时文件
# 清理 dist/ 构建产物和 .build/ 临时文件
```
### 3.4 开发工作流
@@ -847,8 +860,9 @@ bun run clean
#### 日常开发循环
```bash
bun run dev probes.yaml # 启动开发环境(单进程,含 HMR
# 修改前端代码 → HMR 热更新 / 修改后端代码 → --watch 自动重启
bun run dev probes.yaml # 启动双进程开发环境(Vite + API server
# 访问 http://127.0.0.1:5173
# 修改前端代码 → Vite HMR 热更新 / 修改后端代码 → --watch 自动重启
bun run check # 提交前运行完整质量检查
```
@@ -863,17 +877,19 @@ bun run verify
### 3.5 Executable/E2E 验证
`scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、HTML import manifest、SPA fallback 和静态资源行为,应重新设计独立的 executable/E2E 测试。
`scripts/smoke.ts` 覆盖过薄,已从当前工作流移除。后续如需验证 production executable 的 API、静态资源服务、SPA fallback 行为,应重新设计独立的 executable/E2E 测试。
### 3.6 脚本说明
| 脚本 | 文件 | 说明 |
| ---------------------- | ----------------------------------- | ----------------------------------- |
| `bun run dev` | `src/server/dev.ts` | 进程 fullstack 开发服务(含 HMR |
| `bun run build` | `scripts/build.ts` | Bun 编译可执行文件(含前端资源) |
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
| 脚本 | 文件 | 说明 |
| ---------------------- | ----------------------------------- | ---------------------------------------- |
| `bun run dev` | `scripts/dev.ts` | 进程开发服务Vite :5173 + API :3000 |
| `bun run dev:server` | `src/server/dev.ts` | 仅启动后端 API server |
| `bun run dev:web` | Vite CLI | 仅启动 Vite dev server |
| `bun run build` | `scripts/build.ts` | Vite → codegen → Bun compile 三步构建 |
| `bun run schema` | `scripts/generate-config-schema.ts` | 生成 `probe-config.schema.json` |
| `bun run schema:check` | `scripts/generate-config-schema.ts` | 检查配置 schema 导出物是否同步 |
| `bun run clean` | `scripts/clean.ts` | 清理构建缓存与临时文件 |
### 3.7 环境变量
@@ -977,11 +993,40 @@ bun run check # 一键运行 schema:check + typecheck + lint + test
## 测试
项目采用两层测试体系:单元测试 + 组件测试。所有测试使用 `bun:test` 运行。
### 测试分层
| 层级 | 覆盖范围 | 位置 | 命令 |
| -------- | ---------------------- | ----------------------------------------------------------------------------- | --------------------------------------------- |
| 单元测试 | 后端函数、纯函数、常量 | `tests/server/**/*.test.ts``tests/web/{constants,utils,hooks}/**/*.test.ts` | `bun test tests/server``bun test tests/web` |
| 组件测试 | React 组件渲染和交互 | `tests/web/components/**/*.test.tsx` | `bun test tests/web/components` |
### 运行命令
```bash
bun run check # 日常开发(类型检查 + lint含格式 + 单元测试)
bun run verify # 完整验证check + 构建)
bun test # 运行所有单元测试和组件测试
bun test tests/server # 只运行后端单元测试
bun test tests/web # 只运行前端测试(单元 + 组件)
bun run check # 日常开发(类型检查 + lint + 测试)
bun run verify # 完整验证check + 构建)
```
### 组件测试环境
组件测试使用 jsdom 模拟浏览器环境,配置位于 `tests/setup.ts`(通过 `bunfig.toml` preload 加载):
- jsdom 提供完整的 DOM 环境
- TDesign 组件所需的 polyfillResizeObserver、IntersectionObserver、matchMedia、attachEvent
- recharts 图表组件被 mock 为占位元素SVG 渲染在 jsdom 中不可靠)
### 编写规范
- **优先使用 `@testing-library/react`** 的语义化查询getByText、getByRole而非 CSS 选择器
- **测试用户行为而非实现细节**:模拟用户点击、输入等操作,而非直接调用组件方法
- **只 mock 系统边界**mock fetch 返回预设响应,使用真实的 QueryClientProvider 包裹组件
- **组件测试文件命名**`tests/web/components/ComponentName.test.tsx`
## 已知限制
当前不做告警通知、拨测目标动态增删、认证鉴权和分布式部署。

View File

@@ -10,7 +10,7 @@ cp probes.example.yaml probes.yaml
bun run dev probes.yaml
```
`bun run dev` 启动进程 fullstack 开发服务器(后端 API + 前端 SPA + HMR),访问 `http://127.0.0.1:3000`
`bun run dev` 启动进程开发服务器Vite :5173 + Bun API :3000),访问 `http://127.0.0.1:5173`
## 开发验证

317
bun.lock
View File

@@ -19,13 +19,16 @@
"xpath": "^0.0.34",
},
"devDependencies": {
"@commitlint/cli": "^21.0.0",
"@commitlint/config-conventional": "^21.0.0",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@eslint/js": "^10.0.1",
"@tanstack/react-query-devtools": "^5.100.10",
"@types/bun": "^1.3.13",
"@testing-library/react": "^16.3.2",
"@types/bun": "^1.3.14",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
@@ -35,14 +38,24 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.4",
"prettier": "^3.8.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2",
"typescript-eslint": "^8.59.3",
"vite": "^8.0.13",
},
},
},
"packages": {
"@asamuzakjp/css-color": ["@asamuzakjp/css-color@5.1.11", "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@csstools/css-calc": "^3.2.0", "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg=="],
"@asamuzakjp/dom-selector": ["@asamuzakjp/dom-selector@7.1.1", "https://registry.npmmirror.com/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", { "dependencies": { "@asamuzakjp/generational-cache": "^1.0.1", "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", "is-potential-custom-element-name": "^1.0.1" } }, "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ=="],
"@asamuzakjp/generational-cache": ["@asamuzakjp/generational-cache@1.0.1", "https://registry.npmmirror.com/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", {}, "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg=="],
"@asamuzakjp/nwsapi": ["@asamuzakjp/nwsapi@2.3.9", "https://registry.npmmirror.com/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", {}, "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q=="],
"@babel/code-frame": ["@babel/code-frame@7.29.0", "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.3", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.3.tgz", {}, "sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg=="],
@@ -77,42 +90,56 @@
"@babel/types": ["@babel/types@7.29.0", "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="],
"@commitlint/cli": ["@commitlint/cli@21.0.0", "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.0.tgz", { "dependencies": { "@commitlint/format": "^21.0.0", "@commitlint/lint": "^21.0.0", "@commitlint/load": "^21.0.0", "@commitlint/read": "^21.0.0", "@commitlint/types": "^21.0.0", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "./cli.js" } }, "sha512-p3y2oC0G2R45zaadMwBxCiSesS8digi5RDplP3Zrfpzm7xIgrgAj0W4fGzONjpHyg8obDVJDU45g5txzeMcblg=="],
"@bramus/specificity": ["@bramus/specificity@2.4.2", "https://registry.npmmirror.com/@bramus/specificity/-/specificity-2.4.2.tgz", { "dependencies": { "css-tree": "^3.0.0" }, "bin": { "specificity": "bin/cli.js" } }, "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw=="],
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.0", "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-QJX/rPK4Yu3f5J4OCIBy5aXq2e0EEdwSDFZ3NQvFAXTm3gs12ipyZ+yjhZxm3hHn6DB8wuv3zhFTL1I2tYzUBA=="],
"@commitlint/cli": ["@commitlint/cli@21.0.1", "https://registry.npmmirror.com/@commitlint/cli/-/cli-21.0.1.tgz", { "dependencies": { "@commitlint/format": "^21.0.1", "@commitlint/lint": "^21.0.1", "@commitlint/load": "^21.0.1", "@commitlint/read": "^21.0.1", "@commitlint/types": "^21.0.1", "tinyexec": "^1.0.0", "yargs": "^18.0.0" }, "bin": { "commitlint": "cli.js" } }, "sha512-8vq10krmbJwBkvzXKhbs4o4JQEVscd3pqOlWuDUaDBwbeL694/P33UC29tZQFTAgPU9fVJ2+f2m3zw16yKWxHg=="],
"@commitlint/config-validator": ["@commitlint/config-validator@21.0.0", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "ajv": "^8.11.0" } }, "sha512-v0UplTYryNUB463X5WrelzKq5/qyYm9/iUNk38S7ZLnd56Uuk2T9awhYKGlgD2/4L5YuN2gsKkyy4EHpRPPz2Q=="],
"@commitlint/config-conventional": ["@commitlint/config-conventional@21.0.1", "https://registry.npmmirror.com/@commitlint/config-conventional/-/config-conventional-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-conventionalcommits": "^9.2.0" } }, "sha512-gRorrkfWOh/+V5X8GYWWbQvrzPczopGMS4CCNrQdHkK4xWElv82BDvIsDhJZWTlI7TazOlYea6VATufCsFs+sw=="],
"@commitlint/ensure": ["@commitlint/ensure@21.0.0", "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "es-toolkit": "^1.46.0" } }, "sha512-n+OYs0Ws9GKC2WlmAeLNoPz9CUg6n/ZyYMkFF8rJ0aMn2kDTDTG0VqK/2Dco0EB4fhuF3JPIllJmU9/LKTl4aw=="],
"@commitlint/config-validator": ["@commitlint/config-validator@21.0.1", "https://registry.npmmirror.com/@commitlint/config-validator/-/config-validator-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "ajv": "^8.11.0" } }, "sha512-Zd2UFdndeMMaW2O96HK0tdfT4gOImUvidMpAd/pws2zZ4m1nrAZ/9b/v2JYuE8fs86GpXv9F7LNaIuCIWhY+pA=="],
"@commitlint/execute-rule": ["@commitlint/execute-rule@21.0.0", "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.0.tgz", {}, "sha512-3OhTq2gQX1tEheMsbDNqxfcNHsAM6g9cub9plf05I9jCxtbNfn8Y+mhClKyUwhX4dbtmC4OLZ9i+HNmoL1aksA=="],
"@commitlint/ensure": ["@commitlint/ensure@21.0.1", "https://registry.npmmirror.com/@commitlint/ensure/-/ensure-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "es-toolkit": "^1.46.0" } }, "sha512-jJ1037967wU7YN/xkv+iRlOBlmaOXPhPO5KQSqya6GyXzBlwuLzELBFao16DVg9dZyqmNrhewzwZ3SAibetHBQ=="],
"@commitlint/format": ["@commitlint/format@21.0.0", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "picocolors": "^1.1.1" } }, "sha512-RTfGSrueEgofs1piqwi42U05d85wfxiMH2ncMCZnltx1XqPR3N2S48oACBtTy4xRAhWlf5XlHkK2RaDzEQu3dA=="],
"@commitlint/execute-rule": ["@commitlint/execute-rule@21.0.1", "https://registry.npmmirror.com/@commitlint/execute-rule/-/execute-rule-21.0.1.tgz", {}, "sha512-RifH+FmImozKBE6mozhF4K3r2RRKP7SMi/Q/zLCmExtp5e05lhHOUYqGBlFBAGNHaZxU/WYw1XuugYK9jQzqnA=="],
"@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.0", "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "semver": "^7.6.0" } }, "sha512-K3SaaOTVY9VKhge7vl0R3ng7GENRzJQ9MPV43Tu53kAwEgSx/E0HF4US3AcVqdvlvsDUbF2yXvED95dhela83w=="],
"@commitlint/format": ["@commitlint/format@21.0.1", "https://registry.npmmirror.com/@commitlint/format/-/format-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "picocolors": "^1.1.1" } }, "sha512-ksmG2+cHGtuDPQQbhBbC4unwm444+6TiPw0d1bKf67hntgZqZ8E0g1MuYKUuyT5IH4IMmXZhKq22/Z3jBvtQIw=="],
"@commitlint/lint": ["@commitlint/lint@21.0.0", "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.0.tgz", { "dependencies": { "@commitlint/is-ignored": "^21.0.0", "@commitlint/parse": "^21.0.0", "@commitlint/rules": "^21.0.0", "@commitlint/types": "^21.0.0" } }, "sha512-dlUJA0Ka14R1YaR46JVRWE3m/8dOQAgE/D0heUfzYua5Jogtq/zzu2ITAIaB/u25DaKjtEO6kuvASzsFDyrPMw=="],
"@commitlint/is-ignored": ["@commitlint/is-ignored@21.0.1", "https://registry.npmmirror.com/@commitlint/is-ignored/-/is-ignored-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "semver": "^7.6.0" } }, "sha512-iNDP8SFdw8JEkM0CHZ2XFnhTN4Zg5jKUY2d8kBOSFrI2aA+3YJI7fcqVpfgbpJ9xtxFVYpi+DBATU5AvhoTq8g=="],
"@commitlint/load": ["@commitlint/load@21.0.0", "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.0.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.0", "@commitlint/execute-rule": "^21.0.0", "@commitlint/resolve-extends": "^21.0.0", "@commitlint/types": "^21.0.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-l0nBfO/20PKcJXHZqDIgh7kw/TWVVwn8zZJOkVGBK/ig/h328jBu9jK7OiDl2oZr5mLphmKGjYDR2ffEyb2lIA=="],
"@commitlint/lint": ["@commitlint/lint@21.0.1", "https://registry.npmmirror.com/@commitlint/lint/-/lint-21.0.1.tgz", { "dependencies": { "@commitlint/is-ignored": "^21.0.1", "@commitlint/parse": "^21.0.1", "@commitlint/rules": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-gF+iYtUw1gBG3HUH9z3VxwUjGg2R2G5j+nmvPs8aIeYkiB7TtneBu3wO85I0bUl93bYNsvsCNI9Nte2fmDUMww=="],
"@commitlint/message": ["@commitlint/message@21.0.0", "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.0.tgz", {}, "sha512-+daU92JaOHhI2En9KcH+2mvZGJ6D4YSxb/32QDwqkOwSj1Vanjio8PbAqX7dneACdg6B7RgQ7i3mpyYZAws4nw=="],
"@commitlint/load": ["@commitlint/load@21.0.1", "https://registry.npmmirror.com/@commitlint/load/-/load-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/execute-rule": "^21.0.1", "@commitlint/resolve-extends": "^21.0.1", "@commitlint/types": "^21.0.1", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", "picocolors": "^1.1.1" } }, "sha512-Btg1q1mKmiihN4W3x0EsPDrJMOQfMa9NIqlzlJyXAfxvsOGdGXOW5p3R3RcSxDCaY7JabY9flIl+Om1af3PSrw=="],
"@commitlint/parse": ["@commitlint/parse@21.0.0", "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.0.tgz", { "dependencies": { "@commitlint/types": "^21.0.0", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-1dbvFBcQK79aTbpc2QCrgEDc6/MMkQ0Mdz4gGmYkN4AHMnAK9HesSewTHqGTrW5mALrMlYSgcWyvKjloY2w19A=="],
"@commitlint/message": ["@commitlint/message@21.0.1", "https://registry.npmmirror.com/@commitlint/message/-/message-21.0.1.tgz", {}, "sha512-R3dVQeJQ0B6yqrZEjkUHD4r7UJYLV9Lvk2xs3PTOmtWk2G3mI6Xgc+YdRxL1PwcDfBiUjv2SkIkW4AUc976w1w=="],
"@commitlint/read": ["@commitlint/read@21.0.0", "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.0.tgz", { "dependencies": { "@commitlint/top-level": "^21.0.0", "@commitlint/types": "^21.0.0", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-8VKLKLl2vBSKoTMm1LwcySsyxrBeotnqcT5qJi9pPuPfqSapdAD870Ckgh79c41UFywL6kMqtiyY+kxtfcqZGg=="],
"@commitlint/parse": ["@commitlint/parse@21.0.1", "https://registry.npmmirror.com/@commitlint/parse/-/parse-21.0.1.tgz", { "dependencies": { "@commitlint/types": "^21.0.1", "conventional-changelog-angular": "^8.2.0", "conventional-commits-parser": "^6.3.0" } }, "sha512-oh/nCSOqdoeQNA1tO8aAmxkq5EBo8/NzcFQRvv66AWc9HpED28sL2iSicCKU6hPintWuscL6BJEWi77Wq1LPMQ=="],
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.0", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.0.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.0", "@commitlint/types": "^21.0.0", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-hrJYSZRpmecmSoxYrpuJ/1Q4J9JHt4AVVtr5/Ac6upLO/jJ1DnIm2AjD+38gru3KGOec4aHCVqETuWWLJhydWw=="],
"@commitlint/read": ["@commitlint/read@21.0.1", "https://registry.npmmirror.com/@commitlint/read/-/read-21.0.1.tgz", { "dependencies": { "@commitlint/top-level": "^21.0.1", "@commitlint/types": "^21.0.1", "git-raw-commits": "^5.0.0", "tinyexec": "^1.0.0" } }, "sha512-pMEu4lbpC8W0ZgKJj2U6WaobXIZWdFlULpIEewYhkPXx+WZcnoO53YrVPc7QErQuNolq2Me8dP58Wu7YAVXVOA=="],
"@commitlint/rules": ["@commitlint/rules@21.0.0", "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.0.tgz", { "dependencies": { "@commitlint/ensure": "^21.0.0", "@commitlint/message": "^21.0.0", "@commitlint/to-lines": "^21.0.0", "@commitlint/types": "^21.0.0" } }, "sha512-NgQhX1qENA+rbrMw5KKyvVZpZG4D/0wgK8Z4INtcwKbfKtVDFMbn0oNc/Rs8wdyBPBj7ue8Lo/GllUL2Mqjwkg=="],
"@commitlint/resolve-extends": ["@commitlint/resolve-extends@21.0.1", "https://registry.npmmirror.com/@commitlint/resolve-extends/-/resolve-extends-21.0.1.tgz", { "dependencies": { "@commitlint/config-validator": "^21.0.1", "@commitlint/types": "^21.0.1", "es-toolkit": "^1.46.0", "global-directory": "^5.0.0", "resolve-from": "^5.0.0" } }, "sha512-0DhjYWL6uYrY16Efa032fYk3woGJDU4AGWiG1XXltT9AMUNYKyb5cIZU2ivbaMZ3+kKFqUjikD2cjh66Sbh/Sg=="],
"@commitlint/to-lines": ["@commitlint/to-lines@21.0.0", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.0.tgz", {}, "sha512-qMwvrJK/x3dPcXsIAtQAMKV5Q0wTioyqyHKR06vVN4wmBF4cCrrLq5x81FDeY3Ba+GWgDt0/P3Zw/IHGM8lwgg=="],
"@commitlint/rules": ["@commitlint/rules@21.0.1", "https://registry.npmmirror.com/@commitlint/rules/-/rules-21.0.1.tgz", { "dependencies": { "@commitlint/ensure": "^21.0.1", "@commitlint/message": "^21.0.1", "@commitlint/to-lines": "^21.0.1", "@commitlint/types": "^21.0.1" } }, "sha512-VMooYpz4nJg7xlaUso6CCOWEz8D/ChkvsvZUMARcoJ1ZpfKPyFCGrHNha2tbsETNAb6ErgiRuCr2DvghrvPDYQ=="],
"@commitlint/top-level": ["@commitlint/top-level@21.0.0", "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.0.tgz", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-8jPqyWZueuN4hU6/ArKVsZ6i8xWtjIrbzHEOaLaTGUfjhhbZNBfXef/DGjzxy55hAv3yFNxHLINfI1bCJ0/MzA=="],
"@commitlint/to-lines": ["@commitlint/to-lines@21.0.1", "https://registry.npmmirror.com/@commitlint/to-lines/-/to-lines-21.0.1.tgz", {}, "sha512-bd1BFII7p1EQZre9Kaj+kKaMFP3cFCdt21K7DItVux9XP5WjLgJ0/Uy1pJJh9aPwVJ6SKg62PxqlZaHI8hQAXw=="],
"@commitlint/types": ["@commitlint/types@21.0.0", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.0.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-6nEz+M7I90iix4sviA8NLwskOuyt0M98KUU2aYgiKbn46jMSxUm1l2ACtzRd9ec+y38aKyJhW4Fp6NW0z35kJQ=="],
"@commitlint/top-level": ["@commitlint/top-level@21.0.1", "https://registry.npmmirror.com/@commitlint/top-level/-/top-level-21.0.1.tgz", { "dependencies": { "escalade": "^3.2.0" } }, "sha512-4esUYqzY7K0FCgcJ/1xWEZekV7Ch4yZT1+xjEb7KzqbJ05XEkxHVsTfC8ADKNNtlCE2pj98KEbPGZWw9WwEnVw=="],
"@commitlint/types": ["@commitlint/types@21.0.1", "https://registry.npmmirror.com/@commitlint/types/-/types-21.0.1.tgz", { "dependencies": { "conventional-commits-parser": "^6.3.0", "picocolors": "^1.1.1" } }, "sha512-4u7w8jcoCUFWhjWnASYzZHAP34OqOtuFBN87nQmFvqda03YU0T6z+yB4w0gSAMpekiRqqGk5rt+qSlW+a2vSEg=="],
"@conventional-changelog/git-client": ["@conventional-changelog/git-client@2.7.0", "https://registry.npmmirror.com/@conventional-changelog/git-client/-/git-client-2.7.0.tgz", { "dependencies": { "@simple-libs/child-process-utils": "^1.0.0", "@simple-libs/stream-utils": "^1.2.0", "semver": "^7.5.2" }, "peerDependencies": { "conventional-commits-filter": "^5.0.0", "conventional-commits-parser": "^6.4.0" }, "optionalPeers": ["conventional-commits-filter", "conventional-commits-parser"] }, "sha512-j7A8/LBEQ+3rugMzPXoKYzyUPpw/0CBQCyvtTR7Lmu4olG4yRC/Tfkq79Mr3yuPs0SUitlO2HwGP3gitMJnRFw=="],
"@csstools/color-helpers": ["@csstools/color-helpers@6.0.2", "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", {}, "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q=="],
"@csstools/css-calc": ["@csstools/css-calc@3.2.1", "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-3.2.1.tgz", { "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg=="],
"@csstools/css-color-parser": ["@csstools/css-color-parser@4.1.1", "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-4.1.1.tgz", { "dependencies": { "@csstools/color-helpers": "^6.0.2", "@csstools/css-calc": "^3.2.1" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^4.0.0", "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-eZ5XOtyhK+mggRafYUWzA0tvaYOFgdY8AkgQiCJF9qNAePnUo/zmsqqYubBBb3sQ8uNUaSKTY9s9klfRaAXL0g=="],
"@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@4.0.0", "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", { "peerDependencies": { "@csstools/css-tokenizer": "^4.0.0" } }, "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w=="],
"@csstools/css-syntax-patches-for-csstree": ["@csstools/css-syntax-patches-for-csstree@1.1.4", "https://registry.npmmirror.com/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.4.tgz", { "peerDependencies": { "css-tree": "^3.2.1" }, "optionalPeers": ["css-tree"] }, "sha512-wgsqt92b7C7tQhIdPNxj0n9zuUbQlvAuI1exyzeNrOKOi62SD7ren8zqszmpVREjAOqg8cD2FqYhQfAuKjk4sw=="],
"@csstools/css-tokenizer": ["@csstools/css-tokenizer@4.0.0", "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", {}, "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA=="],
"@emnapi/core": ["@emnapi/core@1.10.0", "https://registry.npmmirror.com/@emnapi/core/-/core-1.10.0.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw=="],
"@emnapi/runtime": ["@emnapi/runtime@1.10.0", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.10.0.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA=="],
@@ -135,6 +162,8 @@
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.7.1", "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", { "dependencies": { "@eslint/core": "^1.2.1", "levn": "^0.4.1" } }, "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ=="],
"@exodus/bytes": ["@exodus/bytes@1.15.0", "https://registry.npmmirror.com/@exodus/bytes/-/bytes-1.15.0.tgz", { "peerDependencies": { "@noble/hashes": "^1.8.0 || ^2.0.0" }, "optionalPeers": ["@noble/hashes"] }, "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ=="],
"@humanfs/core": ["@humanfs/core@0.19.2", "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.2.tgz", { "dependencies": { "@humanfs/types": "^0.15.0" } }, "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA=="],
"@humanfs/node": ["@humanfs/node@0.16.8", "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.8.tgz", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ=="],
@@ -157,12 +186,46 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@oxc-project/types": ["@oxc-project/types@0.130.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.130.0.tgz", {}, "sha512-ibD2usx9JRu7f5pu2tMKMI4cpA4NgXJQoYRP4pQ7Pxmn1l6k/53qWtQWZayhYy3X4QZkt90Ot+mJEaeXouio6Q=="],
"@pkgr/core": ["@pkgr/core@0.2.9", "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", {}, "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA=="],
"@popperjs/core": ["@popperjs/core@2.11.8", "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz", {}, "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="],
"@reduxjs/toolkit": ["@reduxjs/toolkit@2.11.2", "https://registry.npmmirror.com/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", { "dependencies": { "@standard-schema/spec": "^1.0.0", "@standard-schema/utils": "^0.3.0", "immer": "^11.0.0", "redux": "^5.0.1", "redux-thunk": "^3.1.0", "reselect": "^5.1.0" }, "peerDependencies": { "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" }, "optionalPeers": ["react", "react-redux"] }, "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-fJI3I0r3C3Oj/zdBCpaCmBRZYf07xpaq4yCfDDoSFm+beWNzbIl26puW8RraUdugoJw/95zerNOn6jasAhzSmg=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-cKnAhWEsV7TPcA/5EAteDp6KcJZBQ2G+BqE7zayMMi7kMvwRsbv7WT9aOnn0WNl4SKEIf43vjS31iUPu80nzXg=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-YKrVwQjIRBPo+5G/u03wGjbdy4q7pyzCe93DK9VJ7zkVmeg8LJ7GbgsiHWdR4xSoe4CAXRD7Bcjgbtr64bkXNg=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-z/oBsREo46SsFqBwYtFe0kpJeBijAT48O/WXLI4suiCLBkr03RTtTJMCzSdDd2znlh8VJizL09XVkQgk8IZonw=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-ik8q7GM11zxvYxFc2PeDcT6TBvhCQMaUxfph/M5l9sKuTs/Sjg3L+Byw0F7w0ZVLBZmx30P+gG0ECzzN+MFcmQ=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-/Mh0Zhq3OP7fVs0kcQHZP6lZEthMGTaSf8UBQYSFEZDWGXXlEC+nJ6EqenaK2t4LBXMe3A+K/G2BVXXdtOr4PQ=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.1.tgz", { "dependencies": { "@emnapi/core": "1.10.0", "@emnapi/runtime": "1.10.0", "@napi-rs/wasm-runtime": "^1.1.4" }, "cpu": "none" }, "sha512-+1xc9X45l8ufsBAm6Gjvx2qDRIY9lTVt0cgWNcJ+1gdhXvkbxePA60yRTwSTuXL09CMhyJmjpV7E3NoyxbqFQQ=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-1D+UqZdfnuR+Jy1GgMJwi85bD40H21uNmOPRWQhw4oRSuolZ/B5rixZ45DK2KXOTCvmVCecauWgEhbw8bI7tOw=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.1", "https://registry.npmmirror.com/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-INAycaWuhlOK3wk4mRHGsdgwYWmd9cChdPdE9bwWmy6rn9VqVNYNFGhOdXrofXUxwHIncSiPNb8tNm8knDVIeQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.1", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", {}, "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw=="],
"@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="],
"@simple-libs/child-process-utils": ["@simple-libs/child-process-utils@1.0.2", "https://registry.npmmirror.com/@simple-libs/child-process-utils/-/child-process-utils-1.0.2.tgz", { "dependencies": { "@simple-libs/stream-utils": "^1.2.0" } }, "sha512-/4R8QKnd/8agJynkNdJmNw2MBxuFTRcNFnE5Sg/G+jkSsV8/UBgULMzhizWWW42p8L5H7flImV2ATi79Ove2Tw=="],
@@ -183,9 +246,15 @@
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.100.10", "https://registry.npmmirror.com/@tanstack/react-query-devtools/-/react-query-devtools-5.100.10.tgz", { "dependencies": { "@tanstack/query-devtools": "5.100.10" }, "peerDependencies": { "@tanstack/react-query": "^5.100.10", "react": "^18 || ^19" } }, "sha512-zes0+o9ef5rAZXJ9f/SeaLs2nufJaeVkZkl/Or9NGrWVF41kL9Od9ED9nCwtQlgiF2VGtrzhEw5AU/igAO+aAg=="],
"@testing-library/dom": ["@testing-library/dom@10.4.1", "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="],
"@testing-library/react": ["@testing-library/react@16.3.2", "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.2", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg=="],
"@types/bun": ["@types/bun@1.3.13", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.13.tgz", { "dependencies": { "bun-types": "1.3.13" } }, "sha512-9fqXWk5YIHGGnUau9TEi+qdlTYDAnOj+xLCmSTwXfAIqXr2x4tytJb43E9uCvt09zJURKXwAtkoH4nLQfzeTXw=="],
"@types/aria-query": ["@types/aria-query@5.0.4", "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="],
"@types/bun": ["@types/bun@1.3.14", "https://registry.npmmirror.com/@types/bun/-/bun-1.3.14.tgz", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="],
"@types/d3-array": ["@types/d3-array@3.2.2", "https://registry.npmmirror.com/@types/d3-array/-/d3-array-3.2.2.tgz", {}, "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw=="],
@@ -209,6 +278,8 @@
"@types/estree": ["@types/estree@1.0.9", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.9.tgz", {}, "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg=="],
"@types/jsdom": ["@types/jsdom@28.0.3", "https://registry.npmmirror.com/@types/jsdom/-/jsdom-28.0.3.tgz", { "dependencies": { "@types/node": "*", "@types/tough-cookie": "*", "parse5": "^8.0.0", "undici-types": "^7.21.0" } }, "sha512-/HQ2uFoetFTXuye8vzIcHw2z6Fwi7Hi/qcgC+RoS9NCyewiqxhVGqlG+ViGB6lkax481R6dmhf1I7lIGlzJStQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
@@ -221,29 +292,31 @@
"@types/sortablejs": ["@types/sortablejs@1.15.9", "https://registry.npmmirror.com/@types/sortablejs/-/sortablejs-1.15.9.tgz", {}, "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ=="],
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "https://registry.npmmirror.com/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "https://registry.npmmirror.com/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="],
"@types/validator": ["@types/validator@13.15.10", "https://registry.npmmirror.com/@types/validator/-/validator-13.15.10.tgz", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.2.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/type-utils": "8.59.2", "@typescript-eslint/utils": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ=="],
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.59.3.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/type-utils": "8.59.3", "@typescript-eslint/utils": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.3", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PwFvSKsXGShKGW6n5bZOhGHEcCZXM8HofLK9fNsEwZXzFRjoY+XT1Vsf1zgyXdwTr0ZYz1/2tkZ0DBTT9jZjhw=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.2.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ=="],
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.59.3.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HPwA+hVkfcriajbNvTmZv4VRauibay+cWArYUYq7u7W7PmGShMxbPxLvrwDme55a6d5alG3nrYfhyJ/G28XlLg=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.3.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.3", "@typescript-eslint/types": "^8.59.3", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ECiUWa/KYRGDFUqTNehaRgzDshnJfkTABJxVemHk4ko22gcr0ukloKjWvyQ64g8YCV/UI47kN1dbmjf/GaQYng=="],
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2" } }, "sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.3.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-PcIJHjmaREXLgIAIzLnSY9VucEzz8FKXsRgFa1DmdGCK/5tJpW03TKJF01Q6VZd1lLdz2sIKPWaDUZN9dp//dw=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ=="],
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-g71d8QD8UaiHGvrJwyIS1hCX5r63w6Jll+4VEYhEAHXTDIqX1JgxhTAbEHtKntL9kuc4jRo7/GWw5xfCepSccQ=="],
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.2.tgz", {}, "sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.3.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.3", "@typescript-eslint/tsconfig-utils": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-CbRjVRAf7Lr9Kr8RopKcbY45p2VfmmHrm0ygOCYFi7oU8q19m0Fs/6iHS7kNOmwpp+ob07ZVcAqlxUod9lYdmg=="],
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.2.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1UQF7ggd42YiwI5wGrRaPsa+P0CINBlrkLPmGfpq/u/I/oVtecoEIfFR9ag/oa1sLOsRNZ6xehf6qMZhQGBDg=="],
"@unrs/resolver-binding-android-arm-eabi": ["@unrs/resolver-binding-android-arm-eabi@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw=="],
@@ -283,6 +356,8 @@
"@unrs/resolver-binding-win32-x64-msvc": ["@unrs/resolver-binding-win32-x64-msvc@1.11.1", "https://registry.npmmirror.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.2", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz", { "dependencies": { "@rolldown/pluginutils": "^1.0.0" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg=="],
"@xmldom/xmldom": ["@xmldom/xmldom@0.9.10", "https://registry.npmmirror.com/@xmldom/xmldom/-/xmldom-0.9.10.tgz", {}, "sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw=="],
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
@@ -293,12 +368,14 @@
"ansi-escapes": ["ansi-escapes@7.3.0", "https://registry.npmmirror.com/ansi-escapes/-/ansi-escapes-7.3.0.tgz", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="],
"ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"ansi-styles": ["ansi-styles@5.2.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
"argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-query": ["aria-query@5.3.0", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
"array-ify": ["array-ify@1.0.0", "https://registry.npmmirror.com/array-ify/-/array-ify-1.0.0.tgz", {}, "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng=="],
@@ -321,13 +398,15 @@
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.28", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.28.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-Ic44hnOtFIgravCunj1ifSoQPSUrkNiJuH9Mf6jr2jjoA74icqV8wU0KuadXeOR8zuIJMOoTv0GuQjZ9ZYNMeA=="],
"bidi-js": ["bidi-js@1.0.3", "https://registry.npmmirror.com/bidi-js/-/bidi-js-1.0.3.tgz", { "dependencies": { "require-from-string": "^2.0.2" } }, "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw=="],
"boolbase": ["boolbase@1.0.0", "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="],
"brace-expansion": ["brace-expansion@5.0.6", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.6.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g=="],
"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
"bun-types": ["bun-types@1.3.13", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.13.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-QXKeHLlOLqQX9LgYaHJfzdBaV21T63HhFJnvuRCcjZiaUDpbs5ED1MgxbMra71CsryN/1dAoXuJJJwIv/2drVA=="],
"bun-types": ["bun-types@1.3.14", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.14.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"call-bind": ["call-bind@1.0.9", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
@@ -373,6 +452,8 @@
"css-select": ["css-select@5.2.2", "https://registry.npmmirror.com/css-select/-/css-select-5.2.2.tgz", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="],
"css-tree": ["css-tree@3.2.1", "https://registry.npmmirror.com/css-tree/-/css-tree-3.2.1.tgz", { "dependencies": { "mdn-data": "2.27.1", "source-map-js": "^1.2.1" } }, "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA=="],
"css-what": ["css-what@6.2.2", "https://registry.npmmirror.com/css-what/-/css-what-6.2.2.tgz", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="],
"csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
@@ -399,6 +480,8 @@
"d3-timer": ["d3-timer@3.0.1", "https://registry.npmmirror.com/d3-timer/-/d3-timer-3.0.1.tgz", {}, "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA=="],
"data-urls": ["data-urls@7.0.0", "https://registry.npmmirror.com/data-urls/-/data-urls-7.0.0.tgz", { "dependencies": { "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.0" } }, "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA=="],
"data-view-buffer": ["data-view-buffer@1.0.2", "https://registry.npmmirror.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ=="],
"data-view-byte-length": ["data-view-byte-length@1.0.2", "https://registry.npmmirror.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ=="],
@@ -409,6 +492,8 @@
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
"decimal.js-light": ["decimal.js-light@2.5.1", "https://registry.npmmirror.com/decimal.js-light/-/decimal.js-light-2.5.1.tgz", {}, "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg=="],
"deep-is": ["deep-is@0.1.4", "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="],
@@ -417,8 +502,14 @@
"define-properties": ["define-properties@1.2.1", "https://registry.npmmirror.com/define-properties/-/define-properties-1.2.1.tgz", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg=="],
"dequal": ["dequal@2.0.3", "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"doctrine": ["doctrine@2.1.0", "https://registry.npmmirror.com/doctrine/-/doctrine-2.1.0.tgz", { "dependencies": { "esutils": "^2.0.2" } }, "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw=="],
"dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
"dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
"dom-serializer": ["dom-serializer@2.0.0", "https://registry.npmmirror.com/dom-serializer/-/dom-serializer-2.0.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="],
@@ -439,7 +530,7 @@
"encoding-sniffer": ["encoding-sniffer@0.2.1", "https://registry.npmmirror.com/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="],
"entities": ["entities@4.5.0", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"entities": ["entities@8.0.0", "https://registry.npmmirror.com/entities/-/entities-8.0.0.tgz", {}, "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA=="],
"env-paths": ["env-paths@2.2.1", "https://registry.npmmirror.com/env-paths/-/env-paths-2.2.1.tgz", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
@@ -527,6 +618,8 @@
"for-each": ["for-each@0.3.5", "https://registry.npmmirror.com/for-each/-/for-each-0.3.5.tgz", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"function.prototype.name": ["function.prototype.name@1.1.8", "https://registry.npmmirror.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q=="],
@@ -577,6 +670,8 @@
"hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "https://registry.npmmirror.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="],
"html-encoding-sniffer": ["html-encoding-sniffer@6.0.0", "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", { "dependencies": { "@exodus/bytes": "^1.6.0" } }, "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg=="],
"htmlparser2": ["htmlparser2@10.1.0", "https://registry.npmmirror.com/htmlparser2/-/htmlparser2-10.1.0.tgz", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="],
"husky": ["husky@9.1.7", "https://registry.npmmirror.com/husky/-/husky-9.1.7.tgz", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="],
@@ -637,6 +732,8 @@
"is-plain-obj": ["is-plain-obj@4.1.0", "https://registry.npmmirror.com/is-plain-obj/-/is-plain-obj-4.1.0.tgz", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"is-potential-custom-element-name": ["is-potential-custom-element-name@1.0.1", "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", {}, "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ=="],
"is-regex": ["is-regex@1.2.1", "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
"is-set": ["is-set@2.0.3", "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
@@ -665,6 +762,8 @@
"js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
"jsdom": ["jsdom@29.1.1", "https://registry.npmmirror.com/jsdom/-/jsdom-29.1.1.tgz", { "dependencies": { "@asamuzakjp/css-color": "^5.1.11", "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", "@csstools/css-syntax-patches-for-csstree": "^1.1.3", "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^6.0.1", "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q=="],
"jsesc": ["jsesc@3.1.0", "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-buffer": ["json-buffer@3.0.1", "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", {}, "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ=="],
@@ -681,6 +780,30 @@
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
"lint-staged": ["lint-staged@17.0.4", "https://registry.npmmirror.com/lint-staged/-/lint-staged-17.0.4.tgz", { "dependencies": { "listr2": "^10.2.1", "picomatch": "^4.0.4", "string-argv": "^0.3.2", "tinyexec": "^1.1.2" }, "optionalDependencies": { "yaml": "^2.8.4" }, "bin": { "lint-staged": "bin/lint-staged.js" } }, "sha512-+rU9lSUyVOZ/hDUmRLVGzyS2v73cDdQjX+XQz1AaOdIE4RysLq0HoPW2HrrgeNCLklkhi904VBU1bmgWLHVnkA=="],
@@ -695,10 +818,14 @@
"loose-envify": ["loose-envify@1.4.0", "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lru-cache": ["lru-cache@11.3.6", "https://registry.npmmirror.com/lru-cache/-/lru-cache-11.3.6.tgz", {}, "sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A=="],
"lz-string": ["lz-string@1.5.0", "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"mdn-data": ["mdn-data@2.27.1", "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.27.1.tgz", {}, "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ=="],
"meow": ["meow@13.2.0", "https://registry.npmmirror.com/meow/-/meow-13.2.0.tgz", {}, "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA=="],
"mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
@@ -711,6 +838,8 @@
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.12", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.12.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ=="],
"napi-postinstall": ["napi-postinstall@0.3.4", "https://registry.npmmirror.com/napi-postinstall/-/napi-postinstall-0.3.4.tgz", { "bin": { "napi-postinstall": "lib/cli.js" } }, "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ=="],
"natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="],
@@ -753,7 +882,7 @@
"parse-json": ["parse-json@5.2.0", "https://registry.npmmirror.com/parse-json/-/parse-json-5.2.0.tgz", { "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", "json-parse-even-better-errors": "^2.3.0", "lines-and-columns": "^1.1.6" } }, "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg=="],
"parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5": ["parse5@8.0.1", "https://registry.npmmirror.com/parse5/-/parse5-8.0.1.tgz", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="],
"parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "https://registry.npmmirror.com/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="],
@@ -773,12 +902,16 @@
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "https://registry.npmmirror.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", {}, "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg=="],
"postcss": ["postcss@8.5.14", "https://registry.npmmirror.com/postcss/-/postcss-8.5.14.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg=="],
"prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="],
"prettier": ["prettier@3.8.3", "https://registry.npmmirror.com/prettier/-/prettier-3.8.3.tgz", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"prettier-linter-helpers": ["prettier-linter-helpers@1.0.1", "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg=="],
"pretty-format": ["pretty-format@27.5.1", "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
"prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
@@ -791,7 +924,7 @@
"react-fast-compare": ["react-fast-compare@3.2.2", "https://registry.npmmirror.com/react-fast-compare/-/react-fast-compare-3.2.2.tgz", {}, "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="],
"react-is": ["react-is@19.2.6", "https://registry.npmmirror.com/react-is/-/react-is-19.2.6.tgz", {}, "sha512-XjBR15BhXuylgWGuslhDKqlSayuqvqBX91BP8pauG8kd1zY8kotkNWbXksTCNRarse4kuGbe2kIY05ARtwNIvw=="],
"react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"react-redux": ["react-redux@9.2.0", "https://registry.npmmirror.com/react-redux/-/react-redux-9.2.0.tgz", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="],
@@ -823,6 +956,8 @@
"rfdc": ["rfdc@1.4.1", "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="],
"rolldown": ["rolldown@1.0.1", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.1.tgz", { "dependencies": { "@oxc-project/types": "=0.130.0", "@rolldown/pluginutils": "^1.0.0" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.1", "@rolldown/binding-darwin-arm64": "1.0.1", "@rolldown/binding-darwin-x64": "1.0.1", "@rolldown/binding-freebsd-x64": "1.0.1", "@rolldown/binding-linux-arm-gnueabihf": "1.0.1", "@rolldown/binding-linux-arm64-gnu": "1.0.1", "@rolldown/binding-linux-arm64-musl": "1.0.1", "@rolldown/binding-linux-ppc64-gnu": "1.0.1", "@rolldown/binding-linux-s390x-gnu": "1.0.1", "@rolldown/binding-linux-x64-gnu": "1.0.1", "@rolldown/binding-linux-x64-musl": "1.0.1", "@rolldown/binding-openharmony-arm64": "1.0.1", "@rolldown/binding-wasm32-wasi": "1.0.1", "@rolldown/binding-win32-arm64-msvc": "1.0.1", "@rolldown/binding-win32-x64-msvc": "1.0.1" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-X0KQHljNnEkWNqqiz9zJrGunh1B0HgOxLXvnFpCOcadzcy5qohZ3tqMEUg00vncoRovXuK3ZqCT9KnnKzoInFQ=="],
"safe-array-concat": ["safe-array-concat@1.1.4", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.4.tgz", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg=="],
"safe-push-apply": ["safe-push-apply@1.0.0", "https://registry.npmmirror.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA=="],
@@ -831,6 +966,8 @@
"safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"saxes": ["saxes@6.0.0", "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz", { "dependencies": { "xmlchars": "^2.2.0" } }, "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA=="],
"scheduler": ["scheduler@0.27.0", "https://registry.npmmirror.com/scheduler/-/scheduler-0.27.0.tgz", {}, "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q=="],
"semver": ["semver@6.3.1", "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
@@ -859,6 +996,8 @@
"sortablejs": ["sortablejs@1.15.7", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="],
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stable-hash-x": ["stable-hash-x@0.2.0", "https://registry.npmmirror.com/stable-hash-x/-/stable-hash-x-0.2.0.tgz", {}, "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ=="],
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "https://registry.npmmirror.com/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ=="],
@@ -879,6 +1018,8 @@
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"symbol-tree": ["symbol-tree@3.2.4", "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz", {}, "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw=="],
"synckit": ["synckit@0.11.12", "https://registry.npmmirror.com/synckit/-/synckit-0.11.12.tgz", { "dependencies": { "@pkgr/core": "^0.2.9" } }, "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ=="],
"tdesign-icons-react": ["tdesign-icons-react@0.6.4", "https://registry.npmmirror.com/tdesign-icons-react/-/tdesign-icons-react-0.6.4.tgz", { "dependencies": { "@babel/runtime": "^7.16.5", "classnames": "^2.2.6" }, "peerDependencies": { "react": ">=16.13.1", "react-dom": ">=16.13.1" } }, "sha512-USAoi9vBWcwcJT45VqR3dRqX1MeAsn/RhHVx4bLwplhrlvE80ZQ1N9V+6F3HqE1Qe9mMDbtRM8Ul80+lALScww=="],
@@ -891,6 +1032,14 @@
"tinyglobby": ["tinyglobby@0.2.16", "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.16.tgz", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg=="],
"tldts": ["tldts@7.0.30", "https://registry.npmmirror.com/tldts/-/tldts-7.0.30.tgz", { "dependencies": { "tldts-core": "^7.0.30" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-ELrFxuqsDdHUwoh0XxDbxuLD3Wnz49Z57IFvTtvWy1hJdcMZjXLIuonjilCiWHlT2GbE4Wlv1wKVTzDFnXH1aw=="],
"tldts-core": ["tldts-core@7.0.30", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.30.tgz", {}, "sha512-uiHN8PIB1VmWyS98eZYja4xzlYqeFZVjb4OuYlJQnZAuJhMw4PbKQOKgHKhBdJR3FE/t5mUQ1Kd80++B+qhD1Q=="],
"tough-cookie": ["tough-cookie@6.0.1", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
"tr46": ["tr46@6.0.0", "https://registry.npmmirror.com/tr46/-/tr46-6.0.0.tgz", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw=="],
"ts-api-utils": ["ts-api-utils@2.5.0", "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-2.5.0.tgz", { "peerDependencies": { "typescript": ">=4.8.4" } }, "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA=="],
"tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
@@ -909,13 +1058,13 @@
"typescript": ["typescript@6.0.3", "https://registry.npmmirror.com/typescript/-/typescript-6.0.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
"typescript-eslint": ["typescript-eslint@8.59.2", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.2.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.2", "@typescript-eslint/parser": "8.59.2", "@typescript-eslint/typescript-estree": "8.59.2", "@typescript-eslint/utils": "8.59.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-pJw051uomb3ZeCzGTpRb8RbEqB5Y4WWet8gl/GcTlU35BSx0PVdZ86/bqkQCyKKuraVQEK7r6kBHQXF+fBhkoQ=="],
"typescript-eslint": ["typescript-eslint@8.59.3", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.59.3.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.3", "@typescript-eslint/parser": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3", "@typescript-eslint/utils": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-KgusgyDgG4LI8Ih/sWaCtZ06tckLAS5CvT5A4D1Q7bYVoAAyzwiZvE4BmwDHkhRVkvhRBepKeASoFzQetha7Fg=="],
"unbox-primitive": ["unbox-primitive@1.1.0", "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
"undici": ["undici@7.25.0", "https://registry.npmmirror.com/undici/-/undici-7.25.0.tgz", {}, "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ=="],
"undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"undici-types": ["undici-types@7.25.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.25.0.tgz", {}, "sha512-AXNgS1Byr27fTI+2bsPEkV9CxkT8H6xNyRI68b3TatlZo3RkzlqQBLL+w7SmGPVpokjHbcuNVQUWE7FRTg+LRA=="],
"unrs-resolver": ["unrs-resolver@1.11.1", "https://registry.npmmirror.com/unrs-resolver/-/unrs-resolver-1.11.1.tgz", { "dependencies": { "napi-postinstall": "^0.3.0" }, "optionalDependencies": { "@unrs/resolver-binding-android-arm-eabi": "1.11.1", "@unrs/resolver-binding-android-arm64": "1.11.1", "@unrs/resolver-binding-darwin-arm64": "1.11.1", "@unrs/resolver-binding-darwin-x64": "1.11.1", "@unrs/resolver-binding-freebsd-x64": "1.11.1", "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", "@unrs/resolver-binding-linux-x64-musl": "1.11.1", "@unrs/resolver-binding-wasm32-wasi": "1.11.1", "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" } }, "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg=="],
@@ -929,9 +1078,17 @@
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
"vite": ["vite@8.0.13", "https://registry.npmmirror.com/vite/-/vite-8.0.13.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.14", "rolldown": "1.0.1", "tinyglobby": "^0.2.16" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.18", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MFtjBYgzmSxmgA4RAfjIyXWpGe1oALnjgUTzzV7QLx/TKxCzjtMH6Fd9/eVK+5Fg1qNoz5VAwsmMs/NofrmJvw=="],
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
"webidl-conversions": ["webidl-conversions@8.0.1", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-8.0.1.tgz", {}, "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ=="],
"whatwg-encoding": ["whatwg-encoding@3.1.1", "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="],
"whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"whatwg-mimetype": ["whatwg-mimetype@5.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", {}, "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw=="],
"whatwg-url": ["whatwg-url@16.0.1", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-16.0.1.tgz", { "dependencies": { "@exodus/bytes": "^1.11.0", "tr46": "^6.0.0", "webidl-conversions": "^8.0.1" } }, "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw=="],
"which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
@@ -947,6 +1104,10 @@
"wrap-ansi": ["wrap-ansi@10.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-10.0.0.tgz", { "dependencies": { "ansi-styles": "^6.2.3", "string-width": "^8.2.0", "strip-ansi": "^7.1.2" } }, "sha512-SGcvg80f0wUy2/fXES19feHMz8E0JoXv2uNgHOu4Dgi2OrCy1lqwFYEJz1BLbDI0exjPMe/ZdzZ/YpGECBG/aQ=="],
"xml-name-validator": ["xml-name-validator@5.0.0", "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz", {}, "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg=="],
"xmlchars": ["xmlchars@2.2.0", "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz", {}, "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw=="],
"xpath": ["xpath@0.0.34", "https://registry.npmmirror.com/xpath/-/xpath-0.0.34.tgz", {}, "sha512-FxF6+rkr1rNSQrhUNYrAFJpRXNzlDoMxeXN5qI84939ylEv3qqPFKa85Oxr6tDaJKqwW6KKyo2v26TSv3k6LeA=="],
"y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -967,6 +1128,8 @@
"@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@commitlint/is-ignored/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"@conventional-changelog/git-client/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
@@ -981,16 +1144,48 @@
"@reduxjs/toolkit/immer": ["immer@11.1.8", "https://registry.npmmirror.com/immer/-/immer-11.1.8.tgz", {}, "sha512-/tbkHMW7y10Lx6i1crLjD4/OhNkRG+Fo7byZHtah0547nIeXYcpIXaUh0IAQY6gO5459qpGGYapcEOHtFXkIuA=="],
"@rolldown/binding-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.4", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow=="],
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"@types/node/undici-types": ["undici-types@7.19.2", "https://registry.npmmirror.com/undici-types/-/undici-types-7.19.2.tgz", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
"@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/parser/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/project-service/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"@typescript-eslint/type-utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"@typescript-eslint/typescript-estree/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-8.59.2.tgz", { "dependencies": { "@typescript-eslint/project-service": "8.59.2", "@typescript-eslint/tsconfig-utils": "8.59.2", "@typescript-eslint/types": "8.59.2", "@typescript-eslint/visitor-keys": "8.59.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg=="],
"@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"cheerio/parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"cheerio/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
"cli-truncate/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"cliui/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"dom-serializer/entities": ["entities@4.5.0", "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="],
"eslint/ajv": ["ajv@6.15.0", "https://registry.npmmirror.com/ajv/-/ajv-6.15.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw=="],
"eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
@@ -1013,20 +1208,60 @@
"log-update/wrap-ansi": ["wrap-ansi@9.0.2", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-9.0.2.tgz", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww=="],
"parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"parse5-htmlparser2-tree-adapter/parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"parse5-parser-stream/parse5": ["parse5@7.3.0", "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"pretty-format/react-is": ["react-is@17.0.2", "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="],
"prop-types/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="],
"tdesign-react/@babel/runtime": ["@babel/runtime@7.26.10", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.10.tgz", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
"tdesign-react/react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
"typescript-eslint/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-8.59.3.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.3", "@typescript-eslint/types": "8.59.3", "@typescript-eslint/typescript-estree": "8.59.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAvT14goBzRzzzZyqq3P9BLArIxTtQURUtFgQ/V7FO+eU+Gg6ES+5ymOPP1wRxXcxAYeivCk4uS3jCKWI1K8Zg=="],
"wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"wrap-ansi/string-width": ["string-width@8.2.1", "https://registry.npmmirror.com/string-width/-/string-width-8.2.1.tgz", { "dependencies": { "get-east-asian-width": "^1.5.0", "strip-ansi": "^7.1.2" } }, "sha512-IIaP0g3iy9Cyy18w3M9YcaDudujEAVHKt3a3QJg1+sr/oX96TbaGUubG0hJyCjCBThFH+tFpcIyoUHUn1ogaLA=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/eslint-plugin/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"@typescript-eslint/type-utils/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/project-service/-/project-service-8.59.2.tgz", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.2", "@typescript-eslint/types": "^8.59.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.59.2.tgz", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.59.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA=="],
"@typescript-eslint/utils/@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "https://registry.npmmirror.com/semver/-/semver-7.8.0.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA=="],
"cheerio/parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"eslint-plugin-import/minimatch/brace-expansion": ["brace-expansion@1.1.14", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.14.tgz", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g=="],
"eslint/ajv/json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
"log-update/slice-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"log-update/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
"parse5-htmlparser2-tree-adapter/parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"parse5-parser-stream/parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-8.59.3.tgz", { "dependencies": { "@typescript-eslint/types": "8.59.3", "@typescript-eslint/visitor-keys": "8.59.3" } }, "sha512-t2LvZnoEfzKtnPjgeEu41xw5gxq9mQVfYy4OoZ4Vlt0sk3JwxmhCca/AR7DwOiHrjWgjAj6as4AhRLKSDfvZIA=="],
"typescript-eslint/@typescript-eslint/utils/@typescript-eslint/types": ["@typescript-eslint/types@8.59.3", "https://registry.npmmirror.com/@typescript-eslint/types/-/types-8.59.3.tgz", {}, "sha512-ePFoH0g4ludssdRFqqDxQePCxU4WQyRa9+XVwjm7yLn0FKhMeoetC+qBEEI1Eyb1pGSDveTIT09Bvw2WhlGayg=="],
"eslint-plugin-import/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}

3
bunfig.toml Normal file
View File

@@ -0,0 +1,3 @@
[test]
preload = ["./tests/setup.ts"]
exclude = ["./tests/e2e/**"]

View File

@@ -5,16 +5,16 @@
## Requirements
### Requirement: 声明式路由注册
系统 SHALL 使用 Bun.serve 的 `routes` 对象以声明式方式注册所有 HTTP 路由,包括 HTML 页面路由和 API 端点路由。
#### Scenario: HTML 页面路由注册
- **WHEN** server 启动时
- **THEN** 系统 SHALL 通过 HTML import 将前端入口注册到 `routes` 对象的 `"/*"` 通配符路径
系统 SHALL 使用 Bun.serve 的 `routes` 对象以声明式方式注册 API 端点路由,非 API 请求由 `fetch` fallback 处理
#### Scenario: API 端点路由注册
- **WHEN** server 启动时
- **THEN** 系统 SHALL 将所有 API 端点以 method handler 对象形式注册到 `routes` 对象
#### Scenario: 非 API 请求处理
- **WHEN** 请求路径不匹配任何 `routes` 中注册的路由
- **THEN** `fetch` fallback SHALL 将请求交给静态资源服务处理production或返回提示文本development
### Requirement: 路径参数支持
系统 SHALL 使用 routes 对象的 `:param` 语法声明路径参数,替代手动 regex 匹配。
@@ -38,12 +38,12 @@
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
### Requirement: Fetch Fallback 处理
系统 SHALL 使用 `fetch` handler 作为兜底,理论上不应被触发(所有路径都被 routes 通配符覆盖)
系统 SHALL 使用 `fetch` handler 作为非 API 请求的入口,负责静态资源服务和 SPA fallback
#### Scenario: 未匹配的 API 路由
- **WHEN** 请求路径以 `/api/` 开头但未在具体 API 路由中注册
- **THEN** `/api/*` 通配符 SHALL 返回 JSON 格式的 404 错误响应
#### Scenario: Production 模式 fetch fallback
- **WHEN** production 模式下请求未匹配 routes 中的 API 路由
- **THEN** `fetch` handler SHALL 调用 `serveStaticAsset` 返回对应静态资源或 SPA fallback
#### Scenario: 未匹配的非 API 路由
- **WHEN** 请求路径不以 `/api/` 开头且未在具体路由中注册
- **THEN** `"/*": homepage` 通配符 SHALL 返回前端入口 HTML 文档(带 HMR 注入)
#### Scenario: Development 模式 fetch fallback
- **WHEN** development 模式下请求未匹配 routes 中的 API 路由
- **THEN** `fetch` handler SHALL 返回提示文本,引导开发者通过 Vite dev server 访问前端

View File

@@ -101,12 +101,19 @@
#### Scenario: 运行快速检查
- **WHEN** 开发者运行 `bun run check`
- **THEN** 系统 SHALL 依次执行类型检查、lint含格式检查)和单元测试
- **THEN** 系统 SHALL 依次执行 schema 检查、类型检查、lint含格式和单元/组件测试(`bun test`
#### Scenario: 快速检查失败
- **WHEN** `check` 中任一子检查失败
- **THEN** `check` MUST 以非零状态退出且不静默忽略失败
### Requirement: 分层测试运行命令
项目 SHALL 提供分层的测试运行命令,支持按需执行不同层级的测试。
#### Scenario: 运行全部单元和组件测试
- **WHEN** 开发者运行 `bun test`
- **THEN** 系统 SHALL 执行 `tests/` 目录下所有 `*.test.ts``*.test.tsx` 文件
### Requirement: 完整验证命令
项目 SHALL 提供完整 `verify` 命令,用于提交前或发布前验证当前源码、测试和生产构建。原 executable smoke test 暂时移除,后续通过独立变更重新设计。

View File

@@ -0,0 +1,72 @@
## Purpose
定义前端组件测试基础设施和覆盖要求,确保所有 React 组件的渲染、交互和状态流转行为经过验证。
## Requirements
### Requirement: jsdom 测试环境配置
项目 SHALL 通过 `tests/setup.ts` preload 脚本为组件测试提供 jsdom DOM 环境,包含 TDesign 组件所需的浏览器 API polyfill。
#### Scenario: 组件测试可以渲染 React 组件
- **WHEN** 组件测试文件使用 `@testing-library/react``render` 函数渲染组件
- **THEN** 组件 SHALL 在 jsdom 环境中正常渲染,可通过 `screen` 查询 DOM 元素
#### Scenario: TDesign 组件依赖的浏览器 API 可用
- **WHEN** 组件测试渲染使用了 TDesign 组件Table、Drawer、Skeleton 等)的业务组件
- **THEN** jsdom 环境 SHALL 提供 `ResizeObserver``IntersectionObserver``window.matchMedia` 等 polyfill不因缺失 API 而抛错
#### Scenario: recharts 图表组件被 mock
- **WHEN** 组件测试渲染包含 recharts 图表的组件
- **THEN** recharts 模块 SHALL 被 mock 为简单占位元素,不依赖 SVG 渲染能力
### Requirement: 组件测试使用 @testing-library/react
项目 SHALL 使用 `@testing-library/react` 作为组件测试工具,遵循"测试用户行为而非实现细节"的原则。
#### Scenario: 通过用户可见内容查询元素
- **WHEN** 测试需要查找页面元素
- **THEN** 测试 SHALL 优先使用 `getByText``getByRole``getByLabelText` 等语义化查询,而非 CSS 选择器或 testId
#### Scenario: 通过用户交互触发行为
- **WHEN** 测试需要模拟用户操作
- **THEN** 测试 SHALL 使用 `fireEvent``userEvent` 模拟点击、输入等操作,而非直接调用组件内部方法
### Requirement: 所有前端组件 SHALL 有组件测试覆盖
项目 SHALL 为 `src/web/components/` 下的每个组件和 `src/web/app.tsx` 提供对应的组件测试文件。
#### Scenario: 纯展示组件测试
- **WHEN** 组件为纯展示组件(如 StatusDot、SummaryCards
- **THEN** 测试 SHALL 验证给定 props 时渲染正确的文本和结构,以及 null/空数据时的条件渲染
#### Scenario: 交互组件测试
- **WHEN** 组件包含用户交互(如 TargetDetailDrawer 的 Tab 切换、RefreshCountdown 的按钮点击)
- **THEN** 测试 SHALL 验证交互触发正确的回调函数调用和参数传递
#### Scenario: 条件渲染测试
- **WHEN** 组件根据 loading/error/empty 状态展示不同内容(如 OverviewTab
- **THEN** 测试 SHALL 覆盖所有条件分支loading skeleton、正常数据渲染、空数据占位
#### Scenario: 数据驱动组件测试
- **WHEN** 组件接收列表数据渲染(如 TargetBoard 的分组、HistoryTab 的表格)
- **THEN** 测试 SHALL 验证数据正确映射到 UI 元素,包括空列表和多项数据的情况
### Requirement: 组件测试的 Mock 边界
组件测试 SHALL 只 mock 系统边界(网络请求),不 mock 内部实现。
#### Scenario: Mock fetch 而非 React Query
- **WHEN** 组件通过 `@tanstack/react-query` 发起数据请求
- **THEN** 测试 SHALL mock `globalThis.fetch` 返回预设响应,使用真实的 `QueryClientProvider` 包裹组件
#### Scenario: 不 mock TDesign 组件
- **WHEN** 业务组件使用 TDesign 组件
- **THEN** 测试 SHALL 真实渲染 TDesign 组件,验证 props 传递和集成行为的正确性
### Requirement: 组件测试文件组织
组件测试文件 SHALL 位于 `tests/web/components/` 目录下,文件名与组件名对应。
#### Scenario: 测试文件命名
- **WHEN** 为 `src/web/components/TargetBoard.tsx` 编写组件测试
- **THEN** 测试文件 SHALL 位于 `tests/web/components/TargetBoard.test.tsx`
#### Scenario: App 组件测试位置
- **WHEN** 为 `src/web/app.tsx` 编写组件测试
- **THEN** 测试文件 SHALL 位于 `tests/web/components/App.test.tsx`

View File

@@ -1,41 +1,45 @@
## Purpose
定义 Bun.serve fullstack + React + TypeScript 前端开发工作流、开发期 API 访问和共享契约的行为要求。
定义基于 Vite dev server + Bun API server 的前端开发工作流、开发期 API 访问和共享契约的行为要求。
## Requirements
### Requirement: Vite React 开发服务器
系统 SHALL 提供基于 Bun.serve fullstack 模式的前端开发工作流,支持热模块替换和 React Fast Refresh。
系统 SHALL 提供基于 Vite dev server 的前端开发工作流,支持热模块替换和 React Fast Refresh。
#### Scenario: 启动前端开发服务器
- **WHEN** 开发者启动开发命令
- **THEN** 前端 SHALL 由 Bun.serve 的 HTML import 机制提供服务,并通过 `development: { hmr: true, console: true }` 启用 HMRReact Fast Refresh 和浏览器 console 回显
- **THEN** 前端 SHALL 由 Vite dev server 提供服务,支持 HMRReact Fast Refresh,监听 :5173 端口
#### Scenario: 构建前端静态资源
- **WHEN** 开发者运行前端生产构建命令
- **THEN** 系统 SHALL 通过 Bun.build 的 HTML import ahead-of-time bundling 产出可由 Bun 后端服务的前端静态资源
- **THEN** 系统 SHALL 通过 Vite buildRolldown产出优化的前端静态资源到 `dist/web/`
### Requirement: 前端开发期 API 代理
前端开发服务器 SHALL 在本地开发期间无需代理配置即可访问后端 API因为前后端运行在同一进程同一端口
前端开发服务器 SHALL 通过 Vite proxy 配置将 API 请求转发到后端 server
#### Scenario: 前端开发期调用拨测 API
- **WHEN** 浏览器从开发服务器请求 `/api/summary``/api/targets` 等拨测 API
- **THEN** Bun.serve SHALL 直接由 routes 中注册的 API handler 处理请求,无需 proxy 转发
- **WHEN** 浏览器从 Vite dev server 请求 `/api/*` 路径
- **THEN** Vite SHALL 通过 proxy 将请求转发到 Bun API server默认 :3000
#### Scenario: 前端开发期访问健康检查
- **WHEN** 浏览器从 Vite dev server 请求 `/health`
- **THEN** Vite SHALL 通过 proxy 将请求转发到 Bun API server
#### Scenario: 开发期访问非 API 前端路由
- **WHEN** 浏览器从开发服务器请求非 API 前端路由
- **THEN** Bun.serve SHALL 将该请求作为前端应用流量处理SPA fallback 返回 HTML
- **WHEN** 浏览器从 Vite dev server 请求非 API 前端路由
- **THEN** Vite SHALL 将该请求作为前端应用流量处理SPA fallback 返回 HTML
### Requirement: 开发期单端口运行
项目 SHALL 保证开发命令中前端页面、HMR 和后端 API 由同一个 Bun.serve 进程在同一端口提供服务
### Requirement: 开发期双进程运行
项目 SHALL 开发命令中同时启动 Vite dev server 和 Bun API server 两个进程
#### Scenario: 使用默认开发端口
- **WHEN** 开发者未提供端口覆盖并运行开发命令
- **THEN** Bun.serve SHALL 在默认端口同时提供前端页面、HMR 和后端 API
- **WHEN** 开发者运行开发命令
- **THEN** Vite dev server SHALL 监听 :5173Bun API server SHALL 监听配置文件指定的端口(默认 :3000
#### Scenario: 使用配置覆盖开发端口
- **WHEN** 开发者通过配置文件覆盖端口并运行开发命令
- **THEN** Bun.serve SHALL 在配置端口同时提供前端页面、HMR 和后端 API
#### Scenario: 开发者访问前端
- **WHEN** 开发者打开浏览器
- **THEN** 开发者 SHALL 访问 Vite dev server 地址(:5173获取前端页面
### Requirement: 前端使用相对 API 路径
除非有文档化的部署配置覆盖该行为,前端代码 MUST 通过相对 `/api/*` URL 调用后端 API。
@@ -46,21 +50,14 @@
#### Scenario: 运行环境变化
- **WHEN** host 或 port 在开发环境和生产环境之间变化
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作
- **THEN** 前端 API 调用 SHALL 无需修改源码即可继续工作(开发期通过 Vite proxy生产期通过同源请求
### Requirement: 集成开发命令
项目 SHALL 提供一个文档化命令,用于在开发期间同时运行前端和后端
项目 SHALL 提供一个文档化命令,用于在开发期间同时运行 Vite dev server 和 Bun API server
#### Scenario: 启动全栈开发
- **WHEN** 开发者运行文档化的全栈开发命令
- **THEN** 系统 SHALL 启动单个 Bun.serve 进程,同时提供前端 HMR 和后端 API 服务
### Requirement: 开发质量命令文档化
项目 SHALL 在前端开发工作流文档中说明日常检查和完整验证命令。
#### Scenario: 查阅开发命令
- **WHEN** 开发者阅读 README 的开发或测试章节
- **THEN** 文档 SHALL 说明 `check` 用于日常开发检查,`verify` 用于提交前或发布前完整验证
- **THEN** 系统 SHALL 同时启动 Vite dev server 和 Bun API server任一进程异常退出时终止另一个
### Requirement: 共享 TypeScript 契约
项目 SHALL 为前端和后端共同使用的请求与响应类型提供共享 TypeScript 边界。

View File

@@ -1,6 +1,6 @@
## Purpose
定义 Bun 全栈应用运行时 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
定义基于 Vite + Bun 全栈应用运行时,包括 HTTP server、API 命名空间、健康检查、生产静态资源服务和 SPA fallback 行为。
## Requirements
@@ -64,26 +64,26 @@
- **THEN** Bun server SHALL 返回成功的、机器可读的健康检查响应
### Requirement: 生产静态资源服务
系统 SHALL 在生产模式下通过 Bun 内置的 HTML import manifest 机制服务前端资源
系统 SHALL 在生产模式下通过自定义 `serveStaticAsset` 函数服务嵌入的 Vite 前端产出
#### Scenario: 请求构建后的资源
- **WHEN** 客户端请求构建后的前端资源
- **THEN** Bun server SHALL 通过 manifest 自动返回该资源并带有适当的 content type 和 content-addressable hash URL
- **WHEN** 客户端请求 `/assets/*` 路径下的前端资源
- **THEN** 系统 SHALL 从 StaticAssets 的 files map 中查找并返回对应资源Content-Type 根据扩展名推断
#### Scenario: 请求前端根路径
- **WHEN** 客户端请求 `/`
- **THEN** Bun server SHALL 通过 routes 中注册的 HTML import 返回前端入口 HTML 文档
- **THEN** 系统 SHALL 返回 StaticAssets 中的 indexHtmlContent-Type 为 `text/html; charset=utf-8`
### Requirement: 生产缓存策略
系统 SHALL 利用 Bun 内置的缓存机制为生产静态资源提供缓存策略。
系统 SHALL 为生产静态资源提供基于文件名 content hash 的缓存策略。
#### Scenario: 请求前端入口 HTML
- **WHEN** 生产 Bun server 返回前端入口 HTML 文档
- **THEN** 响应 SHALL 包含 Bun 自动生成的 ETag header
- **WHEN** 生产 server 返回前端入口 HTML 文档
- **THEN** 响应 SHALL 包含 `Cache-Control: no-cache` header
#### Scenario: 请求构建后的静态资源
- **WHEN** 生产 Bun server 返回构建后的静态资源
- **THEN** 响应 SHALL 包含 Bun 自动生成的 ETag header 和 content-addressable hash URL
- **WHEN** 生产 server 返回 `/assets/*` 路径下的静态资源
- **THEN** 响应 SHALL 包含 `Cache-Control: public, max-age=31536000, immutable` header
### Requirement: 低风险安全响应头
系统 SHALL 在生产运行时的 JSON API 响应中附加低风险安全响应头HTML 和静态资源响应由 Bun HTML import manifest 返回其内置 headers。
@@ -92,20 +92,20 @@
- **WHEN** 生产 Bun server 返回 `/health``/api/*` JSON 响应
- **THEN** 响应 SHALL 包含 `X-Content-Type-Options: nosniff``Referrer-Policy` headers
#### Scenario: 生产 HTML 和静态资源响应使用 Bun 内置 headers
- **WHEN** 生产 Bun server 返回前端 HTML 文档或构建后的静态资源
- **THEN** 响应 SHALL 使用 Bun HTML import manifest 提供的内置 headers不要求附加自定义安全 headers
#### Scenario: 生产静态资源响应
- **WHEN** 生产 server 返回前端 HTML 文档或构建后的静态资源
- **THEN** 响应 SHALL 不要求附加自定义安全 headers(仅需 Content-Type 和 Cache-Control
### Requirement: SPA fallback 行为
系统 SHALL 通过 routes 中注册的 `"/*"` HTML import 通配符为非 API 路径返回前端入口 HTML 文档。
系统 SHALL 通过 fetch fallback 为非 API、非静态资源路径返回前端入口 HTML 文档。
#### Scenario: 刷新前端路由
- **WHEN** 客户端请求前端路由,例`/dashboard`
- **THEN** routes 中的 `"/*"` 通配符 SHALL 返回前端入口 HTML 文档
- **WHEN** 客户端请求不包含文件扩展名的非 API 路径(`/dashboard`
- **THEN** fetch fallback SHALL 返回前端入口 HTML 文档
#### Scenario: 保留 API 错误语义
- **WHEN** 客户端请求未知的 `/api/*` 路由
- **THEN** `/api/*` 通配符 MUST 返回 JSON 404 响应,而不是前端入口 HTML 文档
- **THEN** routes 中的 `/api/*` 通配符 MUST 返回 JSON 404 响应,而不是前端入口 HTML 文档
### Requirement: 优雅关机
系统 SHALL 在收到终止信号时正确清理资源。

View File

@@ -20,7 +20,15 @@ HeadMenu operations 区域 SHALL 提供 RadioGroup 组件供用户选择刷新
- **THEN** 系统 SHALL 立即触发一次数据刷新,然后应用新的刷新间隔
### Requirement: 倒计时显示
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时文本。
RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时文本。倒计时逻辑 SHALL 封装在独立的 `RefreshCountdown` 组件中App 组件 SHALL NOT 持有每秒更新的 `now` state。
#### Scenario: RefreshCountdown 组件封装
- **WHEN** Dashboard 页面渲染
- **THEN** 倒计时显示 SHALL 由独立的 `RefreshCountdown` 组件负责,该组件内部持有 `now` state 和每秒 `setInterval`,渲染边界限制在该组件内部
#### Scenario: RefreshCountdown props
- **WHEN** RefreshCountdown 组件渲染
- **THEN** 组件 SHALL 接收 `dashboardUpdatedAt: number``refreshInterval: number``isFetching: boolean``isManualRefresh: boolean``onRefresh: () => void` 作为 props
#### Scenario: 短时间格式
- **WHEN** 距下次刷新剩余时间小于 60 秒
@@ -38,6 +46,17 @@ RadioGroup 右侧 SHALL 显示距下次自动刷新的倒计时文本。
- **WHEN** 数据正在刷新isFetching=true 且 isLoading=false
- **THEN** 倒计时文本 SHALL 显示为"刷新中..."
### Requirement: App 组件渲染隔离
App 组件 SHALL NOT 持有任何高频更新的 state如每秒更新的时钟确保 App 的重渲染频率与数据刷新频率一致(默认 30 秒一次)。
#### Scenario: App 无 now state
- **WHEN** App 组件渲染
- **THEN** App SHALL NOT 包含 `useState` 管理的时钟 state也 SHALL NOT 包含每秒触发的 `setInterval`
#### Scenario: App 重渲染频率
- **WHEN** Dashboard 处于自动刷新模式
- **THEN** App 组件的重渲染 SHALL 仅由 TanStack Query 的 refetch 触发(频率等于用户选择的刷新间隔),而非每秒触发
### Requirement: 手动刷新按钮
选择"手动"模式时,倒计时区域 SHALL 替换为刷新按钮。

View File

@@ -5,15 +5,15 @@ TBD - 统一服务启动引导函数,封装开发和生产模式的完整启
## Requirements
### Requirement: 统一启动引导函数
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。`bootstrap` SHALL 不接收或传递静态资源对象,前端资源由 Bun HTML import manifest 自动接管。
系统 SHALL 提供 `src/server/bootstrap.ts` 导出 `bootstrap(options: BootstrapOptions)` 函数,封装完整的服务启动序列:加载配置、创建 store、同步 targets、创建并启动 engine、启动 HTTP server、注册 shutdown handler。
#### Scenario: 开发模式启动
- **WHEN** `dev.ts` 调用 `bootstrap({ configPath, mode: "development" })`
- **THEN** 系统 SHALL 完成完整启动序列,不传入 staticAssets
#### Scenario: 生产模式启动
- **WHEN** `main.ts` 调用 `bootstrap({ configPath, mode: "production" })`
- **THEN** 系统 SHALL 完成完整启动序列,并`server.ts` 中的 HTML import 路由接管前端资源
#### Scenario: 生产模式启动(带静态资源)
- **WHEN** code-generated entry 调用 `bootstrap({ configPath, mode: "production", staticAssets })`
- **THEN** 系统 SHALL 完成完整启动序列,并将 staticAssets 传递给 startServer
#### Scenario: 启动失败处理
- **WHEN** 启动过程中任何步骤抛出异常
@@ -24,11 +24,15 @@ TBD - 统一服务启动引导函数,封装开发和生产模式的完整启
- **THEN** bootstrap 注册的 shutdown handler SHALL 调用 engine.stop() 和 store.close() 后退出
### Requirement: BootstrapOptions 接口
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string``mode: RuntimeMode`
`bootstrap` 函数 SHALL 接受 `BootstrapOptions` 参数,包含 `configPath: string``mode: RuntimeMode` 和可选的 `staticAssets?: StaticAssets`
#### Scenario: 最小配置
#### Scenario: 最小配置(开发模式)
- **WHEN** 仅传入 configPath 和 mode
- **THEN** 系统 SHALL 正常启动
- **THEN** 系统 SHALL 正常启动startServer 不接收 staticAssets 参数
#### Scenario: 生产模式配置
- **WHEN** 传入 configPath、mode 和 staticAssets
- **THEN** 系统 SHALL 将 staticAssets 传递给 startServer
### Requirement: dev.ts 和生产入口使用 bootstrap
`dev.ts``src/server/main.ts` SHALL 调用 `bootstrap()` 而非各自维护启动序列。

View File

@@ -1,22 +1,26 @@
## Purpose
定义将 Bun HTML import 前端资源与 Bun 后端打包为单个 standalone executable 的生产构建、运行配置和验证要求。
定义将 Vite 构建的前端资源通过 code generation 嵌入 Bun 后端打包为单个 standalone executable 的生产构建、运行配置和验证要求。
## Requirements
### Requirement: 生产构建顺序
生产构建 MUST 通过 Bun.build 的 HTML import 识别机制一步完成前端资源打包和后端编译
生产构建 MUST 通过三步流水线完成Vite 前端构建 → code generation → Bun compile
#### Scenario: 运行生产构建
- **WHEN** 开发者运行生产构建命令
- **THEN** 系统 MUST 调用 Bun.build自动识别 server 入口中的 HTML import 并完成前端 bundling 和后端编译
- **THEN** 系统 MUST 依次执行 Vite build、资源导入 code generation、Bun.build compile最终输出单可执行文件
#### Scenario: 前端 bundling 失败
- **WHEN** Bun.build 在处理 HTML import 中的前端资源时失败
- **THEN** 系统 MUST 停止生产构建,且不能输出 stale executable
#### Scenario: Vite 构建失败
- **WHEN** Vite build 步骤失败
- **THEN** 系统 MUST 停止后续步骤,不生成 code generation 文件或 executable
#### Scenario: Bun compile 失败
- **WHEN** Bun.build compile 步骤失败
- **THEN** 系统 MUST 清理 `.build/` 临时目录,不保留 stale executable
### Requirement: 单 executable 输出
生产构建 SHALL 输出一个 standalone executable其中包含 Bun 后端和通过 HTML import manifest 嵌入的前端资源。构建流程 SHALL 不再生成项目自定义中间产物目录,构建失败时 SHALL 不保留 stale executable
生产构建 SHALL 输出一个 standalone executable其中包含 Bun 后端和通过 `import with { type: "file" }` 嵌入的 Vite 前端产出
#### Scenario: 在目标机器运行 executable
- **WHEN** 生成的 executable 在兼容目标平台上运行
@@ -24,19 +28,22 @@
#### Scenario: 服务嵌入的前端
- **WHEN** executable 收到前端根路径请求
- **THEN** 它 SHALL 通过 Bun 内置的 HTML import manifest 机制服务前端资源,且不需要外部 `dist/` 目录
- **THEN** 它 SHALL 通过内嵌的 Vite 构建产出服务前端资源,且不需要外部 `dist/` 目录
#### Scenario: 服务嵌入 API 和页面
- **WHEN** 生成的 executable 启动,且浏览器打开前端根路径
- **THEN** 页面 SHALL 展示同一个 executable 进程中 `/api/summary``/api/targets` 返回的数据
#### Scenario: 构建成功不生成自定义中间产物
- **WHEN** 生产构建成功完成并输出 executable
- **THEN** 系统 SHALL 不生成 `.build/` 静态资源清单或 server entry 中间产物
### Requirement: 构建中间产物管理
构建流程 SHALL 使用 `.build/` 临时目录存放 code generation 产物,构建完成后清理。
#### Scenario: 构建失败时不保留 stale executable
- **WHEN** 生产构建在任意步骤失败
- **THEN** 系统 SHALL 不输出上一次构建遗留的 stale executable
#### Scenario: 构建成功后清理中间产物
- **WHEN** 生产构建成功完成并输出 executable
- **THEN** 系统 SHALL 删除 `.build/` 临时目录
#### Scenario: 构建失败时清理中间产物
- **WHEN** 生产构建在 Bun compile 步骤失败
- **THEN** 系统 SHALL 删除 `.build/` 临时目录和 stale executable
### Requirement: 外部运行时配置
executable MUST 将环境相关运行时配置保留在嵌入的前端和 server bundle 之外。

View File

@@ -0,0 +1,58 @@
# Static Asset Embedding
定义构建时将 Vite 产出的前端静态资源嵌入 Bun 可执行文件的 code generation 流程和运行时静态资源服务逻辑。
## Purpose
支持将 Vite 构建的前端资源通过 `import with { type: "file" }` 嵌入 Bun 可执行文件,实现单文件交付的同时保持正确的缓存策略和 Content-Type 处理。
## Requirements
### Requirement: 构建时资源扫描与 Code Generation
构建脚本 SHALL 在 Vite build 完成后扫描 `dist/web/` 目录,自动生成 TypeScript 文件,为每个静态资源创建 `import ... with { type: "file" }` 声明。
#### Scenario: 生成资源导入文件
- **WHEN** 构建脚本扫描 `dist/web/` 目录
- **THEN** 系统 SHALL 在 `.build/static-assets.ts` 中为每个文件生成 `import fN from "<path>" with { type: "file" }` 语句,并导出 `StaticAssets` 对象
#### Scenario: StaticAssets 对象结构
- **WHEN** `static-assets.ts` 被生成
- **THEN** 导出的对象 SHALL 包含 `indexHtml: Blob``files: Record<string, Blob>` 两个字段,其中 files 的 key 为 URL 路径(如 `/assets/index-a1b2c3.js`
#### Scenario: 生成 production server entry
- **WHEN** 构建脚本生成资源导入文件后
- **THEN** 系统 SHALL 在 `.build/server-entry.ts` 中生成 production 入口import bootstrap、config 和 staticAssets 并调用 bootstrap
### Requirement: 运行时静态资源服务
系统 SHALL 提供 `serveStaticAsset` 函数,根据请求路径从 StaticAssets 中查找并返回对应资源。
#### Scenario: 请求根路径
- **WHEN** 请求路径为 `/`
- **THEN** 系统 SHALL 返回 `indexHtml`Content-Type 为 `text/html; charset=utf-8`Cache-Control 为 `no-cache`
#### Scenario: 请求已知静态资源
- **WHEN** 请求路径匹配 `files` 中的某个 key
- **THEN** 系统 SHALL 返回对应 BlobContent-Type 根据文件扩展名推断Cache-Control 为 `public, max-age=31536000, immutable`
#### Scenario: 请求未知带扩展名路径
- **WHEN** 请求路径包含文件扩展名但未匹配任何已知资源
- **THEN** 系统 SHALL 返回 404 响应
#### Scenario: SPA Fallback
- **WHEN** 请求路径不包含文件扩展名且不以 `/api/` 开头
- **THEN** 系统 SHALL 返回 `indexHtml`SPA fallback
### Requirement: Content-Type 推断
系统 SHALL 根据文件扩展名推断正确的 Content-Type header。
#### Scenario: JavaScript 文件
- **WHEN** 请求路径以 `.js``.mjs` 结尾
- **THEN** Content-Type SHALL 为 `text/javascript; charset=utf-8`
#### Scenario: CSS 文件
- **WHEN** 请求路径以 `.css` 结尾
- **THEN** Content-Type SHALL 为 `text/css; charset=utf-8`
#### Scenario: SVG 文件
- **WHEN** 请求路径以 `.svg` 结尾
- **THEN** Content-Type SHALL 为 `image/svg+xml`

View File

@@ -72,6 +72,36 @@ TrendChart 组件 SHALL 仅接收数据 props不处理 loading 状态。
- **WHEN** TrendChart 接收空数组
- **THEN** 组件 SHALL 显示"暂无趋势数据"占位文本
#### Scenario: TrendChart memo 包裹
- **WHEN** TrendChart 的父组件重渲染但 data prop 引用未变
- **THEN** TrendChart SHALL 跳过重渲染(通过 React.memo shallow compare
#### Scenario: chartData useMemo
- **WHEN** TrendChart 渲染
- **THEN** 内部 `chartData` 转换结果 SHALL 通过 `useMemo` 缓存,依赖为 `[data]`data 引用不变时不重新计算
### Requirement: TargetBoard 分组 memoize
TargetBoard 组件的分组计算 SHALL 使用 useMemo 缓存,避免 targets 引用不变时重复计算分组。
#### Scenario: 分组结果 useMemo
- **WHEN** TargetBoard 渲染
- **THEN** 分组逻辑Map 构建 + sortSHALL 通过 `useMemo` 缓存,依赖为 `[targets]`
#### Scenario: targets 引用不变时跳过分组
- **WHEN** TargetBoard 因父组件重渲染而重渲染,但 targets prop 引用未变
- **THEN** 分组计算 SHALL 返回缓存结果,不重新执行 Map 构建和排序
### Requirement: TargetGroup 渲染优化
TargetGroup 组件 SHALL 使用 React.memo 包裹,在 props 引用不变时跳过重渲染。
#### Scenario: TargetGroup memo 包裹
- **WHEN** TargetBoard 重渲染但某个分组的 targets 数组引用未变
- **THEN** 对应的 TargetGroup SHALL 跳过重渲染(通过 React.memo shallow compare
#### Scenario: TargetGroup props 稳定性
- **WHEN** TargetGroup 渲染
- **THEN** 其 propscolumns、name、targets、onTargetClickSHALL 全部具有引用稳定性columns 通过 useMemo、name 为 string 原始值、targets 通过分组 useMemo、onTargetClick 通过 useCallback
### Requirement: StatusBar 参数化
StatusBar 组件 SHALL 支持可配置的格数。
@@ -189,7 +219,7 @@ Drawer 内部 SHALL 使用 TDesign Tabs 组织概览和记录两个面板。Tabs
#### Scenario: 统计区左右布局卡片
- **WHEN** 概览面板渲染且有统计数据
- **THEN** 面板 SHALL 在"统计"区域使用 4 列 × 2 行的 Row/Col 布局,每个统计项使用 Card 包裹Card 内标题左对齐、数值右对齐,数值使用普通文本字号
- **THEN** 面板 SHALL 在"统计"区域使用 4 列 × 2 行的 Row/Col 布局,每个统计项使用 `<div className="overview-stat-card">` 包裹,通过 CSS 类实现背景色和内边距视觉效果
#### Scenario: 统计区内容
- **WHEN** 概览面板渲染

View File

@@ -0,0 +1,46 @@
# Vite Frontend Bundling
定义 Vite 作为前端构建工具的配置、产出结构和优化策略。
## Purpose
使用 Vite 的 Rolldown 引擎完成前端打包,实现 code splitting、vendor chunk 分离和 CSS 优化,解决 Bun bundler 前端性能问题。
## Requirements
### Requirement: Vite 前端构建配置
系统 SHALL 使用 Vite 作为前端构建工具,配置文件位于项目根目录 `vite.config.ts`,以 `src/web` 为 root产出到 `dist/web/`
#### Scenario: 运行 Vite 生产构建
- **WHEN** 构建脚本执行 `bunx --bun vite build`
- **THEN** Vite SHALL 将 `src/web/index.html` 及其引用的所有模块构建到 `dist/web/` 目录,包含 `index.html``assets/` 子目录
#### Scenario: 产出文件名包含 content hash
- **WHEN** Vite 构建完成
- **THEN** `assets/` 目录下的 JS 和 CSS 文件名 SHALL 包含 content hash`index-a1b2c3.js`
### Requirement: Code Splitting 策略
系统 SHALL 配置 Vite 的 Rolldown code splitting将 vendor 库分离为独立 chunks。
#### Scenario: React 相关库分离
- **WHEN** Vite 构建完成
- **THEN** `react``react-dom``scheduler` SHALL 被打包到名为 `vendor-react` 的独立 chunk
#### Scenario: TDesign 相关库分离
- **WHEN** Vite 构建完成
- **THEN** `tdesign-react``tdesign-icons-react` 相关模块 SHALL 被打包到名为 `vendor-tdesign` 的独立 chunk
#### Scenario: 图表库分离
- **WHEN** Vite 构建完成
- **THEN** `recharts``d3-*` 相关模块 SHALL 被打包到名为 `vendor-chart` 的独立 chunk
### Requirement: CSS 处理
系统 SHALL 通过 Vite 处理 CSS 导入,产出独立的 CSS 文件。
#### Scenario: CSS 文件产出
- **WHEN** Vite 构建完成
- **THEN** 所有 CSS 导入 SHALL 被提取为独立的 `.css` 文件到 `assets/` 目录
#### Scenario: CSS 压缩
- **WHEN** Vite 执行生产构建
- **THEN** 产出的 CSS 文件 SHALL 经过压缩处理

View File

@@ -3,7 +3,9 @@
"type": "module",
"private": true,
"scripts": {
"dev": "bun --watch src/server/dev.ts",
"dev": "bun run scripts/dev.ts",
"dev:server": "bun --watch src/server/dev.ts",
"dev:web": "bunx --bun vite --host",
"build": "bun run scripts/build.ts",
"lint": "eslint .",
"format": "prettier . --write",
@@ -17,13 +19,16 @@
"prepare": "husky"
},
"devDependencies": {
"@commitlint/cli": "^21.0.0",
"@commitlint/config-conventional": "^21.0.0",
"@commitlint/cli": "^21.0.1",
"@commitlint/config-conventional": "^21.0.1",
"@eslint/js": "^10.0.1",
"@tanstack/react-query-devtools": "^5.100.10",
"@types/bun": "^1.3.13",
"@testing-library/react": "^16.3.2",
"@types/bun": "^1.3.14",
"@types/jsdom": "^28.0.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.2",
"eslint": "^10.3.0",
"eslint-config-prettier": "^10.1.8",
"eslint-import-resolver-typescript": "^4.4.4",
@@ -33,10 +38,12 @@
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"husky": "^9.1.7",
"jsdom": "^29.1.1",
"lint-staged": "^17.0.4",
"prettier": "^3.8.3",
"typescript": "^6.0.3",
"typescript-eslint": "^8.59.2"
"typescript-eslint": "^8.59.3",
"vite": "^8.0.13"
},
"dependencies": {
"@sinclair/typebox": "^0.34.49",

View File

@@ -1,33 +1,151 @@
import { rm } from "node:fs/promises";
import { readdir, rm, writeFile } from "node:fs/promises";
import { join, relative } from "node:path";
import { fileURLToPath } from "node:url";
const executablePath = fileURLToPath(new URL("../dist/dial-server", import.meta.url));
const entrypoint = fileURLToPath(new URL("../src/server/main.ts", import.meta.url));
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
const distWebDir = join(projectRoot, "dist/web");
const buildDir = join(projectRoot, ".build");
const executablePath = join(projectRoot, "dist/dial-server");
await rm(executablePath, { force: true });
const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"];
const result = await Bun.build({
compile: target
? {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
target: target as Bun.Build.CompileTarget,
}
: {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
},
entrypoints: [entrypoint],
minify: true,
sourcemap: "linked",
});
if (!result.success) {
console.error("构建失败:", result.logs);
process.exit(1);
async function build() {
try {
await viteBuild();
await codeGeneration();
await bunCompile();
await cleanup();
console.log(`Built executable: ${executablePath}`);
} catch (error) {
await cleanup();
console.error("Build failed:", error);
process.exit(1);
}
}
console.log(`Built executable: ${executablePath}`);
async function bunCompile() {
console.log("Step 3/3: Bun compile...");
await rm(executablePath, { force: true });
const target = process.env["BUN_TARGET"] ?? process.env["BUILD_TARGET"];
const result = await Bun.build({
compile: target
? {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
target: target as Bun.Build.CompileTarget,
}
: {
autoloadBunfig: true,
autoloadDotenv: true,
outfile: executablePath,
},
entrypoints: [join(buildDir, "server-entry.ts")],
minify: true,
sourcemap: "linked",
});
if (!result.success) {
console.error("Bun compile failed:", result.logs);
await cleanup();
process.exit(1);
}
}
async function cleanup() {
await rm(buildDir, { force: true, recursive: true });
}
async function codeGeneration() {
console.log("Step 2/3: Code generation...");
await rm(buildDir, { force: true, recursive: true });
await Bun.write(join(buildDir, ".gitkeep"), "");
const allFiles = await scanDir(distWebDir, "/");
const importLines: string[] = [];
const fileEntries: string[] = [];
let indexHtmlVar = "";
for (let i = 0; i < allFiles.length; i++) {
const urlPath = allFiles[i]!;
const varName = `f${i}`;
const filePath = relative(buildDir, join(distWebDir, urlPath.slice(1)));
importLines.push(`import ${varName} from "./${filePath}" with { type: "file" };`);
if (urlPath === "/index.html") {
indexHtmlVar = varName;
} else {
fileEntries.push(` "${urlPath}": Bun.file(${varName}),`);
}
}
if (!indexHtmlVar) {
console.error("index.html not found in dist/web/");
process.exit(1);
}
const staticAssetsTs = [
`import type { StaticAssets } from "../src/server/static";`,
"",
...importLines,
"",
`export const staticAssets: StaticAssets = {`,
` files: {`,
...fileEntries,
` },`,
` indexHtml: Bun.file(${indexHtmlVar}),`,
`};`,
"",
].join("\n");
await writeFile(join(buildDir, "static-assets.ts"), staticAssetsTs);
const serverEntryTs = [
`import { bootstrap } from "../src/server/bootstrap";`,
`import { readRuntimeConfig } from "../src/server/config";`,
`import { staticAssets } from "./static-assets";`,
"",
`async function main() {`,
` const { configPath } = readRuntimeConfig();`,
` await bootstrap({ configPath, mode: "production", staticAssets });`,
`}`,
"",
`void main().catch((error) => {`,
` console.error("启动失败:", error instanceof Error ? error.message : error);`,
` process.exit(1);`,
`});`,
"",
].join("\n");
await writeFile(join(buildDir, "server-entry.ts"), serverEntryTs);
}
async function scanDir(dir: string, prefix: string): Promise<string[]> {
const entries = await readdir(dir, { withFileTypes: true });
const paths: string[] = [];
for (const entry of entries) {
const fullPath = join(dir, entry.name);
const urlPath = `${prefix}${entry.name}`;
if (entry.isDirectory()) {
paths.push(...(await scanDir(fullPath, `${urlPath}/`)));
} else {
paths.push(urlPath);
}
}
return paths;
}
async function viteBuild() {
console.log("Step 1/3: Vite build...");
const proc = Bun.spawn(["bunx", "--bun", "vite", "build"], {
cwd: projectRoot,
stderr: "inherit",
stdout: "inherit",
});
const exitCode = await proc.exited;
if (exitCode !== 0) {
console.error("Vite build failed");
process.exit(1);
}
}
await build();

26
scripts/dev.ts Normal file
View File

@@ -0,0 +1,26 @@
import { fileURLToPath } from "node:url";
const projectRoot = fileURLToPath(new URL("..", import.meta.url));
const apiServer = Bun.spawn(["bun", "--watch", "src/server/dev.ts", ...process.argv.slice(2)], {
cwd: projectRoot,
stderr: "inherit",
stdout: "inherit",
});
const viteServer = Bun.spawn(["bunx", "--bun", "vite", "--host"], {
cwd: projectRoot,
stderr: "inherit",
stdout: "inherit",
});
function shutdown() {
apiServer.kill();
viteServer.kill();
}
process.on("SIGINT", shutdown);
process.on("SIGTERM", shutdown);
await Promise.race([apiServer.exited, viteServer.exited]);
shutdown();

View File

@@ -2,6 +2,7 @@ import { join } from "node:path";
import type { RuntimeMode } from "../shared/api";
import type { StartServerOptions } from "./server";
import type { StaticAssets } from "./static";
import { loadConfig, type ResolvedConfig } from "./checker/config-loader";
import { ProbeEngine } from "./checker/engine";
@@ -26,6 +27,7 @@ export interface BootstrapDependencies {
export interface BootstrapOptions {
configPath: string;
mode: RuntimeMode;
staticAssets?: StaticAssets;
}
type BootstrapEngine = Pick<ProbeEngine, "start" | "stop">;
@@ -69,6 +71,7 @@ export async function bootstrap(options: BootstrapOptions, dependencies: Bootstr
serve({
config: { host: config.host, port: config.port },
mode: options.mode,
staticAssets: options.staticAssets,
store,
});
} catch (error) {

View File

@@ -1,33 +1,36 @@
import type { RuntimeMode } from "../shared/api";
import type { ProbeStore } from "./checker/store";
import type { RuntimeConfig } from "./config";
import type { StaticAssets } from "./static";
import homepage from "../web/index.html";
import { createApiError, jsonResponse } from "./helpers";
import { handleDashboard } from "./routes/dashboard";
import { handleHealth } from "./routes/health";
import { handleHistory } from "./routes/history";
import { handleMeta } from "./routes/meta";
import { handleMetrics } from "./routes/metrics";
import { serveStaticAsset } from "./static";
export interface StartServerOptions {
config: RuntimeConfig;
mode: RuntimeMode;
staticAssets?: StaticAssets;
store: ProbeStore;
}
export function startServer(options: StartServerOptions) {
const { config, mode, store } = options;
const { config, mode, staticAssets, store } = options;
const server = Bun.serve({
development: mode === "development" ? { console: true, hmr: true } : false,
fetch() {
return new Response("Not found", { status: 404 });
fetch(req) {
if (staticAssets) {
return serveStaticAsset(new URL(req.url).pathname, staticAssets);
}
return new Response("Frontend is served by Vite dev server on :5173", { status: 404 });
},
hostname: config.host,
port: config.port,
routes: {
"/*": homepage,
"/api/*": () => jsonResponse(createApiError("API route not found", 404), { mode, status: 404 }),
"/api/dashboard": {
GET: (req) => handleDashboard(new URL(req.url), store, mode),

60
src/server/static.ts Normal file
View File

@@ -0,0 +1,60 @@
export interface StaticAssets {
files: Record<string, Blob>;
indexHtml: Blob;
}
const CONTENT_TYPES: Record<string, string> = {
".css": "text/css; charset=utf-8",
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".json": "application/json; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".png": "image/png",
".svg": "image/svg+xml",
".woff": "font/woff",
".woff2": "font/woff2",
};
export function contentTypeFor(path: string): string {
const dot = path.lastIndexOf(".");
if (dot === -1) return "application/octet-stream";
const ext = path.slice(dot);
return CONTENT_TYPES[ext] ?? "application/octet-stream";
}
export function hasFileExtension(path: string): boolean {
const lastSlash = path.lastIndexOf("/");
const segment = lastSlash === -1 ? path : path.slice(lastSlash + 1);
return segment.includes(".");
}
export function htmlResponse(html: Blob): Response {
return new Response(html, {
headers: {
"Cache-Control": "no-cache",
"Content-Type": "text/html; charset=utf-8",
},
});
}
export function serveStaticAsset(pathname: string, assets: StaticAssets): Response {
if (pathname === "/") {
return htmlResponse(assets.indexHtml);
}
const file = assets.files[pathname];
if (file) {
return new Response(file, {
headers: {
"Cache-Control": "public, max-age=31536000, immutable",
"Content-Type": contentTypeFor(pathname),
},
});
}
if (hasFileExtension(pathname)) {
return new Response("Not found", { status: 404 });
}
return htmlResponse(assets.indexHtml);
}

View File

@@ -1,15 +1,14 @@
import type { SkeletonProps } from "tdesign-react";
import { useEffect, useState } from "react";
import { RefreshIcon } from "tdesign-icons-react";
import { Alert, Button, Layout, Menu, RadioGroup, Skeleton, Typography } from "tdesign-react";
import { useState } from "react";
import { Alert, Layout, Menu, RadioGroup, Skeleton } from "tdesign-react";
import { RefreshCountdown } from "./components/RefreshCountdown";
import { SummaryCards } from "./components/SummaryCards";
import { TargetBoard } from "./components/TargetBoard";
import { TargetDetailDrawer } from "./components/TargetDetailDrawer";
import { useDashboard } from "./hooks/use-queries";
import { useTargetDetail } from "./hooks/use-target-detail";
import { formatCountdown } from "./utils/time";
const { Content, Header } = Layout;
const DEFAULT_REFRESH_INTERVAL_MS = 30000;
@@ -27,7 +26,6 @@ const REFRESH_OPTIONS = [
] as const;
export function App() {
const [now, setNow] = useState(() => new Date());
const [refreshInterval, setRefreshInterval] = useState(DEFAULT_REFRESH_INTERVAL_MS);
const dashboardRefetchInterval = refreshInterval === 0 ? false : refreshInterval;
const {
@@ -54,27 +52,12 @@ export function App() {
timeTo,
} = useTargetDetail();
const isManualRefresh = refreshInterval === 0;
const nextRefreshSeconds =
dashboardUpdatedAt > 0 && !isManualRefresh
? Math.max(0, Math.ceil((dashboardUpdatedAt + refreshInterval - now.getTime()) / 1000))
: null;
const refreshText =
dashboardUpdatedAt > 0
? dashboardFetching && !dashboardLoading
? "刷新中..."
: formatCountdown(nextRefreshSeconds ?? 0)
: "等待首次刷新";
const handleIntervalChange = (value: number) => {
void refetchDashboard();
setRefreshInterval(value);
};
useEffect(() => {
const timer = window.setInterval(() => setNow(new Date()), 1000);
return () => window.clearInterval(timer);
}, []);
return (
<Layout className="dashboard">
<Header>
@@ -95,19 +78,13 @@ export function App() {
variant="default-filled"
/>
<span className="dashboard-countdown">
{isManualRefresh ? (
<Button
aria-label="刷新 Dashboard"
disabled={dashboardFetching}
icon={<RefreshIcon />}
loading={dashboardFetching}
onClick={() => void refetchDashboard()}
shape="circle"
variant="outline"
/>
) : (
<Typography.Text theme="secondary">{refreshText}</Typography.Text>
)}
<RefreshCountdown
dashboardUpdatedAt={dashboardUpdatedAt}
isFetching={dashboardFetching && !dashboardLoading}
isManualRefresh={isManualRefresh}
onRefresh={() => void refetchDashboard()}
refreshInterval={refreshInterval}
/>
</span>
</div>
}

View File

@@ -1,6 +1,6 @@
import type { ReactNode } from "react";
import { Card, Col, Descriptions, Divider, Row, Skeleton, Space, Statistic, Typography } from "tdesign-react";
import { Col, Descriptions, Divider, Row, Skeleton, Space, Statistic, Typography } from "tdesign-react";
import type { TargetMetricsResponse, TargetStatus } from "../../shared/api";
@@ -97,11 +97,11 @@ export function OverviewTab({ metricsData, metricsLoading, target }: OverviewTab
function OverviewStatItem({ color, suffix, title, value }: OverviewStatItemProps) {
return (
<Card bordered={false} className="overview-stat-card" size="small">
<div className="overview-stat-card">
<div className="overview-stat-item">
<Typography.Text theme="secondary">{title}</Typography.Text>
<Statistic className="overview-stat-value" color={color} suffix={suffix} value={value} />
</div>
</Card>
</div>
);
}

View File

@@ -0,0 +1,52 @@
import { useEffect, useState } from "react";
import { RefreshIcon } from "tdesign-icons-react";
import { Button, Typography } from "tdesign-react";
import { formatCountdown } from "../utils/time";
interface RefreshCountdownProps {
dashboardUpdatedAt: number;
isFetching: boolean;
isManualRefresh: boolean;
onRefresh: () => void;
refreshInterval: number;
}
export function RefreshCountdown({
dashboardUpdatedAt,
isFetching,
isManualRefresh,
onRefresh,
refreshInterval,
}: RefreshCountdownProps) {
const [now, setNow] = useState(() => new Date());
useEffect(() => {
const timer = window.setInterval(() => setNow(new Date()), 1000);
return () => window.clearInterval(timer);
}, []);
const nextRefreshSeconds =
dashboardUpdatedAt > 0 && !isManualRefresh
? Math.max(0, Math.ceil((dashboardUpdatedAt + refreshInterval - now.getTime()) / 1000))
: null;
if (isManualRefresh) {
return (
<Button
aria-label="刷新 Dashboard"
disabled={isFetching}
icon={<RefreshIcon />}
loading={isFetching}
onClick={() => void onRefresh()}
shape="circle"
variant="outline"
/>
);
}
const refreshText =
dashboardUpdatedAt > 0 ? (isFetching ? "刷新中..." : formatCountdown(nextRefreshSeconds ?? 0)) : "等待首次刷新";
return <Typography.Text theme="secondary">{refreshText}</Typography.Text>;
}

View File

@@ -19,21 +19,22 @@ export function TargetBoard({ onTargetClick, targets }: TargetBoardProps) {
const checkerTypes = meta?.checkerTypes ?? EMPTY_CHECKER_TYPES;
const columns = useMemo(() => createTargetTableColumns(checkerTypes), [checkerTypes]);
const groups = new Map<string, TargetStatus[]>();
for (const target of targets) {
const group = target.group;
const list = groups.get(group);
if (list) {
list.push(target);
} else {
groups.set(group, [target]);
const sortedGroups = useMemo(() => {
const groups = new Map<string, TargetStatus[]>();
for (const target of targets) {
const group = target.group;
const list = groups.get(group);
if (list) {
list.push(target);
} else {
groups.set(group, [target]);
}
}
}
const sortedGroups = Array.from(groups.entries()).sort(([a]) => {
if (a === "default") return -1;
return 0;
});
return Array.from(groups.entries()).sort(([a]) => {
if (a === "default") return -1;
return 0;
});
}, [targets]);
return (
<Space className="full-width" direction="vertical" size={24}>

View File

@@ -1,5 +1,6 @@
import type { PrimaryTableCol } from "tdesign-react";
import { memo } from "react";
import { Card, PrimaryTable, Space, Tag } from "tdesign-react";
import type { TargetStatus } from "../../shared/api";
@@ -11,7 +12,7 @@ interface TargetGroupProps {
targets: TargetStatus[];
}
export function TargetGroup({ columns, name, onTargetClick, targets }: TargetGroupProps) {
export const TargetGroup = memo(function TargetGroup({ columns, name, onTargetClick, targets }: TargetGroupProps) {
const up = targets.filter((t) => t.latestCheck?.matched).length;
const down = targets.length - up;
const displayName = name === "default" ? "默认分组" : name;
@@ -48,4 +49,4 @@ export function TargetGroup({ columns, name, onTargetClick, targets }: TargetGro
/>
</Card>
);
}
});

View File

@@ -1,3 +1,4 @@
import { memo, useMemo } from "react";
import { Area, CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis } from "recharts";
import type { TrendPoint } from "../../shared/api";
@@ -12,18 +13,24 @@ interface TrendChartProps {
data: TrendPoint[];
}
export function TrendChart({ data }: TrendChartProps) {
export const TrendChart = memo(function TrendChart({ data }: TrendChartProps) {
const chartData = useMemo(
() =>
data.map((point) => ({
...point,
durationRange:
point.minDurationMs !== null && point.maxDurationMs !== null
? [point.minDurationMs, point.maxDurationMs]
: null,
label: formatBucketLabel(point.bucketStart),
})),
[data],
);
if (data.length === 0) {
return <div className="trend-empty"></div>;
}
const chartData = data.map((point) => ({
...point,
durationRange:
point.minDurationMs !== null && point.maxDurationMs !== null ? [point.minDurationMs, point.maxDurationMs] : null,
label: formatBucketLabel(point.bucketStart),
}));
return (
<div className="trend-chart">
<ResponsiveContainer height={240} width="100%">
@@ -69,7 +76,7 @@ export function TrendChart({ data }: TrendChartProps) {
</ResponsiveContainer>
</div>
);
}
});
function formatBucketLabel(bucketStart: string): string {
return new Date(bucketStart).toLocaleTimeString("zh-CN", { hour: "2-digit", hour12: false, minute: "2-digit" });

View File

@@ -172,6 +172,8 @@
.overview-stat-card {
background: var(--td-bg-color-container-hover);
border-radius: var(--td-radius-default);
padding: var(--td-comp-paddingTB-xl) var(--td-comp-paddingLR-xl);
}
.overview-stat-item {

View File

@@ -0,0 +1,109 @@
import { describe, expect, test } from "bun:test";
import { createApiError, createHeaders, formatDuration, jsonResponse } from "../../src/server/helpers";
describe("createApiError", () => {
test("创建错误响应对象", () => {
const result = createApiError("Not found", 404);
expect(result).toEqual({ error: "Not found", status: 404 });
});
test("支持不同的错误消息和状态码", () => {
const badRequest = createApiError("Bad request", 400);
const internalError = createApiError("Internal error", 500);
expect(badRequest).toEqual({ error: "Bad request", status: 400 });
expect(internalError).toEqual({ error: "Internal error", status: 500 });
});
});
describe("createHeaders", () => {
test("生产模式添加安全 headers", () => {
const headers = createHeaders("production", { "Content-Type": "application/json" });
expect(headers.get("X-Content-Type-Options")).toBe("nosniff");
expect(headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
expect(headers.get("Content-Type")).toBe("application/json");
});
test("非生产模式不添加安全 headers", () => {
const headers = createHeaders("test", { "Content-Type": "application/json" });
expect(headers.get("X-Content-Type-Options")).toBeNull();
expect(headers.get("Referrer-Policy")).toBeNull();
expect(headers.get("Content-Type")).toBe("application/json");
});
test("保留传入的自定义 headers", () => {
const headers = createHeaders("production", { "X-Custom-Header": "custom-value" });
expect(headers.get("X-Custom-Header")).toBe("custom-value");
});
});
describe("jsonResponse", () => {
test("创建 JSON 响应", () => {
const body = { message: "Hello" };
const response = jsonResponse(body, { mode: "test" });
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("application/json; charset=utf-8");
});
test("生产模式响应包含安全 headers", () => {
const response = jsonResponse({ data: "test" }, { mode: "production" });
expect(response.headers.get("X-Content-Type-Options")).toBe("nosniff");
expect(response.headers.get("Referrer-Policy")).toBe("strict-origin-when-cross-origin");
});
test("支持自定义状态码", () => {
const response = jsonResponse({ error: "Not found" }, { mode: "test", status: 404 });
expect(response.status).toBe(404);
});
test("支持自定义 headers", () => {
const response = jsonResponse(
{ data: "test" },
{
headers: { "X-Custom": "value" },
mode: "test",
},
);
expect(response.headers.get("X-Custom")).toBe("value");
});
test("响应 body 可以被解析为 JSON", async () => {
const body = { count: 42, message: "Hello" };
const response = jsonResponse(body, { mode: "test" });
const parsed = (await response.json()) as { count: number; message: string };
expect(parsed).toEqual(body);
});
});
describe("formatDuration", () => {
test("毫秒格式化", () => {
expect(formatDuration(100)).toBe("100ms");
expect(formatDuration(999)).toBe("999ms");
});
test("秒格式化(整秒)", () => {
expect(formatDuration(1000)).toBe("1s");
expect(formatDuration(5000)).toBe("5s");
expect(formatDuration(59000)).toBe("59s");
});
test("分钟格式化(整分钟)", () => {
expect(formatDuration(60000)).toBe("1m");
expect(formatDuration(120000)).toBe("2m");
expect(formatDuration(300000)).toBe("5m");
});
test("非整秒/整分钟保持毫秒", () => {
expect(formatDuration(1500)).toBe("1500ms");
expect(formatDuration(61123)).toBe("61123ms");
});
});

View File

@@ -0,0 +1,171 @@
import { describe, expect, test } from "bun:test";
import {
validateDashboardWindow,
validateMetricsBucket,
validatePagination,
validateRecentLimit,
validateTargetId,
validateTimeRange,
} from "../../src/server/middleware";
describe("validateTargetId", () => {
test("有效的 target ID 返回数字", () => {
const result = validateTargetId("123", "production");
expect(result).not.toHaveProperty("status");
expect((result as { id: number }).id).toBe(123);
});
test("无效的 target ID 返回 400", () => {
const invalid = ["0", "-1", "abc", "1.5", ""];
for (const id of invalid) {
const result = validateTargetId(id, "production");
expect(result).toHaveProperty("status", 400);
}
});
});
describe("validateTimeRange", () => {
test("有效的 from/to 返回 ISO 字符串", () => {
const result = validateTimeRange("2024-01-01T00:00:00.000Z", "2024-01-02T00:00:00.000Z", "production");
expect(result).not.toHaveProperty("status");
expect((result as { from: string; to: string }).from).toBe("2024-01-01T00:00:00.000Z");
expect((result as { from: string; to: string }).to).toBe("2024-01-02T00:00:00.000Z");
});
test("缺失 from 或 to 返回 400", () => {
const missingFrom = validateTimeRange(null, "2024-01-02T00:00:00.000Z", "production");
const missingTo = validateTimeRange("2024-01-01T00:00:00.000Z", null, "production");
const missingBoth = validateTimeRange(null, null, "production");
expect(missingFrom).toHaveProperty("status", 400);
expect(missingTo).toHaveProperty("status", 400);
expect(missingBoth).toHaveProperty("status", 400);
});
test("空字符串 from 或 to 返回 400", () => {
const emptyFrom = validateTimeRange("", "2024-01-02T00:00:00.000Z", "production");
const emptyTo = validateTimeRange("2024-01-01T00:00:00.000Z", "", "production");
expect(emptyFrom).toHaveProperty("status", 400);
expect(emptyTo).toHaveProperty("status", 400);
});
test("无效的日期格式返回 400", () => {
const result = validateTimeRange("invalid-date", "2024-01-02T00:00:00.000Z", "production");
expect(result).toHaveProperty("status", 400);
});
test("from 晚于 to 返回 400", () => {
const result = validateTimeRange("2024-01-02T00:00:00.000Z", "2024-01-01T00:00:00.000Z", "production");
expect(result).toHaveProperty("status", 400);
});
});
describe("validatePagination", () => {
test("默认值page=1, pageSize=20", () => {
const result = validatePagination(null, null, "production");
expect(result).toEqual({ page: 1, pageSize: 20 });
});
test("有效的 page 和 pageSize 参数", () => {
const result = validatePagination("2", "50", "production");
expect(result).toEqual({ page: 2, pageSize: 50 });
});
test("无效的 page 参数返回 400", () => {
const invalidPage = ["0", "-1", "abc", "1.5"];
for (const page of invalidPage) {
const result = validatePagination(page, "20", "production");
expect(result).toHaveProperty("status", 400);
}
});
test("无效的 pageSize 参数返回 400", () => {
const invalidPageSize = ["0", "-1", "abc", "1.5"];
for (const pageSize of invalidPageSize) {
const result = validatePagination("1", pageSize, "production");
expect(result).toHaveProperty("status", 400);
}
});
test("pageSize 超过上限返回 400", () => {
const result = validatePagination("1", "201", "production");
expect(result).toHaveProperty("status", 400);
});
test("pageSize 等于上限 200 返回成功", () => {
const result = validatePagination("1", "200", "production");
expect(result).toEqual({ page: 1, pageSize: 200 });
});
});
describe("validateRecentLimit", () => {
test("默认值recentLimit=30", () => {
const result = validateRecentLimit(null, "production");
expect(result).toEqual({ recentLimit: 30 });
});
test("有效的 recentLimit 参数", () => {
const result = validateRecentLimit("50", "production");
expect(result).toEqual({ recentLimit: 50 });
});
test("无效的 recentLimit 参数返回 400", () => {
const invalid = ["0", "-1", "abc", "1.5"];
for (const limit of invalid) {
const result = validateRecentLimit(limit, "production");
expect(result).toHaveProperty("status", 400);
}
});
test("recentLimit 超过上限返回 400", () => {
const result = validateRecentLimit("201", "production");
expect(result).toHaveProperty("status", 400);
});
test("recentLimit 等于上限 200 返回成功", () => {
const result = validateRecentLimit("200", "production");
expect(result).toEqual({ recentLimit: 200 });
});
});
describe("validateDashboardWindow", () => {
test("默认值window=24h", () => {
const result = validateDashboardWindow(null, "production");
expect(result).not.toHaveProperty("status");
expect((result as { label: string }).label).toBe("24h");
});
test("window=24h 返回成功", () => {
const result = validateDashboardWindow("24h", "production");
expect(result).not.toHaveProperty("status");
expect((result as { label: string }).label).toBe("24h");
});
test("不支持的 window 参数返回 400", () => {
const result = validateDashboardWindow("7d", "production");
expect(result).toHaveProperty("status", 400);
});
});
describe("validateMetricsBucket", () => {
test("默认值bucket=1h", () => {
const result = validateMetricsBucket(null, "production");
expect(result).toEqual({ bucket: "1h" });
});
test("bucket=1h 返回成功", () => {
const result = validateMetricsBucket("1h", "production");
expect(result).toEqual({ bucket: "1h" });
});
test("不支持的 bucket 参数返回 400", () => {
const result = validateMetricsBucket("5m", "production");
expect(result).toHaveProperty("status", 400);
});
});

127
tests/server/static.test.ts Normal file
View File

@@ -0,0 +1,127 @@
import { describe, expect, test } from "bun:test";
import {
contentTypeFor,
hasFileExtension,
htmlResponse,
serveStaticAsset,
type StaticAssets,
} from "../../src/server/static";
function createTestAssets(): StaticAssets {
return {
files: {
"/assets/index-a1b2c3.css": new Blob([".app{}"], { type: "text/css" }),
"/assets/index-a1b2c3.js": new Blob(["console.log(1)"], { type: "text/javascript" }),
"/assets/vendor-react-x9y8z7.js": new Blob(["react"], { type: "text/javascript" }),
"/favicon.svg": new Blob(["<svg/>"], { type: "image/svg+xml" }),
},
indexHtml: new Blob(["<!doctype html><html></html>"], { type: "text/html" }),
};
}
describe("contentTypeFor", () => {
test("JavaScript 文件", () => {
expect(contentTypeFor("/assets/index-a1b2c3.js")).toBe("text/javascript; charset=utf-8");
});
test("mjs 文件", () => {
expect(contentTypeFor("/assets/chunk.mjs")).toBe("text/javascript; charset=utf-8");
});
test("CSS 文件", () => {
expect(contentTypeFor("/assets/style.css")).toBe("text/css; charset=utf-8");
});
test("SVG 文件", () => {
expect(contentTypeFor("/icon.svg")).toBe("image/svg+xml");
});
test("未知扩展名返回 octet-stream", () => {
expect(contentTypeFor("/file.xyz")).toBe("application/octet-stream");
});
test("无扩展名返回 octet-stream", () => {
expect(contentTypeFor("/noext")).toBe("application/octet-stream");
});
});
describe("hasFileExtension", () => {
test("有扩展名", () => {
expect(hasFileExtension("/assets/index.js")).toBe(true);
expect(hasFileExtension("/favicon.svg")).toBe(true);
});
test("无扩展名", () => {
expect(hasFileExtension("/dashboard")).toBe(false);
expect(hasFileExtension("/")).toBe(false);
expect(hasFileExtension("/api/targets")).toBe(false);
});
});
describe("htmlResponse", () => {
test("返回 HTML 响应带正确 headers", async () => {
const blob = new Blob(["<html></html>"]);
const response = htmlResponse(blob);
expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
expect(response.headers.get("Cache-Control")).toBe("no-cache");
expect(await response.text()).toBe("<html></html>");
});
});
describe("serveStaticAsset", () => {
test("根路径返回 indexHtml", async () => {
const assets = createTestAssets();
const response = serveStaticAsset("/", assets);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
expect(response.headers.get("Cache-Control")).toBe("no-cache");
expect(await response.text()).toBe("<!doctype html><html></html>");
});
test("已知资源返回对应文件和 immutable 缓存", async () => {
const assets = createTestAssets();
const response = serveStaticAsset("/assets/index-a1b2c3.js", assets);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/javascript; charset=utf-8");
expect(response.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
expect(await response.text()).toBe("console.log(1)");
});
test("未知带扩展名路径返回 404", () => {
const assets = createTestAssets();
const response = serveStaticAsset("/assets/missing.js", assets);
expect(response.status).toBe(404);
});
test("SPA fallback — 无扩展名路径返回 indexHtml", async () => {
const assets = createTestAssets();
const response = serveStaticAsset("/dashboard", assets);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/html; charset=utf-8");
expect(response.headers.get("Cache-Control")).toBe("no-cache");
expect(await response.text()).toBe("<!doctype html><html></html>");
});
test("SVG 资源返回正确 Content-Type", () => {
const assets = createTestAssets();
const response = serveStaticAsset("/favicon.svg", assets);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("image/svg+xml");
expect(response.headers.get("Cache-Control")).toBe("public, max-age=31536000, immutable");
});
test("CSS 资源返回正确 Content-Type", () => {
const assets = createTestAssets();
const response = serveStaticAsset("/assets/index-a1b2c3.css", assets);
expect(response.status).toBe(200);
expect(response.headers.get("Content-Type")).toBe("text/css; charset=utf-8");
});
});

81
tests/setup.ts Normal file
View File

@@ -0,0 +1,81 @@
/**
* 全局测试配置
* 主要为后端测试提供基础环境
* 组件测试使用各自的 test-utils.tsx
*/
/* eslint-disable @typescript-eslint/no-empty-function */
// Set up jsdom for ALL tests (both backend and frontend)
import { JSDOM } from "jsdom";
const dom = new JSDOM("<!DOCTYPE html><html><body></body></html>", {
pretendToBeVisual: true,
url: "http://localhost",
});
globalThis.document = dom.window.document;
globalThis.window = dom.window as unknown as typeof globalThis & Window;
globalThis.navigator = dom.window.navigator;
globalThis.HTMLElement = dom.window.HTMLElement;
globalThis.Element = dom.window.Element;
globalThis.getComputedStyle = dom.window.getComputedStyle;
// Ensure document.body exists
if (!globalThis.document.body) {
const body = globalThis.document.createElement("body");
globalThis.document.documentElement.appendChild(body);
}
// CRITICAL: Set up polyfills BEFORE any other imports
// This ensures @testing-library/react sees these when it loads
// IE-style event handling polyfill (React fallback)
const nodeProto = dom.window.Node.prototype;
const elementProto = dom.window.Element.prototype;
const htmlElementProto = dom.window.HTMLElement.prototype;
const attachEventFn = () => {};
const detachEventFn = () => {};
Object.defineProperty(nodeProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(nodeProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true });
Object.defineProperty(elementProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(elementProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true });
Object.defineProperty(htmlElementProto, "attachEvent", { configurable: true, value: attachEventFn, writable: true });
Object.defineProperty(htmlElementProto, "detachEvent", { configurable: true, value: detachEventFn, writable: true });
// Other polyfills
globalThis.ResizeObserver = class {
disconnect() {}
observe() {}
unobserve() {}
};
globalThis.IntersectionObserver = class {
disconnect() {}
observe() {}
takeRecords() {
return [];
}
unobserve() {}
} as unknown as typeof IntersectionObserver;
globalThis.requestAnimationFrame = (cb: FrameRequestCallback) => setTimeout(cb, 16);
globalThis.cancelAnimationFrame = (id: number) => clearTimeout(id);
Object.defineProperty(dom.window, "matchMedia", {
value: (query: string) => ({
addEventListener: () => {},
addListener: () => {},
dispatchEvent: () => true,
matches: false,
media: query,
onchange: null,
removeEventListener: () => {},
removeListener: () => {},
}),
writable: true,
});
dom.window.Element.prototype.scrollTo = () => {};
dom.window.Element.prototype.scrollIntoView = () => {};

View File

@@ -0,0 +1,127 @@
/* eslint-disable @typescript-eslint/no-require-imports */
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
/* eslint-disable @typescript-eslint/no-unsafe-call */
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "bun:test";
import { App } from "../../../src/web/app";
// Mock hooks
void vi.mock("../../../src/web/hooks/use-queries", () => ({
useDashboard: vi.fn(() => ({
data: {
summary: {
down: 0,
incidents: 0,
lastCheckTime: "2025-01-15T10:00:00.000Z",
total: 0,
up: 0,
window: {
from: "2025-01-14T10:00:00.000Z",
label: "24h",
to: "2025-01-15T10:00:00.000Z",
},
},
targets: [],
},
dataUpdatedAt: Date.now(),
error: null,
isFetching: false,
isLoading: false,
refetch: vi.fn(),
})),
useMeta: vi.fn(() => ({
data: { checkerTypes: ["http", "cmd"] },
})),
}));
void vi.mock("../../../src/web/hooks/use-target-detail", () => ({
useTargetDetail: vi.fn(() => ({
activeTab: "overview",
closeDrawer: vi.fn(),
handlePageChange: vi.fn(),
handleTabChange: vi.fn(),
handleTimeChange: vi.fn(),
historyData: {
items: [],
page: 1,
pageSize: 20,
total: 0,
},
historyLoading: false,
metricsData: null,
metricsLoading: false,
openDrawer: vi.fn(),
selectedTarget: null,
timeFrom: "",
timeTo: "",
})),
}));
describe("App", () => {
test("渲染不崩溃", () => {
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("loading 状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue({
data: null,
dataUpdatedAt: 0,
error: null,
isFetching: true,
isLoading: true,
refetch: vi.fn(),
});
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("错误状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue({
data: null,
dataUpdatedAt: 0,
error: { message: "Network error" },
isFetching: false,
isLoading: false,
refetch: vi.fn(),
});
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
test("有数据状态不崩溃", () => {
const { useDashboard } = require("../../../src/web/hooks/use-queries");
useDashboard.mockReturnValue({
data: {
summary: {
down: 1,
incidents: 0,
lastCheckTime: "2025-01-15T10:00:00.000Z",
total: 2,
up: 1,
window: {
from: "2025-01-14T10:00:00.000Z",
label: "24h",
to: "2025-01-15T10:00:00.000Z",
},
},
targets: [],
},
dataUpdatedAt: Date.now(),
error: null,
isFetching: false,
isLoading: false,
refetch: vi.fn(),
});
const { container } = render(<App />);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,67 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { beforeEach, describe, expect, test, vi } from "bun:test";
import { ErrorBoundary } from "../../../src/web/components/ErrorBoundary";
// 一个正常组件
function NormalComponent() {
return <div>Normal content</div>;
}
// 一个会抛错的组件
function ThrowError() {
throw new Error("Test error");
// TypeScript 需要返回值,虽然这里永远不会执行
return null;
}
describe("ErrorBoundary", () => {
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {
// Mock console.error to suppress error output during tests
});
});
test("捕获子组件渲染错误并显示 fallback", () => {
const { container } = render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
expect(container.firstChild).not.toBeNull();
});
test("正常渲染子组件", () => {
const { container } = render(
<ErrorBoundary>
<NormalComponent />
</ErrorBoundary>,
);
expect(container.firstChild).not.toBeNull();
});
test("刷新按钮不崩溃", () => {
const { container } = render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
expect(container.firstChild).not.toBeNull();
});
test("错误时调用 console.error", () => {
render(
<ErrorBoundary>
<ThrowError />
</ErrorBoundary>,
);
expect(consoleErrorSpy).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,42 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "bun:test";
import type { HistoryResponse } from "../../../src/shared/api";
import { HistoryTab } from "../../../src/web/components/HistoryTab";
describe("HistoryTab", () => {
const historyData: HistoryResponse = {
items: [],
page: 1,
pageSize: 20,
total: 0,
};
const onPageChange = vi.fn();
test("渲染不崩溃", () => {
const { container } = render(
<HistoryTab historyData={historyData} historyLoading={false} onPageChange={onPageChange} />,
);
expect(container.firstChild).not.toBeNull();
});
test("loading 状态不崩溃", () => {
const { container } = render(
<HistoryTab historyData={historyData} historyLoading={true} onPageChange={onPageChange} />,
);
expect(container.firstChild).not.toBeNull();
});
test("空数据不崩溃", () => {
const { container } = render(
<HistoryTab historyData={historyData} historyLoading={false} onPageChange={onPageChange} />,
);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,67 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import type { TargetMetricsResponse, TargetStatus } from "../../../src/shared/api";
import { OverviewTab } from "../../../src/web/components/OverviewTab";
describe("OverviewTab", () => {
const target: TargetStatus = {
currentStreak: null,
group: "default",
id: 1,
interval: "30s",
latestCheck: {
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
timestamp: "2025-01-15T10:00:00.000Z",
},
name: "test-target",
recentSamples: [],
stats: { availability: 100, downChecks: 0, totalChecks: 10, upChecks: 10 },
target: "https://example.com",
type: "http",
};
const metricsData: TargetMetricsResponse = {
stats: {
availability: 95,
avgDurationMs: 150,
currentStreak: { count: 5, up: true },
downChecks: 1,
incidentCount: 1,
longestOutage: 60000,
mttr: 30000,
p95DurationMs: 200,
p99DurationMs: 250,
totalChecks: 20,
upChecks: 19,
},
targetId: 1,
trend: [],
window: { bucket: "1h", from: "", to: "" },
};
test("有数据不崩溃", () => {
const { container } = render(<OverviewTab metricsData={metricsData} metricsLoading={false} target={target} />);
expect(container.firstChild).not.toBeNull();
});
test("loading 状态不崩溃", () => {
const { container } = render(<OverviewTab metricsData={null} metricsLoading={true} target={target} />);
expect(container.firstChild).not.toBeNull();
});
test("无指标数据不崩溃", () => {
const { container } = render(<OverviewTab metricsData={null} metricsLoading={false} target={target} />);
expect(container.firstChild).not.toBeNull();
});
test("显示趋势图表不崩溃", () => {
const { container } = render(<OverviewTab metricsData={metricsData} metricsLoading={false} target={target} />);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,64 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "bun:test";
import { RefreshCountdown } from "../../../src/web/components/RefreshCountdown";
describe("RefreshCountdown", () => {
test("手动模式不崩溃", () => {
const { container } = render(
<RefreshCountdown
dashboardUpdatedAt={0}
isFetching={false}
isManualRefresh={true}
onRefresh={vi.fn()}
refreshInterval={30000}
/>,
);
expect(container.firstChild).not.toBeNull();
});
test("自动模式不崩溃", () => {
const now = Date.now();
const { container } = render(
<RefreshCountdown
dashboardUpdatedAt={now - 10000}
isFetching={false}
isManualRefresh={false}
onRefresh={vi.fn()}
refreshInterval={30000}
/>,
);
expect(container.firstChild).not.toBeNull();
});
test("fetching 状态不崩溃", () => {
const { container } = render(
<RefreshCountdown
dashboardUpdatedAt={1000}
isFetching={true}
isManualRefresh={false}
onRefresh={vi.fn()}
refreshInterval={30000}
/>,
);
expect(container.firstChild).not.toBeNull();
});
test("未刷新状态不崩溃", () => {
const { container } = render(
<RefreshCountdown
dashboardUpdatedAt={0}
isFetching={false}
isManualRefresh={false}
onRefresh={vi.fn()}
refreshInterval={30000}
/>,
);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,31 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import type { RecentSample } from "../../../src/shared/api";
import { StatusBar } from "../../../src/web/components/StatusBar";
describe("StatusBar", () => {
const now = new Date().toISOString();
const samples: RecentSample[] = [
{ durationMs: 100, timestamp: now, up: true },
{ durationMs: 150, timestamp: new Date(Date.now() - 60000).toISOString(), up: false },
];
test("渲染不崩溃", () => {
const { container } = render(<StatusBar maxSlots={5} samples={samples} />);
expect(container.firstChild).not.toBeNull();
});
test("默认 maxSlots 不崩溃", () => {
const { container } = render(<StatusBar samples={samples} />);
expect(container.firstChild).not.toBeNull();
});
test("空 samples 不崩溃", () => {
const { container } = render(<StatusBar maxSlots={3} samples={[]} />);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,17 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import { StatusDot } from "../../../src/web/components/StatusDot";
describe("StatusDot", () => {
test("up=true 不崩溃", () => {
const { container } = render(<StatusDot up={true} />);
expect(container.firstChild).not.toBeNull();
});
test("up=false 不崩溃", () => {
const { container } = render(<StatusDot up={false} />);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,33 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import type { DashboardResponse } from "../../../src/shared/api";
import { SummaryCards } from "../../../src/web/components/SummaryCards";
describe("SummaryCards", () => {
const summary: DashboardResponse["summary"] = {
down: 2,
incidents: 1,
lastCheckTime: "2025-01-15T10:00:00.000Z",
total: 10,
up: 8,
window: {
from: "2025-01-14T10:00:00.000Z",
label: "24h",
to: "2025-01-15T10:00:00.000Z",
},
};
test("summary 为 null 时不渲染", () => {
const { container } = render(<SummaryCards summary={null} />);
expect(container.firstChild).toBeNull();
});
test("有数据不崩溃", () => {
const { container } = render(<SummaryCards summary={summary} />);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,55 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "bun:test";
import type { TargetStatus } from "../../../src/shared/api";
import { TargetBoard } from "../../../src/web/components/TargetBoard";
// Mock useMeta hook
void vi.mock("../../../src/web/hooks/use-queries", () => ({
useMeta: () => ({
data: { checkerTypes: ["http", "cmd"] },
}),
}));
describe("TargetBoard", () => {
const onTargetClick = vi.fn();
const targets: TargetStatus[] = [
{
currentStreak: null,
group: "default",
id: 1,
interval: "30s",
latestCheck: null,
name: "target-1",
recentSamples: [],
stats: { availability: 100, downChecks: 0, totalChecks: 0, upChecks: 0 },
target: "https://example.com",
type: "http",
},
{
currentStreak: null,
group: "production",
id: 2,
interval: "30s",
latestCheck: null,
name: "target-2",
recentSamples: [],
stats: { availability: 100, downChecks: 0, totalChecks: 0, upChecks: 0 },
target: "https://example.org",
type: "http",
},
];
test("有 targets 时不崩溃", () => {
const { container } = render(<TargetBoard onTargetClick={onTargetClick} targets={targets} />);
expect(container.firstChild).not.toBeNull();
});
test("空 targets 列表不崩溃", () => {
const { container } = render(<TargetBoard onTargetClick={onTargetClick} targets={[]} />);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,88 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "bun:test";
import type { HistoryResponse, TargetMetricsResponse, TargetStatus } from "../../../src/shared/api";
import { TargetDetailDrawer } from "../../../src/web/components/TargetDetailDrawer";
describe("TargetDetailDrawer", () => {
const target: TargetStatus = {
currentStreak: null,
group: "default",
id: 1,
interval: "30s",
latestCheck: {
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
timestamp: "2025-01-15T10:00:00.000Z",
},
name: "test-target",
recentSamples: [],
stats: { availability: 100, downChecks: 0, totalChecks: 10, upChecks: 10 },
target: "https://example.com",
type: "http",
};
const metricsData: TargetMetricsResponse = {
stats: {
availability: 95,
avgDurationMs: 150,
currentStreak: null,
downChecks: 1,
incidentCount: 1,
longestOutage: null,
mttr: null,
p95DurationMs: 200,
p99DurationMs: 250,
totalChecks: 20,
upChecks: 19,
},
targetId: 1,
trend: [],
window: { bucket: "1h", from: "", to: "" },
};
const historyData: HistoryResponse = {
items: [],
page: 1,
pageSize: 20,
total: 0,
};
const defaultProps = {
activeTab: "overview",
historyData,
historyLoading: false,
metricsData,
metricsLoading: false,
onClose: vi.fn(),
onPageChange: vi.fn(),
onTabChange: vi.fn(),
onTimeChange: vi.fn(),
target,
timeFrom: "2025-01-15T00:00:00.000Z",
timeTo: "2025-01-15T23:59:59.999Z",
};
test("target 为 null 时不崩溃", () => {
const { container } = render(<TargetDetailDrawer {...defaultProps} target={null} />);
// When target is null, the drawer might not render, which is expected behavior
expect(container).not.toBeNull();
});
test("target 存在时不崩溃", () => {
const { asFragment } = render(<TargetDetailDrawer {...defaultProps} />);
// Just verify rendering doesn't throw
expect(asFragment()).not.toBeNull();
});
test("关闭按钮不崩溃", () => {
const onClose = vi.fn();
const { asFragment } = render(<TargetDetailDrawer {...defaultProps} onClose={onClose} />);
// Just verify rendering doesn't throw
expect(asFragment()).not.toBeNull();
});
});

View File

@@ -0,0 +1,76 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test, vi } from "bun:test";
import type { TargetStatus } from "../../../src/shared/api";
import { TargetGroup } from "../../../src/web/components/TargetGroup";
describe("TargetGroup", () => {
const columns = [
{ colKey: "name", title: "名称" },
{ colKey: "target", title: "目标" },
];
const targets: TargetStatus[] = [
{
currentStreak: null,
group: "default",
id: 1,
interval: "30s",
latestCheck: {
durationMs: 100,
failure: null,
matched: true,
statusDetail: "200 OK",
timestamp: "2025-01-15T10:00:00.000Z",
},
name: "target-1",
recentSamples: [],
stats: { availability: 100, downChecks: 0, totalChecks: 10, upChecks: 10 },
target: "https://example.com",
type: "http",
},
{
currentStreak: null,
group: "default",
id: 2,
interval: "30s",
latestCheck: {
durationMs: 100,
failure: { kind: "error", message: "Failed", path: "$", phase: "status" },
matched: false,
statusDetail: "500 Internal Server Error",
timestamp: "2025-01-15T10:00:00.000Z",
},
name: "target-2",
recentSamples: [],
stats: { availability: 50, downChecks: 1, totalChecks: 2, upChecks: 1 },
target: "https://example.org",
type: "http",
},
];
const onTargetClick = vi.fn();
test("default 分组不崩溃", () => {
const { container } = render(
<TargetGroup columns={columns} name="default" onTargetClick={onTargetClick} targets={targets} />,
);
expect(container.firstChild).not.toBeNull();
});
test("非 default 分组不崩溃", () => {
const { container } = render(
<TargetGroup columns={columns} name="production" onTargetClick={onTargetClick} targets={targets} />,
);
expect(container.firstChild).not.toBeNull();
});
test("空 targets 不崩溃", () => {
const { container } = render(
<TargetGroup columns={columns} name="default" onTargetClick={onTargetClick} targets={[]} />,
);
expect(container.firstChild).not.toBeNull();
});
});

View File

@@ -0,0 +1,53 @@
import "../../../tests/web/test-utils";
import { render } from "@testing-library/react";
import { describe, expect, test } from "bun:test";
import type { TrendPoint } from "../../../src/shared/api";
import { TrendChart } from "../../../src/web/components/TrendChart";
describe("TrendChart", () => {
const data: TrendPoint[] = [
{
availability: 100,
avgDurationMs: 100,
bucketStart: "2025-01-15T10:00:00.000Z",
downChecks: 0,
maxDurationMs: 150,
minDurationMs: 50,
totalChecks: 10,
upChecks: 10,
},
{
availability: 95,
avgDurationMs: 120,
bucketStart: "2025-01-15T11:00:00.000Z",
downChecks: 1,
maxDurationMs: 200,
minDurationMs: 80,
totalChecks: 20,
upChecks: 19,
},
];
test("有数据时不崩溃", () => {
const { container } = render(<TrendChart data={data} />);
expect(container.firstChild).not.toBeNull();
});
test("空数据显示占位", () => {
const { container } = render(<TrendChart data={[]} />);
// 应该显示占位文本
const element = container.querySelector(".trend-empty");
expect(element).not.toBeNull();
});
test("包含 trend-chart className", () => {
const { container } = render(<TrendChart data={data} />);
const element = container.querySelector(".trend-chart");
expect(element).not.toBeNull();
});
});

View File

@@ -4,66 +4,32 @@ import { getAvailabilityProgressColor } from "../../../src/web/constants/color-t
describe("color-threshold", () => {
describe("getAvailabilityProgressColor", () => {
test("0-10% 返回第一档 CSS 变量", () => {
test("首档(0-10%和末档90-100%", () => {
expect(getAvailabilityProgressColor(0)).toBe("var(--avail-0)");
expect(getAvailabilityProgressColor(5)).toBe("var(--avail-0)");
expect(getAvailabilityProgressColor(9.99)).toBe("var(--avail-0)");
});
test("10-20% 返回第二档 CSS 变量", () => {
expect(getAvailabilityProgressColor(10)).toBe("var(--avail-1)");
expect(getAvailabilityProgressColor(15)).toBe("var(--avail-1)");
expect(getAvailabilityProgressColor(19.99)).toBe("var(--avail-1)");
});
test("20-30% 返回第三档 CSS 变量", () => {
expect(getAvailabilityProgressColor(20)).toBe("var(--avail-2)");
expect(getAvailabilityProgressColor(25)).toBe("var(--avail-2)");
});
test("30-40% 返回第四档 CSS 变量", () => {
expect(getAvailabilityProgressColor(30)).toBe("var(--avail-3)");
expect(getAvailabilityProgressColor(35)).toBe("var(--avail-3)");
});
test("40-50% 返回第五档 CSS 变量", () => {
expect(getAvailabilityProgressColor(40)).toBe("var(--avail-4)");
expect(getAvailabilityProgressColor(45)).toBe("var(--avail-4)");
});
test("50-60% 返回第六档 CSS 变量", () => {
expect(getAvailabilityProgressColor(50)).toBe("var(--avail-5)");
expect(getAvailabilityProgressColor(55)).toBe("var(--avail-5)");
});
test("60-70% 返回第七档 CSS 变量", () => {
expect(getAvailabilityProgressColor(60)).toBe("var(--avail-6)");
expect(getAvailabilityProgressColor(65)).toBe("var(--avail-6)");
});
test("70-80% 返回第八档 CSS 变量", () => {
expect(getAvailabilityProgressColor(70)).toBe("var(--avail-7)");
expect(getAvailabilityProgressColor(75)).toBe("var(--avail-7)");
});
test("80-90% 返回第九档 CSS 变量", () => {
expect(getAvailabilityProgressColor(80)).toBe("var(--avail-8)");
expect(getAvailabilityProgressColor(85)).toBe("var(--avail-8)");
});
test("90-100% 返回第十档 CSS 变量", () => {
expect(getAvailabilityProgressColor(90)).toBe("var(--avail-9)");
expect(getAvailabilityProgressColor(95)).toBe("var(--avail-9)");
expect(getAvailabilityProgressColor(99.9)).toBe("var(--avail-9)");
expect(getAvailabilityProgressColor(100)).toBe("var(--avail-9)");
});
test("边界值", () => {
expect(getAvailabilityProgressColor(9.999)).toBe("var(--avail-0)");
test("所有边界值(每档切换点)", () => {
expect(getAvailabilityProgressColor(9.99)).toBe("var(--avail-0)");
expect(getAvailabilityProgressColor(10)).toBe("var(--avail-1)");
expect(getAvailabilityProgressColor(19.999)).toBe("var(--avail-1)");
expect(getAvailabilityProgressColor(19.99)).toBe("var(--avail-1)");
expect(getAvailabilityProgressColor(20)).toBe("var(--avail-2)");
expect(getAvailabilityProgressColor(89.999)).toBe("var(--avail-8)");
expect(getAvailabilityProgressColor(29.99)).toBe("var(--avail-2)");
expect(getAvailabilityProgressColor(30)).toBe("var(--avail-3)");
expect(getAvailabilityProgressColor(39.99)).toBe("var(--avail-3)");
expect(getAvailabilityProgressColor(40)).toBe("var(--avail-4)");
expect(getAvailabilityProgressColor(49.99)).toBe("var(--avail-4)");
expect(getAvailabilityProgressColor(50)).toBe("var(--avail-5)");
expect(getAvailabilityProgressColor(59.99)).toBe("var(--avail-5)");
expect(getAvailabilityProgressColor(60)).toBe("var(--avail-6)");
expect(getAvailabilityProgressColor(69.99)).toBe("var(--avail-6)");
expect(getAvailabilityProgressColor(70)).toBe("var(--avail-7)");
expect(getAvailabilityProgressColor(79.99)).toBe("var(--avail-7)");
expect(getAvailabilityProgressColor(80)).toBe("var(--avail-8)");
expect(getAvailabilityProgressColor(89.99)).toBe("var(--avail-8)");
expect(getAvailabilityProgressColor(90)).toBe("var(--avail-9)");
});
});

View File

@@ -1,81 +0,0 @@
import { describe, expect, test } from "bun:test";
function shouldEnableHistory(
selectedTargetId: null | number,
timeFrom: string,
timeTo: string,
activeTab: string,
): boolean {
return selectedTargetId !== null && !!timeFrom && !!timeTo && activeTab === "history";
}
function shouldEnableMetrics(selectedTargetId: null | number, timeFrom: string, timeTo: string): boolean {
return selectedTargetId !== null && !!timeFrom && !!timeTo;
}
describe("metrics enabled 条件", () => {
test("未选中目标时不启用", () => {
expect(shouldEnableMetrics(null, "", "")).toBe(false);
});
test("选中目标但无时间范围时不启用", () => {
expect(shouldEnableMetrics(1, "", "")).toBe(false);
});
test("选中目标且有时间范围时启用", () => {
expect(shouldEnableMetrics(1, "2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z")).toBe(true);
});
});
describe("history enabled 条件", () => {
test("未选中目标时不启用", () => {
expect(shouldEnableHistory(null, "from", "to", "history")).toBe(false);
});
test("选中目标但概览 Tab 时不启用", () => {
expect(shouldEnableHistory(1, "2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z", "overview")).toBe(false);
});
test("选中目标且记录 Tab 激活但无时间范围时不启用", () => {
expect(shouldEnableHistory(1, "", "", "history")).toBe(false);
});
test("选中目标、有时间范围且记录 Tab 激活时启用", () => {
expect(shouldEnableHistory(1, "2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z", "history")).toBe(true);
});
test("打开 Drawer 默认概览 Tab 时不启用 history", () => {
expect(shouldEnableHistory(1, "2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z", "overview")).toBe(false);
});
test("概览 Tab 时间变化时不启用 history", () => {
expect(shouldEnableHistory(1, "2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z", "overview")).toBe(false);
});
test("记录 Tab 时间变化时启用 history", () => {
expect(shouldEnableHistory(1, "2025-01-01T00:00:00.000Z", "2025-01-02T00:00:00.000Z", "history")).toBe(true);
});
});
describe("默认概览 Tab 行为", () => {
test("打开 Drawer 时 activeTab 应为 overview", () => {
const resetTab = "overview";
expect(resetTab).toBe("overview");
});
test("切换目标时 activeTab 应重置为 overview", () => {
const previousTab = "history";
const resetTab = "overview";
expect(previousTab).not.toBe(resetTab);
expect(resetTab).toBe("overview");
});
});
describe("history 页码重置", () => {
test("时间变化时 historyPage 应重置为 1", () => {
const previousPage = 3;
const resetPage = 1;
expect(previousPage).not.toBe(resetPage);
expect(resetPage).toBe(1);
});
});

52
tests/web/test-utils.tsx Normal file
View File

@@ -0,0 +1,52 @@
import { mock } from "bun:test";
// Note: jsdom and polyfills are now set up in tests/setup.ts
// This file only contains component-specific mocks
// Mock recharts BEFORE any component imports
void mock.module("recharts", () => ({
Area: () => null,
CartesianGrid: () => null,
Line: () => null,
LineChart: ({ children }: { children: unknown }) => children,
ResponsiveContainer: ({ children }: { children: unknown }) => children,
Tooltip: () => null,
XAxis: () => null,
YAxis: () => null,
}));
// Custom test helpers (替代 jest-dom matchers)
export const testHelpers = {
toBeInTheDocument: (element: Element | null) => {
const pass = element !== null && document.contains(element);
return {
message: () => (pass ? "Expected element not to be in document" : "Expected element to be in document"),
pass,
};
},
toHaveAttribute: (element: Element | null, attr: string, value?: string) => {
const pass = value === undefined ? (element?.hasAttribute(attr) ?? false) : element?.getAttribute(attr) === value;
return {
message: () =>
pass ? `Expected element not to have attribute "${attr}"` : `Expected element to have attribute "${attr}"`,
pass,
};
},
toHaveClass: (element: Element | null, className: string) => {
const pass = element?.classList.contains(className) ?? false;
return {
message: () =>
pass ? `Expected element not to have class "${className}"` : `Expected element to have class "${className}"`,
pass,
};
},
toHaveTextContent: (element: Element | null, text: RegExp | string) => {
const pass =
element?.textContent !== null &&
(typeof text === "string" ? element.textContent.includes(text) : text.test(element.textContent));
return {
message: () => (pass ? `Expected element not to have text "${text}"` : `Expected element to have text "${text}"`),
pass,
};
},
};

37
vite.config.ts Normal file
View File

@@ -0,0 +1,37 @@
import react from "@vitejs/plugin-react";
import { defineConfig } from "vite";
export default defineConfig({
build: {
emptyOutDir: true,
outDir: "../../dist/web",
rolldownOptions: {
output: {
codeSplitting: {
groups: [
{
name: "vendor-react",
test: /[\\/]node_modules[\\/](react|react-dom|scheduler)[\\/]/,
},
{
name: "vendor-tdesign",
test: /[\\/]node_modules[\\/](tdesign-react|tdesign-icons-react)[\\/]/,
},
{
name: "vendor-chart",
test: /[\\/]node_modules[\\/](recharts|d3-.*)[\\/]/,
},
],
},
},
},
},
plugins: [react()],
root: "src/web",
server: {
proxy: {
"/api": "http://127.0.0.1:3000",
"/health": "http://127.0.0.1:3000",
},
},
});