diff --git a/README.md b/README.md index 1b04cc0..8fb5017 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ nex/ │ ├── frontend/ # React 前端界面 │ ├── src/ -│ │ ├── main.tsx -│ │ ├── App.tsx -│ │ ├── pages/ -│ │ ├── components/ -│ │ ├── api/ -│ │ └── styles/ +│ │ ├── api/ # API 层(统一请求封装 + 字段转换) +│ │ ├── hooks/ # TanStack Query hooks +│ │ ├── components/ # 通用组件(AppLayout) +│ │ ├── pages/ # 页面(Providers, Stats, NotFound) +│ │ ├── routes/ # React Router 路由配置 +│ │ ├── types/ # TypeScript 类型定义 +│ │ └── __tests__/ # 测试(API、Hooks、组件) +│ ├── e2e/ # Playwright E2E 测试 │ └── package.json │ └── README.md # 本文件 @@ -51,9 +53,13 @@ nex/ ### 前端 - **Bun** - 运行时 - **Vite** - 构建工具 -- **TypeScript** - 类型系统 +- **TypeScript** (strict mode) - 类型系统 - **React** - UI 框架 -- **SCSS** - 样式预处理 +- **Ant Design 5** - UI 组件库 +- **React Router v7** - 路由 +- **TanStack Query v5** - 数据获取 +- **SCSS Modules** - 样式方案 +- **Vitest + Playwright** - 测试 ## 快速开始 diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..a41b3e9 --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1 @@ +VITE_API_BASE= diff --git a/frontend/.env.production b/frontend/.env.production new file mode 100644 index 0000000..0a0c303 --- /dev/null +++ b/frontend/.env.production @@ -0,0 +1 @@ +VITE_API_BASE=/api diff --git a/frontend/README.md b/frontend/README.md index e8b33b2..5fdb700 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -6,25 +6,52 @@ AI 网关管理前端,提供供应商配置和用量统计界面。 - **运行时**: Bun - **构建工具**: Vite -- **语言**: TypeScript +- **语言**: TypeScript (strict mode) - **框架**: React -- **样式**: SCSS +- **UI 组件库**: Ant Design 5 +- **路由**: React Router v7 +- **数据获取**: TanStack Query v5 +- **样式**: SCSS Modules(禁止使用纯 CSS) +- **测试**: Vitest + React Testing Library + Playwright ## 项目结构 ``` frontend/ ├── src/ -│ ├── api/ -│ │ └── client.ts # API 客户端封装 +│ ├── api/ # API 层 +│ │ ├── client.ts # 统一 request() + 字段转换 +│ │ ├── providers.ts # Provider CRUD +│ │ ├── models.ts # Model CRUD +│ │ └── stats.ts # Stats 查询 +│ ├── components/ +│ │ └── AppLayout/ # 顶部导航布局 +│ ├── hooks/ # TanStack Query hooks +│ │ ├── useProviders.ts +│ │ ├── useModels.ts +│ │ └── useStats.ts │ ├── pages/ -│ │ ├── ProvidersPage.tsx # 供应商管理页面 -│ │ └── StatsPage.tsx # 统计查看页面 -│ ├── App.tsx # 主应用组件 -│ ├── App.css # 样式 -│ └── main.tsx # 入口文件 -├── package.json -└── README.md +│ │ ├── Providers/ # 供应商管理(含内嵌模型管理) +│ │ ├── Stats/ # 用量统计 +│ │ └── NotFound.tsx +│ ├── routes/ +│ │ └── index.tsx # 路由配置 +│ ├── types/ +│ │ └── index.ts # 类型定义 +│ ├── __tests__/ # 测试 +│ │ ├── setup.ts +│ │ ├── api/ +│ │ ├── hooks/ +│ │ └── components/ +│ ├── App.tsx +│ ├── main.tsx +│ └── index.scss +├── e2e/ # Playwright E2E 测试 +├── vitest.config.ts +├── playwright.config.ts +├── tsconfig.json +├── vite.config.ts +└── package.json ``` ## 运行方式 @@ -41,7 +68,7 @@ bun install bun run dev ``` -前端将在端口 5173 启动。 +前端将在端口 5173 启动,API 请求通过 Vite proxy 转发到后端(localhost:9826)。 ### 构建生产版本 @@ -49,37 +76,60 @@ bun run dev bun run build ``` +### 代码检查 + +```bash +bun run lint +``` + +## 测试 + +### 单元测试 + 组件测试 + +```bash +bun run test # 运行所有测试 +bun run test:watch # 监听模式 +bun run test:coverage # 生成覆盖率报告 +``` + +### E2E 测试 + +```bash +bun run test:e2e +``` + ## 功能 ### 供应商管理 -- 查看供应商列表 -- 添加新供应商 +- 查看供应商列表(Ant Design Table) +- 添加新供应商(Modal Form) - 编辑供应商配置 -- 删除供应商 -- 启用/禁用供应商 +- 删除供应商(Popconfirm 确认) +- API Key 脱敏显示 +- 启用/禁用状态标签 ### 模型管理 -- 查看模型列表 -- 添加新模型 -- 编辑模型配置 -- 删除模型 -- 按供应商过滤模型 +- 展开供应商行查看关联模型 +- 添加/编辑/删除模型 +- 按供应商筛选模型 ### 用量统计 - 查看统计数据 -- 按供应商过滤 -- 按模型过滤 -- 按日期范围过滤 -- 查看聚合统计 +- 按供应商筛选 +- 按模型筛选 +- 按日期范围筛选(DatePicker.RangePicker) -## API 配置 +## 开发规范 -API 基础地址默认为 `http://localhost:9826/api`,可在 `src/api/client.ts` 中修改。 - -## 开发 +- 所有样式使用 SCSS,禁止使用纯 CSS 文件 +- 组件级样式使用 SCSS Modules(*.module.scss) +- 图标优先使用 @ant-design/icons +- TypeScript strict 模式,禁止 any 类型 +- API 层自动处理 snake_case ↔ camelCase 字段转换 +- 使用路径别名 `@/` 引用 src 目录 ### 环境要求 @@ -87,6 +137,7 @@ API 基础地址默认为 `http://localhost:9826/api`,可在 `src/api/client.t ### 添加新页面 -1. 在 `src/pages/` 创建页面组件 -2. 在 `src/App.tsx` 添加路由 -3. 在导航栏添加链接 +1. 在 `src/pages/` 创建页面目录和组件 +2. 在 `src/hooks/` 创建对应的 TanStack Query hook +3. 在 `src/routes/index.tsx` 添加路由 +4. 在 `src/components/AppLayout/index.tsx` 的 menuItems 添加导航项 diff --git a/frontend/bun.lock b/frontend/bun.lock index 037e4d4..c2b7473 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -5,27 +5,61 @@ "": { "name": "frontend", "dependencies": { + "@ant-design/icons": "^5.6.1", + "@tanstack/react-query": "^5.80.2", + "antd": "^5.24.9", "react": "^19.2.4", "react-dom": "^19.2.4", - "sass": "^1.99.0", + "react-router": "^7.6.1", }, "devDependencies": { "@eslint/js": "^9.39.4", + "@playwright/test": "^1.52.0", + "@tanstack/eslint-plugin-query": "^5.78.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^3.2.1", "eslint": "^9.39.4", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "jsdom": "^26.1.0", + "msw": "^2.8.2", + "sass": "^1.99.0", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", "vite": "^8.0.4", + "vitest": "^3.2.1", }, }, }, "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.4.4.tgz", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "https://registry.npmmirror.com/@ampproject/remapping/-/remapping-2.3.0.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@ant-design/colors": ["@ant-design/colors@7.2.1", "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz", { "dependencies": { "@ant-design/fast-color": "^2.0.6" } }, "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ=="], + + "@ant-design/cssinjs": ["@ant-design/cssinjs@1.24.0", "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "@emotion/hash": "^0.8.0", "@emotion/unitless": "^0.7.5", "classnames": "^2.3.1", "csstype": "^3.1.3", "rc-util": "^5.35.0", "stylis": "^4.3.4" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg=="], + + "@ant-design/cssinjs-utils": ["@ant-design/cssinjs-utils@1.1.3", "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz", { "dependencies": { "@ant-design/cssinjs": "^1.21.0", "@babel/runtime": "^7.23.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg=="], + + "@ant-design/fast-color": ["@ant-design/fast-color@2.0.6", "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz", { "dependencies": { "@babel/runtime": "^7.24.7" } }, "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA=="], + + "@ant-design/icons": ["@ant-design/icons@5.6.1", "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz", { "dependencies": { "@ant-design/colors": "^7.0.0", "@ant-design/icons-svg": "^4.4.0", "@babel/runtime": "^7.24.8", "classnames": "^2.2.6", "rc-util": "^5.31.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg=="], + + "@ant-design/icons-svg": ["@ant-design/icons-svg@4.4.2", "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz", {}, "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA=="], + + "@ant-design/react-slick": ["@ant-design/react-slick@1.1.2", "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.10.4", "classnames": "^2.2.5", "json2mq": "^0.2.0", "resize-observer-polyfill": "^1.5.1", "throttle-debounce": "^5.0.0" }, "peerDependencies": { "react": ">=16.9.0" } }, "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA=="], + + "@asamuzakjp/css-color": ["@asamuzakjp/css-color@3.2.0", "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", { "dependencies": { "@csstools/css-calc": "^2.1.3", "@csstools/css-color-parser": "^3.0.9", "@csstools/css-parser-algorithms": "^3.0.4", "@csstools/css-tokenizer": "^3.0.3", "lru-cache": "^10.4.3" } }, "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw=="], + "@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.0", "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], @@ -52,18 +86,88 @@ "@babel/parser": ["@babel/parser@7.29.2", "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + "@babel/runtime": ["@babel/runtime@7.29.2", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz", {}, "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g=="], + "@babel/template": ["@babel/template@7.28.6", "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], "@babel/traverse": ["@babel/traverse@7.29.0", "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], "@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=="], + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + + "@csstools/color-helpers": ["@csstools/color-helpers@5.1.0", "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", {}, "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA=="], + + "@csstools/css-calc": ["@csstools/css-calc@2.1.4", "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz", { "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ=="], + + "@csstools/css-color-parser": ["@csstools/css-color-parser@3.1.0", "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", { "dependencies": { "@csstools/color-helpers": "^5.1.0", "@csstools/css-calc": "^2.1.4" }, "peerDependencies": { "@csstools/css-parser-algorithms": "^3.0.5", "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA=="], + + "@csstools/css-parser-algorithms": ["@csstools/css-parser-algorithms@3.0.5", "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", { "peerDependencies": { "@csstools/css-tokenizer": "^3.0.4" } }, "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ=="], + + "@csstools/css-tokenizer": ["@csstools/css-tokenizer@3.0.4", "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", {}, "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw=="], + "@emnapi/core": ["@emnapi/core@1.9.2", "https://registry.npmmirror.com/@emnapi/core/-/core-1.9.2.tgz", { "dependencies": { "@emnapi/wasi-threads": "1.2.1", "tslib": "^2.4.0" } }, "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA=="], "@emnapi/runtime": ["@emnapi/runtime@1.9.2", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.9.2.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw=="], "@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.1", "https://registry.npmmirror.com/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w=="], + "@emotion/hash": ["@emotion/hash@0.8.0", "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz", {}, "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow=="], + + "@emotion/unitless": ["@emotion/unitless@0.7.5", "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz", {}, "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg=="], + + "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], + + "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.7.tgz", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], + + "@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", { "os": "android", "cpu": "arm64" }, "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ=="], + + "@esbuild/android-x64": ["@esbuild/android-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.7.tgz", { "os": "android", "cpu": "x64" }, "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg=="], + + "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw=="], + + "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ=="], + + "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w=="], + + "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ=="], + + "@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", { "os": "linux", "cpu": "arm" }, "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA=="], + + "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A=="], + + "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg=="], + + "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", { "os": "linux", "cpu": "none" }, "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q=="], + + "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", { "os": "linux", "cpu": "none" }, "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw=="], + + "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ=="], + + "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", { "os": "linux", "cpu": "none" }, "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ=="], + + "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw=="], + + "@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", { "os": "linux", "cpu": "x64" }, "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA=="], + + "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", { "os": "none", "cpu": "arm64" }, "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w=="], + + "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", { "os": "none", "cpu": "x64" }, "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw=="], + + "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A=="], + + "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg=="], + + "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", { "os": "none", "cpu": "arm64" }, "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw=="], + + "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA=="], + + "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.7", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA=="], + + "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.7", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw=="], + + "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.7", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", { "os": "win32", "cpu": "x64" }, "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg=="], + "@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ=="], "@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", {}, "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew=="], @@ -90,6 +194,20 @@ "@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz", {}, "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ=="], + "@inquirer/ansi": ["@inquirer/ansi@1.0.2", "https://registry.npmmirror.com/@inquirer/ansi/-/ansi-1.0.2.tgz", {}, "sha512-S8qNSZiYzFd0wAcyG5AXCvUHC5Sr7xpZ9wZ2py9XR88jUz8wooStVx5M6dRzczbBWjic9NP7+rY0Xi7qqK/aMQ=="], + + "@inquirer/confirm": ["@inquirer/confirm@5.1.21", "https://registry.npmmirror.com/@inquirer/confirm/-/confirm-5.1.21.tgz", { "dependencies": { "@inquirer/core": "^10.3.2", "@inquirer/type": "^3.0.10" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ=="], + + "@inquirer/core": ["@inquirer/core@10.3.2", "https://registry.npmmirror.com/@inquirer/core/-/core-10.3.2.tgz", { "dependencies": { "@inquirer/ansi": "^1.0.2", "@inquirer/figures": "^1.0.15", "@inquirer/type": "^3.0.10", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.3" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A=="], + + "@inquirer/figures": ["@inquirer/figures@1.0.15", "https://registry.npmmirror.com/@inquirer/figures/-/figures-1.0.15.tgz", {}, "sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g=="], + + "@inquirer/type": ["@inquirer/type@3.0.10", "https://registry.npmmirror.com/@inquirer/type/-/type-3.0.10.tgz", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.6.tgz", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -100,8 +218,16 @@ "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.3", "https://registry.npmmirror.com/@mswjs/interceptors/-/interceptors-0.41.3.tgz", { "dependencies": { "@open-draft/deferred-promise": "^2.2.0", "@open-draft/logger": "^0.3.0", "@open-draft/until": "^2.0.0", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "strict-event-emitter": "^0.5.1" } }, "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA=="], + "@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.3", "https://registry.npmmirror.com/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.3.tgz", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-xK9sGVbJWYb08+mTJt3/YV24WxvxpXcXtP6B172paPZ+Ts69Re9dAr7lKwJoeIx8OoeuimEiRZ7umkiUVClmmQ=="], + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "https://registry.npmmirror.com/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + + "@open-draft/logger": ["@open-draft/logger@0.3.0", "https://registry.npmmirror.com/@open-draft/logger/-/logger-0.3.0.tgz", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + + "@open-draft/until": ["@open-draft/until@2.1.0", "https://registry.npmmirror.com/@open-draft/until/-/until-2.1.0.tgz", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@oxc-project/types": ["@oxc-project/types@0.124.0", "https://registry.npmmirror.com/@oxc-project/types/-/types-0.124.0.tgz", {}, "sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg=="], "@parcel/watcher": ["@parcel/watcher@2.5.6", "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.6.tgz", { "dependencies": { "detect-libc": "^2.0.3", "is-glob": "^4.0.3", "node-addon-api": "^7.0.0", "picomatch": "^4.0.3" }, "optionalDependencies": { "@parcel/watcher-android-arm64": "2.5.6", "@parcel/watcher-darwin-arm64": "2.5.6", "@parcel/watcher-darwin-x64": "2.5.6", "@parcel/watcher-freebsd-x64": "2.5.6", "@parcel/watcher-linux-arm-glibc": "2.5.6", "@parcel/watcher-linux-arm-musl": "2.5.6", "@parcel/watcher-linux-arm64-glibc": "2.5.6", "@parcel/watcher-linux-arm64-musl": "2.5.6", "@parcel/watcher-linux-x64-glibc": "2.5.6", "@parcel/watcher-linux-x64-musl": "2.5.6", "@parcel/watcher-win32-arm64": "2.5.6", "@parcel/watcher-win32-ia32": "2.5.6", "@parcel/watcher-win32-x64": "2.5.6" } }, "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ=="], @@ -132,6 +258,28 @@ "@parcel/watcher-win32-x64": ["@parcel/watcher-win32-x64@2.5.6", "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", { "os": "win32", "cpu": "x64" }, "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@playwright/test": ["@playwright/test@1.59.1", "https://registry.npmmirror.com/@playwright/test/-/test-1.59.1.tgz", { "dependencies": { "playwright": "1.59.1" }, "bin": { "playwright": "cli.js" } }, "sha512-PG6q63nQg5c9rIi4/Z5lR5IVF7yU5MqmKaPOe0HSc0O2cX1fPi96sUQu5j7eo4gKCkB2AnNGoWt7y4/Xx3Kcqg=="], + + "@rc-component/async-validator": ["@rc-component/async-validator@5.1.0", "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.24.4" } }, "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA=="], + + "@rc-component/color-picker": ["@rc-component/color-picker@2.0.1", "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-2.0.1.tgz", { "dependencies": { "@ant-design/fast-color": "^2.0.6", "@babel/runtime": "^7.23.6", "classnames": "^2.2.6", "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q=="], + + "@rc-component/context": ["@rc-component/context@1.4.0", "https://registry.npmmirror.com/@rc-component/context/-/context-1.4.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w=="], + + "@rc-component/mini-decimal": ["@rc-component/mini-decimal@1.1.3", "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz", { "dependencies": { "@babel/runtime": "^7.18.0" } }, "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw=="], + + "@rc-component/mutate-observer": ["@rc-component/mutate-observer@1.1.0", "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw=="], + + "@rc-component/portal": ["@rc-component/portal@1.1.2", "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz", { "dependencies": { "@babel/runtime": "^7.18.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg=="], + + "@rc-component/qrcode": ["@rc-component/qrcode@1.1.1", "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.1.1.tgz", { "dependencies": { "@babel/runtime": "^7.24.7" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA=="], + + "@rc-component/tour": ["@rc-component/tour@1.15.1", "https://registry.npmmirror.com/@rc-component/tour/-/tour-1.15.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/portal": "^1.0.0-9", "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.2", "rc-util": "^5.24.4" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ=="], + + "@rc-component/trigger": ["@rc-component/trigger@2.3.1", "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.1.tgz", { "dependencies": { "@babel/runtime": "^7.23.2", "@rc-component/portal": "^1.1.0", "classnames": "^2.3.2", "rc-motion": "^2.0.0", "rc-resize-observer": "^1.3.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A=="], + "@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YYe6aWruPZDtHNpwu7+qAHEMbQ/yRl6atqb/AhznLTnD3UY99Q1jE7ihLSahNWkF4EqRPVC4SiR4O0UkLK02tA=="], "@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.15.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-oArR/ig8wNTPYsXL+Mzhs0oxhxfuHRfG7Ikw7jXsw8mYOtk71W0OkF2VEVh699pdmzjPQsTjlD1JIOoHkLP1Fg=="], @@ -164,18 +312,94 @@ "@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.7", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz", {}, "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], + + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], + + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw=="], + + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew=="], + + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w=="], + + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g=="], + + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g=="], + + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg=="], + + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ=="], + + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA=="], + + "@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ=="], + + "@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw=="], + + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw=="], + + "@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg=="], + + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg=="], + + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg=="], + + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ=="], + + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg=="], + + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w=="], + + "@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw=="], + + "@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA=="], + + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g=="], + + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg=="], + + "@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg=="], + + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], + + "@rtsao/scc": ["@rtsao/scc@1.1.0", "https://registry.npmmirror.com/@rtsao/scc/-/scc-1.1.0.tgz", {}, "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g=="], + + "@tanstack/eslint-plugin-query": ["@tanstack/eslint-plugin-query@5.99.0", "https://registry.npmmirror.com/@tanstack/eslint-plugin-query/-/eslint-plugin-query-5.99.0.tgz", { "dependencies": { "@typescript-eslint/utils": "^8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": "^5.4.0 || ^6.0.0" }, "optionalPeers": ["typescript"] }, "sha512-jVp1AEL7S7BeuQvH5SN1F5UdrNW/AbryKDeWUUMeAKNzh9C+Ik/bRSa/HeuJLlmaN+WOUkdDFbtCK0go7BxnUQ=="], + + "@tanstack/query-core": ["@tanstack/query-core@5.99.0", "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.99.0.tgz", {}, "sha512-3Jv3WQG0BCcH7G+7lf/bP8QyBfJOXeY+T08Rin3GZ1bshvwlbPt7NrDHMEzGdKIOmOzvIQmxjk28YEQX60k7pQ=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.99.0", "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.99.0.tgz", { "dependencies": { "@tanstack/query-core": "5.99.0" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-OY2bCqPemT1LlqJ8Y2CUau4KELnIhhG9Ol3ZndPbdnB095pRbPo1cHuXTndg8iIwtoHTgwZjyaDnQ0xD0mYwAw=="], + + "@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/jest-dom": ["@testing-library/jest-dom@6.9.1", "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + + "@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=="], + + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "https://registry.npmmirror.com/@testing-library/user-event/-/user-event-14.6.1.tgz", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "https://registry.npmmirror.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], + "@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/chai": ["@types/chai@5.2.3", "https://registry.npmmirror.com/@types/chai/-/chai-5.2.3.tgz", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="], + + "@types/deep-eql": ["@types/deep-eql@4.0.2", "https://registry.npmmirror.com/@types/deep-eql/-/deep-eql-4.0.2.tgz", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="], + "@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@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=="], + "@types/node": ["@types/node@24.12.2", "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="], "@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="], "@types/react-dom": ["@types/react-dom@19.2.3", "https://registry.npmmirror.com/@types/react-dom/-/react-dom-19.2.3.tgz", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/statuses": ["@types/statuses@2.0.6", "https://registry.npmmirror.com/@types/statuses/-/statuses-2.0.6.tgz", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-8.58.2.tgz", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], @@ -198,16 +422,60 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "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-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="], + + "@vitest/expect": ["@vitest/expect@3.2.4", "https://registry.npmmirror.com/@vitest/expect/-/expect-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig=="], + + "@vitest/mocker": ["@vitest/mocker@3.2.4", "https://registry.npmmirror.com/@vitest/mocker/-/mocker-3.2.4.tgz", { "dependencies": { "@vitest/spy": "3.2.4", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ=="], + + "@vitest/pretty-format": ["@vitest/pretty-format@3.2.4", "https://registry.npmmirror.com/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA=="], + + "@vitest/runner": ["@vitest/runner@3.2.4", "https://registry.npmmirror.com/@vitest/runner/-/runner-3.2.4.tgz", { "dependencies": { "@vitest/utils": "3.2.4", "pathe": "^2.0.3", "strip-literal": "^3.0.0" } }, "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ=="], + + "@vitest/snapshot": ["@vitest/snapshot@3.2.4", "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ=="], + + "@vitest/spy": ["@vitest/spy@3.2.4", "https://registry.npmmirror.com/@vitest/spy/-/spy-3.2.4.tgz", { "dependencies": { "tinyspy": "^4.0.3" } }, "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw=="], + + "@vitest/utils": ["@vitest/utils@3.2.4", "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="], + "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=="], "acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], + "agent-base": ["agent-base@7.1.4", "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + "ajv": ["ajv@6.14.0", "https://registry.npmmirror.com/ajv/-/ajv-6.14.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-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="], + "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@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "antd": ["antd@5.29.3", "https://registry.npmmirror.com/antd/-/antd-5.29.3.tgz", { "dependencies": { "@ant-design/colors": "^7.2.1", "@ant-design/cssinjs": "^1.23.0", "@ant-design/cssinjs-utils": "^1.1.3", "@ant-design/fast-color": "^2.0.6", "@ant-design/icons": "^5.6.1", "@ant-design/react-slick": "~1.1.2", "@babel/runtime": "^7.26.0", "@rc-component/color-picker": "~2.0.1", "@rc-component/mutate-observer": "^1.1.0", "@rc-component/qrcode": "~1.1.0", "@rc-component/tour": "~1.15.1", "@rc-component/trigger": "^2.3.0", "classnames": "^2.5.1", "copy-to-clipboard": "^3.3.3", "dayjs": "^1.11.11", "rc-cascader": "~3.34.0", "rc-checkbox": "~3.5.0", "rc-collapse": "~3.9.0", "rc-dialog": "~9.6.0", "rc-drawer": "~7.3.0", "rc-dropdown": "~4.2.1", "rc-field-form": "~2.7.1", "rc-image": "~7.12.0", "rc-input": "~1.8.0", "rc-input-number": "~9.5.0", "rc-mentions": "~2.20.0", "rc-menu": "~9.16.1", "rc-motion": "^2.9.5", "rc-notification": "~5.6.4", "rc-pagination": "~5.1.0", "rc-picker": "~4.11.3", "rc-progress": "~4.0.0", "rc-rate": "~2.13.1", "rc-resize-observer": "^1.4.3", "rc-segmented": "~2.7.0", "rc-select": "~14.16.8", "rc-slider": "~11.1.9", "rc-steps": "~6.0.1", "rc-switch": "~4.1.0", "rc-table": "~7.54.0", "rc-tabs": "~15.7.0", "rc-textarea": "~1.10.2", "rc-tooltip": "~6.4.0", "rc-tree": "~5.13.1", "rc-tree-select": "~5.27.0", "rc-upload": "~4.11.0", "rc-util": "^5.44.4", "scroll-into-view-if-needed": "^3.1.0", "throttle-debounce": "^5.0.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A=="], + "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.2", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="], + + "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-includes": ["array-includes@3.1.9", "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="], + + "array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="], + + "array.prototype.flat": ["array.prototype.flat@1.3.3", "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="], + + "array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "https://registry.npmmirror.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg=="], + + "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], + + "assertion-error": ["assertion-error@2.0.1", "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="], + + "async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], + + "available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="], + "balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.10.19", "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.19.tgz", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-qCkNLi2sfBOn8XhZQ0FXsT1Ki/Yo5P90hrkRamVFRS7/KV9hpfA4HkoWNU152+8w0zPjnxo5psx5NL3PSGgv5g=="], @@ -216,40 +484,124 @@ "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=="], + "cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], + + "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=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "https://registry.npmmirror.com/call-bound/-/call-bound-1.0.4.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + "callsites": ["callsites@3.1.0", "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", {}, "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001788", "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001788.tgz", {}, "sha512-6q8HFp+lOQtcf7wBK+uEenxymVWkGKkjFpCvw5W25cmMwEDU45p1xQFBQv8JDlMMry7eNxyBaR+qxgmTUZkIRQ=="], + "chai": ["chai@5.3.3", "https://registry.npmmirror.com/chai/-/chai-5.3.3.tgz", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw=="], + "chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + "check-error": ["check-error@2.1.3", "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="], + "chokidar": ["chokidar@4.0.3", "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], + "classnames": ["classnames@2.5.1", "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + + "cli-width": ["cli-width@4.1.0", "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@8.0.1", "https://registry.npmmirror.com/cliui/-/cliui-8.0.1.tgz", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], "color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="], + "concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], "convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "copy-to-clipboard": ["copy-to-clipboard@3.3.3", "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz", { "dependencies": { "toggle-selection": "^1.0.6" } }, "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA=="], + "cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "css.escape": ["css.escape@1.5.1", "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + + "cssstyle": ["cssstyle@4.6.0", "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="], + "csstype": ["csstype@3.2.3", "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + "data-urls": ["data-urls@5.0.0", "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz", { "dependencies": { "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.0.0" } }, "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg=="], + + "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=="], + + "data-view-byte-offset": ["data-view-byte-offset@1.0.1", "https://registry.npmmirror.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ=="], + + "dayjs": ["dayjs@1.11.20", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz", {}, "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ=="], + "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=="], + + "deep-eql": ["deep-eql@5.0.2", "https://registry.npmmirror.com/deep-eql/-/deep-eql-5.0.2.tgz", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], + "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=="], + "define-data-property": ["define-data-property@1.1.4", "https://registry.npmmirror.com/define-data-property/-/define-data-property-1.1.4.tgz", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A=="], + + "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.6.3", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + + "dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.336", "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.336.tgz", {}, "sha512-AbH9q9J455r/nLmdNZes0G0ZKcRX73FicwowalLs6ijwOmCJSRRrLX63lcAlzy9ux3dWK1w1+1nsBJEWN11hcQ=="], + "emoji-regex": ["emoji-regex@8.0.0", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "es-abstract": ["es-abstract@1.24.2", "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="], + + "es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-module-lexer": ["es-module-lexer@1.7.0", "https://registry.npmmirror.com/es-module-lexer/-/es-module-lexer-1.7.0.tgz", {}, "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + + "es-shim-unscopables": ["es-shim-unscopables@1.1.0", "https://registry.npmmirror.com/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw=="], + + "es-to-primitive": ["es-to-primitive@1.3.0", "https://registry.npmmirror.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g=="], + + "esbuild": ["esbuild@0.27.7", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.7.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], + "escalade": ["escalade@3.2.0", "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], "escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], "eslint": ["eslint@9.39.4", "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "optionalPeers": ["jiti"], "bin": { "eslint": "bin/eslint.js" } }, "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ=="], + "eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "https://registry.npmmirror.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.10.tgz", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, "sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ=="], + + "eslint-module-utils": ["eslint-module-utils@2.12.1", "https://registry.npmmirror.com/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", { "dependencies": { "debug": "^3.2.7" } }, "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw=="], + + "eslint-plugin-import": ["eslint-plugin-import@2.32.0", "https://registry.npmmirror.com/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA=="], + "eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.0.1", "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA=="], "eslint-plugin-react-refresh": ["eslint-plugin-react-refresh@0.5.2", "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", { "peerDependencies": { "eslint": "^9 || ^10" } }, "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA=="], @@ -266,8 +618,12 @@ "estraverse": ["estraverse@5.3.0", "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + "estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "esutils": ["esutils@2.0.3", "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "expect-type": ["expect-type@1.3.0", "https://registry.npmmirror.com/expect-type/-/expect-type-1.3.0.tgz", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", {}, "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="], @@ -284,20 +640,72 @@ "flatted": ["flatted@3.4.2", "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz", {}, "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA=="], + "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=="], + + "foreground-child": ["foreground-child@3.3.1", "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "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=="], + + "functions-have-names": ["functions-have-names@1.2.3", "https://registry.npmmirror.com/functions-have-names/-/functions-have-names-1.2.3.tgz", {}, "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ=="], + + "generator-function": ["generator-function@2.0.1", "https://registry.npmmirror.com/generator-function/-/generator-function-2.0.1.tgz", {}, "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g=="], + "gensync": ["gensync@1.0.0-beta.2", "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "https://registry.npmmirror.com/get-caller-file/-/get-caller-file-2.0.5.tgz", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "get-symbol-description": ["get-symbol-description@1.1.0", "https://registry.npmmirror.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg=="], + + "glob": ["glob@10.5.0", "https://registry.npmmirror.com/glob/-/glob-10.5.0.tgz", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + "glob-parent": ["glob-parent@6.0.2", "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", { "dependencies": { "is-glob": "^4.0.3" } }, "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A=="], "globals": ["globals@17.5.0", "https://registry.npmmirror.com/globals/-/globals-17.5.0.tgz", {}, "sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g=="], + "globalthis": ["globalthis@1.0.4", "https://registry.npmmirror.com/globalthis/-/globalthis-1.0.4.tgz", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ=="], + + "gopd": ["gopd@1.2.0", "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "graphql": ["graphql@16.13.2", "https://registry.npmmirror.com/graphql/-/graphql-16.13.2.tgz", {}, "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig=="], + + "has-bigints": ["has-bigints@1.1.0", "https://registry.npmmirror.com/has-bigints/-/has-bigints-1.1.0.tgz", {}, "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg=="], + "has-flag": ["has-flag@4.0.0", "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-property-descriptors": ["has-property-descriptors@1.0.2", "https://registry.npmmirror.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", { "dependencies": { "es-define-property": "^1.0.0" } }, "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg=="], + + "has-proto": ["has-proto@1.2.0", "https://registry.npmmirror.com/has-proto/-/has-proto-1.2.0.tgz", { "dependencies": { "dunder-proto": "^1.0.0" } }, "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ=="], + + "has-symbols": ["has-symbols@1.1.0", "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "has-tostringtag": ["has-tostringtag@1.0.2", "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + + "hasown": ["hasown@2.0.2", "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "headers-polyfill": ["headers-polyfill@4.0.3", "https://registry.npmmirror.com/headers-polyfill/-/headers-polyfill-4.0.3.tgz", {}, "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ=="], + "hermes-estree": ["hermes-estree@0.25.1", "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz", {}, "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw=="], "hermes-parser": ["hermes-parser@0.25.1", "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz", { "dependencies": { "hermes-estree": "0.25.1" } }, "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA=="], + "html-encoding-sniffer": ["html-encoding-sniffer@4.0.0", "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", { "dependencies": { "whatwg-encoding": "^3.1.1" } }, "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ=="], + + "html-escaper": ["html-escaper@2.0.2", "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "iconv-lite": ["iconv-lite@0.6.3", "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "ignore": ["ignore@5.3.2", "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], "immutable": ["immutable@5.1.5", "https://registry.npmmirror.com/immutable/-/immutable-5.1.5.tgz", {}, "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A=="], @@ -306,16 +714,84 @@ "imurmurhash": ["imurmurhash@0.1.4", "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="], + "indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + + "internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="], + + "is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="], + + "is-async-function": ["is-async-function@2.1.1", "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="], + + "is-bigint": ["is-bigint@1.1.0", "https://registry.npmmirror.com/is-bigint/-/is-bigint-1.1.0.tgz", { "dependencies": { "has-bigints": "^1.0.2" } }, "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ=="], + + "is-boolean-object": ["is-boolean-object@1.2.2", "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="], + + "is-callable": ["is-callable@1.2.7", "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="], + + "is-core-module": ["is-core-module@2.16.1", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + + "is-data-view": ["is-data-view@1.0.2", "https://registry.npmmirror.com/is-data-view/-/is-data-view-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw=="], + + "is-date-object": ["is-date-object@1.1.0", "https://registry.npmmirror.com/is-date-object/-/is-date-object-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg=="], + "is-extglob": ["is-extglob@2.1.1", "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], + "is-finalizationregistry": ["is-finalizationregistry@1.1.1", "https://registry.npmmirror.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-generator-function": ["is-generator-function@1.1.2", "https://registry.npmmirror.com/is-generator-function/-/is-generator-function-1.1.2.tgz", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA=="], + "is-glob": ["is-glob@4.0.3", "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], + "is-map": ["is-map@2.0.3", "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="], + + "is-negative-zero": ["is-negative-zero@2.0.3", "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="], + + "is-node-process": ["is-node-process@1.2.0", "https://registry.npmmirror.com/is-node-process/-/is-node-process-1.2.0.tgz", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + + "is-number-object": ["is-number-object@1.1.1", "https://registry.npmmirror.com/is-number-object/-/is-number-object-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw=="], + + "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=="], + + "is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="], + + "is-string": ["is-string@1.1.1", "https://registry.npmmirror.com/is-string/-/is-string-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA=="], + + "is-symbol": ["is-symbol@1.1.1", "https://registry.npmmirror.com/is-symbol/-/is-symbol-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w=="], + + "is-typed-array": ["is-typed-array@1.1.15", "https://registry.npmmirror.com/is-typed-array/-/is-typed-array-1.1.15.tgz", { "dependencies": { "which-typed-array": "^1.1.16" } }, "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ=="], + + "is-weakmap": ["is-weakmap@2.0.2", "https://registry.npmmirror.com/is-weakmap/-/is-weakmap-2.0.2.tgz", {}, "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w=="], + + "is-weakref": ["is-weakref@1.1.1", "https://registry.npmmirror.com/is-weakref/-/is-weakref-1.1.1.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew=="], + + "is-weakset": ["is-weakset@2.0.4", "https://registry.npmmirror.com/is-weakset/-/is-weakset-2.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ=="], + + "isarray": ["isarray@2.0.5", "https://registry.npmmirror.com/isarray/-/isarray-2.0.5.tgz", {}, "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw=="], + "isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "https://registry.npmmirror.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "jackspeak": ["jackspeak@3.4.3", "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "js-tokens": ["js-tokens@10.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], "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@26.1.0", "https://registry.npmmirror.com/jsdom/-/jsdom-26.1.0.tgz", { "dependencies": { "cssstyle": "^4.2.1", "data-urls": "^5.0.0", "decimal.js": "^10.5.0", "html-encoding-sniffer": "^4.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", "nwsapi": "^2.2.16", "parse5": "^7.2.1", "rrweb-cssom": "^0.8.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", "tough-cookie": "^5.1.1", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^7.0.0", "whatwg-encoding": "^3.1.1", "whatwg-mimetype": "^4.0.0", "whatwg-url": "^14.1.1", "ws": "^8.18.0", "xml-name-validator": "^5.0.0" }, "peerDependencies": { "canvas": "^3.0.0" }, "optionalPeers": ["canvas"] }, "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg=="], + "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=="], @@ -324,7 +800,9 @@ "json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="], - "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=="], + "json2mq": ["json2mq@0.2.0", "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz", { "dependencies": { "string-convert": "^0.2.0" } }, "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA=="], + + "json5": ["json5@1.0.2", "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="], "keyv": ["keyv@4.5.4", "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", { "dependencies": { "json-buffer": "3.0.1" } }, "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw=="], @@ -358,104 +836,430 @@ "lodash.merge": ["lodash.merge@4.6.2", "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="], + "loupe": ["loupe@3.2.1", "https://registry.npmmirror.com/loupe/-/loupe-3.2.1.tgz", {}, "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ=="], + "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=="], + "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=="], + + "magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], + + "magicast": ["magicast@0.3.5", "https://registry.npmmirror.com/magicast/-/magicast-0.3.5.tgz", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "make-dir": ["make-dir@4.0.0", "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "min-indent": ["min-indent@1.0.1", "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="], + "minimist": ["minimist@1.2.8", "https://registry.npmmirror.com/minimist/-/minimist-1.2.8.tgz", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "minipass": ["minipass@7.1.3", "https://registry.npmmirror.com/minipass/-/minipass-7.1.3.tgz", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + "ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msw": ["msw@2.13.3", "https://registry.npmmirror.com/msw/-/msw-2.13.3.tgz", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-/F49bxavkNGfreMlrKmTxZs6YorjfMbbDLd89Q3pWi+cXGtQQNXXaHt4MkXN7li91xnQJ24HWXqW9QDm5id33w=="], + + "mute-stream": ["mute-stream@2.0.0", "https://registry.npmmirror.com/mute-stream/-/mute-stream-2.0.0.tgz", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="], + "nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], "natural-compare": ["natural-compare@1.4.0", "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", {}, "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw=="], "node-addon-api": ["node-addon-api@7.1.1", "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="], + "node-exports-info": ["node-exports-info@1.6.0", "https://registry.npmmirror.com/node-exports-info/-/node-exports-info-1.6.0.tgz", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw=="], + "node-releases": ["node-releases@2.0.37", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="], + "nwsapi": ["nwsapi@2.2.23", "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="], + + "object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "object-keys": ["object-keys@1.1.1", "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="], + + "object.assign": ["object.assign@4.1.7", "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="], + + "object.entries": ["object.entries@1.1.9", "https://registry.npmmirror.com/object.entries/-/object.entries-1.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw=="], + + "object.fromentries": ["object.fromentries@2.0.8", "https://registry.npmmirror.com/object.fromentries/-/object.fromentries-2.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ=="], + + "object.groupby": ["object.groupby@1.0.3", "https://registry.npmmirror.com/object.groupby/-/object.groupby-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ=="], + + "object.values": ["object.values@1.2.1", "https://registry.npmmirror.com/object.values/-/object.values-1.2.1.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], + "optionator": ["optionator@0.9.4", "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="], + "outvariant": ["outvariant@1.4.3", "https://registry.npmmirror.com/outvariant/-/outvariant-1.4.3.tgz", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + + "own-keys": ["own-keys@1.0.1", "https://registry.npmmirror.com/own-keys/-/own-keys-1.0.1.tgz", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], + "p-limit": ["p-limit@3.1.0", "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], "p-locate": ["p-locate@5.0.0", "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", { "dependencies": { "p-limit": "^3.0.2" } }, "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw=="], + "package-json-from-dist": ["package-json-from-dist@1.0.1", "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parent-module": ["parent-module@1.0.1", "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], + "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=="], + "path-exists": ["path-exists@4.0.0", "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", {}, "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w=="], "path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + + "pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + + "pathval": ["pathval@2.0.1", "https://registry.npmmirror.com/pathval/-/pathval-2.0.1.tgz", {}, "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ=="], + "picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picomatch": ["picomatch@4.0.4", "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.4.tgz", {}, "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A=="], + "playwright": ["playwright@1.59.1", "https://registry.npmmirror.com/playwright/-/playwright-1.59.1.tgz", { "dependencies": { "playwright-core": "1.59.1" }, "optionalDependencies": { "fsevents": "2.3.2" }, "bin": { "playwright": "cli.js" } }, "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw=="], + + "playwright-core": ["playwright-core@1.59.1", "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.59.1.tgz", { "bin": { "playwright-core": "cli.js" } }, "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg=="], + + "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.9", "https://registry.npmmirror.com/postcss/-/postcss-8.5.9.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-7a70Nsot+EMX9fFU3064K/kdHWZqGVY+BADLyXc8Dfv+mTLLVl6JzJpPaCZ2kQL9gIJvKXSLMHhqdRRjwQeFtw=="], "prelude-ls": ["prelude-ls@1.2.1", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", {}, "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g=="], + "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=="], + "punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="], + "rc-cascader": ["rc-cascader@3.34.0", "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz", { "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "^2.3.1", "rc-select": "~14.16.2", "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag=="], + + "rc-checkbox": ["rc-checkbox@3.5.0", "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-3.5.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.25.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg=="], + + "rc-collapse": ["rc-collapse@3.9.0", "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.9.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.3.4", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA=="], + + "rc-dialog": ["rc-dialog@9.6.0", "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/portal": "^1.0.0-8", "classnames": "^2.2.6", "rc-motion": "^2.3.0", "rc-util": "^5.21.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg=="], + + "rc-drawer": ["rc-drawer@7.3.0", "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-7.3.0.tgz", { "dependencies": { "@babel/runtime": "^7.23.9", "@rc-component/portal": "^1.1.1", "classnames": "^2.2.6", "rc-motion": "^2.6.1", "rc-util": "^5.38.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg=="], + + "rc-dropdown": ["rc-dropdown@4.2.1", "https://registry.npmmirror.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-util": "^5.44.1" }, "peerDependencies": { "react": ">=16.11.0", "react-dom": ">=16.11.0" } }, "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA=="], + + "rc-field-form": ["rc-field-form@2.7.1", "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.1.tgz", { "dependencies": { "@babel/runtime": "^7.18.0", "@rc-component/async-validator": "^5.0.3", "rc-util": "^5.32.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A=="], + + "rc-image": ["rc-image@7.12.0", "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/portal": "^1.0.2", "classnames": "^2.2.6", "rc-dialog": "~9.6.0", "rc-motion": "^2.6.2", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q=="], + + "rc-input": ["rc-input@1.8.0", "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.18.1" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA=="], + + "rc-input-number": ["rc-input-number@9.5.0", "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/mini-decimal": "^1.0.1", "classnames": "^2.2.5", "rc-input": "~1.8.0", "rc-util": "^5.40.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag=="], + + "rc-mentions": ["rc-mentions@2.20.0", "https://registry.npmmirror.com/rc-mentions/-/rc-mentions-2.20.0.tgz", { "dependencies": { "@babel/runtime": "^7.22.5", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.6", "rc-input": "~1.8.0", "rc-menu": "~9.16.0", "rc-textarea": "~1.10.0", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ=="], + + "rc-menu": ["rc-menu@9.16.1", "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.0.0", "classnames": "2.x", "rc-motion": "^2.4.3", "rc-overflow": "^1.3.1", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg=="], + + "rc-motion": ["rc-motion@2.9.5", "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-util": "^5.44.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA=="], + + "rc-notification": ["rc-notification@5.6.4", "https://registry.npmmirror.com/rc-notification/-/rc-notification-5.6.4.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.9.0", "rc-util": "^5.20.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw=="], + + "rc-overflow": ["rc-overflow@1.5.0", "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.5.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-resize-observer": "^1.0.0", "rc-util": "^5.37.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg=="], + + "rc-pagination": ["rc-pagination@5.1.0", "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-5.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.3.2", "rc-util": "^5.38.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ=="], + + "rc-picker": ["rc-picker@4.11.3", "https://registry.npmmirror.com/rc-picker/-/rc-picker-4.11.3.tgz", { "dependencies": { "@babel/runtime": "^7.24.7", "@rc-component/trigger": "^2.0.0", "classnames": "^2.2.1", "rc-overflow": "^1.3.2", "rc-resize-observer": "^1.4.0", "rc-util": "^5.43.0" }, "peerDependencies": { "date-fns": ">= 2.x", "dayjs": ">= 1.x", "luxon": ">= 3.x", "moment": ">= 2.x", "react": ">=16.9.0", "react-dom": ">=16.9.0" }, "optionalPeers": ["date-fns", "dayjs", "luxon", "moment"] }, "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg=="], + + "rc-progress": ["rc-progress@4.0.0", "https://registry.npmmirror.com/rc-progress/-/rc-progress-4.0.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.6", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw=="], + + "rc-rate": ["rc-rate@2.13.1", "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.13.1.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.0.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q=="], + + "rc-resize-observer": ["rc-resize-observer@1.4.3", "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz", { "dependencies": { "@babel/runtime": "^7.20.7", "classnames": "^2.2.1", "rc-util": "^5.44.1", "resize-observer-polyfill": "^1.5.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ=="], + + "rc-segmented": ["rc-segmented@2.7.1", "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.7.1.tgz", { "dependencies": { "@babel/runtime": "^7.11.1", "classnames": "^2.2.1", "rc-motion": "^2.4.4", "rc-util": "^5.17.0" }, "peerDependencies": { "react": ">=16.0.0", "react-dom": ">=16.0.0" } }, "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g=="], + + "rc-select": ["rc-select@14.16.8", "https://registry.npmmirror.com/rc-select/-/rc-select-14.16.8.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/trigger": "^2.1.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-overflow": "^1.3.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.2" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg=="], + + "rc-slider": ["rc-slider@11.1.9", "https://registry.npmmirror.com/rc-slider/-/rc-slider-11.1.9.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.5", "rc-util": "^5.36.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A=="], + + "rc-steps": ["rc-steps@6.0.1", "https://registry.npmmirror.com/rc-steps/-/rc-steps-6.0.1.tgz", { "dependencies": { "@babel/runtime": "^7.16.7", "classnames": "^2.2.3", "rc-util": "^5.16.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g=="], + + "rc-switch": ["rc-switch@4.1.0", "https://registry.npmmirror.com/rc-switch/-/rc-switch-4.1.0.tgz", { "dependencies": { "@babel/runtime": "^7.21.0", "classnames": "^2.2.1", "rc-util": "^5.30.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg=="], + + "rc-table": ["rc-table@7.54.0", "https://registry.npmmirror.com/rc-table/-/rc-table-7.54.0.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "@rc-component/context": "^1.4.0", "classnames": "^2.2.5", "rc-resize-observer": "^1.1.0", "rc-util": "^5.44.3", "rc-virtual-list": "^3.14.2" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw=="], + + "rc-tabs": ["rc-tabs@15.7.0", "https://registry.npmmirror.com/rc-tabs/-/rc-tabs-15.7.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.2", "classnames": "2.x", "rc-dropdown": "~4.2.0", "rc-menu": "~9.16.0", "rc-motion": "^2.6.2", "rc-resize-observer": "^1.0.0", "rc-util": "^5.34.1" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA=="], + + "rc-textarea": ["rc-textarea@1.10.2", "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.2.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "^2.2.1", "rc-input": "~1.8.0", "rc-resize-observer": "^1.0.0", "rc-util": "^5.27.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ=="], + + "rc-tooltip": ["rc-tooltip@6.4.0", "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-6.4.0.tgz", { "dependencies": { "@babel/runtime": "^7.11.2", "@rc-component/trigger": "^2.0.0", "classnames": "^2.3.1", "rc-util": "^5.44.3" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g=="], + + "rc-tree": ["rc-tree@5.13.1", "https://registry.npmmirror.com/rc-tree/-/rc-tree-5.13.1.tgz", { "dependencies": { "@babel/runtime": "^7.10.1", "classnames": "2.x", "rc-motion": "^2.0.1", "rc-util": "^5.16.1", "rc-virtual-list": "^3.5.1" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A=="], + + "rc-tree-select": ["rc-tree-select@5.27.0", "https://registry.npmmirror.com/rc-tree-select/-/rc-tree-select-5.27.0.tgz", { "dependencies": { "@babel/runtime": "^7.25.7", "classnames": "2.x", "rc-select": "~14.16.2", "rc-tree": "~5.13.0", "rc-util": "^5.43.0" }, "peerDependencies": { "react": "*", "react-dom": "*" } }, "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww=="], + + "rc-upload": ["rc-upload@4.11.0", "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.11.0.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "classnames": "^2.2.5", "rc-util": "^5.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA=="], + + "rc-util": ["rc-util@5.44.4", "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz", { "dependencies": { "@babel/runtime": "^7.18.3", "react-is": "^18.2.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w=="], + + "rc-virtual-list": ["rc-virtual-list@3.19.2", "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz", { "dependencies": { "@babel/runtime": "^7.20.0", "classnames": "^2.2.6", "rc-resize-observer": "^1.0.0", "rc-util": "^5.36.0" }, "peerDependencies": { "react": ">=16.9.0", "react-dom": ">=16.9.0" } }, "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA=="], + "react": ["react@19.2.5", "https://registry.npmmirror.com/react/-/react-19.2.5.tgz", {}, "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA=="], "react-dom": ["react-dom@19.2.5", "https://registry.npmmirror.com/react-dom/-/react-dom-19.2.5.tgz", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.5" } }, "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag=="], + "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-router": ["react-router@7.14.1", "https://registry.npmmirror.com/react-router/-/react-router-7.14.1.tgz", { "dependencies": { "cookie": "^1.0.1", "set-cookie-parser": "^2.6.0" }, "peerDependencies": { "react": ">=18", "react-dom": ">=18" }, "optionalPeers": ["react-dom"] }, "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg=="], + "readdirp": ["readdirp@4.1.2", "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], + "redent": ["redent@3.0.0", "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + + "reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="], + + "regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "https://registry.npmmirror.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA=="], + + "require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "resize-observer-polyfill": ["resize-observer-polyfill@1.5.1", "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz", {}, "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg=="], + + "resolve": ["resolve@2.0.0-next.6", "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="], + "resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "rettime": ["rettime@0.11.7", "https://registry.npmmirror.com/rettime/-/rettime-0.11.7.tgz", {}, "sha512-DoAm1WjR1eH7z8sHPtvvUMIZh4/CSKkGCz6CxPqOrEAnOGtOuHSnSE9OC+razqxKuf4ub7pAYyl/vZV0vGs5tg=="], + "rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="], + "rollup": ["rollup@4.60.1", "https://registry.npmmirror.com/rollup/-/rollup-4.60.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], + + "rrweb-cssom": ["rrweb-cssom@0.8.0", "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", {}, "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw=="], + + "safe-array-concat": ["safe-array-concat@1.1.3", "https://registry.npmmirror.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q=="], + + "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=="], + + "safe-regex-test": ["safe-regex-test@1.1.0", "https://registry.npmmirror.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw=="], + + "safer-buffer": ["safer-buffer@2.1.2", "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + "sass": ["sass@1.99.0", "https://registry.npmmirror.com/sass/-/sass-1.99.0.tgz", { "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.1.5", "source-map-js": ">=0.6.2 <2.0.0" }, "optionalDependencies": { "@parcel/watcher": "^2.4.1" }, "bin": { "sass": "sass.js" } }, "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q=="], + "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=="], + "scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="], + "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=="], + "set-cookie-parser": ["set-cookie-parser@2.7.2", "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", {}, "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw=="], + + "set-function-length": ["set-function-length@1.2.2", "https://registry.npmmirror.com/set-function-length/-/set-function-length-1.2.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg=="], + + "set-function-name": ["set-function-name@2.0.2", "https://registry.npmmirror.com/set-function-name/-/set-function-name-2.0.2.tgz", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ=="], + + "set-proto": ["set-proto@1.0.0", "https://registry.npmmirror.com/set-proto/-/set-proto-1.0.0.tgz", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw=="], + "shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "side-channel": ["side-channel@1.1.0", "https://registry.npmmirror.com/side-channel/-/side-channel-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.1", "https://registry.npmmirror.com/side-channel-list/-/side-channel-list-1.0.1.tgz", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w=="], + + "side-channel-map": ["side-channel-map@1.0.1", "https://registry.npmmirror.com/side-channel-map/-/side-channel-map-1.0.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "https://registry.npmmirror.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], + + "signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "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=="], + "stackback": ["stackback@0.0.2", "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], + + "statuses": ["statuses@2.0.2", "https://registry.npmmirror.com/statuses/-/statuses-2.0.2.tgz", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + + "std-env": ["std-env@3.10.0", "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "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=="], + + "strict-event-emitter": ["strict-event-emitter@0.5.1", "https://registry.npmmirror.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], + + "string-convert": ["string-convert@0.2.1", "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz", {}, "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A=="], + + "string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string.prototype.trim": ["string.prototype.trim@1.2.10", "https://registry.npmmirror.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA=="], + + "string.prototype.trimend": ["string.prototype.trimend@1.0.9", "https://registry.npmmirror.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ=="], + + "string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="], + + "strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-bom": ["strip-bom@3.0.0", "https://registry.npmmirror.com/strip-bom/-/strip-bom-3.0.0.tgz", {}, "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA=="], + + "strip-indent": ["strip-indent@3.0.0", "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "strip-json-comments": ["strip-json-comments@3.1.1", "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", {}, "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig=="], + "strip-literal": ["strip-literal@3.1.0", "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], + + "stylis": ["stylis@4.3.6", "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz", {}, "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ=="], + "supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "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=="], + + "tagged-tag": ["tagged-tag@1.0.0", "https://registry.npmmirror.com/tagged-tag/-/tagged-tag-1.0.0.tgz", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + + "test-exclude": ["test-exclude@7.0.2", "https://registry.npmmirror.com/test-exclude/-/test-exclude-7.0.2.tgz", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="], + + "throttle-debounce": ["throttle-debounce@5.0.2", "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz", {}, "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A=="], + + "tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], + + "tinyexec": ["tinyexec@0.3.2", "https://registry.npmmirror.com/tinyexec/-/tinyexec-0.3.2.tgz", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], + "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=="], + "tinypool": ["tinypool@1.1.1", "https://registry.npmmirror.com/tinypool/-/tinypool-1.1.1.tgz", {}, "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg=="], + + "tinyrainbow": ["tinyrainbow@2.0.0", "https://registry.npmmirror.com/tinyrainbow/-/tinyrainbow-2.0.0.tgz", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], + + "tinyspy": ["tinyspy@4.0.4", "https://registry.npmmirror.com/tinyspy/-/tinyspy-4.0.4.tgz", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + + "tldts": ["tldts@6.1.86", "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz", { "dependencies": { "tldts-core": "^6.1.86" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ=="], + + "tldts-core": ["tldts-core@6.1.86", "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz", {}, "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA=="], + + "toggle-selection": ["toggle-selection@1.0.6", "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz", {}, "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ=="], + + "tough-cookie": ["tough-cookie@5.1.2", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz", { "dependencies": { "tldts": "^6.1.32" } }, "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A=="], + + "tr46": ["tr46@5.1.1", "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz", { "dependencies": { "punycode": "^2.3.1" } }, "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw=="], + "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=="], + "tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="], + "type-fest": ["type-fest@5.5.0", "https://registry.npmmirror.com/type-fest/-/type-fest-5.5.0.tgz", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g=="], + + "typed-array-buffer": ["typed-array-buffer@1.0.3", "https://registry.npmmirror.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw=="], + + "typed-array-byte-length": ["typed-array-byte-length@1.0.3", "https://registry.npmmirror.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg=="], + + "typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "https://registry.npmmirror.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ=="], + + "typed-array-length": ["typed-array-length@1.0.7", "https://registry.npmmirror.com/typed-array-length/-/typed-array-length-1.0.7.tgz", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], + "typescript": ["typescript@6.0.2", "https://registry.npmmirror.com/typescript/-/typescript-6.0.2.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], "typescript-eslint": ["typescript-eslint@8.58.2", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.58.2.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], + "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-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + "until-async": ["until-async@3.0.2", "https://registry.npmmirror.com/until-async/-/until-async-3.0.2.tgz", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "uri-js": ["uri-js@4.4.1", "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg=="], "vite": ["vite@8.0.8", "https://registry.npmmirror.com/vite/-/vite-8.0.8.tgz", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.15", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "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-dbU7/iLVa8KZALJyLOBOQ88nOXtNG8vxKuOT4I2mD+Ya70KPceF4IAmDsmU0h1Qsn5bPrvsY9HJstCRh3hG6Uw=="], + "vite-node": ["vite-node@3.2.4", "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], + + "vitest": ["vitest@3.2.4", "https://registry.npmmirror.com/vitest/-/vitest-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="], + + "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@7.0.0", "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz", {}, "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="], + + "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-url": ["whatwg-url@14.2.0", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="], + "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=="], + "which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="], + + "which-builtin-type": ["which-builtin-type@1.2.1", "https://registry.npmmirror.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q=="], + + "which-collection": ["which-collection@1.0.2", "https://registry.npmmirror.com/which-collection/-/which-collection-1.0.2.tgz", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw=="], + + "which-typed-array": ["which-typed-array@1.1.20", "https://registry.npmmirror.com/which-typed-array/-/which-typed-array-1.1.20.tgz", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg=="], + + "why-is-node-running": ["why-is-node-running@2.3.0", "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], + "word-wrap": ["word-wrap@1.2.5", "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], + "wrap-ansi": ["wrap-ansi@6.2.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.20.0", "https://registry.npmmirror.com/ws/-/ws-8.20.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA=="], + + "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=="], + + "y18n": ["y18n@5.0.8", "https://registry.npmmirror.com/y18n/-/y18n-5.0.8.tgz", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yargs": ["yargs@17.7.2", "https://registry.npmmirror.com/yargs/-/yargs-17.7.2.tgz", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "https://registry.npmmirror.com/yargs-parser/-/yargs-parser-21.1.1.tgz", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "yocto-queue": ["yocto-queue@0.1.0", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "yoctocolors-cjs": ["yoctocolors-cjs@2.1.3", "https://registry.npmmirror.com/yoctocolors-cjs/-/yoctocolors-cjs-2.1.3.tgz", {}, "sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw=="], + "zod": ["zod@4.3.6", "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz", {}, "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg=="], "zod-validation-error": ["zod-validation-error@4.0.2", "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ=="], + "@asamuzakjp/css-color/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "@babel/code-frame/js-tokens": ["js-tokens@4.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "@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=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.2.0.tgz", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@testing-library/dom/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=="], + + "@testing-library/dom/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=="], + "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -464,10 +1268,56 @@ "@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="], + "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "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=="], + + "eslint-module-utils/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "eslint-plugin-import/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + + "glob/minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + + "make-dir/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], + + "msw/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=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "playwright/fsevents": ["fsevents@2.3.2", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz", { "os": "darwin" }, "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA=="], + + "pretty-format/ansi-styles": ["ansi-styles@5.2.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="], + + "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=="], + "rolldown/@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.15", "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.15.tgz", {}, "sha512-UromN0peaE53IaBRe9W7CjrZgXl90fqGpK+mIZbA3qSTeYqg3pqpROBdIPvOG3F5ereDHNwoHBI2e50n1BDr1g=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + + "test-exclude/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + + "vite-node/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + + "vitest/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.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", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="], + + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "@isaacs/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=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "msw/tough-cookie/tldts": ["tldts@7.0.28", "https://registry.npmmirror.com/tldts/-/tldts-7.0.28.tgz", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="], + + "test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], + "@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], + + "msw/tough-cookie/tldts/tldts-core": ["tldts-core@7.0.28", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.28.tgz", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="], + + "test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } } diff --git a/frontend/e2e/providers.spec.ts b/frontend/e2e/providers.spec.ts new file mode 100644 index 0000000..a608a38 --- /dev/null +++ b/frontend/e2e/providers.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from '@playwright/test'; + +test.describe('供应商管理', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/providers'); + }); + + test('应显示供应商管理页面', async ({ page }) => { + await expect(page.getByRole('heading', { name: '供应商管理' })).toBeVisible(); + await expect(page.getByText('供应商列表')).toBeVisible(); + }); + + test('应显示添加供应商按钮', async ({ page }) => { + await expect(page.getByRole('button', { name: '添加供应商' })).toBeVisible(); + }); + + test('应通过顶部导航切换页面', async ({ page }) => { + await page.getByText('用量统计').click(); + await expect(page.getByRole('heading', { name: '用量统计' })).toBeVisible(); + + await page.getByText('供应商管理').click(); + await expect(page.getByRole('heading', { name: '供应商管理' })).toBeVisible(); + }); +}); diff --git a/frontend/e2e/stats.spec.ts b/frontend/e2e/stats.spec.ts new file mode 100644 index 0000000..a837bea --- /dev/null +++ b/frontend/e2e/stats.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from '@playwright/test'; + +test.describe('用量统计', () => { + test.beforeEach(async ({ page }) => { + await page.goto('/stats'); + }); + + test('应显示用量统计页面', async ({ page }) => { + await expect(page.getByRole('heading', { name: '用量统计' })).toBeVisible(); + }); + + test('应显示筛选控件', async ({ page }) => { + await expect(page.getByText('所有供应商')).toBeVisible(); + }); + + test('应通过导航返回供应商页面', async ({ page }) => { + await page.getByText('供应商管理').click(); + await expect(page.getByRole('heading', { name: '供应商管理' })).toBeVisible(); + }); +}); diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js new file mode 100644 index 0000000..32e3be5 --- /dev/null +++ b/frontend/eslint.config.js @@ -0,0 +1,51 @@ +import js from '@eslint/js' +import globals from 'globals' +import reactHooks from 'eslint-plugin-react-hooks' +import reactRefresh from 'eslint-plugin-react-refresh' +import tseslint from 'typescript-eslint' +import importPlugin from 'eslint-plugin-import' +import tanstackQuery from '@tanstack/eslint-plugin-query' + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2023, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + import: importPlugin, + '@tanstack/query': tanstackQuery, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + 'import/order': [ + 'warn', + { + groups: [ + 'builtin', + 'external', + 'internal', + 'parent', + 'sibling', + 'index', + 'type', + ], + 'newlines-between': 'never', + alphabetize: { order: 'asc', caseInsensitive: true }, + pathGroups: [ + { pattern: '@/**', group: 'internal', position: 'before' }, + ], + }, + ], + }, + }, +) diff --git a/frontend/package.json b/frontend/package.json index 7bfa5de..013d9cb 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -7,25 +7,43 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", - "preview": "vite preview" + "preview": "vite preview", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "test:e2e": "playwright test" }, "dependencies": { + "@ant-design/icons": "^5.6.1", + "@tanstack/react-query": "^5.80.2", + "antd": "^5.24.9", "react": "^19.2.4", "react-dom": "^19.2.4", - "sass": "^1.99.0" + "react-router": "^7.6.1" }, "devDependencies": { "@eslint/js": "^9.39.4", + "@playwright/test": "^1.52.0", + "@tanstack/eslint-plugin-query": "^5.78.0", + "@testing-library/jest-dom": "^6.6.3", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", "@types/node": "^24.12.2", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.1", + "@vitest/coverage-v8": "^3.2.1", "eslint": "^9.39.4", + "eslint-plugin-import": "^2.31.0", "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.5.2", "globals": "^17.4.0", + "jsdom": "^26.1.0", + "msw": "^2.8.2", + "sass": "^1.99.0", "typescript": "~6.0.2", "typescript-eslint": "^8.58.0", - "vite": "^8.0.4" + "vite": "^8.0.4", + "vitest": "^3.2.1" } } diff --git a/frontend/playwright.config.ts b/frontend/playwright.config.ts new file mode 100644 index 0000000..b556a95 --- /dev/null +++ b/frontend/playwright.config.ts @@ -0,0 +1,25 @@ +import { defineConfig, devices } from '@playwright/test' + +export default defineConfig({ + testDir: './e2e', + fullyParallel: true, + forbidOnly: !!process.env.CI, + retries: process.env.CI ? 2 : 0, + workers: process.env.CI ? 1 : undefined, + reporter: 'html', + use: { + baseURL: 'http://localhost:5173', + trace: 'on-first-retry', + }, + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, + ], + webServer: { + command: 'bun run dev', + url: 'http://localhost:5173', + reuseExistingServer: !process.env.CI, + }, +}) diff --git a/frontend/src/App.css b/frontend/src/App.css deleted file mode 100644 index c664bcf..0000000 --- a/frontend/src/App.css +++ /dev/null @@ -1,221 +0,0 @@ -.app { - min-height: 100vh; -} - -.navbar { - background: #1a1a2e; - color: #fff; - padding: 1rem 2rem; - display: flex; - justify-content: space-between; - align-items: center; -} - -.navbar h1 { - font-size: 1.5rem; - font-weight: 600; -} - -.nav-links { - display: flex; - gap: 1rem; -} - -.nav-links button { - background: transparent; - border: none; - color: #aaa; - font-size: 1rem; - padding: 0.5rem 1rem; - cursor: pointer; - border-radius: 4px; - transition: all 0.2s; -} - -.nav-links button:hover { - color: #fff; - background: rgba(255, 255, 255, 0.1); -} - -.nav-links button.active { - color: #fff; - background: #4361ee; -} - -.content { - padding: 2rem; - max-width: 1200px; - margin: 0 auto; -} - -h1 { - font-size: 1.75rem; - margin-bottom: 1.5rem; - color: #1a1a2e; -} - -h2 { - font-size: 1.25rem; - margin-bottom: 1rem; - color: #333; -} - -.section { - background: #fff; - border-radius: 8px; - padding: 1.5rem; - margin-bottom: 1.5rem; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.section h2 { - display: flex; - justify-content: space-between; - align-items: center; -} - -table { - width: 100%; - border-collapse: collapse; - margin-top: 1rem; -} - -th, td { - text-align: left; - padding: 0.75rem 1rem; - border-bottom: 1px solid #eee; -} - -th { - background: #fafafa; - font-weight: 600; - color: #555; - font-size: 0.9rem; -} - -tr:hover { - background: #fafafa; -} - -button { - background: #4361ee; - color: #fff; - border: none; - padding: 0.5rem 1rem; - border-radius: 4px; - cursor: pointer; - font-size: 0.9rem; - transition: background 0.2s; -} - -button:hover { - background: #3a56d4; -} - -button:disabled { - background: #ccc; - cursor: not-allowed; -} - -.modal { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: rgba(0, 0, 0, 0.5); - display: flex; - align-items: center; - justify-content: center; - z-index: 1000; -} - -.modal-content { - background: #fff; - border-radius: 8px; - padding: 2rem; - width: 100%; - max-width: 500px; - max-height: 90vh; - overflow-y: auto; -} - -.modal-content h2 { - margin-bottom: 1.5rem; -} - -.form-group { - margin-bottom: 1rem; -} - -.form-group label { - display: block; - margin-bottom: 0.5rem; - font-weight: 500; - color: #555; -} - -.form-group input[type="text"], -.form-group input[type="password"], -.form-group input[type="url"], -.form-group input[type="date"], -.form-group select { - width: 100%; - padding: 0.625rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 1rem; -} - -.form-group input:focus, -.form-group select:focus { - outline: none; - border-color: #4361ee; -} - -.form-actions { - display: flex; - gap: 1rem; - margin-top: 1.5rem; -} - -.form-actions button:first-child { - flex: 1; -} - -.error { - background: #fee2e2; - color: #dc2626; - padding: 0.75rem; - border-radius: 4px; - margin-bottom: 1rem; -} - -.loading { - text-align: center; - padding: 2rem; - color: #666; -} - -.filter-form { - display: flex; - gap: 1rem; - flex-wrap: wrap; - margin-bottom: 1.5rem; - background: #fff; - padding: 1.5rem; - border-radius: 8px; - box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); -} - -.filter-form select, -.filter-form input { - padding: 0.625rem; - border: 1px solid #ddd; - border-radius: 4px; - font-size: 0.95rem; -} - -.filter-form button { - padding: 0.625rem 1.5rem; -} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 212b869..697005d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,36 +1,24 @@ -import { useState } from 'react'; -import { ProvidersPage } from './pages/ProvidersPage'; -import { StatsPage } from './pages/StatsPage'; -import './App.css'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { BrowserRouter } from 'react-router'; +import { AppRoutes } from '@/routes'; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + staleTime: 30_000, + retry: 1, + refetchOnWindowFocus: false, + }, + }, +}); function App() { - const [currentPage, setCurrentPage] = useState<'providers' | 'stats'>('providers'); - return ( -
- - -
- {currentPage === 'providers' && } - {currentPage === 'stats' && } -
-
+ + + + + ); } diff --git a/frontend/src/__tests__/api/client.test.ts b/frontend/src/__tests__/api/client.test.ts new file mode 100644 index 0000000..ef4f2f3 --- /dev/null +++ b/frontend/src/__tests__/api/client.test.ts @@ -0,0 +1,208 @@ +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; +import { request, fromApi, toApi } from '@/api/client'; +import { ApiError } from '@/types'; + +describe('fromApi', () => { + it('converts snake_case keys to camelCase', () => { + const input = { first_name: 'John', last_name: 'Doe' }; + const result = fromApi<{ firstName: string; lastName: string }>(input); + expect(result).toEqual({ firstName: 'John', lastName: 'Doe' }); + }); + + it('converts nested objects recursively', () => { + const input = { + user_name: 'alice', + contact_info: { email_address: 'alice@example.com' }, + }; + const result = fromApi<{ + userName: string; + contactInfo: { emailAddress: string }; + }>(input); + expect(result).toEqual({ + userName: 'alice', + contactInfo: { emailAddress: 'alice@example.com' }, + }); + }); + + it('converts arrays recursively', () => { + const input = [ + { item_name: 'a' }, + { item_name: 'b' }, + ]; + const result = fromApi>(input); + expect(result).toEqual([{ itemName: 'a' }, { itemName: 'b' }]); + }); + + it('returns primitives unchanged', () => { + expect(fromApi('hello')).toBe('hello'); + expect(fromApi(42)).toBe(42); + expect(fromApi(null)).toBeNull(); + }); +}); + +describe('toApi', () => { + it('converts camelCase keys to snake_case', () => { + const input = { firstName: 'John', lastName: 'Doe' }; + const result = toApi<{ first_name: string; last_name: string }>(input); + expect(result).toEqual({ first_name: 'John', last_name: 'Doe' }); + }); + + it('converts nested objects recursively', () => { + const input = { + userName: 'alice', + contactInfo: { emailAddress: 'alice@example.com' }, + }; + const result = toApi<{ + user_name: string; + contact_info: { email_address: string }; + }>(input); + expect(result).toEqual({ + user_name: 'alice', + contact_info: { email_address: 'alice@example.com' }, + }); + }); + + it('converts arrays recursively', () => { + const input = [{ itemName: 'a' }, { itemName: 'b' }]; + const result = toApi>(input); + expect(result).toEqual([{ item_name: 'a' }, { item_name: 'b' }]); + }); + + it('returns primitives unchanged', () => { + expect(toApi('hello')).toBe('hello'); + expect(toApi(42)).toBe(42); + expect(toApi(null)).toBeNull(); + }); +}); + +describe('request', () => { + const mswServer = setupServer(); + + beforeAll(() => mswServer.listen({ onUnhandledRequest: 'bypass' })); + afterEach(() => mswServer.resetHandlers()); + afterAll(() => mswServer.close()); + + it('parses JSON and converts snake_case keys to camelCase on success', async () => { + mswServer.use( + http.get('/api/test', () => { + return HttpResponse.json({ + id: '1', + created_at: '2025-01-01', + nested_obj: { inner_key: 'value' }, + }); + }), + ); + + const result = await request<{ + id: string; + createdAt: string; + nestedObj: { innerKey: string }; + }>('GET', '/api/test'); + + expect(result).toEqual({ + id: '1', + createdAt: '2025-01-01', + nestedObj: { innerKey: 'value' }, + }); + }); + + it('throws ApiError with status and message on HTTP error', async () => { + mswServer.use( + http.get('/api/test', () => { + return HttpResponse.json( + { message: 'Not found' }, + { status: 404 }, + ); + }), + ); + + await expect(request('GET', '/api/test')).rejects.toThrow(ApiError); + try { + await request('GET', '/api/test'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + const apiError = error as ApiError; + expect(apiError.status).toBe(404); + expect(apiError.message).toBe('Not found'); + } + }); + + it('throws ApiError with default message when error body has no message', async () => { + mswServer.use( + http.get('/api/test', () => { + return HttpResponse.json( + { error: 'something' }, + { status: 500 }, + ); + }), + ); + + try { + await request('GET', '/api/test'); + } catch (error) { + expect(error).toBeInstanceOf(ApiError); + const apiError = error as ApiError; + expect(apiError.status).toBe(500); + expect(apiError.message).toContain('500'); + } + }); + + it('throws Error on network failure', async () => { + mswServer.use( + http.get('/api/test', () => { + return HttpResponse.error(); + }), + ); + + await expect(request('GET', '/api/test')).rejects.toThrow(); + }); + + it('returns undefined for 204 No Content', async () => { + mswServer.use( + http.delete('/api/test/1', () => { + return new HttpResponse(null, { status: 204 }); + }), + ); + + const result = await request('DELETE', '/api/test/1'); + expect(result).toBeUndefined(); + }); + + it('sends body with camelCase keys converted to snake_case', async () => { + let receivedBody: Record | null = null; + + mswServer.use( + http.post('/api/test', async ({ request }) => { + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ id: '1' }); + }), + ); + + await request('POST', '/api/test', { + providerId: 'prov-1', + modelName: 'gpt-4', + }); + + expect(receivedBody).toEqual({ + provider_id: 'prov-1', + model_name: 'gpt-4', + }); + }); + + it('sends Content-Type header as application/json', async () => { + let contentType: string | null = null; + + mswServer.use( + http.post('/api/test', async ({ request }) => { + contentType = request.headers.get('Content-Type'); + return HttpResponse.json({ id: '1' }); + }), + ); + + await request('POST', '/api/test', { name: 'test' }); + + expect(contentType).toBe('application/json'); + }); +}); diff --git a/frontend/src/__tests__/api/models.test.ts b/frontend/src/__tests__/api/models.test.ts new file mode 100644 index 0000000..c55a547 --- /dev/null +++ b/frontend/src/__tests__/api/models.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; +import { listModels, createModel, updateModel, deleteModel } from '@/api/models'; + +const mockModels = [ + { + id: 'gpt-4', + provider_id: 'prov-1', + model_name: 'gpt-4', + enabled: true, + created_at: '2025-01-01T00:00:00Z', + }, + { + id: 'claude-3', + provider_id: 'prov-2', + model_name: 'claude-3', + enabled: false, + created_at: '2025-01-02T00:00:00Z', + }, +]; + +describe('models API', () => { + const server = setupServer(); + + beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + describe('listModels', () => { + it('returns array of Model objects with camelCase keys', async () => { + server.use( + http.get('/api/models', () => { + return HttpResponse.json(mockModels); + }), + ); + + const result = await listModels(); + + expect(result).toEqual([ + { + id: 'gpt-4', + providerId: 'prov-1', + modelName: 'gpt-4', + enabled: true, + createdAt: '2025-01-01T00:00:00Z', + }, + { + id: 'claude-3', + providerId: 'prov-2', + modelName: 'claude-3', + enabled: false, + createdAt: '2025-01-02T00:00:00Z', + }, + ]); + }); + + it('appends provider_id query parameter when providerId is given', async () => { + let receivedUrl: string | null = null; + + server.use( + http.get('/api/models', ({ request }) => { + receivedUrl = request.url; + return HttpResponse.json([mockModels[0]]); + }), + ); + + const result = await listModels('prov-1'); + + expect(receivedUrl).toContain('provider_id=prov-1'); + expect(result).toHaveLength(1); + expect(result[0].providerId).toBe('prov-1'); + }); + }); + + describe('createModel', () => { + it('sends POST with correct body and returns model', async () => { + let receivedMethod: string | null = null; + let receivedBody: Record | null = null; + + server.use( + http.post('/api/models', async ({ request }) => { + receivedMethod = request.method; + receivedBody = (await request.json()) as Record; + return HttpResponse.json(mockModels[0]); + }), + ); + + const input = { + id: 'gpt-4', + providerId: 'prov-1', + modelName: 'gpt-4', + enabled: true, + }; + + const result = await createModel(input); + + expect(receivedMethod).toBe('POST'); + expect(receivedBody).toEqual({ + id: 'gpt-4', + provider_id: 'prov-1', + model_name: 'gpt-4', + enabled: true, + }); + expect(result.id).toBe('gpt-4'); + expect(result.providerId).toBe('prov-1'); + expect(result.modelName).toBe('gpt-4'); + }); + }); + + describe('updateModel', () => { + it('sends PUT with correct body and returns model', async () => { + let receivedMethod: string | null = null; + let receivedUrl: string | null = null; + let receivedBody: Record | null = null; + + server.use( + http.put('/api/models/:id', async ({ request }) => { + receivedMethod = request.method; + receivedUrl = new URL(request.url).pathname; + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ + ...mockModels[0], + model_name: 'gpt-4-turbo', + enabled: false, + }); + }), + ); + + const result = await updateModel('gpt-4', { + modelName: 'gpt-4-turbo', + enabled: false, + }); + + expect(receivedMethod).toBe('PUT'); + expect(receivedUrl).toBe('/api/models/gpt-4'); + expect(receivedBody).toEqual({ + model_name: 'gpt-4-turbo', + enabled: false, + }); + expect(result.modelName).toBe('gpt-4-turbo'); + expect(result.enabled).toBe(false); + }); + }); + + describe('deleteModel', () => { + it('sends DELETE and returns void', async () => { + let receivedMethod: string | null = null; + let receivedUrl: string | null = null; + + server.use( + http.delete('/api/models/:id', ({ request }) => { + receivedMethod = request.method; + receivedUrl = new URL(request.url).pathname; + return new HttpResponse(null, { status: 204 }); + }), + ); + + const result = await deleteModel('gpt-4'); + + expect(receivedMethod).toBe('DELETE'); + expect(receivedUrl).toBe('/api/models/gpt-4'); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/frontend/src/__tests__/api/providers.test.ts b/frontend/src/__tests__/api/providers.test.ts new file mode 100644 index 0000000..9b59cd4 --- /dev/null +++ b/frontend/src/__tests__/api/providers.test.ts @@ -0,0 +1,165 @@ +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; +import { listProviders, createProvider, updateProvider, deleteProvider } from '@/api/providers'; + +const mockProviders = [ + { + id: 'prov-1', + name: 'OpenAI', + api_key: 'sk-xxx', + base_url: 'https://api.openai.com', + enabled: true, + created_at: '2025-01-01T00:00:00Z', + updated_at: '2025-01-01T00:00:00Z', + }, + { + id: 'prov-2', + name: 'Anthropic', + api_key: 'sk-yyy', + base_url: 'https://api.anthropic.com', + enabled: false, + created_at: '2025-01-02T00:00:00Z', + updated_at: '2025-01-02T00:00:00Z', + }, +]; + +describe('providers API', () => { + const server = setupServer(); + + beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + describe('listProviders', () => { + it('returns array of Provider objects with camelCase keys', async () => { + server.use( + http.get('/api/providers', () => { + return HttpResponse.json(mockProviders); + }), + ); + + const result = await listProviders(); + + expect(result).toEqual([ + { + id: 'prov-1', + name: 'OpenAI', + apiKey: 'sk-xxx', + baseUrl: 'https://api.openai.com', + enabled: true, + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2025-01-01T00:00:00Z', + }, + { + id: 'prov-2', + name: 'Anthropic', + apiKey: 'sk-yyy', + baseUrl: 'https://api.anthropic.com', + enabled: false, + createdAt: '2025-01-02T00:00:00Z', + updatedAt: '2025-01-02T00:00:00Z', + }, + ]); + }); + }); + + describe('createProvider', () => { + it('sends POST with correct body and returns provider', async () => { + let receivedMethod: string | null = null; + let receivedBody: Record | null = null; + + server.use( + http.post('/api/providers', async ({ request }) => { + receivedMethod = request.method; + receivedBody = (await request.json()) as Record; + return HttpResponse.json(mockProviders[0]); + }), + ); + + const input = { + id: 'prov-1', + name: 'OpenAI', + apiKey: 'sk-xxx', + baseUrl: 'https://api.openai.com', + enabled: true, + }; + + const result = await createProvider(input); + + expect(receivedMethod).toBe('POST'); + expect(receivedBody).toEqual({ + id: 'prov-1', + name: 'OpenAI', + api_key: 'sk-xxx', + base_url: 'https://api.openai.com', + enabled: true, + }); + expect(result).toEqual({ + id: 'prov-1', + name: 'OpenAI', + apiKey: 'sk-xxx', + baseUrl: 'https://api.openai.com', + enabled: true, + createdAt: '2025-01-01T00:00:00Z', + updatedAt: '2025-01-01T00:00:00Z', + }); + }); + }); + + describe('updateProvider', () => { + it('sends PUT with correct body and returns provider', async () => { + let receivedMethod: string | null = null; + let receivedUrl: string | null = null; + let receivedBody: Record | null = null; + + server.use( + http.put('/api/providers/:id', async ({ request, params }) => { + receivedMethod = request.method; + receivedUrl = new URL(request.url).pathname; + receivedBody = (await request.json()) as Record; + return HttpResponse.json({ + ...mockProviders[0], + name: 'Updated', + api_key: 'sk-updated', + }); + }), + ); + + const result = await updateProvider('prov-1', { + name: 'Updated', + apiKey: 'sk-updated', + }); + + expect(receivedMethod).toBe('PUT'); + expect(receivedUrl).toBe('/api/providers/prov-1'); + expect(receivedBody).toEqual({ + name: 'Updated', + api_key: 'sk-updated', + }); + expect(result.name).toBe('Updated'); + expect(result.apiKey).toBe('sk-updated'); + }); + }); + + describe('deleteProvider', () => { + it('sends DELETE and returns void', async () => { + let receivedMethod: string | null = null; + let receivedUrl: string | null = null; + + server.use( + http.delete('/api/providers/:id', ({ request, params }) => { + receivedMethod = request.method; + receivedUrl = new URL(request.url).pathname; + return new HttpResponse(null, { status: 204 }); + }), + ); + + const result = await deleteProvider('prov-1'); + + expect(receivedMethod).toBe('DELETE'); + expect(receivedUrl).toBe('/api/providers/prov-1'); + expect(result).toBeUndefined(); + }); + }); +}); diff --git a/frontend/src/__tests__/api/stats.test.ts b/frontend/src/__tests__/api/stats.test.ts new file mode 100644 index 0000000..bc1b74c --- /dev/null +++ b/frontend/src/__tests__/api/stats.test.ts @@ -0,0 +1,131 @@ +import { describe, it, expect, beforeAll, afterEach, afterAll } from 'vitest'; +import { setupServer } from 'msw/node'; +import { http, HttpResponse } from 'msw'; +import { getStats } from '@/api/stats'; + +const mockStats = [ + { + id: 1, + provider_id: 'prov-1', + model_name: 'gpt-4', + request_count: 100, + date: '2025-01-15', + }, + { + id: 2, + provider_id: 'prov-2', + model_name: 'claude-3', + request_count: 50, + date: '2025-01-16', + }, +]; + +describe('stats API', () => { + const server = setupServer(); + + beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); + afterEach(() => server.resetHandlers()); + afterAll(() => server.close()); + + describe('getStats', () => { + it('calls /api/stats without params', async () => { + let receivedUrl: string | null = null; + + server.use( + http.get('/api/stats', ({ request }) => { + receivedUrl = request.url; + return HttpResponse.json(mockStats); + }), + ); + + const result = await getStats(); + + expect(receivedUrl).toMatch(/\/api\/stats$/); + expect(result).toEqual([ + { + id: 1, + providerId: 'prov-1', + modelName: 'gpt-4', + requestCount: 100, + date: '2025-01-15', + }, + { + id: 2, + providerId: 'prov-2', + modelName: 'claude-3', + requestCount: 50, + date: '2025-01-16', + }, + ]); + }); + + it('builds correct query string with snake_case keys when params are provided', async () => { + let receivedUrl: string | null = null; + + server.use( + http.get('/api/stats', ({ request }) => { + receivedUrl = request.url; + return HttpResponse.json([]); + }), + ); + + await getStats({ + providerId: 'prov-1', + modelName: 'gpt-4', + startDate: '2025-01-01', + endDate: '2025-01-31', + }); + + expect(receivedUrl).toContain('provider_id=prov-1'); + expect(receivedUrl).toContain('model_name=gpt-4'); + expect(receivedUrl).toContain('start_date=2025-01-01'); + expect(receivedUrl).toContain('end_date=2025-01-31'); + }); + + it('omits undefined params from query string', async () => { + let receivedUrl: string | null = null; + + server.use( + http.get('/api/stats', ({ request }) => { + receivedUrl = request.url; + return HttpResponse.json([]); + }), + ); + + await getStats({ + providerId: 'prov-1', + }); + + expect(receivedUrl).toContain('provider_id=prov-1'); + expect(receivedUrl).not.toContain('model_name'); + expect(receivedUrl).not.toContain('start_date'); + expect(receivedUrl).not.toContain('end_date'); + }); + + it('returns UsageStats array with camelCase keys', async () => { + server.use( + http.get('/api/stats', () => { + return HttpResponse.json(mockStats); + }), + ); + + const result = await getStats(); + + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + id: 1, + providerId: 'prov-1', + modelName: 'gpt-4', + requestCount: 100, + date: '2025-01-15', + }); + expect(result[1]).toEqual({ + id: 2, + providerId: 'prov-2', + modelName: 'claude-3', + requestCount: 50, + date: '2025-01-16', + }); + }); + }); +}); diff --git a/frontend/src/__tests__/components/ModelForm.test.tsx b/frontend/src/__tests__/components/ModelForm.test.tsx new file mode 100644 index 0000000..826b63d --- /dev/null +++ b/frontend/src/__tests__/components/ModelForm.test.tsx @@ -0,0 +1,144 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { ModelForm } from '@/pages/Providers/ModelForm'; +import type { Provider, Model } from '@/types'; + +const mockProviders: Provider[] = [ + { + id: 'openai', + name: 'OpenAI', + apiKey: 'sk-test', + baseUrl: 'https://api.openai.com/v1', + enabled: true, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + { + id: 'anthropic', + name: 'Anthropic', + apiKey: 'sk-ant-test', + baseUrl: 'https://api.anthropic.com', + enabled: true, + createdAt: '2024-01-02T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + }, +]; + +const mockModel: Model = { + id: 'gpt-4o', + providerId: 'openai', + modelName: 'gpt-4o', + enabled: true, + createdAt: '2024-01-01T00:00:00Z', +}; + +const defaultProps = { + open: true, + providerId: 'openai', + providers: mockProviders, + onSave: vi.fn(), + onCancel: vi.fn(), + loading: false, +}; + +function getDialog() { + return screen.getByRole('dialog'); +} + +describe('ModelForm', () => { + it('renders form with provider select', () => { + render(); + + const dialog = getDialog(); + expect(within(dialog).getByText('添加模型')).toBeInTheDocument(); + expect(within(dialog).getByText('ID')).toBeInTheDocument(); + expect(within(dialog).getByText('供应商')).toBeInTheDocument(); + expect(within(dialog).getByText('模型名称')).toBeInTheDocument(); + expect(within(dialog).getByText('启用')).toBeInTheDocument(); + + // The selected provider (OpenAI) is shown; Anthropic is not rendered until dropdown opens + expect(within(dialog).getByText('OpenAI')).toBeInTheDocument(); + }); + + it('defaults providerId to the passed providerId in create mode', () => { + render(); + + const dialog = getDialog(); + const selectionItem = dialog.querySelector('.ant-select-selection-item'); + expect(selectionItem).toBeInTheDocument(); + expect(selectionItem?.textContent).toBe('OpenAI'); + }); + + it('shows validation error messages for required fields', async () => { + const user = userEvent.setup(); + render( + , + ); + + const dialog = getDialog(); + const okButton = within(dialog).getByRole('button', { name: /保/ }); + await user.click(okButton); + + expect(await screen.findByText('请输入模型 ID')).toBeInTheDocument(); + expect(screen.getByText('请选择供应商')).toBeInTheDocument(); + expect(screen.getByText('请输入模型名称')).toBeInTheDocument(); + }); + + it('calls onSave with form values on successful submission', async () => { + const user = userEvent.setup(); + const onSave = vi.fn(); + render(); + + const dialog = getDialog(); + // There are two inputs with placeholder "例如: gpt-4o": ID field (index 0) and model name (index 1) + const inputs = within(dialog).getAllByPlaceholderText('例如: gpt-4o'); + + // Type into the ID field + await user.type(inputs[0], 'gpt-4o-mini'); + // Type into the model name field + await user.type(inputs[1], 'gpt-4o-mini'); + + const okButton = within(dialog).getByRole('button', { name: /保/ }); + await user.click(okButton); + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'gpt-4o-mini', + providerId: 'openai', + modelName: 'gpt-4o-mini', + enabled: true, + }), + ); + }); + + it('renders pre-filled fields in edit mode', () => { + render(); + + const dialog = getDialog(); + expect(within(dialog).getByText('编辑模型')).toBeInTheDocument(); + + const inputs = within(dialog).getAllByPlaceholderText('例如: gpt-4o'); + const idInput = inputs[0] as HTMLInputElement; + expect(idInput.value).toBe('gpt-4o'); + expect(idInput).toBeDisabled(); + + const modelNameInput = inputs[1] as HTMLInputElement; + expect(modelNameInput.value).toBe('gpt-4o'); + }); + + it('calls onCancel when clicking cancel button', async () => { + const user = userEvent.setup(); + const onCancel = vi.fn(); + render(); + + const dialog = getDialog(); + const cancelButton = within(dialog).getByRole('button', { name: /取/ }); + await user.click(cancelButton); + expect(onCancel).toHaveBeenCalledTimes(1); + }); +}); diff --git a/frontend/src/__tests__/components/ProviderForm.test.tsx b/frontend/src/__tests__/components/ProviderForm.test.tsx new file mode 100644 index 0000000..4f81bd1 --- /dev/null +++ b/frontend/src/__tests__/components/ProviderForm.test.tsx @@ -0,0 +1,147 @@ +import { render, screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { ProviderForm } from '@/pages/Providers/ProviderForm'; +import type { Provider } from '@/types'; + +const mockProvider: Provider = { + id: 'openai', + name: 'OpenAI', + apiKey: 'sk-old-key', + baseUrl: 'https://api.openai.com/v1', + enabled: true, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', +}; + +const defaultProps = { + open: true, + onSave: vi.fn(), + onCancel: vi.fn(), + loading: false, +}; + +function getDialog() { + return screen.getByRole('dialog'); +} + +describe('ProviderForm', () => { + it('renders form fields in create mode', () => { + render(); + + const dialog = getDialog(); + expect(within(dialog).getByText('添加供应商')).toBeInTheDocument(); + expect(within(dialog).getByText('ID')).toBeInTheDocument(); + expect(within(dialog).getByText('名称')).toBeInTheDocument(); + expect(within(dialog).getByText('API Key')).toBeInTheDocument(); + expect(within(dialog).getByText('Base URL')).toBeInTheDocument(); + expect(within(dialog).getByText('启用')).toBeInTheDocument(); + expect(within(dialog).getByPlaceholderText('例如: openai')).toBeInTheDocument(); + expect(within(dialog).getByPlaceholderText('例如: OpenAI')).toBeInTheDocument(); + expect(within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1')).toBeInTheDocument(); + }); + + it('renders pre-filled fields in edit mode', () => { + render(); + + const dialog = getDialog(); + expect(within(dialog).getByText('编辑供应商')).toBeInTheDocument(); + + const idInput = within(dialog).getByPlaceholderText('例如: openai') as HTMLInputElement; + expect(idInput.value).toBe('openai'); + expect(idInput).toBeDisabled(); + + const nameInput = within(dialog).getByPlaceholderText('例如: OpenAI') as HTMLInputElement; + expect(nameInput.value).toBe('OpenAI'); + + const baseUrlInput = within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1') as HTMLInputElement; + expect(baseUrlInput.value).toBe('https://api.openai.com/v1'); + }); + + it('shows API Key label variant in edit mode', () => { + render(); + + const dialog = getDialog(); + expect(within(dialog).getByText('API Key(留空则不修改)')).toBeInTheDocument(); + }); + + it('shows validation error messages for required fields', async () => { + const user = userEvent.setup(); + render(); + + const dialog = getDialog(); + const okButton = within(dialog).getByRole('button', { name: /保/ }); + await user.click(okButton); + + // Wait for validation messages to appear + expect(await screen.findByText('请输入供应商 ID')).toBeInTheDocument(); + expect(screen.getByText('请输入名称')).toBeInTheDocument(); + expect(screen.getByText('请输入 API Key')).toBeInTheDocument(); + expect(screen.getByText('请输入 Base URL')).toBeInTheDocument(); + }); + + it('calls onSave with form values on successful submission', async () => { + const user = userEvent.setup(); + const onSave = vi.fn(); + render(); + + const dialog = getDialog(); + await user.type(within(dialog).getByPlaceholderText('例如: openai'), 'test-provider'); + await user.type(within(dialog).getByPlaceholderText('例如: OpenAI'), 'Test Provider'); + await user.type(within(dialog).getByPlaceholderText('sk-...'), 'sk-test-key'); + await user.type(within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1'), 'https://api.test.com/v1'); + + const okButton = within(dialog).getByRole('button', { name: /保/ }); + await user.click(okButton); + + expect(onSave).toHaveBeenCalledWith( + expect.objectContaining({ + id: 'test-provider', + name: 'Test Provider', + apiKey: 'sk-test-key', + baseUrl: 'https://api.test.com/v1', + enabled: true, + }), + ); + }); + + it('calls onCancel when clicking cancel button', async () => { + const user = userEvent.setup(); + const onCancel = vi.fn(); + render(); + + const dialog = getDialog(); + const cancelButton = within(dialog).getByRole('button', { name: /取/ }); + await user.click(cancelButton); + expect(onCancel).toHaveBeenCalledTimes(1); + }); + + it('shows confirm loading state', () => { + render(); + const dialog = getDialog(); + const okButton = within(dialog).getByRole('button', { name: /保/ }); + expect(okButton).toHaveClass('ant-btn-loading'); + }); + + it('shows validation error for invalid URL format', async () => { + const user = userEvent.setup(); + render(); + + const dialog = getDialog(); + + // Fill in required fields + await user.type(within(dialog).getByPlaceholderText('例如: openai'), 'test-provider'); + await user.type(within(dialog).getByPlaceholderText('例如: OpenAI'), 'Test Provider'); + await user.type(within(dialog).getByPlaceholderText('sk-...'), 'sk-test-key'); + + // Enter an invalid URL in the Base URL field + await user.type(within(dialog).getByPlaceholderText('例如: https://api.openai.com/v1'), 'not-a-url'); + + // Submit the form + const okButton = within(dialog).getByRole('button', { name: /保/ }); + await user.click(okButton); + + // Verify that a URL validation error message appears + expect(await screen.findByText('请输入有效的 URL')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/ProviderTable.test.tsx b/frontend/src/__tests__/components/ProviderTable.test.tsx new file mode 100644 index 0000000..46c0912 --- /dev/null +++ b/frontend/src/__tests__/components/ProviderTable.test.tsx @@ -0,0 +1,140 @@ +import { render, screen, fireEvent } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { ProviderTable } from '@/pages/Providers/ProviderTable'; +import type { Provider } from '@/types'; + +const mockModelsData = [ + { id: 'model-1', providerId: 'openai', modelName: 'gpt-4o', enabled: true }, + { id: 'model-2', providerId: 'openai', modelName: 'gpt-3.5-turbo', enabled: false }, +]; + +vi.mock('@/hooks/useModels', () => ({ + useModels: vi.fn(() => ({ data: mockModelsData, isLoading: false })), + useDeleteModel: vi.fn(() => ({ mutate: vi.fn() })), +})); + +const mockProviders: Provider[] = [ + { + id: 'openai', + name: 'OpenAI', + apiKey: 'sk-abcdefgh12345678', + baseUrl: 'https://api.openai.com/v1', + enabled: true, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + { + id: 'anthropic', + name: 'Anthropic', + apiKey: 'sk-ant-test', + baseUrl: 'https://api.anthropic.com', + enabled: false, + createdAt: '2024-01-02T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + }, +]; + +const defaultProps = { + providers: mockProviders, + loading: false, + onAdd: vi.fn(), + onEdit: vi.fn(), + onDelete: vi.fn(), + onAddModel: vi.fn(), + onEditModel: vi.fn(), +}; + +describe('ProviderTable', () => { + it('renders provider list with name, baseUrl, masked apiKey, and status tags', () => { + render(); + + expect(screen.getByText('供应商列表')).toBeInTheDocument(); + + expect(screen.getByText('OpenAI')).toBeInTheDocument(); + expect(screen.getByText('https://api.openai.com/v1')).toBeInTheDocument(); + expect(screen.getByText('****5678')).toBeInTheDocument(); + + expect(screen.getByText('Anthropic')).toBeInTheDocument(); + expect(screen.getByText('https://api.anthropic.com')).toBeInTheDocument(); + expect(screen.getByText('****test')).toBeInTheDocument(); + + const enabledTags = screen.getAllByText('启用'); + const disabledTags = screen.getAllByText('禁用'); + expect(enabledTags.length).toBeGreaterThanOrEqual(1); + expect(disabledTags.length).toBeGreaterThanOrEqual(1); + }); + + it('renders short api keys fully masked', () => { + const shortKeyProvider: Provider[] = [ + { + ...mockProviders[0], + id: 'short', + name: 'ShortKey', + apiKey: 'ab', + }, + ]; + render(); + + expect(screen.getByText('****')).toBeInTheDocument(); + }); + + it('calls onAdd when clicking "添加供应商" button', async () => { + const user = userEvent.setup(); + const onAdd = vi.fn(); + render(); + + await user.click(screen.getByRole('button', { name: '添加供应商' })); + expect(onAdd).toHaveBeenCalledTimes(1); + }); + + it('calls onEdit with correct provider when clicking "编辑"', async () => { + const user = userEvent.setup(); + const onEdit = vi.fn(); + render(); + + const editButtons = screen.getAllByRole('button', { name: '编辑' }); + await user.click(editButtons[0]); + + expect(onEdit).toHaveBeenCalledTimes(1); + expect(onEdit).toHaveBeenCalledWith(mockProviders[0]); + }); + + it('calls onDelete with correct provider ID when delete is confirmed', async () => { + const user = userEvent.setup(); + const onDelete = vi.fn(); + render(); + + // Find and click the delete button for the first row + const deleteButtons = screen.getAllByRole('button', { name: '删除' }); + await user.click(deleteButtons[0]); + + // Find and click the "确 定" confirm button in the Popconfirm popup + // antd renders the text with spaces between Chinese characters + const confirmButtons = await screen.findAllByText('确 定'); + await user.click(confirmButtons[0]); + + // Assert that onDelete was called with the correct provider ID + expect(onDelete).toHaveBeenCalledTimes(1); + expect(onDelete).toHaveBeenCalledWith('openai'); + }); + + it('shows loading state', () => { + render(); + expect(document.querySelector('.ant-spin')).toBeInTheDocument(); + }); + + it('renders expandable ModelTable when row is expanded', async () => { + const user = userEvent.setup(); + render(); + + // Find and click the expand button for the first row + const expandButtons = screen.getAllByRole('button', { name: /expand/i }); + expect(expandButtons.length).toBeGreaterThanOrEqual(1); + await user.click(expandButtons[0]); + + // Verify that ModelTable content is rendered with data from mocked useModels + expect(await screen.findByText('gpt-4o')).toBeInTheDocument(); + expect(screen.getByText('gpt-3.5-turbo')).toBeInTheDocument(); + }); +}); diff --git a/frontend/src/__tests__/components/StatsTable.test.tsx b/frontend/src/__tests__/components/StatsTable.test.tsx new file mode 100644 index 0000000..b4340e2 --- /dev/null +++ b/frontend/src/__tests__/components/StatsTable.test.tsx @@ -0,0 +1,159 @@ +import { render, screen, within, fireEvent } from '@testing-library/react'; +import { describe, it, expect, vi, afterEach } from 'vitest'; +import { StatsTable } from '@/pages/Stats/StatsTable'; +import type { Provider, UsageStats } from '@/types'; + +const mockProviders: Provider[] = [ + { + id: 'openai', + name: 'OpenAI', + apiKey: 'sk-test', + baseUrl: 'https://api.openai.com/v1', + enabled: true, + createdAt: '2024-01-01T00:00:00Z', + updatedAt: '2024-01-01T00:00:00Z', + }, + { + id: 'anthropic', + name: 'Anthropic', + apiKey: 'sk-ant-test', + baseUrl: 'https://api.anthropic.com', + enabled: true, + createdAt: '2024-01-02T00:00:00Z', + updatedAt: '2024-01-02T00:00:00Z', + }, +]; + +const mockStats: UsageStats[] = [ + { + id: 1, + providerId: 'openai', + modelName: 'gpt-4o', + requestCount: 100, + date: '2024-01-15', + }, + { + id: 2, + providerId: 'anthropic', + modelName: 'claude-3-opus', + requestCount: 50, + date: '2024-01-15', + }, +]; + +const mockUseStats = vi.fn(() => ({ + data: mockStats, + isLoading: false, +})); + +vi.mock('@/hooks/useStats', () => ({ + useStats: (...args: unknown[]) => mockUseStats(...args), +})); + +describe('StatsTable', () => { + afterEach(() => { + vi.clearAllMocks(); + }); + + it('renders stats table with data', () => { + render(); + + expect(screen.getByText('gpt-4o')).toBeInTheDocument(); + expect(screen.getByText('claude-3-opus')).toBeInTheDocument(); + // Both rows share the same date + const dateCells = screen.getAllByText('2024-01-15'); + expect(dateCells.length).toBe(2); + expect(screen.getByText('100')).toBeInTheDocument(); + expect(screen.getByText('50')).toBeInTheDocument(); + }); + + it('shows provider name from providers prop instead of providerId', () => { + render(); + + expect(screen.getByText('OpenAI')).toBeInTheDocument(); + // "Anthropic" appears in both the provider column and the filter select options + const allAnthropic = screen.getAllByText('Anthropic'); + expect(allAnthropic.length).toBeGreaterThanOrEqual(1); + }); + + it('renders filter controls with Select, Input, and DatePicker', () => { + render(); + + // Check that the select element exists for provider filter + const selects = document.querySelectorAll('.ant-select'); + expect(selects.length).toBeGreaterThanOrEqual(1); + + // Check that the Input element exists for model name filter + const modelInput = screen.getByPlaceholderText('模型名称'); + expect(modelInput).toBeInTheDocument(); + + // Verify placeholder text is rendered + expect(screen.getByText('所有供应商')).toBeInTheDocument(); + + const rangePicker = document.querySelector('.ant-picker-range'); + expect(rangePicker).toBeInTheDocument(); + }); + + it('renders table headers correctly', () => { + render(); + + expect(screen.getByText('供应商')).toBeInTheDocument(); + expect(screen.getByText('模型')).toBeInTheDocument(); + expect(screen.getByText('日期')).toBeInTheDocument(); + expect(screen.getByText('请求数')).toBeInTheDocument(); + }); + + it('falls back to providerId when provider not found in providers prop', () => { + const limitedProviders = [mockProviders[0]]; // only OpenAI + render(); + + // OpenAI should show name + expect(screen.getByText('OpenAI')).toBeInTheDocument(); + // Anthropic is not in providers list, so providerId "anthropic" should show + expect(screen.getByText('anthropic')).toBeInTheDocument(); + }); + + it('renders with empty stats data', () => { + mockUseStats.mockReturnValueOnce({ + data: [], + isLoading: false, + }); + + render(); + + // Table should still be rendered, just empty + expect(screen.getByText('供应商')).toBeInTheDocument(); + expect(screen.getByText('模型')).toBeInTheDocument(); + }); + + it('updates provider filter when selecting a provider', () => { + render(); + + // Initially useStats should be called with no providerId filter + expect(mockUseStats).toHaveBeenLastCalledWith( + expect.objectContaining({ + providerId: undefined, + }), + ); + + // Find the provider Select and change its value + const selectElement = document.querySelector('.ant-select'); + expect(selectElement).toBeInTheDocument(); + + // Open the select dropdown + fireEvent.mouseDown(selectElement!.querySelector('.ant-select-selector')!); + + // Click on the "OpenAI" option from the dropdown + const dropdown = document.querySelector('.ant-select-dropdown'); + expect(dropdown).toBeInTheDocument(); + const openaiOption = within(dropdown as HTMLElement).getByText('OpenAI'); + fireEvent.click(openaiOption); + + // After selecting, useStats should be called with providerId set to 'openai' + expect(mockUseStats).toHaveBeenLastCalledWith( + expect.objectContaining({ + providerId: 'openai', + }), + ); + }); +}); diff --git a/frontend/src/__tests__/hooks/useModels.test.tsx b/frontend/src/__tests__/hooks/useModels.test.tsx new file mode 100644 index 0000000..adf9ed0 --- /dev/null +++ b/frontend/src/__tests__/hooks/useModels.test.tsx @@ -0,0 +1,287 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { useModels, useCreateModel, useUpdateModel, useDeleteModel } from '@/hooks/useModels'; +import type { Model, CreateModelInput, UpdateModelInput } from '@/types'; + +// Mock antd message since it uses DOM APIs not available in jsdom +vi.mock('antd', () => ({ + message: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +import { message } from 'antd'; + +// Test data +const mockModels: Model[] = [ + { + id: 'model-1', + providerId: 'provider-1', + modelName: 'gpt-4o', + enabled: true, + createdAt: '2026-01-01T00:00:00Z', + }, + { + id: 'model-2', + providerId: 'provider-1', + modelName: 'gpt-4o-mini', + enabled: true, + createdAt: '2026-01-02T00:00:00Z', + }, +]; + +const mockFilteredModels: Model[] = [ + { + id: 'model-3', + providerId: 'provider-2', + modelName: 'claude-sonnet-4-5', + enabled: true, + createdAt: '2026-02-01T00:00:00Z', + }, +]; + +const mockCreatedModel: Model = { + id: 'model-4', + providerId: 'provider-1', + modelName: 'gpt-4.1', + enabled: true, + createdAt: '2026-03-01T00:00:00Z', +}; + +// MSW handlers +const handlers = [ + http.get('/api/models', ({ request }) => { + const url = new URL(request.url); + const providerId = url.searchParams.get('provider_id'); + if (providerId === 'provider-2') { + return HttpResponse.json(mockFilteredModels); + } + return HttpResponse.json(mockModels); + }), + http.post('/api/models', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ + ...mockCreatedModel, + ...body, + }); + }), + http.put('/api/models/:id', async ({ request, params }) => { + const body = await request.json() as Record; + const existing = mockModels.find((m) => m.id === params['id']); + return HttpResponse.json({ ...existing, ...body }); + }), + http.delete('/api/models/:id', () => { + return new HttpResponse(null, { status: 204 }); + }), +]; + +const server = setupServer(...handlers); + +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); +} + +function createWrapper() { + const testQueryClient = createTestQueryClient(); + return function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + }; +} + +beforeAll(() => server.listen()); +afterEach(() => { + server.resetHandlers(); + vi.clearAllMocks(); +}); +afterAll(() => server.close()); + +describe('useModels', () => { + it('fetches model list', async () => { + const { result } = renderHook(() => useModels(), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual(mockModels); + expect(result.current.data).toHaveLength(2); + expect(result.current.data![0]!.modelName).toBe('gpt-4o'); + }); + + it('with providerId passes it to API and returns filtered models', async () => { + const { result } = renderHook(() => useModels('provider-2'), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual(mockFilteredModels); + expect(result.current.data).toHaveLength(1); + expect(result.current.data![0]!.modelName).toBe('claude-sonnet-4-5'); + }); +}); + +describe('useCreateModel', () => { + it('calls API and invalidates model queries', async () => { + const queryClient = createTestQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + const { result } = renderHook(() => useCreateModel(), { + wrapper: Wrapper, + }); + + const input: CreateModelInput = { + id: 'model-4', + providerId: 'provider-1', + modelName: 'gpt-4.1', + enabled: true, + }; + + result.current.mutate(input); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toMatchObject({ + id: 'model-4', + modelName: 'gpt-4.1', + }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] }); + expect(message.success).toHaveBeenCalledWith('模型创建成功'); + }); + + it('calls message.error on failure', async () => { + server.use( + http.post('/api/models', () => { + return HttpResponse.json({ message: '创建失败' }, { status: 500 }); + }), + ); + + const { result } = renderHook(() => useCreateModel(), { + wrapper: createWrapper(), + }); + + const input: CreateModelInput = { + id: 'model-4', + providerId: 'provider-1', + modelName: 'gpt-4.1', + enabled: true, + }; + + result.current.mutate(input); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(message.error).toHaveBeenCalled(); + }); +}); + +describe('useUpdateModel', () => { + it('calls API and invalidates model queries', async () => { + const queryClient = createTestQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + const { result } = renderHook(() => useUpdateModel(), { + wrapper: Wrapper, + }); + + const input: UpdateModelInput = { modelName: 'gpt-4o-updated' }; + + result.current.mutate({ id: 'model-1', input }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toMatchObject({ + modelName: 'gpt-4o-updated', + }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] }); + expect(message.success).toHaveBeenCalledWith('模型更新成功'); + }); + + it('calls message.error on failure', async () => { + server.use( + http.put('/api/models/:id', () => { + return HttpResponse.json({ message: '更新失败' }, { status: 500 }); + }), + ); + + const { result } = renderHook(() => useUpdateModel(), { + wrapper: createWrapper(), + }); + + result.current.mutate({ id: 'model-1', input: { modelName: 'Updated' } }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(message.error).toHaveBeenCalled(); + }); +}); + +describe('useDeleteModel', () => { + it('calls API and invalidates model queries', async () => { + const queryClient = createTestQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + const { result } = renderHook(() => useDeleteModel(), { + wrapper: Wrapper, + }); + + result.current.mutate('model-1'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['models'] }); + expect(message.success).toHaveBeenCalledWith('模型删除成功'); + }); + + it('calls message.error on failure', async () => { + server.use( + http.delete('/api/models/:id', () => { + return HttpResponse.json({ message: '删除失败' }, { status: 500 }); + }), + ); + + const { result } = renderHook(() => useDeleteModel(), { + wrapper: createWrapper(), + }); + + result.current.mutate('model-1'); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(message.error).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/__tests__/hooks/useProviders.test.tsx b/frontend/src/__tests__/hooks/useProviders.test.tsx new file mode 100644 index 0000000..78da55c --- /dev/null +++ b/frontend/src/__tests__/hooks/useProviders.test.tsx @@ -0,0 +1,270 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders'; +import type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types'; + +// Mock antd message since it uses DOM APIs not available in jsdom +vi.mock('antd', () => ({ + message: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +// Import the mocked message for assertions +import { message } from 'antd'; + +// Test data +const mockProviders: Provider[] = [ + { + id: 'provider-1', + name: 'OpenAI', + apiKey: 'sk-xxx', + baseUrl: 'https://api.openai.com', + enabled: true, + createdAt: '2026-01-01T00:00:00Z', + updatedAt: '2026-01-01T00:00:00Z', + }, + { + id: 'provider-2', + name: 'Anthropic', + apiKey: 'sk-yyy', + baseUrl: 'https://api.anthropic.com', + enabled: false, + createdAt: '2026-02-01T00:00:00Z', + updatedAt: '2026-02-01T00:00:00Z', + }, +]; + +const mockCreatedProvider: Provider = { + id: 'provider-3', + name: 'NewProvider', + apiKey: 'sk-zzz', + baseUrl: 'https://api.newprovider.com', + enabled: true, + createdAt: '2026-03-01T00:00:00Z', + updatedAt: '2026-03-01T00:00:00Z', +}; + +// MSW handlers +const handlers = [ + http.get('/api/providers', () => { + return HttpResponse.json(mockProviders); + }), + http.post('/api/providers', async ({ request }) => { + const body = await request.json() as Record; + return HttpResponse.json({ + ...mockCreatedProvider, + ...body, + }); + }), + http.put('/api/providers/:id', async ({ request, params }) => { + const body = await request.json() as Record; + const existing = mockProviders.find((p) => p.id === params['id']); + return HttpResponse.json({ ...existing, ...body }); + }), + http.delete('/api/providers/:id', () => { + return new HttpResponse(null, { status: 204 }); + }), +]; + +const server = setupServer(...handlers); + +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); +} + +function createWrapper() { + const testQueryClient = createTestQueryClient(); + return function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + }; +} + +beforeAll(() => server.listen()); +afterEach(() => { + server.resetHandlers(); + vi.clearAllMocks(); +}); +afterAll(() => server.close()); + +describe('useProviders', () => { + it('fetches and returns provider list', async () => { + const { result } = renderHook(() => useProviders(), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual(mockProviders); + expect(result.current.data).toHaveLength(2); + expect(result.current.data![0]!.name).toBe('OpenAI'); + expect(result.current.data![1]!.name).toBe('Anthropic'); + }); +}); + +describe('useCreateProvider', () => { + it('calls API and invalidates provider queries', async () => { + const queryClient = createTestQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + const { result } = renderHook(() => useCreateProvider(), { + wrapper: Wrapper, + }); + + const input: CreateProviderInput = { + id: 'provider-3', + name: 'NewProvider', + apiKey: 'sk-zzz', + baseUrl: 'https://api.newprovider.com', + enabled: true, + }; + + result.current.mutate(input); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toMatchObject({ + id: 'provider-3', + name: 'NewProvider', + }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] }); + expect(message.success).toHaveBeenCalledWith('供应商创建成功'); + }); + + it('calls message.error on failure', async () => { + server.use( + http.post('/api/providers', () => { + return HttpResponse.json({ message: '创建失败' }, { status: 500 }); + }), + ); + + const { result } = renderHook(() => useCreateProvider(), { + wrapper: createWrapper(), + }); + + const input: CreateProviderInput = { + id: 'provider-3', + name: 'NewProvider', + apiKey: 'sk-zzz', + baseUrl: 'https://api.newprovider.com', + enabled: true, + }; + + result.current.mutate(input); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(message.error).toHaveBeenCalled(); + }); +}); + +describe('useUpdateProvider', () => { + it('calls API and invalidates provider queries', async () => { + const queryClient = createTestQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + const { result } = renderHook(() => useUpdateProvider(), { + wrapper: Wrapper, + }); + + const input: UpdateProviderInput = { name: 'UpdatedProvider' }; + + result.current.mutate({ id: 'provider-1', input }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toMatchObject({ + name: 'UpdatedProvider', + }); + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] }); + expect(message.success).toHaveBeenCalledWith('供应商更新成功'); + }); + + it('calls message.error on failure', async () => { + server.use( + http.put('/api/providers/:id', () => { + return HttpResponse.json({ message: '更新失败' }, { status: 500 }); + }), + ); + + const { result } = renderHook(() => useUpdateProvider(), { + wrapper: createWrapper(), + }); + + result.current.mutate({ id: 'provider-1', input: { name: 'Updated' } }); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(message.error).toHaveBeenCalled(); + }); +}); + +describe('useDeleteProvider', () => { + it('calls API and invalidates provider queries', async () => { + const queryClient = createTestQueryClient(); + const invalidateSpy = vi.spyOn(queryClient, 'invalidateQueries'); + + function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + } + + const { result } = renderHook(() => useDeleteProvider(), { + wrapper: Wrapper, + }); + + result.current.mutate('provider-1'); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(invalidateSpy).toHaveBeenCalledWith({ queryKey: ['providers'] }); + expect(message.success).toHaveBeenCalledWith('供应商删除成功'); + }); + + it('calls message.error on failure', async () => { + server.use( + http.delete('/api/providers/:id', () => { + return HttpResponse.json({ message: '删除失败' }, { status: 500 }); + }), + ); + + const { result } = renderHook(() => useDeleteProvider(), { + wrapper: createWrapper(), + }); + + result.current.mutate('provider-1'); + + await waitFor(() => expect(result.current.isError).toBe(true)); + expect(message.error).toHaveBeenCalled(); + }); +}); diff --git a/frontend/src/__tests__/hooks/useStats.test.tsx b/frontend/src/__tests__/hooks/useStats.test.tsx new file mode 100644 index 0000000..daa97bb --- /dev/null +++ b/frontend/src/__tests__/hooks/useStats.test.tsx @@ -0,0 +1,140 @@ +import { renderHook, waitFor } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import React from 'react'; +import { http, HttpResponse } from 'msw'; +import { setupServer } from 'msw/node'; +import { useStats } from '@/hooks/useStats'; +import type { UsageStats, StatsQueryParams } from '@/types'; + +// Test data +const mockStats: UsageStats[] = [ + { + id: 1, + providerId: 'provider-1', + modelName: 'gpt-4o', + requestCount: 100, + date: '2026-04-01', + }, + { + id: 2, + providerId: 'provider-1', + modelName: 'gpt-4o-mini', + requestCount: 50, + date: '2026-04-01', + }, +]; + +const mockFilteredStats: UsageStats[] = [ + { + id: 3, + providerId: 'provider-2', + modelName: 'claude-sonnet-4-5', + requestCount: 200, + date: '2026-04-01', + }, +]; + +// Track the request URL for assertions +let capturedUrl: URL | null = null; + +// MSW handlers +const handlers = [ + http.get('/api/stats', ({ request }) => { + capturedUrl = new URL(request.url); + const providerId = capturedUrl.searchParams.get('provider_id'); + if (providerId === 'provider-2') { + return HttpResponse.json(mockFilteredStats); + } + return HttpResponse.json(mockStats); + }), +]; + +const server = setupServer(...handlers); + +function createTestQueryClient() { + return new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); +} + +function createWrapper() { + const testQueryClient = createTestQueryClient(); + return function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); + }; +} + +beforeAll(() => server.listen()); +afterEach(() => { + server.resetHandlers(); + capturedUrl = null; +}); +afterAll(() => server.close()); + +describe('useStats', () => { + it('fetches stats without params', async () => { + const { result } = renderHook(() => useStats(), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual(mockStats); + expect(result.current.data).toHaveLength(2); + expect(result.current.data![0]!.modelName).toBe('gpt-4o'); + expect(result.current.data![1]!.requestCount).toBe(50); + + // Verify no query params were sent + expect(capturedUrl!.search).toBe(''); + }); + + it('with filter params passes them correctly', async () => { + const params: StatsQueryParams = { + providerId: 'provider-2', + modelName: 'claude-sonnet-4-5', + startDate: '2026-04-01', + endDate: '2026-04-15', + }; + + const { result } = renderHook(() => useStats(params), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + expect(result.current.data).toEqual(mockFilteredStats); + expect(result.current.data).toHaveLength(1); + expect(result.current.data![0]!.modelName).toBe('claude-sonnet-4-5'); + + // Verify query params were passed correctly (snake_case) + expect(capturedUrl!.searchParams.get('provider_id')).toBe('provider-2'); + expect(capturedUrl!.searchParams.get('model_name')).toBe('claude-sonnet-4-5'); + expect(capturedUrl!.searchParams.get('start_date')).toBe('2026-04-01'); + expect(capturedUrl!.searchParams.get('end_date')).toBe('2026-04-15'); + }); + + it('with partial filter params only sends provided params', async () => { + const params: StatsQueryParams = { + providerId: 'provider-1', + }; + + const { result } = renderHook(() => useStats(params), { + wrapper: createWrapper(), + }); + + await waitFor(() => expect(result.current.isSuccess).toBe(true)); + + // Verify only provider_id was sent + expect(capturedUrl!.searchParams.get('provider_id')).toBe('provider-1'); + expect(capturedUrl!.searchParams.get('model_name')).toBeNull(); + expect(capturedUrl!.searchParams.get('start_date')).toBeNull(); + expect(capturedUrl!.searchParams.get('end_date')).toBeNull(); + }); +}); diff --git a/frontend/src/__tests__/setup.ts b/frontend/src/__tests__/setup.ts new file mode 100644 index 0000000..ef9a3a2 --- /dev/null +++ b/frontend/src/__tests__/setup.ts @@ -0,0 +1,26 @@ +import '@testing-library/jest-dom/vitest'; + +// Polyfill window.matchMedia for jsdom (required by antd) +Object.defineProperty(window, 'matchMedia', { + writable: true, + value: (query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + }), +}); + +// Polyfill window.getComputedStyle to suppress jsdom warnings +const originalGetComputedStyle = window.getComputedStyle; +window.getComputedStyle = (elt: Element, pseudoElt?: string | null) => { + try { + return originalGetComputedStyle(elt, pseudoElt); + } catch { + return {} as CSSStyleDeclaration; + } +}; diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 4378543..fa7591e 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -1,129 +1,71 @@ -const API_BASE = 'http://localhost:9826/api'; +import { ApiError } from '@/types'; -export interface Provider { - id: string; - name: string; - api_key: string; - base_url: string; - enabled: boolean; - created_at: string; - updated_at: string; +const API_BASE = import.meta.env.VITE_API_BASE || ''; + +function toCamelCase(str: string): string { + return str.replace(/_([a-z])/g, (_, letter: string) => letter.toUpperCase()); } -export interface Model { - id: string; - provider_id: string; - model_name: string; - enabled: boolean; - created_at: string; +function toSnakeCase(str: string): string { + return str.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); } -export interface UsageStats { - id: number; - provider_id: string; - model_name: string; - request_count: number; - date: string; +function transformKeys(obj: unknown, transformer: (key: string) => string): T { + if (Array.isArray(obj)) { + return obj.map((item) => transformKeys(item, transformer)) as T; + } + if (obj !== null && typeof obj === 'object') { + const result: Record = {}; + for (const [key, value] of Object.entries(obj as Record)) { + result[transformer(key)] = transformKeys(value, transformer); + } + return result as T; + } + return obj as T; } -// Provider API -export async function listProviders(): Promise { - const response = await fetch(`${API_BASE}/providers`); - if (!response.ok) throw new Error('Failed to fetch providers'); - return response.json(); +export function fromApi(data: unknown): T { + return transformKeys(data, toCamelCase); } -export async function createProvider(provider: Omit): Promise { - const response = await fetch(`${API_BASE}/providers`, { - method: 'POST', +export function toApi(data: unknown): T { + return transformKeys(data, toSnakeCase); +} + +export async function request( + method: string, + path: string, + body?: unknown, +): Promise { + const url = `${API_BASE}${path}`; + const options: RequestInit = { + method, headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(provider), - }); - if (!response.ok) throw new Error('Failed to create provider'); - return response.json(); -} - -export async function updateProvider(id: string, updates: Partial): Promise { - const response = await fetch(`${API_BASE}/providers/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updates), - }); - if (!response.ok) throw new Error('Failed to update provider'); - return response.json(); -} - -export async function deleteProvider(id: string): Promise { - const response = await fetch(`${API_BASE}/providers/${id}`, { method: 'DELETE' }); - if (!response.ok) throw new Error('Failed to delete provider'); -} - -// Model API -export async function listModels(providerId?: string): Promise { - const url = providerId ? `${API_BASE}/models?provider_id=${providerId}` : `${API_BASE}/models`; - const response = await fetch(url); - if (!response.ok) throw new Error('Failed to fetch models'); - return response.json(); -} - -export async function createModel(model: Omit): Promise { - const response = await fetch(`${API_BASE}/models`, { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(model), - }); - if (!response.ok) throw new Error('Failed to create model'); - return response.json(); -} - -export async function updateModel(id: string, updates: Partial): Promise { - const response = await fetch(`${API_BASE}/models/${id}`, { - method: 'PUT', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(updates), - }); - if (!response.ok) throw new Error('Failed to update model'); - return response.json(); -} - -export async function deleteModel(id: string): Promise { - const response = await fetch(`${API_BASE}/models/${id}`, { method: 'DELETE' }); - if (!response.ok) throw new Error('Failed to delete model'); -} - -// Stats API -export async function getStats(params?: { - provider_id?: string; - model_name?: string; - start_date?: string; - end_date?: string; -}): Promise { - const query = new URLSearchParams(); - if (params?.provider_id) query.set('provider_id', params.provider_id); - if (params?.model_name) query.set('model_name', params.model_name); - if (params?.start_date) query.set('start_date', params.start_date); - if (params?.end_date) query.set('end_date', params.end_date); - - const response = await fetch(`${API_BASE}/stats?${query}`); - if (!response.ok) throw new Error('Failed to fetch stats'); - return response.json(); -} - -export async function getAggregatedStats(params?: { - provider_id?: string; - model_name?: string; - start_date?: string; - end_date?: string; - group_by?: 'provider' | 'model' | 'date'; -}): Promise { - const query = new URLSearchParams(); - if (params?.provider_id) query.set('provider_id', params.provider_id); - if (params?.model_name) query.set('model_name', params.model_name); - if (params?.start_date) query.set('start_date', params.start_date); - if (params?.end_date) query.set('end_date', params.end_date); - if (params?.group_by) query.set('group_by', params.group_by); - - const response = await fetch(`${API_BASE}/stats/aggregate?${query}`); - if (!response.ok) throw new Error('Failed to fetch aggregated stats'); - return response.json(); + }; + + if (body !== undefined) { + options.body = JSON.stringify(toApi(body)); + } + + const response = await fetch(url, options); + + if (!response.ok) { + let message = `请求失败 (${response.status})`; + try { + const errorData = await response.json(); + if (typeof errorData === 'object' && errorData !== null && 'message' in errorData) { + message = (errorData as { message: string }).message; + } + } catch { + // ignore JSON parse error + } + throw new ApiError(response.status, message); + } + + if (response.status === 204) { + return undefined as T; + } + + const data = await response.json(); + return fromApi(data); } diff --git a/frontend/src/api/models.ts b/frontend/src/api/models.ts new file mode 100644 index 0000000..8326023 --- /dev/null +++ b/frontend/src/api/models.ts @@ -0,0 +1,24 @@ +import type { Model, CreateModelInput, UpdateModelInput } from '@/types'; +import { request } from './client'; + +export async function listModels(providerId?: string): Promise { + const path = providerId + ? `/api/models?provider_id=${encodeURIComponent(providerId)}` + : '/api/models'; + return request('GET', path); +} + +export async function createModel(input: CreateModelInput): Promise { + return request('POST', '/api/models', input); +} + +export async function updateModel( + id: string, + input: UpdateModelInput, +): Promise { + return request('PUT', `/api/models/${id}`, input); +} + +export async function deleteModel(id: string): Promise { + return request('DELETE', `/api/models/${id}`); +} diff --git a/frontend/src/api/providers.ts b/frontend/src/api/providers.ts new file mode 100644 index 0000000..9370f88 --- /dev/null +++ b/frontend/src/api/providers.ts @@ -0,0 +1,21 @@ +import type { Provider, CreateProviderInput, UpdateProviderInput } from '@/types'; +import { request } from './client'; + +export async function listProviders(): Promise { + return request('GET', '/api/providers'); +} + +export async function createProvider(input: CreateProviderInput): Promise { + return request('POST', '/api/providers', input); +} + +export async function updateProvider( + id: string, + input: UpdateProviderInput, +): Promise { + return request('PUT', `/api/providers/${id}`, input); +} + +export async function deleteProvider(id: string): Promise { + return request('DELETE', `/api/providers/${id}`); +} diff --git a/frontend/src/api/stats.ts b/frontend/src/api/stats.ts new file mode 100644 index 0000000..b1a5d2a --- /dev/null +++ b/frontend/src/api/stats.ts @@ -0,0 +1,26 @@ +import type { UsageStats, StatsQueryParams } from '@/types'; +import { request } from './client'; + +export async function getStats(params?: StatsQueryParams): Promise { + if (!params) { + return request('GET', '/api/stats'); + } + + const query = new URLSearchParams(); + const snakeParams: Record = { + provider_id: params.providerId, + model_name: params.modelName, + start_date: params.startDate, + end_date: params.endDate, + }; + + for (const [key, value] of Object.entries(snakeParams)) { + if (value) { + query.set(key, value); + } + } + + const queryString = query.toString(); + const path = queryString ? `/api/stats?${queryString}` : '/api/stats'; + return request('GET', path); +} diff --git a/frontend/src/assets/hero.png b/frontend/src/assets/hero.png deleted file mode 100644 index cc51a3d..0000000 Binary files a/frontend/src/assets/hero.png and /dev/null differ diff --git a/frontend/src/assets/react.svg b/frontend/src/assets/react.svg deleted file mode 100644 index 6c87de9..0000000 --- a/frontend/src/assets/react.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/frontend/src/assets/vite.svg b/frontend/src/assets/vite.svg deleted file mode 100644 index 5101b67..0000000 --- a/frontend/src/assets/vite.svg +++ /dev/null @@ -1 +0,0 @@ -Vite diff --git a/frontend/src/components/AppLayout/AppLayout.module.scss b/frontend/src/components/AppLayout/AppLayout.module.scss new file mode 100644 index 0000000..9f6db47 --- /dev/null +++ b/frontend/src/components/AppLayout/AppLayout.module.scss @@ -0,0 +1,30 @@ +.layout { + min-height: 100vh; +} + +.header { + display: flex; + align-items: center; + padding: 0 2rem; +} + +.logo { + color: #fff; + font-size: 1.25rem; + font-weight: 600; + margin-right: 2rem; + white-space: nowrap; +} + +.menu { + flex: 1; + min-width: 0; +} + +.content { + padding: 2rem; + max-width: 1400px; + width: 100%; + margin: 0 auto; + box-sizing: border-box; +} diff --git a/frontend/src/components/AppLayout/index.tsx b/frontend/src/components/AppLayout/index.tsx new file mode 100644 index 0000000..c95bdfa --- /dev/null +++ b/frontend/src/components/AppLayout/index.tsx @@ -0,0 +1,36 @@ +import { Layout, Menu } from 'antd'; +import { + CloudServerOutlined, + BarChartOutlined, +} from '@ant-design/icons'; +import { Outlet, useLocation, useNavigate } from 'react-router'; +import styles from './AppLayout.module.scss'; + +const menuItems = [ + { key: '/providers', label: '供应商管理', icon: }, + { key: '/stats', label: '用量统计', icon: }, +]; + +export function AppLayout() { + const location = useLocation(); + const navigate = useNavigate(); + + return ( + + +
AI Gateway
+ navigate(key)} + className={styles.menu} + /> + + + + + + ); +} diff --git a/frontend/src/components/ModelForm.tsx b/frontend/src/components/ModelForm.tsx deleted file mode 100644 index e6d3258..0000000 --- a/frontend/src/components/ModelForm.tsx +++ /dev/null @@ -1,117 +0,0 @@ -import { useState } from 'react'; -import * as api from '../api/client'; - -interface ModelFormProps { - model?: api.Model; - providers: api.Provider[]; - onSave: () => void; - onCancel: () => void; -} - -export function ModelForm({ model, providers, onSave, onCancel }: ModelFormProps) { - const [id, setId] = useState(model?.id || ''); - const [providerId, setProviderId] = useState(model?.provider_id || (providers[0]?.id || '')); - const [modelName, setModelName] = useState(model?.model_name || ''); - const [enabled, setEnabled] = useState(model?.enabled ?? true); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const isEdit = !!model; - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setLoading(true); - setError(null); - - try { - if (isEdit) { - const updates: any = {}; - if (providerId !== model.provider_id) updates.provider_id = providerId; - if (modelName !== model.model_name) updates.model_name = modelName; - if (enabled !== model.enabled) updates.enabled = enabled; - - if (Object.keys(updates).length > 0) { - await api.updateModel(model.id, updates); - } - } else { - await api.createModel({ - id, - provider_id: providerId, - model_name: modelName, - enabled, - }); - } - onSave(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); - } - } - - return ( -
-
-

{isEdit ? '编辑模型' : '添加模型'}

- - {error &&
{error}
} - -
-
- - setId(e.target.value)} - disabled={isEdit} - required - /> -
- -
- - -
- -
- - setModelName(e.target.value)} - required - /> -
- -
- -
- -
- - -
-
-
-
- ); -} diff --git a/frontend/src/components/ProviderForm.tsx b/frontend/src/components/ProviderForm.tsx deleted file mode 100644 index 9bf6518..0000000 --- a/frontend/src/components/ProviderForm.tsx +++ /dev/null @@ -1,130 +0,0 @@ -import { useState } from 'react'; -import * as api from '../api/client'; - -interface ProviderFormProps { - provider?: api.Provider; - onSave: () => void; - onCancel: () => void; -} - -export function ProviderForm({ provider, onSave, onCancel }: ProviderFormProps) { - const [id, setId] = useState(provider?.id || ''); - const [name, setName] = useState(provider?.name || ''); - const [apiKey, setApiKey] = useState(''); - const [baseUrl, setBaseUrl] = useState(provider?.base_url || ''); - const [enabled, setEnabled] = useState(provider?.enabled ?? true); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const isEdit = !!provider; - - async function handleSubmit(e: React.FormEvent) { - e.preventDefault(); - setLoading(true); - setError(null); - - try { - if (isEdit) { - const updates: any = {}; - if (name !== provider.name) updates.name = name; - if (apiKey) updates.api_key = apiKey; - if (baseUrl !== provider.base_url) updates.base_url = baseUrl; - if (enabled !== provider.enabled) updates.enabled = enabled; - - if (Object.keys(updates).length > 0) { - await api.updateProvider(provider.id, updates); - } - } else { - await api.createProvider({ - id, - name, - api_key: apiKey, - base_url: baseUrl, - enabled, - }); - } - onSave(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); - } - } - - return ( -
-
-

{isEdit ? '编辑供应商' : '添加供应商'}

- - {error &&
{error}
} - -
-
- - setId(e.target.value)} - disabled={isEdit} - required - /> -
- -
- - setName(e.target.value)} - required - /> -
- -
- - setApiKey(e.target.value)} - required={!isEdit} - /> -
- -
- - setBaseUrl(e.target.value)} - placeholder="例如: https://api.openai.com/v1 或 https://open.bigmodel.cn/api/paas/v4" - required - /> - - 配置到 API 版本路径,不包含 /chat/completions - -
- -
- -
- -
- - -
-
-
-
- ); -} diff --git a/frontend/src/hooks/useModels.ts b/frontend/src/hooks/useModels.ts new file mode 100644 index 0000000..a3c986f --- /dev/null +++ b/frontend/src/hooks/useModels.ts @@ -0,0 +1,62 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { message } from 'antd'; +import type { CreateModelInput, UpdateModelInput } from '@/types'; +import * as api from '@/api/models'; + +export const modelKeys = { + all: ['models'] as const, + filtered: (providerId?: string) => ['models', providerId] as const, +}; + +export function useModels(providerId?: string) { + return useQuery({ + queryKey: modelKeys.filtered(providerId), + queryFn: () => api.listModels(providerId), + }); +} + +export function useCreateModel() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: CreateModelInput) => api.createModel(input), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: modelKeys.all }); + message.success('模型创建成功'); + }, + onError: (error: Error) => { + message.error(error.message); + }, + }); +} + +export function useUpdateModel() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, input }: { id: string; input: UpdateModelInput }) => + api.updateModel(id, input), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: modelKeys.all }); + message.success('模型更新成功'); + }, + onError: (error: Error) => { + message.error(error.message); + }, + }); +} + +export function useDeleteModel() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => api.deleteModel(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: modelKeys.all }); + message.success('模型删除成功'); + }, + onError: (error: Error) => { + message.error(error.message); + }, + }); +} diff --git a/frontend/src/hooks/useProviders.ts b/frontend/src/hooks/useProviders.ts new file mode 100644 index 0000000..5664270 --- /dev/null +++ b/frontend/src/hooks/useProviders.ts @@ -0,0 +1,61 @@ +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; +import { message } from 'antd'; +import type { CreateProviderInput, UpdateProviderInput } from '@/types'; +import * as api from '@/api/providers'; + +export const providerKeys = { + all: ['providers'] as const, +}; + +export function useProviders() { + return useQuery({ + queryKey: providerKeys.all, + queryFn: api.listProviders, + }); +} + +export function useCreateProvider() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (input: CreateProviderInput) => api.createProvider(input), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: providerKeys.all }); + message.success('供应商创建成功'); + }, + onError: (error: Error) => { + message.error(error.message); + }, + }); +} + +export function useUpdateProvider() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: ({ id, input }: { id: string; input: UpdateProviderInput }) => + api.updateProvider(id, input), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: providerKeys.all }); + message.success('供应商更新成功'); + }, + onError: (error: Error) => { + message.error(error.message); + }, + }); +} + +export function useDeleteProvider() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: (id: string) => api.deleteProvider(id), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: providerKeys.all }); + message.success('供应商删除成功'); + }, + onError: (error: Error) => { + message.error(error.message); + }, + }); +} diff --git a/frontend/src/hooks/useStats.ts b/frontend/src/hooks/useStats.ts new file mode 100644 index 0000000..8d0867e --- /dev/null +++ b/frontend/src/hooks/useStats.ts @@ -0,0 +1,14 @@ +import { useQuery } from '@tanstack/react-query'; +import type { StatsQueryParams } from '@/types'; +import * as api from '@/api/stats'; + +export const statsKeys = { + filtered: (params?: StatsQueryParams) => ['stats', params] as const, +}; + +export function useStats(params?: StatsQueryParams) { + return useQuery({ + queryKey: statsKeys.filtered(params), + queryFn: () => api.getStats(params), + }); +} diff --git a/frontend/src/index.css b/frontend/src/index.css deleted file mode 100644 index 26d6918..0000000 --- a/frontend/src/index.css +++ /dev/null @@ -1,12 +0,0 @@ -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif; - background: #f5f5f5; - color: #333; - line-height: 1.6; -} diff --git a/frontend/src/index.scss b/frontend/src/index.scss new file mode 100644 index 0000000..8793119 --- /dev/null +++ b/frontend/src/index.scss @@ -0,0 +1,20 @@ +*, +*::before, +*::after { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +body { + font-family: + -apple-system, + BlinkMacSystemFont, + 'Segoe UI', + Roboto, + 'Helvetica Neue', + Arial, + sans-serif; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx index bef5202..6d5552e 100644 --- a/frontend/src/main.tsx +++ b/frontend/src/main.tsx @@ -1,7 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' -import './index.css' -import App from './App.tsx' +import './index.scss' +import App from './App' createRoot(document.getElementById('root')!).render( diff --git a/frontend/src/pages/NotFound.tsx b/frontend/src/pages/NotFound.tsx new file mode 100644 index 0000000..c79bcf9 --- /dev/null +++ b/frontend/src/pages/NotFound.tsx @@ -0,0 +1,19 @@ +import { Button, Result } from 'antd'; +import { useNavigate } from 'react-router'; + +export function NotFound() { + const navigate = useNavigate(); + + return ( + navigate('/providers')}> + 返回首页 + + } + /> + ); +} diff --git a/frontend/src/pages/Providers/ModelForm.tsx b/frontend/src/pages/Providers/ModelForm.tsx new file mode 100644 index 0000000..3269ad5 --- /dev/null +++ b/frontend/src/pages/Providers/ModelForm.tsx @@ -0,0 +1,94 @@ +import { useEffect } from 'react'; +import { Modal, Form, Input, Select, Switch } from 'antd'; +import type { Provider, Model } from '@/types'; + +interface ModelFormValues { + id: string; + providerId: string; + modelName: string; + enabled: boolean; +} + +interface ModelFormProps { + open: boolean; + model?: Model; + providerId: string; + providers: Provider[]; + onSave: (values: ModelFormValues) => void; + onCancel: () => void; + loading: boolean; +} + +export function ModelForm({ + open, + model, + providerId, + providers, + onSave, + onCancel, + loading, +}: ModelFormProps) { + const [form] = Form.useForm(); + const isEdit = !!model; + + useEffect(() => { + if (open) { + if (model) { + form.setFieldsValue({ + id: model.id, + providerId: model.providerId, + modelName: model.modelName, + enabled: model.enabled, + }); + } else { + form.resetFields(); + form.setFieldsValue({ providerId }); + } + } + }, [open, model, providerId, form]); + + return ( + form.submit()} + onCancel={onCancel} + confirmLoading={loading} + okText="保存" + cancelText="取消" + destroyOnClose + > +
+ + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/Providers/ModelTable.tsx b/frontend/src/pages/Providers/ModelTable.tsx new file mode 100644 index 0000000..5d98121 --- /dev/null +++ b/frontend/src/pages/Providers/ModelTable.tsx @@ -0,0 +1,76 @@ +import { Button, Table, Tag, Popconfirm, Space } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import type { Model } from '@/types'; +import { useModels, useDeleteModel } from '@/hooks/useModels'; + +interface ModelTableProps { + providerId: string; + onAdd?: () => void; + onEdit?: (model: Model) => void; +} + +export function ModelTable({ providerId, onAdd, onEdit }: ModelTableProps) { + const { data: models = [], isLoading } = useModels(providerId); + const deleteModel = useDeleteModel(); + + const columns: ColumnsType = [ + { + title: '模型名称', + dataIndex: 'modelName', + key: 'modelName', + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + render: (enabled: boolean) => + enabled ? 启用 : 禁用, + width: 80, + }, + { + title: '操作', + key: 'action', + width: 120, + render: (_, record) => ( + + {onEdit && ( + + )} + deleteModel.mutate(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ]; + + return ( +
+
+ 关联模型 ({models.length}) + {onAdd && ( + + )} +
+ + columns={columns} + dataSource={models} + rowKey="id" + loading={isLoading} + pagination={false} + size="small" + /> +
+ ); +} diff --git a/frontend/src/pages/Providers/ProviderForm.tsx b/frontend/src/pages/Providers/ProviderForm.tsx new file mode 100644 index 0000000..b40fd11 --- /dev/null +++ b/frontend/src/pages/Providers/ProviderForm.tsx @@ -0,0 +1,92 @@ +import { useEffect } from 'react'; +import { Modal, Form, Input, Switch } from 'antd'; +import type { Provider } from '@/types'; + +interface ProviderFormValues { + id: string; + name: string; + apiKey: string; + baseUrl: string; + enabled: boolean; +} + +interface ProviderFormProps { + open: boolean; + provider?: Provider; + onSave: (values: ProviderFormValues) => void; + onCancel: () => void; + loading: boolean; +} + +export function ProviderForm({ + open, + provider, + onSave, + onCancel, + loading, +}: ProviderFormProps) { + const [form] = Form.useForm(); + const isEdit = !!provider; + + useEffect(() => { + if (open) { + if (provider) { + form.setFieldsValue({ + id: provider.id, + name: provider.name, + apiKey: '', + baseUrl: provider.baseUrl, + enabled: provider.enabled, + }); + } else { + form.resetFields(); + } + } + }, [open, provider, form]); + + return ( + form.submit()} + onCancel={onCancel} + confirmLoading={loading} + okText="保存" + cancelText="取消" + destroyOnClose + > +
+ + + + + + + + + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/src/pages/Providers/ProviderTable.tsx b/frontend/src/pages/Providers/ProviderTable.tsx new file mode 100644 index 0000000..5baf1f4 --- /dev/null +++ b/frontend/src/pages/Providers/ProviderTable.tsx @@ -0,0 +1,106 @@ +import { Button, Table, Tag, Popconfirm, Space } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import type { Provider, Model } from '@/types'; +import { ModelTable } from './ModelTable'; + +interface ProviderTableProps { + providers: Provider[]; + loading: boolean; + onAdd: () => void; + onEdit: (provider: Provider) => void; + onDelete: (id: string) => void; + onAddModel: (providerId: string) => void; + onEditModel: (model: Model) => void; +} + +function maskApiKey(key: string | null | undefined): string { + if (!key) return '****'; + if (key.length <= 4) return '****'; + return `****${key.slice(-4)}`; +} + +export function ProviderTable({ + providers, + loading, + onAdd, + onEdit, + onDelete, + onAddModel, + onEditModel, +}: ProviderTableProps) { + const columns: ColumnsType = [ + { + title: '名称', + dataIndex: 'name', + key: 'name', + }, + { + title: 'Base URL', + dataIndex: 'baseUrl', + key: 'baseUrl', + }, + { + title: 'API Key', + dataIndex: 'apiKey', + key: 'apiKey', + render: (key: string | null | undefined) => maskApiKey(key), + }, + { + title: '状态', + dataIndex: 'enabled', + key: 'enabled', + render: (enabled: boolean) => + enabled ? 启用 : 禁用, + width: 80, + }, + { + title: '操作', + key: 'action', + width: 160, + render: (_, record) => ( + + + onDelete(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ]; + + return ( + <> +
+

供应商列表

+ +
+ + columns={columns} + dataSource={providers} + rowKey="id" + loading={loading} + expandable={{ + expandedRowRender: (record) => ( + onAddModel(record.id)} + onEdit={onEditModel} + /> + ), + }} + pagination={false} + /> + + ); +} diff --git a/frontend/src/pages/Providers/index.tsx b/frontend/src/pages/Providers/index.tsx new file mode 100644 index 0000000..1b91f87 --- /dev/null +++ b/frontend/src/pages/Providers/index.tsx @@ -0,0 +1,101 @@ +import { useState } from 'react'; +import type { Provider, Model, UpdateProviderInput, UpdateModelInput } from '@/types'; +import { useProviders, useCreateProvider, useUpdateProvider, useDeleteProvider } from '@/hooks/useProviders'; +import { useCreateModel, useUpdateModel } from '@/hooks/useModels'; +import { ProviderTable } from './ProviderTable'; +import { ProviderForm } from './ProviderForm'; +import { ModelForm } from './ModelForm'; + +export function ProvidersPage() { + const { data: providers = [], isLoading } = useProviders(); + const createProvider = useCreateProvider(); + const updateProvider = useUpdateProvider(); + const deleteProvider = useDeleteProvider(); + const createModel = useCreateModel(); + const updateModel = useUpdateModel(); + + const [providerFormOpen, setProviderFormOpen] = useState(false); + const [editingProvider, setEditingProvider] = useState(); + const [modelFormOpen, setModelFormOpen] = useState(false); + const [editingModel, setEditingModel] = useState(); + const [modelFormProviderId, setModelFormProviderId] = useState(''); + + return ( +
+

供应商管理

+ + { + setEditingProvider(undefined); + setProviderFormOpen(true); + }} + onEdit={(provider) => { + setEditingProvider(provider); + setProviderFormOpen(true); + }} + onDelete={(id) => deleteProvider.mutate(id)} + onAddModel={(providerId) => { + setEditingModel(undefined); + setModelFormProviderId(providerId); + setModelFormOpen(true); + }} + onEditModel={(model) => { + setEditingModel(model); + setModelFormProviderId(model.providerId); + setModelFormOpen(true); + }} + /> + + { + if (editingProvider) { + const input: Partial = {}; + if (values.name !== editingProvider.name) input.name = values.name; + if (values.apiKey) input.apiKey = values.apiKey; + if (values.baseUrl !== editingProvider.baseUrl) input.baseUrl = values.baseUrl; + if (values.enabled !== editingProvider.enabled) input.enabled = values.enabled; + updateProvider.mutate( + { id: editingProvider.id, input }, + { onSuccess: () => setProviderFormOpen(false) }, + ); + } else { + createProvider.mutate(values, { + onSuccess: () => setProviderFormOpen(false), + }); + } + }} + onCancel={() => setProviderFormOpen(false)} + /> + + { + if (editingModel) { + const input: Partial = {}; + if (values.providerId !== editingModel.providerId) input.providerId = values.providerId; + if (values.modelName !== editingModel.modelName) input.modelName = values.modelName; + if (values.enabled !== editingModel.enabled) input.enabled = values.enabled; + updateModel.mutate( + { id: editingModel.id, input }, + { onSuccess: () => setModelFormOpen(false) }, + ); + } else { + createModel.mutate(values, { + onSuccess: () => setModelFormOpen(false), + }); + } + }} + onCancel={() => setModelFormOpen(false)} + /> +
+ ); +} diff --git a/frontend/src/pages/ProvidersPage.tsx b/frontend/src/pages/ProvidersPage.tsx deleted file mode 100644 index 2c9d85b..0000000 --- a/frontend/src/pages/ProvidersPage.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import { useState, useEffect } from 'react'; -import * as api from '../api/client'; -import { ProviderForm } from '../components/ProviderForm'; -import { ModelForm } from '../components/ModelForm'; - -export function ProvidersPage() { - const [providers, setProviders] = useState([]); - const [models, setModels] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // 表单状态 - const [showProviderForm, setShowProviderForm] = useState(false); - const [editingProvider, setEditingProvider] = useState(null); - const [showModelForm, setShowModelForm] = useState(false); - const [editingModel, setEditingModel] = useState(null); - - useEffect(() => { - loadData(); - }, []); - - async function loadData() { - try { - setLoading(true); - const [providersData, modelsData] = await Promise.all([ - api.listProviders(), - api.listModels(), - ]); - setProviders(providersData); - setModels(modelsData); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); - } - } - - async function handleDeleteProvider(id: string) { - if (!confirm('确定要删除这个供应商吗?关联的模型也会被删除。')) return; - try { - await api.deleteProvider(id); - loadData(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } - } - - async function handleDeleteModel(id: string) { - if (!confirm('确定要删除这个模型吗?')) return; - try { - await api.deleteModel(id); - loadData(); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } - } - - if (loading) return
加载中...
; - if (error) return
{error}
; - - return ( -
-

供应商管理

- -
-

供应商列表

- - - - - - - - - - - - - - - {providers.map(p => ( - - - - - - - - - ))} - -
ID名称API KeyBase URL状态操作
{p.id}{p.name}{p.api_key}{p.base_url}{p.enabled ? '启用' : '禁用'} - - -
-
- -
-

模型列表

- - - - - - - - - - - - - - {models.map(m => { - const provider = providers.find(p => p.id === m.provider_id); - return ( - - - - - - - - ); - })} - -
ID供应商模型名称状态操作
{m.id}{provider?.name || m.provider_id}{m.model_name}{m.enabled ? '启用' : '禁用'} - - -
-
- - {/* 供应商表单 */} - {showProviderForm && ( - { - setShowProviderForm(false); - setEditingProvider(null); - loadData(); - }} - onCancel={() => { - setShowProviderForm(false); - setEditingProvider(null); - }} - /> - )} - - {/* 模型表单 */} - {showModelForm && ( - { - setShowModelForm(false); - setEditingModel(null); - loadData(); - }} - onCancel={() => { - setShowModelForm(false); - setEditingModel(null); - }} - /> - )} -
- ); -} diff --git a/frontend/src/pages/Stats/StatsTable.tsx b/frontend/src/pages/Stats/StatsTable.tsx new file mode 100644 index 0000000..86a0c52 --- /dev/null +++ b/frontend/src/pages/Stats/StatsTable.tsx @@ -0,0 +1,96 @@ +import { useState, useMemo } from 'react'; +import { Table, Select, Input, DatePicker, Space, Card } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import type { Dayjs } from 'dayjs'; +import type { UsageStats, Provider } from '@/types'; +import { useStats } from '@/hooks/useStats'; + +interface StatsTableProps { + providers: Provider[]; +} + +export function StatsTable({ providers }: StatsTableProps) { + const [providerId, setProviderId] = useState(); + const [modelName, setModelName] = useState(); + const [dateRange, setDateRange] = useState<[Dayjs | null, Dayjs | null] | null>(null); + + const params = useMemo( + () => ({ + providerId, + modelName, + startDate: dateRange?.[0]?.format('YYYY-MM-DD'), + endDate: dateRange?.[1]?.format('YYYY-MM-DD'), + }), + [providerId, modelName, dateRange], + ); + + const { data: stats = [], isLoading } = useStats(params); + + const providerMap = useMemo(() => { + const map = new Map(); + for (const p of providers) { + map.set(p.id, p.name); + } + return map; + }, [providers]); + + const columns: ColumnsType = [ + { + title: '供应商', + dataIndex: 'providerId', + key: 'providerId', + render: (id: string) => providerMap.get(id) ?? id, + }, + { + title: '模型', + dataIndex: 'modelName', + key: 'modelName', + }, + { + title: '日期', + dataIndex: 'date', + key: 'date', + }, + { + title: '请求数', + dataIndex: 'requestCount', + key: 'requestCount', + }, + ]; + + return ( + <> + + + setModelName(e.target.value || undefined)} + /> + setDateRange(dates)} + /> + + + + + columns={columns} + dataSource={stats} + rowKey="id" + loading={isLoading} + pagination={{ pageSize: 20 }} + /> + + ); +} diff --git a/frontend/src/pages/Stats/index.tsx b/frontend/src/pages/Stats/index.tsx new file mode 100644 index 0000000..a5c692f --- /dev/null +++ b/frontend/src/pages/Stats/index.tsx @@ -0,0 +1,13 @@ +import { useProviders } from '@/hooks/useProviders'; +import { StatsTable } from './StatsTable'; + +export function StatsPage() { + const { data: providers = [] } = useProviders(); + + return ( +
+

用量统计

+ +
+ ); +} diff --git a/frontend/src/pages/StatsPage.tsx b/frontend/src/pages/StatsPage.tsx deleted file mode 100644 index 4e8f669..0000000 --- a/frontend/src/pages/StatsPage.tsx +++ /dev/null @@ -1,110 +0,0 @@ -import { useState, useEffect } from 'react'; -import * as api from '../api/client'; - -export function StatsPage() { - const [stats, setStats] = useState([]); - const [providers, setProviders] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - - // 过滤条件 - const [providerId, setProviderId] = useState(''); - const [modelName, setModelName] = useState(''); - const [startDate, setStartDate] = useState(''); - const [endDate, setEndDate] = useState(''); - - useEffect(() => { - loadData(); - }, []); - - async function loadData() { - try { - setLoading(true); - const [statsData, providersData] = await Promise.all([ - api.getStats({ - provider_id: providerId || undefined, - model_name: modelName || undefined, - start_date: startDate || undefined, - end_date: endDate || undefined, - }), - api.listProviders(), - ]); - setStats(statsData); - setProviders(providersData); - } catch (err) { - setError(err instanceof Error ? err.message : 'Unknown error'); - } finally { - setLoading(false); - } - } - - function handleFilter(e: React.FormEvent) { - e.preventDefault(); - loadData(); - } - - if (loading) return
加载中...
; - if (error) return
{error}
; - - return ( -
-

用量统计

- -
- - - setModelName(e.target.value)} - /> - - setStartDate(e.target.value)} - /> - - setEndDate(e.target.value)} - /> - - -
- - - - - - - - - - - - {stats.map(s => { - const provider = providers.find(p => p.id === s.provider_id); - return ( - - - - - - - ); - })} - -
供应商模型日期请求数
{provider?.name || s.provider_id}{s.model_name}{s.date}{s.request_count}
-
- ); -} diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx new file mode 100644 index 0000000..f254811 --- /dev/null +++ b/frontend/src/routes/index.tsx @@ -0,0 +1,18 @@ +import { Routes, Route, Navigate } from 'react-router'; +import { AppLayout } from '@/components/AppLayout'; +import { ProvidersPage } from '@/pages/Providers'; +import { StatsPage } from '@/pages/Stats'; +import { NotFound } from '@/pages/NotFound'; + +export function AppRoutes() { + return ( + + }> + } /> + } /> + } /> + } /> + + + ); +} diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..427004b --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,73 @@ +export interface Provider { + id: string; + name: string; + apiKey: string; + baseUrl: string; + enabled: boolean; + createdAt: string; + updatedAt: string; +} + +export interface Model { + id: string; + providerId: string; + modelName: string; + enabled: boolean; + createdAt: string; +} + +export interface UsageStats { + id: number; + providerId: string; + modelName: string; + requestCount: number; + date: string; +} + +export interface CreateProviderInput { + id: string; + name: string; + apiKey: string; + baseUrl: string; + enabled: boolean; +} + +export interface UpdateProviderInput { + name?: string; + apiKey?: string; + baseUrl?: string; + enabled?: boolean; +} + +export interface CreateModelInput { + id: string; + providerId: string; + modelName: string; + enabled: boolean; +} + +export interface UpdateModelInput { + providerId?: string; + modelName?: string; + enabled?: boolean; +} + +export interface StatsQueryParams { + providerId?: string; + modelName?: string; + startDate?: string; + endDate?: string; +} + +export class ApiError extends Error { + status: number; + + constructor( + status: number, + message: string, + ) { + super(message); + this.name = 'ApiError'; + this.status = status; + } +} diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json deleted file mode 100644 index 1d29c88..0000000 --- a/frontend/tsconfig.app.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023", "DOM", "DOM.Iterable"], - "module": "esnext", - "types": ["vite/client"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["src"] -} diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index 1ffef60..0b16bd7 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -1,7 +1,26 @@ { - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] + "compilerOptions": { + "target": "ES2023", + "lib": ["ES2023", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "moduleDetection": "force", + "jsx": "react-jsx", + "strict": true, + "noUncheckedIndexedAccess": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "erasableSyntaxOnly": true, + "verbatimModuleSyntax": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "paths": { "@/*": ["./src/*"] }, + "types": ["vite/client"] + }, + "include": ["src", "vite.config.ts"], + "exclude": ["src/__tests__", "e2e"] } diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json deleted file mode 100644 index d3c52ea..0000000 --- a/frontend/tsconfig.node.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "target": "es2023", - "lib": ["ES2023"], - "module": "esnext", - "types": ["node"], - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "verbatimModuleSyntax": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "noUnusedLocals": true, - "noUnusedParameters": true, - "erasableSyntaxOnly": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] -} diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts index 8b0f57b..58c7a54 100644 --- a/frontend/vite.config.ts +++ b/frontend/vite.config.ts @@ -1,7 +1,21 @@ import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import path from 'node:path' // https://vite.dev/config/ export default defineConfig({ plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + server: { + proxy: { + '/api': { + target: 'http://localhost:9826', + changeOrigin: true, + }, + }, + }, }) diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts new file mode 100644 index 0000000..88e5aaa --- /dev/null +++ b/frontend/vitest.config.ts @@ -0,0 +1,28 @@ +import { defineConfig } from 'vitest/config' +import react from '@vitejs/plugin-react' +import path from 'node:path' + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, + test: { + globals: true, + environment: 'jsdom', + setupFiles: ['./src/__tests__/setup.ts'], + include: ['src/**/*.{test,spec}.{ts,tsx}'], + coverage: { + provider: 'v8', + include: ['src/**/*.{ts,tsx}'], + exclude: [ + 'src/__tests__/**', + 'src/main.tsx', + 'src/**/*.module.scss', + 'src/types/**', + ], + }, + }, +}) diff --git a/openspec/specs/frontend-config-ui/spec.md b/openspec/specs/frontend-config-ui/spec.md index 2e4e79c..454443d 100644 --- a/openspec/specs/frontend-config-ui/spec.md +++ b/openspec/specs/frontend-config-ui/spec.md @@ -8,98 +8,96 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 ### Requirement: 提供供应商管理页面 -前端 SHALL 提供用于管理供应商配置的网页。 +前端 SHALL 使用 Ant Design 组件提供供应商管理页面。 #### Scenario: 显示供应商列表 - **WHEN** 加载供应商管理页面 -- **THEN** 前端 SHALL 显示所有已配置供应商的列表 -- **THEN** 每个供应商 SHALL 显示 id, name, base_url 和 enabled 状态 -- **THEN** API Key SHALL 被掩码 +- **THEN** 前端 SHALL 使用 Ant Design Table 显示所有已配置供应商 +- **THEN** 每个供应商 SHALL 显示 name、base_url 和 enabled 状态(使用 Tag 组件) +- **THEN** API Key SHALL 被脱敏显示(掩码处理) +- **THEN** 表格 SHALL 支持展开行以显示关联模型 #### Scenario: 添加新供应商 - **WHEN** 用户点击"添加供应商"按钮 -- **THEN** 前端 SHALL 显示输入供应商详情的表单 -- **THEN** 表单 SHALL 包含 id, name, api_key, base_url 字段 +- **THEN** 前端 SHALL 使用 Ant Design Modal + Form 显示输入表单 +- **THEN** 表单 SHALL 包含 id、name、api_key、base_url 字段,带校验规则 - **WHEN** 用户提交包含有效数据的表单 -- **THEN** 前端 SHALL 向 `/api/providers` 发送 POST 请求 -- **THEN** 前端 SHALL 刷新供应商列表 +- **THEN** 前端 SHALL 通过 useMutation 调用创建 API +- **THEN** 成功后 SHALL 关闭 Modal 并刷新供应商列表 +- **THEN** 失败 SHALL 使用 message.error() 提示 #### Scenario: 编辑现有供应商 - **WHEN** 用户点击供应商的"编辑"按钮 -- **THEN** 前端 SHALL 显示预填充供应商当前数据的表单 +- **THEN** 前端 SHALL 使用 Ant Design Modal + Form 显示预填充数据的表单 - **WHEN** 用户提交包含更新数据的表单 -- **THEN** 前端 SHALL 向 `/api/providers/:id` 发送 PUT 请求 -- **THEN** 前端 SHALL 刷新供应商列表 +- **THEN** 前端 SHALL 通过 useMutation 调用更新 API +- **THEN** 成功后 SHALL 关闭 Modal 并刷新供应商列表 #### Scenario: 删除供应商 - **WHEN** 用户点击供应商的"删除"按钮 -- **THEN** 前端 SHALL 提示确认 +- **THEN** 前端 SHALL 使用 Ant Design Popconfirm 弹出确认 - **WHEN** 用户确认删除 -- **THEN** 前端 SHALL 向 `/api/providers/:id` 发送 DELETE 请求 -- **THEN** 前端 SHALL 刷新供应商列表 +- **THEN** 前端 SHALL 通过 useMutation 调用删除 API +- **THEN** 成功后 SHALL 刷新供应商列表 ### Requirement: 提供模型管理界面 -前端 SHALL 在供应商页面中提供管理模型配置的界面。 +前端 SHALL 在供应商页面展开行中提供模型管理。 #### Scenario: 显示供应商的模型 -- **WHEN** 选择或展开供应商 +- **WHEN** 展开供应商行 - **THEN** 前端 SHALL 显示该供应商的模型列表 - **THEN** 每个模型 SHALL 显示 model_name 和 enabled 状态 #### Scenario: 为供应商添加模型 -- **WHEN** 用户点击供应商的"添加模型" -- **THEN** 前端 SHALL 显示输入 model_name 的表单 +- **WHEN** 用户在展开行中点击"添加模型" +- **THEN** 前端 SHALL 显示 Ant Design Modal + Form +- **THEN** provider_id SHALL 自动关联当前供应商 - **WHEN** 用户提交表单 -- **THEN** 前端 SHALL 向 `/api/models` 发送 POST 请求,携带 provider_id -- **THEN** 前端 SHALL 刷新模型列表 +- **THEN** 前端 SHALL 通过 useMutation 调用创建 API +- **THEN** 成功后 SHALL 刷新模型列表 #### Scenario: 编辑模型 - **WHEN** 用户点击模型的"编辑" -- **THEN** 前端 SHALL 显示编辑 model_name 的表单 +- **THEN** 前端 SHALL 显示编辑表单 - **WHEN** 用户提交表单 -- **THEN** 前端 SHALL 向 `/api/models/:id` 发送 PUT 请求 -- **THEN** 前端 SHALL 刷新模型列表 +- **THEN** 前端 SHALL 通过 useMutation 调用更新 API +- **THEN** 成功后 SHALL 刷新模型列表 #### Scenario: 删除模型 - **WHEN** 用户点击模型的"删除" -- **THEN** 前端 SHALL 提示确认 +- **THEN** 前端 SHALL 使用 Popconfirm 弹出确认 - **WHEN** 用户确认删除 -- **THEN** 前端 SHALL 向 `/api/models/:id` 发送 DELETE 请求 -- **THEN** 前端 SHALL 刷新模型列表 +- **THEN** 前端 SHALL 通过 useMutation 调用删除 API +- **THEN** 成功后 SHALL 刷新模型列表 ### Requirement: 提供统计查看页面 -前端 SHALL 提供查看用量统计的页面。 +前端 SHALL 使用 Ant Design 组件提供统计查看页面。 #### Scenario: 显示统计概览 - **WHEN** 加载统计页面 -- **THEN** 前端 SHALL 显示所有供应商和模型的统计 -- **THEN** 前端 SHALL 按供应商和模型分组显示请求计数 +- **THEN** 前端 SHALL 使用 Ant Design Table 显示统计数据 +- **THEN** 统计数据 SHALL 按供应商和模型显示请求计数 #### Scenario: 按供应商过滤统计 -- **WHEN** 用户从下拉菜单选择供应商 -- **THEN** 前端 SHALL 过滤统计,仅显示该供应商的数据 - -#### Scenario: 按模型过滤统计 - -- **WHEN** 用户从下拉菜单选择模型 -- **THEN** 前端 SHALL 过滤统计,仅显示该模型的数据 +- **WHEN** 用户从 Ant Design Select 选择供应商 +- **THEN** 前端 SHALL 自动查询并过滤统计 #### Scenario: 按日期范围过滤统计 -- **WHEN** 用户选择开始和结束日期 -- **THEN** 前端 SHALL 过滤统计,仅显示该范围内的数据 +- **WHEN** 用户使用 Ant Design DatePicker.RangePicker 选择日期范围 +- **THEN** 前端 SHALL 自动查询并过滤统计 ### Requirement: 优雅处理 API 错误 @@ -108,8 +106,8 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 #### Scenario: API 请求失败 - **WHEN** API 请求失败(网络错误、4xx、5xx) -- **THEN** 前端 SHALL 向用户显示错误消息 -- **THEN** 错误消息 SHALL 具有描述性和可操作性 +- **THEN** 前端 SHALL 使用 Ant Design 的 message.error() 显示全局错误提示 +- **THEN** 错误消息 SHALL 具有描述性 #### Scenario: 验证错误 @@ -119,64 +117,98 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 ### Requirement: 提供响应式布局 -前端 SHALL 提供适应不同屏幕尺寸的响应式布局。 +前端 SHALL 使用 Ant Design Layout 提供顶部导航布局。 #### Scenario: 桌面布局 - **WHEN** 在桌面屏幕上查看前端 -- **THEN** 布局 SHALL 使用多列设计以高效利用空间 +- **THEN** 布局 SHALL 使用 Ant Design Layout.Header + Menu(horizontal 模式) +- **THEN** 导航菜单 SHALL 在顶部水平排列 -#### Scenario: 移动布局 +#### Scenario: 页面内容区域 -- **WHEN** 在移动屏幕上查看前端 -- **THEN** 布局 SHALL 适应为单列设计 -- **THEN** 所有功能 SHALL 保持可访问 +- **WHEN** 显示页面内容 +- **THEN** 内容区域 SHALL 有合理的最大宽度和内边距 +- **THEN** 页面之间 SHALL 通过 React Router Outlet 渲染 ### Requirement: 使用无组件库的最小 UI -前端 SHALL 使用自定义组件,不使用外部 UI 库。 +前端 SHALL 使用 Ant Design 5 作为 UI 组件库。 -#### Scenario: 自定义组件 +#### Scenario: Ant Design 组件使用 - **WHEN** 实现前端 -- **THEN** 它 SHALL 使用自定义 HTML/CSS 组件 -- **THEN** 它 SHALL NOT 使用外部 UI 库,如 Ant Design、Material-UI 或 shadcn/ui +- **THEN** 它 SHALL 使用 Ant Design 5 组件(Table、Form、Modal、Menu、Tag、Popconfirm、DatePicker、Button、Select 等) +- **THEN** 它 SHALL 使用 @ant-design/icons 提供图标 +- **THEN** 图标 SHALL 优先使用图标库中已有的图标 -#### Scenario: SCSS 样式 +#### Scenario: Ant Design 默认主题 + +- **WHEN** 配置 Ant Design 主题 +- **THEN** 前端 SHALL 使用 Ant Design 默认主题,不进行自定义主题色配置 +- **THEN** 前端 SHALL NOT 支持暗色模式切换 + +### Requirement: SCSS 样式 + +前端样式 SHALL 全部使用 SCSS,禁止使用纯 CSS 文件。 + +#### Scenario: 样式文件规范 - **WHEN** 编写样式 -- **THEN** 前端 SHALL 使用 SCSS 进行样式设计 -- **THEN** 样式 SHALL 有组织且可维护 +- **THEN** 前端 SHALL 使用 SCSS(*.scss 文件) +- **THEN** 前端 SHALL NOT 使用纯 CSS 文件(*.css) +- **THEN** 前端 SHALL NOT 使用 CSS-in-JS 方案 + +#### Scenario: SCSS Modules 使用 + +- **WHEN** 编写组件级样式 +- **THEN** 前端 SHALL 使用 SCSS Modules(*.module.scss) +- **THEN** 全局样式仅保留 index.scss 做 reset 和 CSS variables ### Requirement: 提供导航 -前端 SHALL 在不同页面间提供导航。 +前端 SHALL 使用 React Router v7 提供导航。 -#### Scenario: 导航到供应商页面 +#### Scenario: 路由配置 -- **WHEN** 用户点击导航中的"供应商" -- **THEN** 前端 SHALL 导航到供应商管理页面 +- **WHEN** 应用启动 +- **THEN** 前端 SHALL 使用 React Router v7 Library 模式(BrowserRouter) +- **THEN** `/providers` 路径 SHALL 显示供应商管理页面 +- **THEN** `/stats` 路径 SHALL 显示用量统计页面 +- **THEN** `/` 路径 SHALL 重定向到 `/providers` +- **THEN** 不存在的路径 SHALL 显示 404 页面 -#### Scenario: 导航到统计页面 +#### Scenario: 导航菜单 -- **WHEN** 用户点击导航中的"统计" -- **THEN** 前端 SHALL 导航到统计查看页面 +- **WHEN** 用户点击导航中的"供应商管理" +- **THEN** 前端 SHALL 导航到 `/providers` 并高亮当前菜单项 +- **WHEN** 用户点击导航中的"用量统计" +- **THEN** 前端 SHALL 导航到 `/stats` 并高亮当前菜单项 + +#### Scenario: URL 同步 + +- **WHEN** 用户在供应商页面刷新浏览器 +- **THEN** 前端 SHALL 保持在供应商页面(URL 持久化) +- **WHEN** 用户使用浏览器后退按钮 +- **THEN** 前端 SHALL 正确导航到上一个页面 ### Requirement: 使用 React 和 TypeScript -前端 SHALL 使用 React 和 TypeScript 实现。 +前端 SHALL 使用 React 和 TypeScript 实现,遵循 strict 模式。 -#### Scenario: TypeScript 使用 +#### Scenario: TypeScript strict 模式 - **WHEN** 编写前端代码 -- **THEN** 它 SHALL 使用 TypeScript 提供类型安全 -- **THEN** 所有组件和函数 SHALL 具有适当的类型定义 +- **THEN** TypeScript 配置 SHALL 开启 strict: true +- **THEN** TypeScript 配置 SHALL 开启 noUncheckedIndexedAccess +- **THEN** 所有代码 SHALL NOT 使用 any 类型 +- **THEN** tsconfig SHALL 合并为单文件(不使用 project references) -#### Scenario: React 组件 +#### Scenario: React 函数组件 - **WHEN** 实现 UI - **THEN** 它 SHALL 使用 React 函数组件 -- **THEN** 它 SHALL 使用 React hooks 进行状态管理 +- **THEN** 它 SHALL 使用自定义 Hooks 封装业务逻辑 ### Requirement: 使用 Vite 构建 @@ -194,15 +226,46 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面 ### Requirement: 与后端 API 通信 -前端 SHALL 使用 fetch 或类似方法与后端 API 通信。 +前端 SHALL 使用 TanStack Query v5 和统一 API 客户端与后端通信。 #### Scenario: API 基础 URL 配置 - **WHEN** 前端发起 API 请求 -- **THEN** 它 SHALL 使用配置的后端 API 基础 URL(默认:http://localhost:9826) +- **THEN** 开发环境 SHALL 通过 Vite proxy 转发 /api 请求到后端 +- **THEN** 生产环境 SHALL 使用环境变量 VITE_API_BASE 配置基础 URL +- **THEN** 前端 SHALL NOT 硬编码 API 基础 URL -#### Scenario: API 客户端封装 +#### Scenario: 统一 API 客户端 - **WHEN** 进行 API 调用 -- **THEN** 它们 SHALL 封装在专用的 API 客户端模块中 -- **THEN** 错误处理 SHALL 集中化 +- **THEN** 所有调用 SHALL 通过 api/client.ts 的 request() 方法 +- **THEN** 错误处理 SHALL 统一抛出 ApiError(包含 status 和 message) +- **THEN** 开发环境 SHALL 使用 Vite proxy 转发 API 请求 + +#### Scenario: 字段名转换 + +- **WHEN** 接收后端 API 响应 +- **THEN** API 层 SHALL 将 snake_case 字段转换为 camelCase +- **WHEN** 发送请求到后端 API +- **THEN** API 层 SHALL 将 camelCase 字段转换为 snake_case +- **THEN** hooks 和组件 SHALL 仅使用 camelCase 字段 + +#### Scenario: TanStack Query 数据管理 + +- **WHEN** 页面加载数据 +- **THEN** 前端 SHALL 使用 TanStack Query 的 useQuery hook +- **THEN** 前端 SHALL 自动缓存请求结果 +- **THEN** 前端 SHALL 自动处理加载和错误状态 + +#### Scenario: TanStack Query 写操作 + +- **WHEN** 用户执行创建、更新或删除操作 +- **THEN** 前端 SHALL 使用 TanStack Query 的 useMutation hook +- **THEN** 操作成功后 SHALL 自动失效相关查询缓存 +- **THEN** 操作失败 SHALL 使用 Ant Design message.error() 显示错误提示 + +#### Scenario: 错误提示 + +- **WHEN** API 请求失败(网络错误、4xx、5xx) +- **THEN** 前端 SHALL 使用 Ant Design 的 message.error() 显示全局错误提示 +- **THEN** 错误消息 SHALL 具有描述性 diff --git a/openspec/specs/frontend-testing/spec.md b/openspec/specs/frontend-testing/spec.md new file mode 100644 index 0000000..b46b3b8 --- /dev/null +++ b/openspec/specs/frontend-testing/spec.md @@ -0,0 +1,188 @@ +# 前端测试体系 + +## Purpose + +建立前端测试体系,覆盖单元测试、组件测试和 E2E 测试,确保前端代码质量和功能正确性。 + +## Requirements + +### Requirement: 建立前端单元测试体系 + +前端 SHALL 使用 Vitest 建立单元测试体系,覆盖 API 层和自定义 Hooks。 + +#### Scenario: API 客户端单测 + +- **WHEN** 运行 api/client.ts 的单元测试 +- **THEN** SHALL 覆盖 request() 的正常响应解析 +- **THEN** SHALL 覆盖 HTTP 错误状态码(4xx、5xx)的 ApiError 抛出 +- **THEN** SHALL 覆盖网络错误的错误处理 +- **THEN** SHALL 覆盖 snake_case → camelCase 响应字段转换 +- **THEN** SHALL 覆盖 camelCase → snake_case 请求字段转换 + +#### Scenario: API 模块单测 + +- **WHEN** 运行 api/providers.ts、api/models.ts、api/stats.ts 的单元测试 +- **THEN** SHALL 覆盖所有 CRUD 函数的正确调用 +- **THEN** SHALL 覆盖参数传递和返回值类型 + +#### Scenario: 自定义 Hooks 单测 + +- **WHEN** 运行 hooks/ 目录下的单元测试 +- **THEN** SHALL 使用 @tanstack/react-query 的 renderHook 测试工具 +- **THEN** SHALL 覆盖 useQuery 数据获取(成功和失败) +- **THEN** SHALL 覆盖 useMutation 写操作后的缓存失效 +- **THEN** SHALL 使用独立的测试 QueryClient(关闭 retry 和缓存) + +### Requirement: 建立前端组件测试体系 + +前端 SHALL 使用 React Testing Library 建立组件测试体系。 + +#### Scenario: ProviderTable 组件测试 + +- **WHEN** 运行 ProviderTable 组件测试 +- **THEN** SHALL 验证供应商列表正确渲染 +- **THEN** SHALL 验证点击"添加"按钮弹出 Modal +- **THEN** SHALL 验证点击"编辑"按钮弹出预填充的 Modal +- **THEN** SHALL 验证删除操作触发 Popconfirm 确认 + +#### Scenario: ProviderForm 组件测试 + +- **WHEN** 运行 ProviderForm 组件测试 +- **THEN** SHALL 验证表单校验规则(必填字段、URL 格式) +- **THEN** SHALL 验证提交成功后调用 onSave 回调 +- **THEN** SHALL 验证编辑模式下字段预填充 + +#### Scenario: StatsTable 组件测试 + +- **WHEN** 运行 StatsTable 组件测试 +- **THEN** SHALL 验证筛选条件交互(供应商选择、日期范围) +- **THEN** SHALL 验证统计数据正确展示 + +### Requirement: 建立 E2E 测试体系 + +前端 SHALL 使用 Playwright 建立端到端测试体系。 + +#### Scenario: 供应商管理 E2E 测试 + +- **WHEN** 运行供应商管理的 E2E 测试 +- **THEN** SHALL 测试完整的供应商创建流程 +- **THEN** SHALL 测试供应商编辑流程 +- **THEN** SHALL 测试供应商删除流程(含确认弹窗) +- **THEN** SHALL 测试供应商展开后的模型管理 + +#### Scenario: 统计查询 E2E 测试 + +- **WHEN** 运行统计查询的 E2E 测试 +- **THEN** SHALL 测试页面加载和默认数据展示 +- **THEN** SHALL 测试按供应商筛选 +- **THEN** SHALL 测试按日期范围筛选 + +### Requirement: 使用 MSW 进行 API Mock + +前端测试 SHALL 使用 MSW (Mock Service Worker) 模拟后端 API 响应。 + +#### Scenario: 测试环境 MSW 配置 + +- **WHEN** 初始化测试环境 +- **THEN** SHALL 配置 MSW server 处理所有 /api/* 请求 +- **THEN** SHALL 在 setup.ts 中全局启动和清理 MSW server + +#### Scenario: Mock 响应定义 + +- **WHEN** 编写需要 API 交互的测试 +- **THEN** SHALL 使用 MSW handler 定义期望的 API 响应 +- **THEN** SHALL 支持成功和失败两种响应场景 +- **THEN** SHALL 在每个测试用例后重置 handler + +### Requirement: 测试文件组织 + +前端测试文件 SHALL 按层级组织在 src/__tests__/ 目录下。 + +#### Scenario: 目录结构 + +- **WHEN** 组织测试文件 +- **THEN** src/__tests__/setup.ts SHALL 包含全局测试配置 +- **THEN** src/__tests__/api/ SHALL 包含 API 层单测 +- **THEN** src/__tests__/hooks/ SHALL 包含 Hooks 单测 +- **THEN** src/__tests__/components/ SHALL 包含组件测试 +- **THEN** e2e/ 目录(项目根目录下)SHALL 包含 Playwright E2E 测试 + +### Requirement: Vitest 配置 + +前端 SHALL 配置 Vitest 作为测试运行器。 + +#### Scenario: 测试环境配置 + +- **WHEN** 运行 vitest +- **THEN** SHALL 使用 jsdom 作为测试环境 +- **THEN** SHALL 配置 setupFiles 指向 src/__tests__/setup.ts +- **THEN** SHALL 配置路径别名 @/ 与 tsconfig 一致 +- **THEN** SHALL 配置 @testing-library/jest-dom 匹配器 + +#### Scenario: 覆盖率配置 + +- **WHEN** 运行覆盖率报告 +- **THEN** SHALL 使用 @vitest/coverage-v8 提供者 +- **THEN** SHALL 覆盖 src/ 下的所有源文件 + +### Requirement: 建立前端单元测试覆盖 + +前端代码 SHALL 建立单元测试覆盖,纳入整体测试覆盖率统计。 + +#### Scenario: API 层测试覆盖 + +- **WHEN** 运行前端 API 层的单元测试 +- **THEN** SHALL 覆盖 api/client.ts 的请求封装和字段转换逻辑 +- **THEN** SHALL 覆盖 api/providers.ts、api/models.ts、api/stats.ts 的所有函数 +- **THEN** SHALL 使用 MSW mock API 响应 + +#### Scenario: Hooks 测试覆盖 + +- **WHEN** 运行前端 Hooks 的单元测试 +- **THEN** SHALL 覆盖 useProviders、useModels、useStats 的查询和变更逻辑 +- **THEN** SHALL 验证缓存失效和自动刷新行为 + +### Requirement: 建立前端组件测试覆盖 + +前端 SHALL 使用 React Testing Library 建立组件测试覆盖。 + +#### Scenario: 页面组件测试覆盖 + +- **WHEN** 运行前端组件测试 +- **THEN** SHALL 覆盖 ProviderTable 的列表渲染和交互操作 +- **THEN** SHALL 覆盖 ProviderForm 的表单校验和提交 +- **THEN** SHALL 覆盖 ModelForm 的表单校验和提交 +- **THEN** SHALL 覆盖 StatsTable 的筛选和数据展示 + +### Requirement: 建立前端 E2E 测试覆盖 + +前端 SHALL 使用 Playwright 建立 E2E 测试覆盖。 + +#### Scenario: 供应商管理 E2E 覆盖 + +- **WHEN** 运行 E2E 测试 +- **THEN** SHALL 覆盖供应商创建、编辑、删除的完整用户流程 +- **THEN** SHALL 覆盖模型创建、编辑、删除的完整用户流程 + +#### Scenario: 统计查询 E2E 覆盖 + +- **WHEN** 运行 E2E 测试 +- **THEN** SHALL 覆盖统计页面的加载和筛选查询流程 +- **THEN** SHALL 覆盖页面间的导航切换 + +### Requirement: 前端测试集成到构建流程 + +前端测试 SHALL 集成到项目的构建和验证流程中。 + +#### Scenario: 运行前端测试命令 + +- **WHEN** 在 frontend/ 目录执行测试命令 +- **THEN** SHALL 运行所有 Vitest 单元测试和组件测试 +- **THEN** SHALL 显示测试结果 +- **THEN** SHALL 在测试失败时返回非零退出码 + +#### Scenario: 运行前端 E2E 测试命令 + +- **WHEN** 在 frontend/ 目录执行 E2E 测试命令 +- **THEN** SHALL 启动 Playwright 运行 E2E 测试 +- **THEN** SHALL 在测试失败时返回非零退出码 diff --git a/openspec/specs/test-coverage/spec.md b/openspec/specs/test-coverage/spec.md index 09cf130..0614fbe 100644 --- a/openspec/specs/test-coverage/spec.md +++ b/openspec/specs/test-coverage/spec.md @@ -104,3 +104,65 @@ - **THEN** SHALL 运行测试并生成覆盖率报告 - **THEN** SHALL 检查覆盖率是否达标 - **THEN** SHALL 在覆盖率不足时返回非零退出码 + +### Requirement: 建立前端单元测试覆盖 + +前端代码 SHALL 建立单元测试覆盖,纳入整体测试覆盖率统计。 + +#### Scenario: API 层测试覆盖 + +- **WHEN** 运行前端 API 层的单元测试 +- **THEN** SHALL 覆盖 api/client.ts 的请求封装和字段转换逻辑 +- **THEN** SHALL 覆盖 api/providers.ts、api/models.ts、api/stats.ts 的所有函数 +- **THEN** SHALL 使用 MSW mock API 响应 + +#### Scenario: Hooks 测试覆盖 + +- **WHEN** 运行前端 Hooks 的单元测试 +- **THEN** SHALL 覆盖 useProviders、useModels、useStats 的查询和变更逻辑 +- **THEN** SHALL 验证缓存失效和自动刷新行为 + +### Requirement: 建立前端组件测试覆盖 + +前端 SHALL 使用 React Testing Library 建立组件测试覆盖。 + +#### Scenario: 页面组件测试覆盖 + +- **WHEN** 运行前端组件测试 +- **THEN** SHALL 覆盖 ProviderTable 的列表渲染和交互操作 +- **THEN** SHALL 覆盖 ProviderForm 的表单校验和提交 +- **THEN** SHALL 覆盖 ModelForm 的表单校验和提交 +- **THEN** SHALL 覆盖 StatsTable 的筛选和数据展示 + +### Requirement: 建立前端 E2E 测试覆盖 + +前端 SHALL 使用 Playwright 建立 E2E 测试覆盖。 + +#### Scenario: 供应商管理 E2E 覆盖 + +- **WHEN** 运行 E2E 测试 +- **THEN** SHALL 覆盖供应商创建、编辑、删除的完整用户流程 +- **THEN** SHALL 覆盖模型创建、编辑、删除的完整用户流程 + +#### Scenario: 统计查询 E2E 覆盖 + +- **WHEN** 运行 E2E 测试 +- **THEN** SHALL 覆盖统计页面的加载和筛选查询流程 +- **THEN** SHALL 覆盖页面间的导航切换 + +### Requirement: 前端测试集成到构建流程 + +前端测试 SHALL 集成到项目的构建和验证流程中。 + +#### Scenario: 运行前端测试命令 + +- **WHEN** 在 frontend/ 目录执行测试命令 +- **THEN** SHALL 运行所有 Vitest 单元测试和组件测试 +- **THEN** SHALL 显示测试结果 +- **THEN** SHALL 在测试失败时返回非零退出码 + +#### Scenario: 运行前端 E2E 测试命令 + +- **WHEN** 在 frontend/ 目录执行 E2E 测试命令 +- **THEN** SHALL 启动 Playwright 运行 E2E 测试 +- **THEN** SHALL 在测试失败时返回非零退出码