diff --git a/frontend/admin/.env.development b/frontend/admin/.env.development
new file mode 100644
index 0000000..4a55014
--- /dev/null
+++ b/frontend/admin/.env.development
@@ -0,0 +1,2 @@
+# 开发环境配置
+VITE_API_BASE_URL=/api/v1
diff --git a/frontend/admin/.env.example b/frontend/admin/.env.example
new file mode 100644
index 0000000..6ae8a96
--- /dev/null
+++ b/frontend/admin/.env.example
@@ -0,0 +1,5 @@
+# Admin Frontend 环境变量配置示例
+# 复制此文件为 .env.local 进行本地开发配置
+
+# API 基础地址
+VITE_API_BASE_URL=/api/v1
diff --git a/frontend/admin/.env.production b/frontend/admin/.env.production
new file mode 100644
index 0000000..898688e
--- /dev/null
+++ b/frontend/admin/.env.production
@@ -0,0 +1,3 @@
+# 生产环境配置
+# 部署时根据实际后端地址修改
+VITE_API_BASE_URL=/api/v1
diff --git a/frontend/admin/.gitignore b/frontend/admin/.gitignore
new file mode 100644
index 0000000..a547bf3
--- /dev/null
+++ b/frontend/admin/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/frontend/admin/README.md b/frontend/admin/README.md
new file mode 100644
index 0000000..7dbf7eb
--- /dev/null
+++ b/frontend/admin/README.md
@@ -0,0 +1,73 @@
+# React + TypeScript + Vite
+
+This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
+
+Currently, two official plugins are available:
+
+- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
+- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
+
+## React Compiler
+
+The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
+
+## Expanding the ESLint configuration
+
+If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
+
+```js
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+
+ // Remove tseslint.configs.recommended and replace with this
+ tseslint.configs.recommendedTypeChecked,
+ // Alternatively, use this for stricter rules
+ tseslint.configs.strictTypeChecked,
+ // Optionally, add this for stylistic rules
+ tseslint.configs.stylisticTypeChecked,
+
+ // Other configs...
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
+
+You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
+
+```js
+// eslint.config.js
+import reactX from 'eslint-plugin-react-x'
+import reactDom from 'eslint-plugin-react-dom'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ // Other configs...
+ // Enable lint rules for React
+ reactX.configs['recommended-typescript'],
+ // Enable lint rules for React DOM
+ reactDom.configs.recommended,
+ ],
+ languageOptions: {
+ parserOptions: {
+ project: ['./tsconfig.node.json', './tsconfig.app.json'],
+ tsconfigRootDir: import.meta.dirname,
+ },
+ // other options...
+ },
+ },
+])
+```
diff --git a/frontend/admin/eslint.config.js b/frontend/admin/eslint.config.js
new file mode 100644
index 0000000..b50d4e1
--- /dev/null
+++ b/frontend/admin/eslint.config.js
@@ -0,0 +1,23 @@
+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 { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist', 'coverage']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs.flat.recommended,
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ },
+])
diff --git a/frontend/admin/index.html b/frontend/admin/index.html
new file mode 100644
index 0000000..6910800
--- /dev/null
+++ b/frontend/admin/index.html
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+ 用户管理系统
+
+
+
+
+
+
diff --git a/frontend/admin/package-lock.json b/frontend/admin/package-lock.json
new file mode 100644
index 0000000..08639c7
--- /dev/null
+++ b/frontend/admin/package-lock.json
@@ -0,0 +1,5451 @@
+{
+ "name": "admin",
+ "version": "0.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "admin",
+ "version": "0.0.0",
+ "dependencies": {
+ "@ant-design/icons": "^6.1.0",
+ "antd": "^5.29.3",
+ "dayjs": "^1.11.20",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.30.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@playwright/test": "^1.49.1",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.5.2",
+ "@types/node": "^24.12.0",
+ "@types/react": "^18.3.28",
+ "@types/react-dom": "^18.3.7",
+ "@vitejs/plugin-react": "^6.0.0",
+ "@vitest/coverage-v8": "^4.1.2",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "jsdom": "^26.0.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.57.2",
+ "vite": "^8.0.3",
+ "vitest": "^4.1.2"
+ }
+ },
+ "node_modules/@adobe/css-tools": {
+ "version": "4.4.4",
+ "resolved": "https://registry.npmmirror.com/@adobe/css-tools/-/css-tools-4.4.4.tgz",
+ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@ant-design/colors": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-8.0.1.tgz",
+ "integrity": "sha512-foPVl0+SWIslGUtD/xBr1p9U4AKzPhNYEseXYRRo5QSzGACYZrQbe11AYJbYfAWnWSpGBx6JjBmSeugUsD9vqQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@ant-design/fast-color": "^3.0.0"
+ }
+ },
+ "node_modules/@ant-design/cssinjs": {
+ "version": "1.24.0",
+ "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz",
+ "integrity": "sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/@ant-design/cssinjs-utils": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz",
+ "integrity": "sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/@ant-design/fast-color": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-3.0.1.tgz",
+ "integrity": "sha512-esKJegpW4nckh0o6kV3Tkb7NPIZYbPnnFxmQDUmL08ukXZAvV85TZBr70eGuke/CIArLaP6aw8lt9KILjnWuOw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.x"
+ }
+ },
+ "node_modules/@ant-design/icons": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-6.1.0.tgz",
+ "integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@ant-design/colors": "^8.0.0",
+ "@ant-design/icons-svg": "^4.4.0",
+ "@rc-component/util": "^1.3.0",
+ "clsx": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": ">=16.0.0",
+ "react-dom": ">=16.0.0"
+ }
+ },
+ "node_modules/@ant-design/icons-svg": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
+ "integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
+ "license": "MIT"
+ },
+ "node_modules/@ant-design/react-slick": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz",
+ "integrity": "sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/@asamuzakjp/css-color/-/css-color-3.2.0.tgz",
+ "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": {
+ "version": "10.4.3",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz",
+ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/@babel/code-frame/-/code-frame-7.29.0.tgz",
+ "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "js-tokens": "^4.0.0",
+ "picocolors": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/compat-data": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/@babel/compat-data/-/compat-data-7.29.0.tgz",
+ "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/core": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/@babel/core/-/core-7.29.0.tgz",
+ "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.29.0",
+ "@babel/generator": "^7.29.0",
+ "@babel/helper-compilation-targets": "^7.28.6",
+ "@babel/helper-module-transforms": "^7.28.6",
+ "@babel/helpers": "^7.28.6",
+ "@babel/parser": "^7.29.0",
+ "@babel/template": "^7.28.6",
+ "@babel/traverse": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/remapping": "^2.3.5",
+ "convert-source-map": "^2.0.0",
+ "debug": "^4.1.0",
+ "gensync": "^1.0.0-beta.2",
+ "json5": "^2.2.3",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/babel"
+ }
+ },
+ "node_modules/@babel/generator": {
+ "version": "7.29.1",
+ "resolved": "https://registry.npmmirror.com/@babel/generator/-/generator-7.29.1.tgz",
+ "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "@jridgewell/gen-mapping": "^0.3.12",
+ "@jridgewell/trace-mapping": "^0.3.28",
+ "jsesc": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-compilation-targets": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
+ "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/compat-data": "^7.28.6",
+ "@babel/helper-validator-option": "^7.27.1",
+ "browserslist": "^4.24.0",
+ "lru-cache": "^5.1.1",
+ "semver": "^6.3.1"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-globals": {
+ "version": "7.28.0",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
+ "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-imports": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
+ "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/traverse": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-module-transforms": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
+ "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-module-imports": "^7.28.6",
+ "@babel/helper-validator-identifier": "^7.28.5",
+ "@babel/traverse": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ },
+ "peerDependencies": {
+ "@babel/core": "^7.0.0"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
+ "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.28.5",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
+ "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-option": {
+ "version": "7.27.1",
+ "resolved": "https://registry.npmmirror.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
+ "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helpers": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmmirror.com/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/template": "^7.28.6",
+ "@babel/types": "^7.29.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.29.2.tgz",
+ "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.29.0"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/template": {
+ "version": "7.28.6",
+ "resolved": "https://registry.npmmirror.com/@babel/template/-/template-7.28.6.tgz",
+ "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/code-frame": "^7.28.6",
+ "@babel/parser": "^7.28.6",
+ "@babel/types": "^7.28.6"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/traverse": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/@babel/traverse/-/traverse-7.29.0.tgz",
+ "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.29.0",
+ "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.29.0.tgz",
+ "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.27.1",
+ "@babel/helper-validator-identifier": "^7.28.5"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bcoe/v8-coverage": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+ "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/color-helpers": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/color-helpers/-/color-helpers-5.1.0.tgz",
+ "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT-0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@csstools/css-calc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-calc/-/css-calc-2.1.4.tgz",
+ "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-color-parser": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz",
+ "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@csstools/color-helpers": "^5.1.0",
+ "@csstools/css-calc": "^2.1.4"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^3.0.5",
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz",
+ "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^3.0.4"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmmirror.com/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz",
+ "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emotion/hash": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
+ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
+ "license": "MIT"
+ },
+ "node_modules/@emotion/unitless": {
+ "version": "0.7.5",
+ "resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz",
+ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
+ "license": "MIT"
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.9.1",
+ "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
+ "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint-visitor-keys": "^3.4.3"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
+ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.12.2",
+ "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
+ "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/config-array": {
+ "version": "0.21.2",
+ "resolved": "https://registry.npmmirror.com/@eslint/config-array/-/config-array-0.21.2.tgz",
+ "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/object-schema": "^2.1.7",
+ "debug": "^4.3.1",
+ "minimatch": "^3.1.5"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/config-helpers": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmmirror.com/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
+ "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/core": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmmirror.com/@eslint/core/-/core-0.17.0.tgz",
+ "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/json-schema": "^7.0.15"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-3.3.5.tgz",
+ "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ajv": "^6.14.0",
+ "debug": "^4.3.2",
+ "espree": "^10.0.1",
+ "globals": "^14.0.0",
+ "ignore": "^5.2.0",
+ "import-fresh": "^3.2.1",
+ "js-yaml": "^4.1.1",
+ "minimatch": "^3.1.5",
+ "strip-json-comments": "^3.1.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/eslintrc/node_modules/globals": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz",
+ "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-9.39.4.tgz",
+ "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ }
+ },
+ "node_modules/@eslint/object-schema": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmmirror.com/@eslint/object-schema/-/object-schema-2.1.7.tgz",
+ "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@eslint/plugin-kit": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
+ "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@eslint/core": "^0.17.0",
+ "levn": "^0.4.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ }
+ },
+ "node_modules/@humanfs/core": {
+ "version": "0.19.1",
+ "resolved": "https://registry.npmmirror.com/@humanfs/core/-/core-0.19.1.tgz",
+ "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanfs/node": {
+ "version": "0.16.7",
+ "resolved": "https://registry.npmmirror.com/@humanfs/node/-/node-0.16.7.tgz",
+ "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@humanfs/core": "^0.19.1",
+ "@humanwhocodes/retry": "^0.4.0"
+ },
+ "engines": {
+ "node": ">=18.18.0"
+ }
+ },
+ "node_modules/@humanwhocodes/module-importer": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
+ "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.22"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@humanwhocodes/retry": {
+ "version": "0.4.3",
+ "resolved": "https://registry.npmmirror.com/@humanwhocodes/retry/-/retry-0.4.3.tgz",
+ "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18.18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/nzakas"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/remapping": {
+ "version": "2.3.5",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz",
+ "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz",
+ "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1",
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.122.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz",
+ "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@playwright/test": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmmirror.com/@playwright/test/-/test-1.58.2.tgz",
+ "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@rc-component/async-validator": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.1.0.tgz",
+ "integrity": "sha512-n4HcR5siNUXRX23nDizbZBQPO0ZM/5oTtmKZ6/eqL0L2bo747cklFdZGRN2f+c9qWGICwDzrhW0H7tE9PptdcA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.24.4"
+ },
+ "engines": {
+ "node": ">=14.x"
+ }
+ },
+ "node_modules/@rc-component/color-picker": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-2.0.1.tgz",
+ "integrity": "sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/@rc-component/color-picker/node_modules/@ant-design/fast-color": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
+ "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.24.7"
+ },
+ "engines": {
+ "node": ">=8.x"
+ }
+ },
+ "node_modules/@rc-component/context": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/@rc-component/context/-/context-1.4.0.tgz",
+ "integrity": "sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.10.1",
+ "rc-util": "^5.27.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/@rc-component/mini-decimal": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.3.tgz",
+ "integrity": "sha512-bk/FJ09fLf+NLODMAFll6CfYrHPBioTedhW6lxDBuuWucJEqFUd4l/D/5JgIi3dina6sYahB8iuPAZTNz2pMxw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.0"
+ },
+ "engines": {
+ "node": ">=8.x"
+ }
+ },
+ "node_modules/@rc-component/mutate-observer": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz",
+ "integrity": "sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.0",
+ "classnames": "^2.3.2",
+ "rc-util": "^5.24.4"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/@rc-component/portal": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz",
+ "integrity": "sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.0",
+ "classnames": "^2.3.2",
+ "rc-util": "^5.24.4"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/@rc-component/qrcode": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.1.1.tgz",
+ "integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.24.7"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/@rc-component/tour": {
+ "version": "1.15.1",
+ "resolved": "https://registry.npmmirror.com/@rc-component/tour/-/tour-1.15.1.tgz",
+ "integrity": "sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/@rc-component/trigger": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.1.tgz",
+ "integrity": "sha512-ORENF39PeXTzM+gQEshuk460Z8N4+6DkjpxlpE7Q3gYy1iBpLrx0FOJz3h62ryrJZ/3zCAUIkT1Pb/8hHWpb3A==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/@rc-component/util": {
+ "version": "1.10.0",
+ "resolved": "https://registry.npmmirror.com/@rc-component/util/-/util-1.10.0.tgz",
+ "integrity": "sha512-aY9GLBuiUdpyfIUpAWSYer4Tu3mVaZCo5A0q9NtXcazT3MRiI3/WNHCR+DUn5VAtR6iRRf0ynCqQUcHli5UdYw==",
+ "license": "MIT",
+ "dependencies": {
+ "is-mobile": "^5.0.0",
+ "react-is": "^18.2.0"
+ },
+ "peerDependencies": {
+ "react": ">=18.0.0",
+ "react-dom": ">=18.0.0"
+ }
+ },
+ "node_modules/@remix-run/router": {
+ "version": "1.23.2",
+ "resolved": "https://registry.npmmirror.com/@remix-run/router/-/router-1.23.2.tgz",
+ "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz",
+ "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz",
+ "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz",
+ "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz",
+ "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz",
+ "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@napi-rs/wasm-runtime": "^1.1.1"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz",
+ "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.7",
+ "resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.7.tgz",
+ "integrity": "sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@standard-schema/spec": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
+ "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/dom": {
+ "version": "10.4.1",
+ "resolved": "https://registry.npmmirror.com/@testing-library/dom/-/dom-10.4.1.tgz",
+ "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "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"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@testing-library/jest-dom": {
+ "version": "6.9.1",
+ "resolved": "https://registry.npmmirror.com/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
+ "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
+ "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@testing-library/react": {
+ "version": "16.3.2",
+ "resolved": "https://registry.npmmirror.com/@testing-library/react/-/react-16.3.2.tgz",
+ "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.12.5"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "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"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@testing-library/user-event": {
+ "version": "14.6.1",
+ "resolved": "https://registry.npmmirror.com/@testing-library/user-event/-/user-event-14.6.1.tgz",
+ "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12",
+ "npm": ">=6"
+ },
+ "peerDependencies": {
+ "@testing-library/dom": ">=7.21.4"
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/aria-query": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmmirror.com/@types/aria-query/-/aria-query-5.0.4.tgz",
+ "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/@types/chai": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
+ "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/deep-eql": "*",
+ "assertion-error": "^2.0.1"
+ }
+ },
+ "node_modules/@types/deep-eql": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz",
+ "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/json-schema": {
+ "version": "7.0.15",
+ "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz",
+ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "24.12.0",
+ "resolved": "https://registry.npmmirror.com/@types/node/-/node-24.12.0.tgz",
+ "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.16.0"
+ }
+ },
+ "node_modules/@types/prop-types": {
+ "version": "15.7.15",
+ "resolved": "https://registry.npmmirror.com/@types/prop-types/-/prop-types-15.7.15.tgz",
+ "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "18.3.28",
+ "resolved": "https://registry.npmmirror.com/@types/react/-/react-18.3.28.tgz",
+ "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/prop-types": "*",
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-dom": {
+ "version": "18.3.7",
+ "resolved": "https://registry.npmmirror.com/@types/react-dom/-/react-dom-18.3.7.tgz",
+ "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "^18.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz",
+ "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/regexpp": "^4.12.2",
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/type-utils": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
+ "ignore": "^7.0.5",
+ "natural-compare": "^1.4.0",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "@typescript-eslint/parser": "^8.57.2",
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
+ "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/@typescript-eslint/parser": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz",
+ "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/project-service": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz",
+ "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/tsconfig-utils": "^8.57.2",
+ "@typescript-eslint/types": "^8.57.2",
+ "debug": "^4.4.3"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/scope-manager": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz",
+ "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/tsconfig-utils": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz",
+ "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/type-utils": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz",
+ "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2",
+ "debug": "^4.4.3",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/types": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz",
+ "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz",
+ "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/project-service": "8.57.2",
+ "@typescript-eslint/tsconfig-utils": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/visitor-keys": "8.57.2",
+ "debug": "^4.4.3",
+ "minimatch": "^10.2.2",
+ "semver": "^7.7.3",
+ "tinyglobby": "^0.2.15",
+ "ts-api-utils": "^2.4.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
+ "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
+ "version": "10.2.4",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz",
+ "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==",
+ "dev": true,
+ "license": "BlueOak-1.0.0",
+ "dependencies": {
+ "brace-expansion": "^5.0.2"
+ },
+ "engines": {
+ "node": "18 || 20 || >=22"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@typescript-eslint/utils": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz",
+ "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.9.1",
+ "@typescript-eslint/scope-manager": "8.57.2",
+ "@typescript-eslint/types": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz",
+ "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/types": "8.57.2",
+ "eslint-visitor-keys": "^5.0.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ }
+ },
+ "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz",
+ "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^20.19.0 || ^22.13.0 || >=24"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz",
+ "integrity": "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "1.0.0-rc.7"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/coverage-v8": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz",
+ "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bcoe/v8-coverage": "^1.0.2",
+ "@vitest/utils": "4.1.2",
+ "ast-v8-to-istanbul": "^1.0.0",
+ "istanbul-lib-coverage": "^3.2.2",
+ "istanbul-lib-report": "^3.0.1",
+ "istanbul-reports": "^3.2.0",
+ "magicast": "^0.5.2",
+ "obug": "^2.1.1",
+ "std-env": "^4.0.0-rc.1",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@vitest/browser": "4.1.2",
+ "vitest": "4.1.2"
+ },
+ "peerDependenciesMeta": {
+ "@vitest/browser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/expect": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz",
+ "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@standard-schema/spec": "^1.1.0",
+ "@types/chai": "^5.2.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "chai": "^6.2.2",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/mocker": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz",
+ "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/spy": "4.1.2",
+ "estree-walker": "^3.0.3",
+ "magic-string": "^0.30.21"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "msw": "^2.4.9",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "msw": {
+ "optional": true
+ },
+ "vite": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vitest/pretty-format": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz",
+ "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/runner": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz",
+ "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/utils": "4.1.2",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/snapshot": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz",
+ "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "magic-string": "^0.30.21",
+ "pathe": "^2.0.3"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/spy": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz",
+ "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/@vitest/utils": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz",
+ "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/pretty-format": "4.1.2",
+ "convert-source-map": "^2.0.0",
+ "tinyrainbow": "^3.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/acorn-jsx": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
+ "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/agent-base": {
+ "version": "7.1.4",
+ "resolved": "https://registry.npmmirror.com/agent-base/-/agent-base-7.1.4.tgz",
+ "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/ajv": {
+ "version": "6.14.0",
+ "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz",
+ "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/antd": {
+ "version": "5.29.3",
+ "resolved": "https://registry.npmmirror.com/antd/-/antd-5.29.3.tgz",
+ "integrity": "sha512-3DdbGCa9tWAJGcCJ6rzR8EJFsv2CtyEbkVabZE14pfgUHfCicWCj0/QzQVLDYg8CPfQk9BH7fHCoTXHTy7MP/A==",
+ "license": "MIT",
+ "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"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/ant-design"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/antd/node_modules/@ant-design/colors": {
+ "version": "7.2.1",
+ "resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz",
+ "integrity": "sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@ant-design/fast-color": "^2.0.6"
+ }
+ },
+ "node_modules/antd/node_modules/@ant-design/fast-color": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz",
+ "integrity": "sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.24.7"
+ },
+ "engines": {
+ "node": ">=8.x"
+ }
+ },
+ "node_modules/antd/node_modules/@ant-design/icons": {
+ "version": "5.6.1",
+ "resolved": "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz",
+ "integrity": "sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "peerDependencies": {
+ "react": ">=16.0.0",
+ "react-dom": ">=16.0.0"
+ }
+ },
+ "node_modules/argparse": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz",
+ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
+ "dev": true,
+ "license": "Python-2.0"
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/assertion-error": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
+ "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz",
+ "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/trace-mapping": "^0.3.31",
+ "estree-walker": "^3.0.3",
+ "js-tokens": "^10.0.0"
+ }
+ },
+ "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz",
+ "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/balanced-match": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.8",
+ "resolved": "https://registry.npmmirror.com/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz",
+ "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/brace-expansion": {
+ "version": "1.1.13",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
+ "integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "balanced-match": "^1.0.0",
+ "concat-map": "0.0.1"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.1",
+ "resolved": "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.1.tgz",
+ "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.9.0",
+ "caniuse-lite": "^1.0.30001759",
+ "electron-to-chromium": "^1.5.263",
+ "node-releases": "^2.0.27",
+ "update-browserslist-db": "^1.2.0"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/callsites": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz",
+ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001780",
+ "resolved": "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz",
+ "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chai": {
+ "version": "6.2.2",
+ "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz",
+ "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/classnames": {
+ "version": "2.5.1",
+ "resolved": "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz",
+ "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==",
+ "license": "MIT"
+ },
+ "node_modules/clsx": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/clsx/-/clsx-2.1.1.tgz",
+ "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/compute-scroll-into-view": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
+ "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
+ "license": "MIT"
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/convert-source-map": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz",
+ "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/copy-to-clipboard": {
+ "version": "3.3.3",
+ "resolved": "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz",
+ "integrity": "sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==",
+ "license": "MIT",
+ "dependencies": {
+ "toggle-selection": "^1.0.6"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css.escape": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz",
+ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cssstyle": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz",
+ "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@asamuzakjp/css-color": "^3.2.0",
+ "rrweb-cssom": "^0.8.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT"
+ },
+ "node_modules/data-urls": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/data-urls/-/data-urls-5.0.0.tgz",
+ "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-mimetype": "^4.0.0",
+ "whatwg-url": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.20",
+ "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.20.tgz",
+ "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==",
+ "license": "MIT"
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/deep-is": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz",
+ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/dom-accessibility-api": {
+ "version": "0.5.16",
+ "resolved": "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
+ "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.321",
+ "resolved": "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.321.tgz",
+ "integrity": "sha512-L2C7Q279W2D/J4PLZLk7sebOILDSWos7bMsMNN06rK482umHUrh/3lM8G7IlHFOYip2oAg5nha1rCMxr/rs6ZQ==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/es-module-lexer": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz",
+ "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/escape-string-regexp": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
+ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/eslint": {
+ "version": "9.39.4",
+ "resolved": "https://registry.npmmirror.com/eslint/-/eslint-9.39.4.tgz",
+ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmmirror.com/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
+ "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react-refresh": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmmirror.com/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz",
+ "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "eslint": "^9 || ^10"
+ }
+ },
+ "node_modules/eslint-scope": {
+ "version": "8.4.0",
+ "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-8.4.0.tgz",
+ "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-visitor-keys": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
+ "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/espree": {
+ "version": "10.4.0",
+ "resolved": "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz",
+ "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "acorn": "^8.15.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^4.2.1"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.7.0",
+ "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz",
+ "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "estraverse": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/esrecurse": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz",
+ "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estraverse": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz",
+ "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz",
+ "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0"
+ }
+ },
+ "node_modules/esutils": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz",
+ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/expect-type": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz",
+ "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
+ "node_modules/fast-deep-equal": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
+ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-json-stable-stringify": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
+ "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fast-levenshtein": {
+ "version": "2.0.6",
+ "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
+ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
+ "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flat-cache": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ }
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/flat-cache": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-4.0.1.tgz",
+ "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.4"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.4.2",
+ "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.4.2.tgz",
+ "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/gensync": {
+ "version": "1.0.0-beta.2",
+ "resolved": "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz",
+ "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/glob-parent": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz",
+ "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "is-glob": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=10.13.0"
+ }
+ },
+ "node_modules/globals": {
+ "version": "17.4.0",
+ "resolved": "https://registry.npmmirror.com/globals/-/globals-17.4.0.tgz",
+ "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/hermes-estree": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmmirror.com/hermes-estree/-/hermes-estree-0.25.1.tgz",
+ "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/hermes-parser": {
+ "version": "0.25.1",
+ "resolved": "https://registry.npmmirror.com/hermes-parser/-/hermes-parser-0.25.1.tgz",
+ "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "hermes-estree": "0.25.1"
+ }
+ },
+ "node_modules/html-encoding-sniffer": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
+ "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-encoding": "^3.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/html-escaper": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/html-escaper/-/html-escaper-2.0.2.tgz",
+ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/http-proxy-agent": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmmirror.com/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz",
+ "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.0",
+ "debug": "^4.3.4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/https-proxy-agent": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz",
+ "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "agent-base": "^7.1.2",
+ "debug": "4"
+ },
+ "engines": {
+ "node": ">= 14"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.6.3",
+ "resolved": "https://registry.npmmirror.com/iconv-lite/-/iconv-lite-0.6.3.tgz",
+ "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ignore": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz",
+ "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/import-fresh": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz",
+ "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "parent-module": "^1.0.0",
+ "resolve-from": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/imurmurhash": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz",
+ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.19"
+ }
+ },
+ "node_modules/indent-string": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz",
+ "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-mobile": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/is-mobile/-/is-mobile-5.0.0.tgz",
+ "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-potential-custom-element-name": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz",
+ "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/istanbul-lib-coverage": {
+ "version": "3.2.2",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+ "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/istanbul-lib-report": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+ "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "istanbul-lib-coverage": "^3.0.0",
+ "make-dir": "^4.0.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/istanbul-reports": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmmirror.com/istanbul-reports/-/istanbul-reports-3.2.0.tgz",
+ "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "html-escaper": "^2.0.0",
+ "istanbul-lib-report": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/js-tokens": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz",
+ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
+ "license": "MIT"
+ },
+ "node_modules/js-yaml": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz",
+ "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "node_modules/jsdom": {
+ "version": "26.1.0",
+ "resolved": "https://registry.npmmirror.com/jsdom/-/jsdom-26.1.0.tgz",
+ "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "canvas": "^3.0.0"
+ },
+ "peerDependenciesMeta": {
+ "canvas": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/jsesc": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
+ "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "jsesc": "bin/jsesc"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/json-buffer": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz",
+ "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-schema-traverse": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json-stable-stringify-without-jsonify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
+ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/json2mq": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz",
+ "integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
+ "license": "MIT",
+ "dependencies": {
+ "string-convert": "^0.2.0"
+ }
+ },
+ "node_modules/json5": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz",
+ "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "json5": "lib/cli.js"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/keyv": {
+ "version": "4.5.4",
+ "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz",
+ "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "json-buffer": "3.0.1"
+ }
+ },
+ "node_modules/levn": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz",
+ "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1",
+ "type-check": "~0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmmirror.com/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash.merge": {
+ "version": "4.6.2",
+ "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz",
+ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/lru-cache/-/lru-cache-5.1.1.tgz",
+ "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "yallist": "^3.0.2"
+ }
+ },
+ "node_modules/lz-string": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/lz-string/-/lz-string-1.5.0.tgz",
+ "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "bin": {
+ "lz-string": "bin/bin.js"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/magicast": {
+ "version": "0.5.2",
+ "resolved": "https://registry.npmmirror.com/magicast/-/magicast-0.5.2.tgz",
+ "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.29.0",
+ "@babel/types": "^7.29.0",
+ "source-map-js": "^1.2.1"
+ }
+ },
+ "node_modules/make-dir": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/make-dir/-/make-dir-4.0.0.tgz",
+ "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/make-dir/node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/min-indent": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz",
+ "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/minimatch": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz",
+ "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "brace-expansion": "^1.1.7"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/natural-compare": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz",
+ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.36",
+ "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.36.tgz",
+ "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/nwsapi": {
+ "version": "2.2.23",
+ "resolved": "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz",
+ "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/obug": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
+ "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==",
+ "dev": true,
+ "funding": [
+ "https://github.com/sponsors/sxzz",
+ "https://opencollective.com/debug"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/optionator": {
+ "version": "0.9.4",
+ "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz",
+ "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
+ "dev": true,
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/parent-module": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz",
+ "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "callsites": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmmirror.com/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/pathe": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
+ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmmirror.com/playwright/-/playwright-1.58.2.tgz",
+ "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.58.2"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.58.2",
+ "resolved": "https://registry.npmmirror.com/playwright-core/-/playwright-core-1.58.2.tgz",
+ "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/playwright/node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.8",
+ "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.8.tgz",
+ "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/prelude-ls": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz",
+ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/pretty-format": {
+ "version": "27.5.1",
+ "resolved": "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz",
+ "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1",
+ "ansi-styles": "^5.0.0",
+ "react-is": "^17.0.1"
+ },
+ "engines": {
+ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0"
+ }
+ },
+ "node_modules/pretty-format/node_modules/ansi-styles": {
+ "version": "5.2.0",
+ "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz",
+ "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/pretty-format/node_modules/react-is": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-17.0.2.tgz",
+ "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
+ "dev": true,
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/punycode": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz",
+ "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/rc-cascader": {
+ "version": "3.34.0",
+ "resolved": "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz",
+ "integrity": "sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-checkbox": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-3.5.0.tgz",
+ "integrity": "sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-collapse": {
+ "version": "3.9.0",
+ "resolved": "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.9.0.tgz",
+ "integrity": "sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-dialog": {
+ "version": "9.6.0",
+ "resolved": "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz",
+ "integrity": "sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-drawer": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-7.3.0.tgz",
+ "integrity": "sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-dropdown": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmmirror.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz",
+ "integrity": "sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-field-form": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.1.tgz",
+ "integrity": "sha512-vKeSifSJ6HoLaAB+B8aq/Qgm8a3dyxROzCtKNCsBQgiverpc4kWDQihoUwzUj+zNWJOykwSY4dNX3QrGwtVb9A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.0",
+ "@rc-component/async-validator": "^5.0.3",
+ "rc-util": "^5.32.2"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-image": {
+ "version": "7.12.0",
+ "resolved": "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz",
+ "integrity": "sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-input": {
+ "version": "1.8.0",
+ "resolved": "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz",
+ "integrity": "sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-input-number": {
+ "version": "9.5.0",
+ "resolved": "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz",
+ "integrity": "sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-mentions": {
+ "version": "2.20.0",
+ "resolved": "https://registry.npmmirror.com/rc-mentions/-/rc-mentions-2.20.0.tgz",
+ "integrity": "sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-menu": {
+ "version": "9.16.1",
+ "resolved": "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz",
+ "integrity": "sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-motion": {
+ "version": "2.9.5",
+ "resolved": "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz",
+ "integrity": "sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-notification": {
+ "version": "5.6.4",
+ "resolved": "https://registry.npmmirror.com/rc-notification/-/rc-notification-5.6.4.tgz",
+ "integrity": "sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "2.x",
+ "rc-motion": "^2.9.0",
+ "rc-util": "^5.20.1"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-overflow": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.5.0.tgz",
+ "integrity": "sha512-Lm/v9h0LymeUYJf0x39OveU52InkdRXqnn2aYXfWmo8WdOonIKB2kfau+GF0fWq6jPgtdO9yMqveGcK6aIhJmg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-pagination": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-5.1.0.tgz",
+ "integrity": "sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-picker": {
+ "version": "4.11.3",
+ "resolved": "https://registry.npmmirror.com/rc-picker/-/rc-picker-4.11.3.tgz",
+ "integrity": "sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "date-fns": ">= 2.x",
+ "dayjs": ">= 1.x",
+ "luxon": ">= 3.x",
+ "moment": ">= 2.x",
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ },
+ "peerDependenciesMeta": {
+ "date-fns": {
+ "optional": true
+ },
+ "dayjs": {
+ "optional": true
+ },
+ "luxon": {
+ "optional": true
+ },
+ "moment": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/rc-progress": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/rc-progress/-/rc-progress-4.0.0.tgz",
+ "integrity": "sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-rate": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.13.1.tgz",
+ "integrity": "sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-util": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-resize-observer": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz",
+ "integrity": "sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-segmented": {
+ "version": "2.7.1",
+ "resolved": "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.7.1.tgz",
+ "integrity": "sha512-izj1Nw/Dw2Vb7EVr+D/E9lUTkBe+kKC+SAFSU9zqr7WV2W5Ktaa9Gc7cB2jTqgk8GROJayltaec+DBlYKc6d+g==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-select": {
+ "version": "14.16.8",
+ "resolved": "https://registry.npmmirror.com/rc-select/-/rc-select-14.16.8.tgz",
+ "integrity": "sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-dom": "*"
+ }
+ },
+ "node_modules/rc-slider": {
+ "version": "11.1.9",
+ "resolved": "https://registry.npmmirror.com/rc-slider/-/rc-slider-11.1.9.tgz",
+ "integrity": "sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.10.1",
+ "classnames": "^2.2.5",
+ "rc-util": "^5.36.0"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-steps": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmmirror.com/rc-steps/-/rc-steps-6.0.1.tgz",
+ "integrity": "sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.16.7",
+ "classnames": "^2.2.3",
+ "rc-util": "^5.16.1"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-switch": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmmirror.com/rc-switch/-/rc-switch-4.1.0.tgz",
+ "integrity": "sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-table": {
+ "version": "7.54.0",
+ "resolved": "https://registry.npmmirror.com/rc-table/-/rc-table-7.54.0.tgz",
+ "integrity": "sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-tabs": {
+ "version": "15.7.0",
+ "resolved": "https://registry.npmmirror.com/rc-tabs/-/rc-tabs-15.7.0.tgz",
+ "integrity": "sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-textarea": {
+ "version": "1.10.2",
+ "resolved": "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.2.tgz",
+ "integrity": "sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-tooltip": {
+ "version": "6.4.0",
+ "resolved": "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-6.4.0.tgz",
+ "integrity": "sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-tree": {
+ "version": "5.13.1",
+ "resolved": "https://registry.npmmirror.com/rc-tree/-/rc-tree-5.13.1.tgz",
+ "integrity": "sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==",
+ "license": "MIT",
+ "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"
+ },
+ "engines": {
+ "node": ">=10.x"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-dom": "*"
+ }
+ },
+ "node_modules/rc-tree-select": {
+ "version": "5.27.0",
+ "resolved": "https://registry.npmmirror.com/rc-tree-select/-/rc-tree-select-5.27.0.tgz",
+ "integrity": "sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==",
+ "license": "MIT",
+ "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": "*"
+ }
+ },
+ "node_modules/rc-upload": {
+ "version": "4.11.0",
+ "resolved": "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.11.0.tgz",
+ "integrity": "sha512-ZUyT//2JAehfHzjWowqROcwYJKnZkIUGWaTE/VogVrepSl7AFNbQf4+zGfX4zl9Vrj/Jm8scLO0R6UlPDKK4wA==",
+ "license": "MIT",
+ "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"
+ }
+ },
+ "node_modules/rc-util": {
+ "version": "5.44.4",
+ "resolved": "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz",
+ "integrity": "sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.18.3",
+ "react-is": "^18.2.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/rc-virtual-list": {
+ "version": "3.19.2",
+ "resolved": "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz",
+ "integrity": "sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.20.0",
+ "classnames": "^2.2.6",
+ "rc-resize-observer": "^1.0.0",
+ "rc-util": "^5.36.0"
+ },
+ "engines": {
+ "node": ">=8.x"
+ },
+ "peerDependencies": {
+ "react": ">=16.9.0",
+ "react-dom": ">=16.9.0"
+ }
+ },
+ "node_modules/react": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmmirror.com/react/-/react-18.3.1.tgz",
+ "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz",
+ "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.2"
+ },
+ "peerDependencies": {
+ "react": "^18.3.1"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "18.3.1",
+ "resolved": "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz",
+ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
+ "license": "MIT"
+ },
+ "node_modules/react-router": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmmirror.com/react-router/-/react-router-6.30.3.tgz",
+ "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8"
+ }
+ },
+ "node_modules/react-router-dom": {
+ "version": "6.30.3",
+ "resolved": "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-6.30.3.tgz",
+ "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
+ "license": "MIT",
+ "dependencies": {
+ "@remix-run/router": "1.23.2",
+ "react-router": "6.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "react": ">=16.8",
+ "react-dom": ">=16.8"
+ }
+ },
+ "node_modules/redent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/redent/-/redent-3.0.0.tgz",
+ "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "indent-string": "^4.0.0",
+ "strip-indent": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/resize-observer-polyfill": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
+ "integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
+ "license": "MIT"
+ },
+ "node_modules/resolve-from": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz",
+ "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz",
+ "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.122.0",
+ "@rolldown/pluginutils": "1.0.0-rc.12"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.12",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12"
+ }
+ },
+ "node_modules/rolldown/node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.12",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz",
+ "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/rrweb-cssom": {
+ "version": "0.8.0",
+ "resolved": "https://registry.npmmirror.com/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz",
+ "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/saxes": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmmirror.com/saxes/-/saxes-6.0.0.tgz",
+ "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "xmlchars": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=v12.22.7"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.23.2",
+ "resolved": "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz",
+ "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "node_modules/scroll-into-view-if-needed": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
+ "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
+ "license": "MIT",
+ "dependencies": {
+ "compute-scroll-into-view": "^3.0.2"
+ }
+ },
+ "node_modules/semver": {
+ "version": "6.3.1",
+ "resolved": "https://registry.npmmirror.com/semver/-/semver-6.3.1.tgz",
+ "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
+ "dev": true,
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ }
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/siginfo": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz",
+ "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stackback": {
+ "version": "0.0.2",
+ "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
+ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/std-env": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz",
+ "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/string-convert": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz",
+ "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
+ "license": "MIT"
+ },
+ "node_modules/strip-indent": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmmirror.com/strip-indent/-/strip-indent-3.0.0.tgz",
+ "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "min-indent": "^1.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-json-comments": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
+ "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/stylis": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz",
+ "integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
+ "license": "MIT"
+ },
+ "node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/symbol-tree": {
+ "version": "3.2.4",
+ "resolved": "https://registry.npmmirror.com/symbol-tree/-/symbol-tree-3.2.4.tgz",
+ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/throttle-debounce": {
+ "version": "5.0.2",
+ "resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
+ "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.22"
+ }
+ },
+ "node_modules/tinybench": {
+ "version": "2.9.0",
+ "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+ "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tinyexec": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz",
+ "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.15",
+ "resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
+ "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/tinyrainbow": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz",
+ "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tldts": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmmirror.com/tldts/-/tldts-6.1.86.tgz",
+ "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^6.1.86"
+ },
+ "bin": {
+ "tldts": "bin/cli.js"
+ }
+ },
+ "node_modules/tldts-core": {
+ "version": "6.1.86",
+ "resolved": "https://registry.npmmirror.com/tldts-core/-/tldts-core-6.1.86.tgz",
+ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/toggle-selection": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz",
+ "integrity": "sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==",
+ "license": "MIT"
+ },
+ "node_modules/tough-cookie": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-5.1.2.tgz",
+ "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "tldts": "^6.1.32"
+ },
+ "engines": {
+ "node": ">=16"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "5.1.1",
+ "resolved": "https://registry.npmmirror.com/tr46/-/tr46-5.1.1.tgz",
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/ts-api-utils": {
+ "version": "2.5.0",
+ "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz",
+ "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.12"
+ },
+ "peerDependencies": {
+ "typescript": ">=4.8.4"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD",
+ "optional": true
+ },
+ "node_modules/type-check": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz",
+ "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "prelude-ls": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8.0"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/typescript-eslint": {
+ "version": "8.57.2",
+ "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz",
+ "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@typescript-eslint/eslint-plugin": "8.57.2",
+ "@typescript-eslint/parser": "8.57.2",
+ "@typescript-eslint/typescript-estree": "8.57.2",
+ "@typescript-eslint/utils": "8.57.2"
+ },
+ "engines": {
+ "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/typescript-eslint"
+ },
+ "peerDependencies": {
+ "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0",
+ "typescript": ">=4.8.4 <6.0.0"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.16.0",
+ "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz",
+ "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmmirror.com/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/uri-js": {
+ "version": "4.4.1",
+ "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz",
+ "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "punycode": "^2.1.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.0.3",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz",
+ "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.12",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.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"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vitest": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz",
+ "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vitest/expect": "4.1.2",
+ "@vitest/mocker": "4.1.2",
+ "@vitest/pretty-format": "4.1.2",
+ "@vitest/runner": "4.1.2",
+ "@vitest/snapshot": "4.1.2",
+ "@vitest/spy": "4.1.2",
+ "@vitest/utils": "4.1.2",
+ "es-module-lexer": "^2.0.0",
+ "expect-type": "^1.3.0",
+ "magic-string": "^0.30.21",
+ "obug": "^2.1.1",
+ "pathe": "^2.0.3",
+ "picomatch": "^4.0.3",
+ "std-env": "^4.0.0-rc.1",
+ "tinybench": "^2.9.0",
+ "tinyexec": "^1.0.2",
+ "tinyglobby": "^0.2.15",
+ "tinyrainbow": "^3.1.0",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0",
+ "why-is-node-running": "^2.3.0"
+ },
+ "bin": {
+ "vitest": "vitest.mjs"
+ },
+ "engines": {
+ "node": "^20.0.0 || ^22.0.0 || >=24.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/vitest"
+ },
+ "peerDependencies": {
+ "@edge-runtime/vm": "*",
+ "@opentelemetry/api": "^1.9.0",
+ "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0",
+ "@vitest/browser-playwright": "4.1.2",
+ "@vitest/browser-preview": "4.1.2",
+ "@vitest/browser-webdriverio": "4.1.2",
+ "@vitest/ui": "4.1.2",
+ "happy-dom": "*",
+ "jsdom": "*",
+ "vite": "^6.0.0 || ^7.0.0 || ^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@edge-runtime/vm": {
+ "optional": true
+ },
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@types/node": {
+ "optional": true
+ },
+ "@vitest/browser-playwright": {
+ "optional": true
+ },
+ "@vitest/browser-preview": {
+ "optional": true
+ },
+ "@vitest/browser-webdriverio": {
+ "optional": true
+ },
+ "@vitest/ui": {
+ "optional": true
+ },
+ "happy-dom": {
+ "optional": true
+ },
+ "jsdom": {
+ "optional": true
+ },
+ "vite": {
+ "optional": false
+ }
+ }
+ },
+ "node_modules/w3c-xmlserializer": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz",
+ "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "xml-name-validator": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/webidl-conversions": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/whatwg-encoding": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
+ "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "iconv-lite": "0.6.3"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-mimetype": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz",
+ "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/whatwg-url": {
+ "version": "14.2.0",
+ "resolved": "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz",
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "^5.1.0",
+ "webidl-conversions": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/why-is-node-running": {
+ "version": "2.3.0",
+ "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
+ "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "siginfo": "^2.0.0",
+ "stackback": "0.0.2"
+ },
+ "bin": {
+ "why-is-node-running": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/word-wrap": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz",
+ "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "8.19.0",
+ "resolved": "https://registry.npmmirror.com/ws/-/ws-8.19.0.tgz",
+ "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": ">=5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xml-name-validator": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-5.0.0.tgz",
+ "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/xmlchars": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmmirror.com/xmlchars/-/xmlchars-2.2.0.tgz",
+ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/yallist": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz",
+ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/zod": {
+ "version": "4.3.6",
+ "resolved": "https://registry.npmmirror.com/zod/-/zod-4.3.6.tgz",
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/zod-validation-error": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmmirror.com/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
+ "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "peerDependencies": {
+ "zod": "^3.25.0 || ^4.0.0"
+ }
+ }
+ }
+}
diff --git a/frontend/admin/package.json b/frontend/admin/package.json
new file mode 100644
index 0000000..a812c77
--- /dev/null
+++ b/frontend/admin/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "admin",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --configLoader native",
+ "build": "tsc -b && vite build --configLoader native",
+ "lint": "eslint .",
+ "preview": "vite preview",
+ "test": "node ./scripts/run-vitest.mjs",
+ "test:ui": "node ./scripts/run-vitest.mjs --ui",
+ "test:coverage": "node ./scripts/run-vitest.mjs --run --coverage",
+ "test:run": "node ./scripts/run-vitest.mjs --run",
+ "e2e": "node ./scripts/run-playwright-cdp-e2e.mjs",
+ "e2e:full": "node ./scripts/run-playwright-cdp-e2e.mjs",
+ "e2e:full:win": "powershell -ExecutionPolicy Bypass -File ./scripts/run-playwright-auth-e2e.ps1",
+ "e2e:smoke": "node ./scripts/run-cdp-smoke.mjs",
+ "e2e:smoke:win": "powershell -ExecutionPolicy Bypass -File ./scripts/run-cdp-smoke-bootstrap.ps1",
+ "e2e:auth-smoke:win": "powershell -ExecutionPolicy Bypass -File ./scripts/run-cdp-auth-smoke.ps1",
+ "e2e:report": "node -e \"console.error('Playwright runner report is not supported in this environment; use docs/evidence instead.'); process.exit(1)\""
+ },
+ "dependencies": {
+ "@ant-design/icons": "^6.1.0",
+ "antd": "^5.29.3",
+ "dayjs": "^1.11.20",
+ "react": "^18.3.1",
+ "react-dom": "^18.3.1",
+ "react-router-dom": "^6.30.3"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.39.4",
+ "@playwright/test": "^1.49.1",
+ "@testing-library/jest-dom": "^6.6.3",
+ "@testing-library/react": "^16.1.0",
+ "@testing-library/user-event": "^14.5.2",
+ "@types/node": "^24.12.0",
+ "@types/react": "^18.3.28",
+ "@types/react-dom": "^18.3.7",
+ "@vitejs/plugin-react": "^6.0.0",
+ "@vitest/coverage-v8": "^4.1.2",
+ "eslint": "^9.39.4",
+ "eslint-plugin-react-hooks": "^7.0.1",
+ "eslint-plugin-react-refresh": "^0.5.2",
+ "globals": "^17.4.0",
+ "jsdom": "^26.0.0",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.57.2",
+ "vite": "^8.0.3",
+ "vitest": "^4.1.2"
+ },
+ "overrides": {
+ "picomatch": "4.0.4",
+ "minimatch@3": {
+ "brace-expansion": "1.1.13"
+ },
+ "minimatch@10": {
+ "brace-expansion": "5.0.5"
+ }
+ }
+}
diff --git a/frontend/admin/public/favicon.svg b/frontend/admin/public/favicon.svg
new file mode 100644
index 0000000..6893eb1
--- /dev/null
+++ b/frontend/admin/public/favicon.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/frontend/admin/public/icons.svg b/frontend/admin/public/icons.svg
new file mode 100644
index 0000000..e952219
--- /dev/null
+++ b/frontend/admin/public/icons.svg
@@ -0,0 +1,24 @@
+
diff --git a/frontend/admin/scripts/mock-smtp-capture.mjs b/frontend/admin/scripts/mock-smtp-capture.mjs
new file mode 100644
index 0000000..3b1d318
--- /dev/null
+++ b/frontend/admin/scripts/mock-smtp-capture.mjs
@@ -0,0 +1,185 @@
+import process from 'node:process'
+import path from 'node:path'
+import net from 'node:net'
+import { appendFile, mkdir, writeFile } from 'node:fs/promises'
+
+function parseArgs(argv) {
+ const args = new Map()
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const value = argv[index]
+ if (!value.startsWith('--')) {
+ continue
+ }
+
+ const key = value.slice(2)
+ const nextValue = argv[index + 1]
+ if (nextValue && !nextValue.startsWith('--')) {
+ args.set(key, nextValue)
+ index += 1
+ continue
+ }
+
+ args.set(key, 'true')
+ }
+
+ return args
+}
+
+const args = parseArgs(process.argv.slice(2))
+const port = Number(args.get('port') ?? process.env.SMTP_CAPTURE_PORT ?? 2525)
+const outputPath = path.resolve(args.get('output') ?? process.env.SMTP_CAPTURE_OUTPUT ?? './smtp-capture.jsonl')
+
+if (!Number.isInteger(port) || port <= 0) {
+ throw new Error(`Invalid SMTP capture port: ${port}`)
+}
+
+await mkdir(path.dirname(outputPath), { recursive: true })
+await writeFile(outputPath, '', 'utf8')
+
+let writeQueue = Promise.resolve()
+
+function queueMessageWrite(message) {
+ writeQueue = writeQueue.then(() => appendFile(outputPath, `${JSON.stringify(message)}\n`, 'utf8'))
+ return writeQueue
+}
+
+function createSessionState() {
+ return {
+ buffer: '',
+ dataMode: false,
+ mailFrom: '',
+ rcptTo: [],
+ data: '',
+ }
+}
+
+const server = net.createServer((socket) => {
+ socket.setEncoding('utf8')
+ let session = createSessionState()
+
+ const reply = (line) => {
+ socket.write(`${line}\r\n`)
+ }
+
+ const resetMessageState = () => {
+ session.dataMode = false
+ session.mailFrom = ''
+ session.rcptTo = []
+ session.data = ''
+ }
+
+ const flushBuffer = async () => {
+ while (true) {
+ if (session.dataMode) {
+ const messageTerminatorIndex = session.buffer.indexOf('\r\n.\r\n')
+ if (messageTerminatorIndex === -1) {
+ session.data += session.buffer
+ session.buffer = ''
+ return
+ }
+
+ session.data += session.buffer.slice(0, messageTerminatorIndex)
+ session.buffer = session.buffer.slice(messageTerminatorIndex + 5)
+
+ const capturedMessage = {
+ timestamp: new Date().toISOString(),
+ mailFrom: session.mailFrom,
+ rcptTo: session.rcptTo,
+ data: session.data.replace(/\r\n\.\./g, '\r\n.'),
+ }
+
+ await queueMessageWrite(capturedMessage)
+ resetMessageState()
+ reply('250 OK')
+ continue
+ }
+
+ const lineEndIndex = session.buffer.indexOf('\r\n')
+ if (lineEndIndex === -1) {
+ return
+ }
+
+ const line = session.buffer.slice(0, lineEndIndex)
+ session.buffer = session.buffer.slice(lineEndIndex + 2)
+ const normalized = line.toUpperCase()
+
+ if (normalized.startsWith('EHLO')) {
+ socket.write('250-localhost\r\n250 OK\r\n')
+ continue
+ }
+
+ if (normalized.startsWith('HELO')) {
+ reply('250 OK')
+ continue
+ }
+
+ if (normalized.startsWith('MAIL FROM:')) {
+ resetMessageState()
+ session.mailFrom = line.slice('MAIL FROM:'.length).trim()
+ reply('250 OK')
+ continue
+ }
+
+ if (normalized.startsWith('RCPT TO:')) {
+ session.rcptTo.push(line.slice('RCPT TO:'.length).trim())
+ reply('250 OK')
+ continue
+ }
+
+ if (normalized === 'DATA') {
+ session.dataMode = true
+ session.data = ''
+ reply('354 End data with .')
+ continue
+ }
+
+ if (normalized === 'RSET') {
+ resetMessageState()
+ reply('250 OK')
+ continue
+ }
+
+ if (normalized === 'NOOP') {
+ reply('250 OK')
+ continue
+ }
+
+ if (normalized === 'QUIT') {
+ reply('221 Bye')
+ socket.end()
+ return
+ }
+
+ reply('250 OK')
+ }
+ }
+
+ socket.on('data', (chunk) => {
+ session.buffer += chunk
+ void flushBuffer().catch((error) => {
+ console.error(error?.stack ?? String(error))
+ socket.destroy(error)
+ })
+ })
+
+ socket.on('error', () => {})
+ reply('220 localhost ESMTP ready')
+})
+
+server.listen(port, '127.0.0.1', () => {
+ console.log(`SMTP capture listening on 127.0.0.1:${port}`)
+})
+
+async function shutdown() {
+ server.close()
+ await writeQueue.catch(() => {})
+}
+
+process.on('SIGINT', () => {
+ void shutdown().finally(() => process.exit(0))
+})
+
+process.on('SIGTERM', () => {
+ void shutdown().finally(() => process.exit(0))
+})
diff --git a/frontend/admin/scripts/run-cdp-auth-smoke.ps1 b/frontend/admin/scripts/run-cdp-auth-smoke.ps1
new file mode 100644
index 0000000..b3206ce
--- /dev/null
+++ b/frontend/admin/scripts/run-cdp-auth-smoke.ps1
@@ -0,0 +1,316 @@
+param(
+ [string]$AdminUsername = 'e2e_admin',
+ [string]$AdminPassword = 'E2EAdmin@123456',
+ [string]$AdminEmail = 'e2e_admin@example.com',
+ [int]$BrowserPort = 0
+)
+
+$ErrorActionPreference = 'Stop'
+
+$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
+$frontendRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
+$tempCacheRoot = Join-Path $env:TEMP 'ums-e2e-cache'
+$goCacheDir = Join-Path $tempCacheRoot 'go-build'
+$goModCacheDir = Join-Path $tempCacheRoot 'gomod'
+$goPathDir = Join-Path $tempCacheRoot 'gopath'
+$serverExePath = Join-Path $env:TEMP ("ums-server-e2e-" + [guid]::NewGuid().ToString('N') + '.exe')
+
+New-Item -ItemType Directory -Force $goCacheDir, $goModCacheDir, $goPathDir | Out-Null
+
+function Test-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url
+ )
+
+ try {
+ $response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2
+ return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500
+ } catch {
+ return $false
+ }
+}
+
+function Wait-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url,
+ [Parameter(Mandatory = $true)][string]$Label,
+ [int]$RetryCount = 120,
+ [int]$DelayMs = 500
+ )
+
+ for ($i = 0; $i -lt $RetryCount; $i++) {
+ if (Test-UrlReady -Url $Url) {
+ return
+ }
+ Start-Sleep -Milliseconds $DelayMs
+ }
+
+ throw "$Label did not become ready: $Url"
+}
+
+function Start-ManagedProcess {
+ param(
+ [Parameter(Mandatory = $true)][string]$Name,
+ [Parameter(Mandatory = $true)][string]$FilePath,
+ [string[]]$ArgumentList = @(),
+ [Parameter(Mandatory = $true)][string]$WorkingDirectory
+ )
+
+ $stdoutPath = Join-Path $env:TEMP "$Name-stdout.log"
+ $stderrPath = Join-Path $env:TEMP "$Name-stderr.log"
+ Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
+
+ if ($ArgumentList -and $ArgumentList.Count -gt 0) {
+ $process = Start-Process `
+ -FilePath $FilePath `
+ -ArgumentList $ArgumentList `
+ -WorkingDirectory $WorkingDirectory `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+ } else {
+ $process = Start-Process `
+ -FilePath $FilePath `
+ -WorkingDirectory $WorkingDirectory `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+ }
+
+ return [pscustomobject]@{
+ Name = $Name
+ Process = $process
+ StdOut = $stdoutPath
+ StdErr = $stderrPath
+ }
+}
+
+function Stop-ManagedProcess {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if ($Handle.Process -and -not $Handle.Process.HasExited) {
+ try {
+ taskkill /PID $Handle.Process.Id /T /F *> $null
+ } catch {
+ Stop-Process -Id $Handle.Process.Id -Force -ErrorAction SilentlyContinue
+ }
+ }
+}
+
+function Show-ManagedProcessLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if (Test-Path $Handle.StdOut) {
+ Get-Content $Handle.StdOut -ErrorAction SilentlyContinue
+ }
+ if (Test-Path $Handle.StdErr) {
+ Get-Content $Handle.StdErr -ErrorAction SilentlyContinue
+ }
+}
+
+function Remove-ManagedProcessLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ Remove-Item $Handle.StdOut, $Handle.StdErr -Force -ErrorAction SilentlyContinue
+}
+
+$backendHandle = $null
+$frontendHandle = $null
+$startedBackend = $false
+$startedFrontend = $false
+$adminInitialized = $false
+
+try {
+ Push-Location $projectRoot
+ try {
+ $env:GOCACHE = $goCacheDir
+ $env:GOMODCACHE = $goModCacheDir
+ $env:GOPATH = $goPathDir
+ go build -o $serverExePath .\cmd\server\main.go
+ if ($LASTEXITCODE -ne 0) {
+ throw 'server build failed'
+ }
+ } finally {
+ Pop-Location
+ Remove-Item Env:GOCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOMODCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOPATH -ErrorAction SilentlyContinue
+ }
+
+ $backendWasRunning = Test-UrlReady -Url 'http://127.0.0.1:8080/health'
+
+ Push-Location $projectRoot
+ try {
+ $env:GOCACHE = $goCacheDir
+ $env:GOMODCACHE = $goModCacheDir
+ $env:GOPATH = $goPathDir
+ $env:UMS_ADMIN_USERNAME = $AdminUsername
+ $env:UMS_ADMIN_PASSWORD = $AdminPassword
+ $env:UMS_ADMIN_EMAIL = $AdminEmail
+ $env:UMS_ADMIN_RESET_PASSWORD = 'true'
+
+ $previousErrorActionPreference = $ErrorActionPreference
+ $ErrorActionPreference = 'Continue'
+ $initOutput = go run .\tools\init_admin.go 2>&1 | Out-String
+ $initExitCode = $LASTEXITCODE
+ $ErrorActionPreference = $previousErrorActionPreference
+
+ if ($initExitCode -eq 0) {
+ $adminInitialized = $true
+ } else {
+ $verifyOutput = go run .\tools\verify_admin.go 2>&1 | Out-String
+ if ($LASTEXITCODE -eq 0 -and $verifyOutput -match 'password valid: True|password valid: true') {
+ Write-Host 'init_admin fallback: existing admin credentials verified'
+ $adminInitialized = $true
+ } else {
+ Write-Host $initOutput
+ }
+ }
+ } finally {
+ Pop-Location
+ Remove-Item Env:GOCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOMODCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOPATH -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_USERNAME -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_PASSWORD -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_EMAIL -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_RESET_PASSWORD -ErrorAction SilentlyContinue
+ }
+
+ if (-not $adminInitialized -and -not $backendWasRunning) {
+ $backendHandle = Start-ManagedProcess `
+ -Name 'ums-backend-bootstrap' `
+ -FilePath $serverExePath `
+ -WorkingDirectory $projectRoot
+ $startedBackend = $true
+
+ try {
+ Wait-UrlReady -Url 'http://127.0.0.1:8080/health' -Label 'backend bootstrap'
+ } catch {
+ Show-ManagedProcessLogs $backendHandle
+ throw
+ }
+
+ Stop-ManagedProcess $backendHandle
+ Remove-ManagedProcessLogs $backendHandle
+ $backendHandle = $null
+ Start-Sleep -Seconds 1
+
+ Push-Location $projectRoot
+ try {
+ $env:GOCACHE = $goCacheDir
+ $env:GOMODCACHE = $goModCacheDir
+ $env:GOPATH = $goPathDir
+ $env:UMS_ADMIN_USERNAME = $AdminUsername
+ $env:UMS_ADMIN_PASSWORD = $AdminPassword
+ $env:UMS_ADMIN_EMAIL = $AdminEmail
+ $env:UMS_ADMIN_RESET_PASSWORD = 'true'
+
+ $previousErrorActionPreference = $ErrorActionPreference
+ $ErrorActionPreference = 'Continue'
+ $initOutput = go run .\tools\init_admin.go 2>&1 | Out-String
+ $initExitCode = $LASTEXITCODE
+ $ErrorActionPreference = $previousErrorActionPreference
+
+ if ($initExitCode -eq 0) {
+ $adminInitialized = $true
+ } else {
+ $verifyOutput = go run .\tools\verify_admin.go 2>&1 | Out-String
+ if ($LASTEXITCODE -eq 0 -and $verifyOutput -match 'password valid: True|password valid: true') {
+ Write-Host 'init_admin fallback: existing admin credentials verified'
+ $adminInitialized = $true
+ } else {
+ Write-Host $initOutput
+ }
+ }
+ } finally {
+ Pop-Location
+ Remove-Item Env:GOCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOMODCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOPATH -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_USERNAME -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_PASSWORD -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_EMAIL -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_ADMIN_RESET_PASSWORD -ErrorAction SilentlyContinue
+ }
+ }
+
+ if (-not $adminInitialized) {
+ throw 'init_admin failed'
+ }
+
+ if (-not $backendWasRunning) {
+ $backendHandle = Start-ManagedProcess `
+ -Name 'ums-backend' `
+ -FilePath $serverExePath `
+ -ArgumentList @() `
+ -WorkingDirectory $projectRoot
+
+ try {
+ Wait-UrlReady -Url 'http://127.0.0.1:8080/health' -Label 'backend'
+ } catch {
+ Show-ManagedProcessLogs $backendHandle
+ throw
+ }
+ }
+
+ if (-not (Test-UrlReady -Url 'http://127.0.0.1:3000')) {
+ $frontendHandle = Start-ManagedProcess `
+ -Name 'ums-frontend' `
+ -FilePath 'npm.cmd' `
+ -ArgumentList @('run', 'dev', '--', '--host', '127.0.0.1', '--port', '3000') `
+ -WorkingDirectory $frontendRoot
+ $startedFrontend = $true
+
+ try {
+ Wait-UrlReady -Url 'http://127.0.0.1:3000' -Label 'frontend'
+ } catch {
+ Show-ManagedProcessLogs $frontendHandle
+ throw
+ }
+ }
+
+ $env:E2E_LOGIN_USERNAME = $AdminUsername
+ $env:E2E_LOGIN_PASSWORD = $AdminPassword
+
+ Push-Location $frontendRoot
+ try {
+ & (Join-Path $PSScriptRoot 'run-cdp-smoke.ps1') -Port $BrowserPort
+ } finally {
+ Pop-Location
+ Remove-Item Env:E2E_LOGIN_USERNAME -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_LOGIN_PASSWORD -ErrorAction SilentlyContinue
+ }
+} finally {
+ if ($startedFrontend) {
+ Stop-ManagedProcess $frontendHandle
+ Remove-ManagedProcessLogs $frontendHandle
+ }
+
+ if ($startedBackend) {
+ Stop-ManagedProcess $backendHandle
+ Remove-ManagedProcessLogs $backendHandle
+ }
+
+ Remove-Item $serverExePath -Force -ErrorAction SilentlyContinue
+}
diff --git a/frontend/admin/scripts/run-cdp-smoke-bootstrap.ps1 b/frontend/admin/scripts/run-cdp-smoke-bootstrap.ps1
new file mode 100644
index 0000000..23d2180
--- /dev/null
+++ b/frontend/admin/scripts/run-cdp-smoke-bootstrap.ps1
@@ -0,0 +1,205 @@
+param(
+ [int]$BrowserPort = 0
+)
+
+$ErrorActionPreference = 'Stop'
+
+$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
+$frontendRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
+$tempCacheRoot = Join-Path $env:TEMP 'ums-e2e-cache'
+$goCacheDir = Join-Path $tempCacheRoot 'go-build'
+$goModCacheDir = Join-Path $tempCacheRoot 'gomod'
+$goPathDir = Join-Path $tempCacheRoot 'gopath'
+$serverExePath = Join-Path $env:TEMP ("ums-server-smoke-" + [guid]::NewGuid().ToString('N') + '.exe')
+
+New-Item -ItemType Directory -Force $goCacheDir, $goModCacheDir, $goPathDir | Out-Null
+
+function Test-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url
+ )
+
+ try {
+ $response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2
+ return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500
+ } catch {
+ return $false
+ }
+}
+
+function Wait-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url,
+ [Parameter(Mandatory = $true)][string]$Label,
+ [int]$RetryCount = 120,
+ [int]$DelayMs = 500
+ )
+
+ for ($i = 0; $i -lt $RetryCount; $i++) {
+ if (Test-UrlReady -Url $Url) {
+ return
+ }
+ Start-Sleep -Milliseconds $DelayMs
+ }
+
+ throw "$Label did not become ready: $Url"
+}
+
+function Start-ManagedProcess {
+ param(
+ [Parameter(Mandatory = $true)][string]$Name,
+ [Parameter(Mandatory = $true)][string]$FilePath,
+ [string[]]$ArgumentList = @(),
+ [Parameter(Mandatory = $true)][string]$WorkingDirectory
+ )
+
+ $stdoutPath = Join-Path $env:TEMP "$Name-stdout.log"
+ $stderrPath = Join-Path $env:TEMP "$Name-stderr.log"
+ Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
+
+ if ($ArgumentList -and $ArgumentList.Count -gt 0) {
+ $process = Start-Process `
+ -FilePath $FilePath `
+ -ArgumentList $ArgumentList `
+ -WorkingDirectory $WorkingDirectory `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+ } else {
+ $process = Start-Process `
+ -FilePath $FilePath `
+ -WorkingDirectory $WorkingDirectory `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+ }
+
+ return [pscustomobject]@{
+ Name = $Name
+ Process = $process
+ StdOut = $stdoutPath
+ StdErr = $stderrPath
+ }
+}
+
+function Stop-ManagedProcess {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if ($Handle.Process -and -not $Handle.Process.HasExited) {
+ try {
+ taskkill /PID $Handle.Process.Id /T /F *> $null
+ } catch {
+ Stop-Process -Id $Handle.Process.Id -Force -ErrorAction SilentlyContinue
+ }
+ }
+}
+
+function Show-ManagedProcessLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if (Test-Path $Handle.StdOut) {
+ Get-Content $Handle.StdOut -ErrorAction SilentlyContinue
+ }
+ if (Test-Path $Handle.StdErr) {
+ Get-Content $Handle.StdErr -ErrorAction SilentlyContinue
+ }
+}
+
+function Remove-ManagedProcessLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ Remove-Item $Handle.StdOut, $Handle.StdErr -Force -ErrorAction SilentlyContinue
+}
+
+$backendHandle = $null
+$frontendHandle = $null
+$startedBackend = $false
+$startedFrontend = $false
+
+try {
+ Push-Location $projectRoot
+ try {
+ $env:GOCACHE = $goCacheDir
+ $env:GOMODCACHE = $goModCacheDir
+ $env:GOPATH = $goPathDir
+ go build -o $serverExePath .\cmd\server\main.go
+ if ($LASTEXITCODE -ne 0) {
+ throw 'server build failed'
+ }
+ } finally {
+ Pop-Location
+ Remove-Item Env:GOCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOMODCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOPATH -ErrorAction SilentlyContinue
+ }
+
+ if (-not (Test-UrlReady -Url 'http://127.0.0.1:8080/health')) {
+ $backendHandle = Start-ManagedProcess `
+ -Name 'ums-backend-smoke' `
+ -FilePath $serverExePath `
+ -WorkingDirectory $projectRoot
+ $startedBackend = $true
+
+ try {
+ Wait-UrlReady -Url 'http://127.0.0.1:8080/health' -Label 'backend smoke'
+ } catch {
+ Show-ManagedProcessLogs $backendHandle
+ throw
+ }
+ }
+
+ if (-not (Test-UrlReady -Url 'http://127.0.0.1:3000')) {
+ $frontendHandle = Start-ManagedProcess `
+ -Name 'ums-frontend-smoke' `
+ -FilePath 'npm.cmd' `
+ -ArgumentList @('run', 'dev', '--', '--host', '127.0.0.1', '--port', '3000') `
+ -WorkingDirectory $frontendRoot
+ $startedFrontend = $true
+
+ try {
+ Wait-UrlReady -Url 'http://127.0.0.1:3000' -Label 'frontend smoke'
+ } catch {
+ Show-ManagedProcessLogs $frontendHandle
+ throw
+ }
+ }
+
+ Push-Location $frontendRoot
+ try {
+ & (Join-Path $PSScriptRoot 'run-cdp-smoke.ps1') -Port $BrowserPort
+ } finally {
+ Pop-Location
+ }
+} finally {
+ if ($startedFrontend) {
+ Stop-ManagedProcess $frontendHandle
+ Remove-ManagedProcessLogs $frontendHandle
+ }
+
+ if ($startedBackend) {
+ Stop-ManagedProcess $backendHandle
+ Remove-ManagedProcessLogs $backendHandle
+ }
+
+ Remove-Item $serverExePath -Force -ErrorAction SilentlyContinue
+}
diff --git a/frontend/admin/scripts/run-cdp-smoke.mjs b/frontend/admin/scripts/run-cdp-smoke.mjs
new file mode 100644
index 0000000..709896a
--- /dev/null
+++ b/frontend/admin/scripts/run-cdp-smoke.mjs
@@ -0,0 +1,1624 @@
+import { mkdtemp, readdir, rm, access, mkdir } from 'node:fs/promises'
+import { constants as fsConstants } from 'node:fs'
+import { spawn } from 'node:child_process'
+import { tmpdir } from 'node:os'
+import path from 'node:path'
+import process from 'node:process'
+import net from 'node:net'
+
+const TEXT = {
+ appTitle: '\u7528\u6237\u7ba1\u7406\u7cfb\u7edf',
+ welcomeLogin: '\u6b22\u8fce\u767b\u5f55',
+ loginAction: '\u767b\u5f55',
+ passwordLogin: '\u5bc6\u7801\u767b\u5f55',
+ emailCodeLogin: '\u90ae\u7bb1\u9a8c\u8bc1\u7801',
+ smsCodeLogin: '\u77ed\u4fe1\u9a8c\u8bc1\u7801',
+ forgotPassword: '\u5fd8\u8bb0\u5bc6\u7801',
+ usernamePlaceholder: '\u7528\u6237\u540d',
+ passwordPlaceholder: '\u5bc6\u7801',
+ emailPlaceholder: '\u90ae\u7bb1',
+ phonePlaceholder: '\u624b\u673a',
+ codePlaceholder: '\u9a8c\u8bc1\u7801',
+ dashboard: '\u603b\u89c8',
+ users: '\u7528\u6237\u7ba1\u7406',
+ roles: '\u89d2\u8272\u7ba1\u7406',
+ logout: '\u9000\u51fa\u767b\u5f55',
+ usersFilter: '\u7528\u6237\u540d/\u90ae\u7bb1/\u624b\u673a\u53f7',
+ rolesFilter: '\u89d2\u8272\u540d\u79f0/\u4ee3\u7801',
+ userDetail: '\u7528\u6237\u8be6\u60c5',
+ assignRoles: '\u5206\u914d\u89d2\u8272',
+ assignPermissions: '\u5206\u914d\u6743\u9650',
+ assignableRoles: '\u53ef\u5206\u914d\u89d2\u8272',
+ assignedRoles: '\u5df2\u5206\u914d\u89d2\u8272',
+ createRole: '\u521b\u5efa\u89d2\u8272',
+ roleNameCode: '\u89d2\u8272\u540d\u79f0/\u4ee3\u7801',
+ adminRoleName: '\u7ba1\u7406\u5458',
+ userId: '\u7528\u6237 ID',
+ totalUsers: '\u7528\u6237\u603b\u6570',
+ todaySuccessLogins: '\u4eca\u65e5\u6210\u529f\u767b\u5f55',
+ permissionsHint: '\u9009\u62e9\u8981\u5206\u914d\u7ed9\u8be5\u89d2\u8272\u7684\u6743\u9650',
+}
+
+const DEFAULT_BASE_URL = process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3000'
+const DEFAULT_LOGIN_PATH = process.env.E2E_LOGIN_PATH ?? '/login'
+const BASE_URL = new URL(DEFAULT_BASE_URL).toString().replace(/\/$/, '')
+const LOGIN_URL = new URL(DEFAULT_LOGIN_PATH, `${BASE_URL}/`).toString()
+const DASHBOARD_URL = new URL('/dashboard', `${BASE_URL}/`).toString()
+const USERS_URL = new URL('/users', `${BASE_URL}/`).toString()
+
+const LOGIN_USERNAME = (process.env.E2E_LOGIN_USERNAME ?? '').trim()
+const LOGIN_PASSWORD = process.env.E2E_LOGIN_PASSWORD ?? ''
+
+const EXTERNAL_BROWSER = process.env.E2E_SKIP_BROWSER_LAUNCH === '1'
+const EXTERNAL_CDP_PORT = Number(process.env.E2E_CDP_PORT ?? 0)
+const EXTERNAL_CDP_JSON_URL = process.env.E2E_CDP_JSON_URL ?? ''
+const EXTERNAL_CDP_BASE_URL = process.env.E2E_CDP_BASE_URL ?? ''
+
+const STARTUP_TIMEOUT_MS = Number(process.env.E2E_STARTUP_TIMEOUT_MS ?? 30000)
+const ASSERT_TIMEOUT_MS = Number(process.env.E2E_ASSERT_TIMEOUT_MS ?? 15000)
+const NAVIGATION_TIMEOUT_MS = Number(process.env.E2E_NAVIGATION_TIMEOUT_MS ?? 15000)
+const COMMAND_TIMEOUT_MS = Number(process.env.E2E_COMMAND_TIMEOUT_MS ?? 120000)
+const DEBUG = process.env.E2E_DEBUG === '1'
+
+async function main() {
+ let browserPath
+ let port
+ let profileDir
+ let browser
+ let connection
+
+ try {
+ await waitForHttp(`${BASE_URL}/`, STARTUP_TIMEOUT_MS, 'frontend dev server')
+
+ let cdpBaseUrl
+ if (EXTERNAL_BROWSER) {
+ cdpBaseUrl = resolveExternalCdpBaseUrl()
+ } else {
+ browserPath = await resolveBrowserPath()
+ port = await getFreePort()
+ profileDir = await createBrowserProfileDir(browserPath, port)
+ browser = startBrowser(browserPath, port, profileDir)
+ cdpBaseUrl = `http://127.0.0.1:${port}`
+ }
+
+ logDebug(`connecting to ${cdpBaseUrl}`)
+ const version = await waitForJson(`${cdpBaseUrl}/json/version`, STARTUP_TIMEOUT_MS)
+ let summary
+
+ for (let attempt = 1; attempt <= 2; attempt++) {
+ const pageTarget = await getOrCreatePageTarget(cdpBaseUrl, version, STARTUP_TIMEOUT_MS)
+ connection = await CDPConnection.connect(pageTarget.webSocketDebuggerUrl)
+
+ try {
+ logDebug(`running smoke attempt ${attempt}`)
+ summary = await runSmoke(connection, {
+ loginUrl: LOGIN_URL,
+ dashboardUrl: DASHBOARD_URL,
+ usersUrl: USERS_URL,
+ assertTimeoutMs: ASSERT_TIMEOUT_MS,
+ navigationTimeoutMs: NAVIGATION_TIMEOUT_MS,
+ browserVersion: version.Browser ?? version.product ?? 'unknown',
+ loginUsername: LOGIN_USERNAME,
+ loginPassword: LOGIN_PASSWORD,
+ })
+ break
+ } catch (error) {
+ const shouldRetry = attempt < 2 && isRetryableCDPError(error)
+ await connection?.close().catch(() => {})
+ connection = null
+
+ if (!shouldRetry) {
+ throw error
+ }
+
+ logDebug(`retrying smoke after transient CDP failure: ${formatError(error)}`)
+ await delay(1000)
+ }
+ }
+
+ printSummary(summary)
+ } finally {
+ logDebug('closing connection')
+ await connection?.close().catch(() => {})
+
+ if (browser) {
+ await killBrowserTree(browser)
+ }
+
+ if (profileDir) {
+ await rm(profileDir, { recursive: true, force: true }).catch(() => {})
+ }
+ }
+}
+
+function resolveExternalCdpBaseUrl() {
+ if (EXTERNAL_CDP_BASE_URL) {
+ return EXTERNAL_CDP_BASE_URL.replace(/\/$/, '')
+ }
+
+ if (EXTERNAL_CDP_JSON_URL) {
+ return new URL(EXTERNAL_CDP_JSON_URL).origin
+ }
+
+ if (EXTERNAL_CDP_PORT > 0) {
+ return `http://127.0.0.1:${EXTERNAL_CDP_PORT}`
+ }
+
+ throw new Error(
+ 'external browser mode requires E2E_CDP_PORT, E2E_CDP_BASE_URL, or E2E_CDP_JSON_URL',
+ )
+}
+
+function startBrowser(browserPath, port, profileDir) {
+ const args = [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, '--no-sandbox']
+
+ if (isHeadlessShell(browserPath)) {
+ args.push('--single-process')
+ } else {
+ args.push(
+ '--disable-dev-shm-usage',
+ '--disable-background-networking',
+ '--disable-background-timer-throttling',
+ '--disable-renderer-backgrounding',
+ '--disable-sync',
+ '--headless=new',
+ )
+ }
+
+ args.push('about:blank')
+
+ const browser = spawn(browserPath, args, {
+ stdio: 'ignore',
+ windowsHide: true,
+ })
+
+ browser.on('exit', (code, signal) => {
+ if (code !== 0 && signal == null) {
+ console.error(`browser exited unexpectedly with code ${code}`)
+ }
+ })
+
+ return browser
+}
+
+async function createBrowserProfileDir(browserPath, port) {
+ if (!isHeadlessShell(browserPath)) {
+ return await mkdtemp(path.join(tmpdir(), 'pw-profile-cdp-'))
+ }
+
+ const profileRoot = path.join(process.cwd(), '.cache', 'cdp-profiles')
+ await mkdir(profileRoot, { recursive: true })
+ return path.join(profileRoot, `pw-profile-cdp-smoke-node-${port}`)
+}
+
+async function resolveBrowserPath() {
+ const envPath =
+ process.env.CHROME_HEADLESS_SHELL_PATH ??
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
+
+ if (envPath) {
+ await assertFileExists(envPath)
+ return envPath
+ }
+
+ for (const candidate of [
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
+ ]) {
+ try {
+ await assertFileExists(candidate)
+ return candidate
+ } catch {
+ continue
+ }
+ }
+
+ const baseDir = path.join(process.env.LOCALAPPDATA ?? '', 'ms-playwright')
+ const candidates = []
+
+ try {
+ const entries = await readdir(baseDir, { withFileTypes: true })
+ for (const entry of entries) {
+ if (!entry.isDirectory()) {
+ continue
+ }
+ if (!entry.name.startsWith('chromium_headless_shell-')) {
+ continue
+ }
+ candidates.push(
+ path.join(
+ baseDir,
+ entry.name,
+ 'chrome-headless-shell-win64',
+ 'chrome-headless-shell.exe',
+ ),
+ )
+ }
+ } catch {
+ throw new Error('failed to scan Playwright browser cache under LOCALAPPDATA')
+ }
+
+ candidates.sort().reverse()
+
+ for (const candidate of candidates) {
+ try {
+ await assertFileExists(candidate)
+ return candidate
+ } catch {
+ continue
+ }
+ }
+
+ throw new Error('chrome-headless-shell.exe not found; set CHROME_HEADLESS_SHELL_PATH')
+}
+
+async function assertFileExists(filePath) {
+ await access(filePath, fsConstants.F_OK)
+}
+
+function isHeadlessShell(browserPath) {
+ return path.basename(browserPath).toLowerCase().includes('headless-shell')
+}
+
+async function killBrowserTree(browser) {
+ if (browser.exitCode != null || browser.pid == null) {
+ return
+ }
+
+ await new Promise((resolve) => {
+ const killer = spawn('taskkill', ['/PID', String(browser.pid), '/T', '/F'], {
+ stdio: 'ignore',
+ windowsHide: true,
+ })
+
+ killer.once('error', () => {
+ try {
+ browser.kill('SIGKILL')
+ } catch {
+ // ignore
+ }
+ resolve()
+ })
+
+ killer.once('exit', () => resolve())
+ })
+}
+
+async function getFreePort() {
+ return await new Promise((resolve, reject) => {
+ const server = net.createServer()
+ server.unref()
+ server.on('error', reject)
+ server.listen(0, '127.0.0.1', () => {
+ const address = server.address()
+ if (address == null || typeof address === 'string') {
+ server.close(() => reject(new Error('failed to resolve a free port')))
+ return
+ }
+
+ server.close((error) => {
+ if (error) {
+ reject(error)
+ return
+ }
+ resolve(address.port)
+ })
+ })
+ })
+}
+
+async function waitForHttp(url, timeoutMs, label) {
+ const startedAt = Date.now()
+ let lastError
+
+ while (Date.now() - startedAt < timeoutMs) {
+ try {
+ const response = await fetch(url)
+ if (response.ok) {
+ return response
+ }
+ lastError = new Error(`${label} returned ${response.status}`)
+ } catch (error) {
+ lastError = error
+ }
+ await delay(250)
+ }
+
+ throw new Error(`timed out waiting for ${label}: ${formatError(lastError)}`)
+}
+
+async function waitForJson(url, timeoutMs) {
+ const response = await waitForHttp(url, timeoutMs, url)
+ return await response.json()
+}
+
+async function waitForPageTarget(cdpBaseUrl, timeoutMs) {
+ const startedAt = Date.now()
+ let lastTargets = []
+
+ while (Date.now() - startedAt < timeoutMs) {
+ try {
+ const targets = await waitForJson(`${cdpBaseUrl}/json/list`, 5000)
+ lastTargets = Array.isArray(targets) ? targets : []
+ const pageTarget = lastTargets.find(
+ (target) => target.type === 'page' && typeof target.webSocketDebuggerUrl === 'string',
+ )
+ if (pageTarget) {
+ return pageTarget
+ }
+ } catch {
+ // retry
+ }
+
+ await delay(250)
+ }
+
+ throw new Error(`timed out waiting for page target: ${JSON.stringify(lastTargets)}`)
+}
+
+async function getOrCreatePageTarget(cdpBaseUrl, version, timeoutMs) {
+ try {
+ return await waitForPageTarget(cdpBaseUrl, Math.min(timeoutMs, 5000))
+ } catch (firstError) {
+ const browserWsUrl = version?.webSocketDebuggerUrl
+ if (typeof browserWsUrl !== 'string' || browserWsUrl.length === 0) {
+ throw firstError
+ }
+
+ await createPageTarget(browserWsUrl)
+ return await waitForPageTarget(cdpBaseUrl, timeoutMs)
+ }
+}
+
+async function createPageTarget(browserWsUrl) {
+ const browserConnection = await CDPConnection.connect(browserWsUrl)
+ try {
+ await browserConnection.send('Target.createTarget', { url: 'about:blank' })
+ } finally {
+ await browserConnection.close().catch(() => {})
+ }
+}
+
+function delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+function isRetryableCDPError(error) {
+ const message = formatError(error)
+ return (
+ message === 'CDP websocket closed' ||
+ message.startsWith('CDP command timed out:')
+ )
+}
+
+class CDPConnection {
+ static async connect(wsUrl) {
+ const WebSocketImpl = globalThis.WebSocket ?? (await import('ws')).default
+ const ws = new WebSocketImpl(wsUrl)
+
+ return await new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ try {
+ ws.close()
+ } catch {
+ // ignore
+ }
+ reject(new Error('timed out opening CDP websocket'))
+ }, STARTUP_TIMEOUT_MS)
+
+ ws.addEventListener('open', () => {
+ clearTimeout(timeoutId)
+ resolve(new CDPConnection(ws))
+ })
+
+ ws.addEventListener('error', () => {
+ clearTimeout(timeoutId)
+ reject(new Error('CDP websocket error'))
+ })
+ })
+ }
+
+ constructor(ws) {
+ this.ws = ws
+ this.lastId = 0
+ this.pending = new Map()
+ this.listeners = new Set()
+
+ ws.addEventListener('message', (event) => {
+ const payload = JSON.parse(event.data.toString())
+
+ if (payload.id != null) {
+ const request = this.pending.get(payload.id)
+ if (!request) {
+ return
+ }
+
+ this.pending.delete(payload.id)
+ clearTimeout(request.timeoutId)
+
+ if (payload.error) {
+ request.reject(new Error(payload.error.message ?? 'CDP command failed'))
+ return
+ }
+
+ request.resolve(payload.result ?? {})
+ return
+ }
+
+ for (const listener of this.listeners) {
+ listener(payload)
+ }
+ })
+
+ ws.addEventListener('close', () => {
+ for (const request of this.pending.values()) {
+ clearTimeout(request.timeoutId)
+ request.reject(new Error('CDP websocket closed'))
+ }
+ this.pending.clear()
+ })
+ }
+
+ send(method, params = {}) {
+ const id = ++this.lastId
+ const message = { id, method, params }
+
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ this.pending.delete(id)
+ reject(new Error(`CDP command timed out: ${method}`))
+ }, COMMAND_TIMEOUT_MS)
+
+ this.pending.set(id, { resolve, reject, timeoutId })
+
+ try {
+ this.ws.send(JSON.stringify(message))
+ } catch (error) {
+ clearTimeout(timeoutId)
+ this.pending.delete(id)
+ reject(error)
+ }
+ })
+ }
+
+ onEvent(listener) {
+ this.listeners.add(listener)
+ return () => this.listeners.delete(listener)
+ }
+
+ waitForEvent(predicate, timeoutMs, label) {
+ return new Promise((resolve, reject) => {
+ const timeoutId = setTimeout(() => {
+ unsubscribe()
+ reject(new Error(`timed out waiting for ${label}`))
+ }, timeoutMs)
+
+ const unsubscribe = this.onEvent((event) => {
+ if (!predicate(event)) {
+ return
+ }
+ clearTimeout(timeoutId)
+ unsubscribe()
+ resolve(event)
+ })
+ })
+ }
+
+ async close() {
+ if (this.ws.readyState === 3) {
+ return
+ }
+
+ await Promise.race([
+ new Promise((resolve) => {
+ this.ws.addEventListener('close', () => resolve(), { once: true })
+ this.ws.close()
+ }),
+ delay(3000),
+ ])
+ }
+}
+
+async function runSmoke(connection, options) {
+ const consoleErrors = []
+ const runtimeExceptions = []
+ const networkFailures = []
+ const consoleEntries = []
+ const javascriptDialogs = []
+ const popupWindows = []
+
+ const unsubscribe = connection.onEvent((event) => {
+ if (event.method === 'Runtime.consoleAPICalled') {
+ const entry = formatConsoleEntry(event.params)
+ consoleEntries.push(entry)
+ if (entry.type === 'error' && !isIgnorableConsoleError(entry.text)) {
+ consoleErrors.push(entry.text)
+ }
+ return
+ }
+
+ if (event.method === 'Runtime.exceptionThrown') {
+ runtimeExceptions.push(event.params.exceptionDetails?.text ?? 'unknown exception')
+ return
+ }
+
+ if (event.method === 'Page.javascriptDialogOpening') {
+ const dialog = {
+ type: event.params.type ?? 'unknown',
+ message: event.params.message ?? '',
+ defaultPrompt: event.params.defaultPrompt ?? '',
+ }
+ javascriptDialogs.push(dialog)
+ void connection.send('Page.handleJavaScriptDialog', { accept: false }).catch(() => {})
+ return
+ }
+
+ if (event.method === 'Page.windowOpen') {
+ popupWindows.push({
+ url: event.params.url ?? '',
+ windowName: event.params.windowName ?? '',
+ userGesture: event.params.userGesture ?? false,
+ })
+ return
+ }
+
+ if (event.method === 'Network.loadingFailed') {
+ const failure = {
+ errorText: event.params.errorText,
+ canceled: event.params.canceled,
+ type: event.params.type,
+ }
+ if (!isIgnorableNetworkFailure(failure)) {
+ networkFailures.push(failure)
+ }
+ }
+ })
+
+ try {
+ logDebug('enabling domains')
+ await connection.send('Page.enable')
+ await connection.send('Runtime.enable')
+ await connection.send('Log.enable')
+ await connection.send('Network.enable')
+
+ const loadTimings = []
+
+ logDebug('checking protected route redirect')
+ const protectedRedirects = {
+ dashboard: await assertProtectedRouteRedirect(
+ connection,
+ options.dashboardUrl,
+ '/dashboard',
+ options,
+ ),
+ users: await assertProtectedRouteRedirect(connection, options.usersUrl, '/users', options),
+ }
+
+ logDebug('navigating to login')
+ const initialLoadMs = await navigateAndWait(connection, options.loginUrl, options)
+ loadTimings.push({ name: 'login-initial', ms: initialLoadMs })
+
+ logDebug('checking initial page state')
+ const initialState = await waitForCondition(
+ connection,
+ `
+ (() => {
+ const isVisible = (element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ }
+
+ const visibleInputs = Array.from(document.querySelectorAll('input'))
+ .filter(isVisible)
+ .map((input) => input.getAttribute('placeholder') ?? '')
+
+ const visibleLinks = Array.from(document.querySelectorAll('a'))
+ .filter(isVisible)
+ .map((link) => link.textContent?.trim() ?? '')
+
+ const visibleTabs = Array.from(document.querySelectorAll('[role="tab"], .ant-tabs-tab-btn'))
+ .filter(isVisible)
+ .map((tab) => tab.textContent?.trim() ?? '')
+ .filter(Boolean)
+
+ return {
+ title: document.title,
+ lang: document.documentElement.lang,
+ bodyText: document.body?.innerText ?? '',
+ path: location.pathname,
+ visibleInputs,
+ visibleLinks,
+ visibleTabs,
+ capabilities: null,
+ }
+ })()
+ `,
+ (state) =>
+ state.path === '/login' &&
+ state.title.includes(TEXT.appTitle) &&
+ state.lang === 'zh-CN' &&
+ state.bodyText.includes(TEXT.welcomeLogin) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.usernamePlaceholder)) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.passwordPlaceholder)),
+ options.assertTimeoutMs,
+ 'login page state',
+ )
+
+ const authCapabilities =
+ (await evaluate(
+ connection,
+ `
+ (async () => {
+ const response = await fetch('/api/v1/auth/capabilities')
+ if (!response.ok) {
+ return null
+ }
+ const payload = await response.json()
+ return payload?.data ?? null
+ })()
+ `,
+ )) ??
+ {
+ password: true,
+ email_code: initialState.visibleTabs.some((tab) => tab.includes(TEXT.emailCodeLogin)),
+ sms_code: initialState.visibleTabs.some((tab) => tab.includes(TEXT.smsCodeLogin)),
+ password_reset: initialState.visibleLinks.some((link) => link.includes(TEXT.forgotPassword)),
+ oauth_providers: [],
+ }
+ if (authCapabilities.password !== true) {
+ throw new Error(`unexpected auth capabilities: ${JSON.stringify(authCapabilities)}`)
+ }
+ if (
+ Boolean(authCapabilities.email_code) !==
+ initialState.visibleTabs.some((tab) => tab.includes(TEXT.emailCodeLogin))
+ ) {
+ throw new Error(
+ `email capability mismatch: capabilities=${JSON.stringify(authCapabilities)} state=${JSON.stringify(initialState)}`,
+ )
+ }
+ if (
+ Boolean(authCapabilities.sms_code) !==
+ initialState.visibleTabs.some((tab) => tab.includes(TEXT.smsCodeLogin))
+ ) {
+ throw new Error(
+ `sms capability mismatch: capabilities=${JSON.stringify(authCapabilities)} state=${JSON.stringify(initialState)}`,
+ )
+ }
+ if (
+ Boolean(authCapabilities.password_reset) !==
+ initialState.visibleLinks.some((link) => link.includes(TEXT.forgotPassword))
+ ) {
+ throw new Error(
+ `password reset capability mismatch: capabilities=${JSON.stringify(authCapabilities)} state=${JSON.stringify(initialState)}`,
+ )
+ }
+
+ let emailState = null
+ if (authCapabilities.email_code) {
+ logDebug('switching to email tab')
+ await clickText(connection, '.ant-tabs-tab-btn, [role="tab"]', TEXT.emailCodeLogin)
+ emailState = await waitForCondition(
+ connection,
+ `
+ (() => Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? '')
+ )()
+ `,
+ (placeholders) =>
+ placeholders.some((placeholder) => placeholder.includes(TEXT.emailPlaceholder)) &&
+ placeholders.some((placeholder) => placeholder.includes(TEXT.codePlaceholder)),
+ options.assertTimeoutMs,
+ 'email login tab',
+ )
+ }
+
+ let smsState = null
+ if (authCapabilities.sms_code) {
+ logDebug('switching to sms tab')
+ await clickText(connection, '.ant-tabs-tab-btn, [role="tab"]', TEXT.smsCodeLogin)
+ smsState = await waitForCondition(
+ connection,
+ `
+ (() => Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? '')
+ )()
+ `,
+ (placeholders) =>
+ placeholders.some((placeholder) => placeholder.includes(TEXT.phonePlaceholder)) &&
+ placeholders.some((placeholder) => placeholder.includes(TEXT.codePlaceholder)),
+ options.assertTimeoutMs,
+ 'sms login tab',
+ )
+ }
+
+ let forgotState = null
+ if (authCapabilities.password_reset) {
+ logDebug('opening forgot password route')
+ await navigateAndWait(connection, options.loginUrl, options)
+ await clickText(connection, 'a', TEXT.forgotPassword)
+ forgotState = await waitForCondition(
+ connection,
+ `
+ (() => ({
+ path: location.pathname,
+ bodyText: document.body?.innerText ?? '',
+ title: document.title,
+ }))()
+ `,
+ (state) => state.path === '/forgot-password' && state.bodyText.includes(TEXT.forgotPassword),
+ options.assertTimeoutMs,
+ 'forgot password route',
+ )
+ }
+
+ logDebug('running responsive checks')
+ const responsiveChecks = []
+ for (const viewport of [
+ { name: 'desktop', width: 1920, height: 1080, mobile: false },
+ { name: 'tablet', width: 768, height: 1024, mobile: false },
+ { name: 'mobile', width: 375, height: 667, mobile: true },
+ ]) {
+ logDebug(`viewport ${viewport.name}`)
+ await connection.send('Emulation.setDeviceMetricsOverride', {
+ width: viewport.width,
+ height: viewport.height,
+ deviceScaleFactor: 1,
+ mobile: viewport.mobile,
+ })
+
+ const loadMs = await navigateAndWait(connection, options.loginUrl, options)
+ loadTimings.push({ name: `login-${viewport.name}`, ms: loadMs })
+
+ const state = await waitForCondition(
+ connection,
+ `
+ (() => ({
+ innerWidth: window.innerWidth,
+ bodyScrollWidth: document.body.scrollWidth,
+ path: location.pathname,
+ visibleInputs: Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? ''),
+ visibleLinks: Array.from(document.querySelectorAll('a'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((link) => link.textContent?.trim() ?? ''),
+ visibleTabs: Array.from(document.querySelectorAll('[role="tab"], .ant-tabs-tab-btn'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((tab) => tab.textContent?.trim() ?? ''),
+ }))()
+ `,
+ (state) =>
+ state.path === '/login' &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.usernamePlaceholder)) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.passwordPlaceholder)) &&
+ Boolean(authCapabilities.email_code) ===
+ state.visibleTabs.some((tab) => tab.includes(TEXT.emailCodeLogin)) &&
+ Boolean(authCapabilities.sms_code) ===
+ state.visibleTabs.some((tab) => tab.includes(TEXT.smsCodeLogin)) &&
+ Boolean(authCapabilities.password_reset) ===
+ state.visibleLinks.some((link) => link.includes(TEXT.forgotPassword)),
+ options.assertTimeoutMs,
+ `${viewport.name} viewport`,
+ )
+
+ if (Math.abs(state.innerWidth - viewport.width) > 1) {
+ throw new Error(
+ `${viewport.name} viewport width mismatch: expected ${viewport.width}, got ${state.innerWidth}`,
+ )
+ }
+
+ if (state.bodyScrollWidth > state.innerWidth + 4) {
+ throw new Error(
+ `${viewport.name} viewport overflows horizontally: ${state.bodyScrollWidth} > ${state.innerWidth}`,
+ )
+ }
+
+ responsiveChecks.push({
+ name: viewport.name,
+ width: state.innerWidth,
+ bodyScrollWidth: state.bodyScrollWidth,
+ })
+ }
+
+ let authFlow = null
+ if (options.loginUsername && options.loginPassword) {
+ await setViewport(connection, { width: 1920, height: 1080, mobile: false })
+ logDebug('running authenticated flow')
+ authFlow = await runAuthenticatedFlow(connection, options)
+ }
+
+ if (consoleErrors.length > 0) {
+ throw new Error(`console errors detected: ${consoleErrors.join(' | ')}`)
+ }
+
+ if (runtimeExceptions.length > 0) {
+ throw new Error(`runtime exceptions detected: ${runtimeExceptions.join(' | ')}`)
+ }
+
+ if (networkFailures.length > 0) {
+ throw new Error(`network failures detected: ${JSON.stringify(networkFailures)}`)
+ }
+
+ if (javascriptDialogs.length > 0) {
+ throw new Error(`javascript dialogs detected: ${JSON.stringify(javascriptDialogs)}`)
+ }
+
+ if (popupWindows.length > 0) {
+ throw new Error(`popup windows detected: ${JSON.stringify(popupWindows)}`)
+ }
+
+ return {
+ browserVersion: options.browserVersion,
+ protectedRedirects,
+ initialState,
+ authCapabilities,
+ emailState,
+ smsState,
+ forgotState,
+ responsiveChecks,
+ loadTimings,
+ consoleEntries,
+ authFlow,
+ }
+ } finally {
+ unsubscribe()
+ }
+}
+
+async function assertProtectedRouteRedirect(connection, url, expectedFromPath, options) {
+ await navigateAndWait(connection, url, options)
+
+ return await waitForCondition(
+ connection,
+ `
+ (() => ({
+ path: location.pathname,
+ bodyText: document.body?.innerText ?? '',
+ title: document.title,
+ redirectFrom: history.state?.usr?.from?.pathname ?? null,
+ visibleInputs: Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? ''),
+ }))()
+ `,
+ (state) =>
+ state.path === '/login' &&
+ state.title.includes(TEXT.appTitle) &&
+ state.bodyText.includes(TEXT.loginAction) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.usernamePlaceholder)) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.passwordPlaceholder)) &&
+ state.redirectFrom === expectedFromPath,
+ options.assertTimeoutMs,
+ `protected route redirect for ${expectedFromPath}`,
+ )
+}
+
+async function runAuthenticatedFlow(connection, options) {
+ const preLoginRedirect = await assertProtectedRouteRedirect(
+ connection,
+ options.usersUrl,
+ '/users',
+ options,
+ )
+
+ await setInputValue(
+ connection,
+ 'input[autocomplete="username"], input[type="text"], input[type="email"]',
+ options.loginUsername,
+ )
+ await setInputValue(
+ connection,
+ 'input[autocomplete="current-password"], input[type="password"]',
+ options.loginPassword,
+ )
+ await clickFirstVisible(connection, 'button[type="submit"]')
+
+ const loginState = await waitForUsersPage(connection, options, 'login success')
+
+ const userDetailState = await openUserDetailDrawer(connection, options.loginUsername, options)
+ logDebug('reloading users page after user detail drawer')
+ await navigateAndWait(connection, options.usersUrl, options)
+ await waitForUsersPage(connection, options, 'users page after user detail drawer')
+
+ logDebug('opening assign roles modal')
+ const assignRolesState = await openAssignRolesModal(connection, options.loginUsername, options)
+ logDebug('reloading users page after assign roles modal')
+ await navigateAndWait(connection, options.usersUrl, options)
+ await waitForUsersPage(connection, options, 'users page after assign roles modal')
+
+ logDebug('navigating to roles page from sidebar')
+ await clickSidebarMenuItem(connection, TEXT.roles)
+
+ logDebug('waiting for roles page')
+ const rolesState = await waitForRolesPage(connection, options)
+ logDebug('opening role permissions modal')
+ const rolePermissionsState = await openRolePermissionsModal(connection, options)
+
+ logDebug('navigating to dashboard after roles page checks')
+ await navigateAndWait(connection, options.dashboardUrl, options)
+
+ logDebug('waiting for dashboard page')
+ const dashboardState = await waitForDashboardPage(connection, options)
+
+ logDebug('opening user menu for logout')
+ await hoverFirstVisible(connection, '.ant-dropdown-trigger, [class*="userTrigger"]')
+ await waitForCondition(
+ connection,
+ `(() => document.body?.innerText ?? '')()`,
+ (bodyText) => typeof bodyText === 'string' && bodyText.includes(TEXT.logout),
+ Math.max(options.assertTimeoutMs, 10000),
+ 'logout menu',
+ )
+ await clickText(
+ connection,
+ '[role="menuitem"], .ant-dropdown-menu-item, .ant-dropdown-menu-title-content',
+ TEXT.logout,
+ )
+
+ const logoutState = await waitForCondition(
+ connection,
+ `
+ (() => ({
+ path: location.pathname,
+ bodyText: document.body?.innerText ?? '',
+ refreshToken: localStorage.getItem('admin_refresh_token'),
+ visibleInputs: Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? ''),
+ }))()
+ `,
+ (state) =>
+ state.path === '/login' &&
+ state.bodyText.includes(TEXT.loginAction) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.usernamePlaceholder)) &&
+ state.visibleInputs.some((placeholder) => placeholder.includes(TEXT.passwordPlaceholder)) &&
+ state.refreshToken === null,
+ Math.max(options.assertTimeoutMs, 20000),
+ 'logout success',
+ )
+
+ const postLogoutRedirects = {
+ dashboard: await assertProtectedRouteRedirect(
+ connection,
+ options.dashboardUrl,
+ '/dashboard',
+ options,
+ ),
+ users: await assertProtectedRouteRedirect(connection, options.usersUrl, '/users', options),
+ }
+
+ return {
+ preLoginRedirect,
+ loginState,
+ userDetailState,
+ assignRolesState,
+ rolesState,
+ rolePermissionsState,
+ dashboardState,
+ logoutState,
+ postLogoutRedirects,
+ }
+}
+
+async function waitForDashboardPage(connection, options) {
+ return await waitForCondition(
+ connection,
+ `
+ (() => ({
+ path: location.pathname,
+ title: document.title,
+ bodyText: document.body?.innerText ?? '',
+ refreshToken: localStorage.getItem('admin_refresh_token'),
+ }))()
+ `,
+ (state) =>
+ state.path === '/dashboard' &&
+ state.title.includes(TEXT.appTitle) &&
+ state.bodyText.includes(TEXT.dashboard) &&
+ state.bodyText.includes(TEXT.totalUsers) &&
+ state.bodyText.includes(TEXT.todaySuccessLogins) &&
+ typeof state.refreshToken === 'string' &&
+ state.refreshToken.length > 0,
+ Math.max(options.assertTimeoutMs, 20000),
+ 'dashboard access after login',
+ )
+}
+
+async function waitForUsersPage(connection, options, label) {
+ return await waitForCondition(
+ connection,
+ `
+ (() => ({
+ path: location.pathname,
+ title: document.title,
+ bodyText: document.body?.innerText ?? '',
+ refreshToken: localStorage.getItem('admin_refresh_token'),
+ rowTexts: Array.from(document.querySelectorAll('tbody tr'))
+ .map((row) => row.textContent?.trim() ?? '')
+ .filter(Boolean),
+ visibleInputs: Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? '')
+ .filter(Boolean),
+ }))()
+ `,
+ (state) =>
+ state.path === '/users' &&
+ state.title.includes(TEXT.appTitle) &&
+ state.bodyText.includes(TEXT.users) &&
+ state.bodyText.includes(options.loginUsername) &&
+ state.visibleInputs.some((text) => text.includes(TEXT.usersFilter)) &&
+ state.rowTexts.some((text) => text.includes(options.loginUsername)) &&
+ typeof state.refreshToken === 'string' &&
+ state.refreshToken.length > 0,
+ Math.max(options.assertTimeoutMs, 20000),
+ label,
+ )
+}
+
+async function waitForRolesPage(connection, options) {
+ return await waitForCondition(
+ connection,
+ `
+ (() => ({
+ path: location.pathname,
+ title: document.title,
+ bodyText: document.body?.innerText ?? '',
+ rowTexts: Array.from(document.querySelectorAll('tbody tr'))
+ .map((row) => row.textContent?.trim() ?? '')
+ .filter(Boolean),
+ visibleInputs: Array.from(document.querySelectorAll('input'))
+ .filter((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ })
+ .map((input) => input.getAttribute('placeholder') ?? '')
+ .filter(Boolean),
+ }))()
+ `,
+ (state) =>
+ state.path === '/roles' &&
+ state.title.includes(TEXT.appTitle) &&
+ state.bodyText.includes(TEXT.roles) &&
+ state.bodyText.includes(TEXT.createRole) &&
+ state.visibleInputs.some((text) => text.includes(TEXT.rolesFilter)) &&
+ state.rowTexts.some((text) => text.includes(TEXT.adminRoleName)),
+ Math.max(options.assertTimeoutMs, 20000),
+ 'roles page',
+ )
+}
+
+async function openUserDetailDrawer(connection, username, options) {
+ logDebug(`opening user detail drawer for ${username}`)
+ await clickActionInTableRow(connection, username, '\u8be6\u60c5')
+
+ return await waitForCondition(
+ connection,
+ `
+ (() => {
+ const drawer = Array.from(document.querySelectorAll('.ant-drawer'))
+ .find((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ return element.getClientRects().length > 0
+ })
+
+ return {
+ path: location.pathname,
+ title: drawer?.querySelector('.ant-drawer-title')?.textContent?.trim() ?? '',
+ bodyText: drawer?.textContent?.trim() ?? '',
+ }
+ })()
+ `,
+ (state) =>
+ state.path === '/users' &&
+ state.title.includes(TEXT.userDetail) &&
+ state.bodyText.includes(TEXT.userId) &&
+ state.bodyText.includes(username),
+ Math.max(options.assertTimeoutMs, 20000),
+ 'user detail drawer',
+ )
+}
+
+async function openAssignRolesModal(connection, username, options) {
+ logDebug(`opening assign roles modal for ${username}`)
+ await clickActionInTableRow(connection, username, '\u89d2\u8272')
+
+ return await waitForCondition(
+ connection,
+ `
+ (() => {
+ const modal = Array.from(document.querySelectorAll('.ant-modal-root'))
+ .find((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ return element.getClientRects().length > 0
+ })
+
+ return {
+ path: location.pathname,
+ title: modal?.querySelector('.ant-modal-title')?.textContent?.trim() ?? '',
+ bodyText: modal?.textContent?.trim() ?? '',
+ }
+ })()
+ `,
+ (state) =>
+ state.path === '/users' &&
+ state.title.includes(TEXT.assignRoles) &&
+ state.title.includes(username) &&
+ state.bodyText.includes(TEXT.assignableRoles) &&
+ state.bodyText.includes(TEXT.assignedRoles),
+ Math.max(options.assertTimeoutMs, 20000),
+ 'assign roles modal',
+ )
+}
+
+async function openRolePermissionsModal(connection, options) {
+ logDebug(`opening role permissions modal for ${TEXT.adminRoleName}`)
+ await clickActionInTableRow(connection, TEXT.adminRoleName, '\u6743\u9650')
+
+ return await waitForCondition(
+ connection,
+ `
+ (() => {
+ const modal = Array.from(document.querySelectorAll('.ant-modal-root'))
+ .find((element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ return element.getClientRects().length > 0
+ })
+
+ return {
+ path: location.pathname,
+ title: modal?.querySelector('.ant-modal-title')?.textContent?.trim() ?? '',
+ bodyText: modal?.textContent?.trim() ?? '',
+ treeNodeCount: modal?.querySelectorAll('.ant-tree-treenode').length ?? 0,
+ }
+ })()
+ `,
+ (state) =>
+ state.path === '/roles' &&
+ state.title.includes(TEXT.assignPermissions) &&
+ state.title.includes(TEXT.adminRoleName) &&
+ state.bodyText.includes(TEXT.permissionsHint) &&
+ (state.treeNodeCount > 0 || state.bodyText.includes('\u6682\u65e0\u6743\u9650\u6570\u636e')),
+ Math.max(options.assertTimeoutMs, 20000),
+ 'role permissions modal',
+ )
+}
+
+async function navigateAndWait(connection, url, options) {
+ const startedAt = Date.now()
+ const previousHref =
+ (await evaluate(connection, `(() => location.href)()`).catch(() => null)) ?? null
+ const loadEvent = connection.waitForEvent(
+ (event) => event.method === 'Page.loadEventFired',
+ options.navigationTimeoutMs,
+ `load event for ${url}`,
+ )
+
+ const result = await connection.send('Page.navigate', { url })
+ if (result.errorText) {
+ throw new Error(`navigation failed for ${url}: ${result.errorText}`)
+ }
+
+ try {
+ await loadEvent
+ } catch (error) {
+ logDebug(`load event fallback for ${url}: ${formatError(error)}`)
+ await waitForCondition(
+ connection,
+ `
+ (() => ({
+ readyState: document.readyState,
+ href: location.href,
+ }))()
+ `,
+ (state) =>
+ state.readyState === 'complete' &&
+ (state.href === url || state.href !== previousHref),
+ Math.max(options.navigationTimeoutMs, 15000),
+ `document ready after navigation to ${url}`,
+ )
+ }
+ return Date.now() - startedAt
+}
+
+async function setViewport(connection, viewport) {
+ await connection.send('Emulation.setDeviceMetricsOverride', {
+ width: viewport.width,
+ height: viewport.height,
+ deviceScaleFactor: 1,
+ mobile: viewport.mobile,
+ })
+}
+
+async function evaluate(connection, expression, options = {}) {
+ const result = await connection.send('Runtime.evaluate', {
+ expression,
+ awaitPromise: true,
+ returnByValue: true,
+ userGesture: options.userGesture ?? false,
+ })
+
+ if (result.exceptionDetails) {
+ const description =
+ result.exceptionDetails.exception?.description ??
+ result.exceptionDetails.exception?.value ??
+ result.exceptionDetails.text ??
+ 'Runtime.evaluate failed'
+ throw new Error(String(description))
+ }
+
+ return result.result?.value
+}
+
+async function waitForCondition(connection, expression, predicate, timeoutMs, label) {
+ const startedAt = Date.now()
+ let lastValue
+
+ while (Date.now() - startedAt < timeoutMs) {
+ lastValue = await evaluate(connection, expression)
+ if (predicate(lastValue)) {
+ return lastValue
+ }
+ await delay(150)
+ }
+
+ throw new Error(`timed out waiting for ${label}: ${JSON.stringify(lastValue)}`)
+}
+
+async function clickText(connection, selector, text) {
+ const clicked = await evaluate(
+ connection,
+ `
+ (() => {
+ const isVisible = (element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ }
+
+ const element = Array.from(document.querySelectorAll(${JSON.stringify(selector)}))
+ .find((node) => isVisible(node) && (node.textContent ?? '').includes(${JSON.stringify(text)}))
+
+ if (!element) {
+ return false
+ }
+
+ const target = element.closest('[role="menuitem"], button, a, li, .ant-dropdown-menu-item') || element
+ target.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
+ target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
+ target.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ if (typeof target.click === 'function') {
+ target.click()
+ }
+ return true
+ })()
+ `,
+ { userGesture: true },
+ )
+
+ if (!clicked) {
+ throw new Error(`failed to find clickable text "${text}" using selector "${selector}"`)
+ }
+}
+
+async function clickFirstVisible(connection, selector) {
+ const clicked = await evaluate(
+ connection,
+ `
+ (() => {
+ const isVisible = (element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ }
+
+ const element = Array.from(document.querySelectorAll(${JSON.stringify(selector)}))
+ .find((node) => isVisible(node))
+
+ if (!element) {
+ return false
+ }
+
+ element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
+ element.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ element.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
+ element.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ if (typeof element.click === 'function') {
+ element.click()
+ }
+ return true
+ })()
+ `,
+ { userGesture: true },
+ )
+
+ if (!clicked) {
+ throw new Error(`failed to find visible selector "${selector}"`)
+ }
+}
+
+async function clickActionInTableRow(connection, rowText, actionText) {
+ const clicked = await evaluate(
+ connection,
+ `
+ (() => {
+ const isVisible = (element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ }
+
+ const rows = Array.from(document.querySelectorAll('tbody tr'))
+ .filter((row) => isVisible(row) && (row.textContent ?? '').includes(${JSON.stringify(rowText)}))
+
+ const action = rows
+ .flatMap((row) => Array.from(row.querySelectorAll('button, a, [role="button"]')))
+ .find((node) => isVisible(node) && (node.textContent ?? '').includes(${JSON.stringify(actionText)}))
+
+ if (!(action instanceof HTMLElement)) {
+ return false
+ }
+
+ action.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
+ action.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ action.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
+ action.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ if (typeof action.click === 'function') {
+ action.click()
+ }
+ return true
+ })()
+ `,
+ { userGesture: true },
+ )
+
+ if (!clicked) {
+ throw new Error(`failed to click action "${actionText}" in row "${rowText}"`)
+ }
+}
+
+async function clickSidebarMenuItem(connection, text) {
+ await clickText(
+ connection,
+ '.ant-layout-sider .ant-menu-item, .ant-layout-sider [role="menuitem"]',
+ text,
+ )
+}
+
+async function setInputValue(connection, selector, value) {
+ const updated = await evaluate(
+ connection,
+ `
+ (() => {
+ const isVisible = (element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ }
+
+ const input = Array.from(document.querySelectorAll(${JSON.stringify(selector)}))
+ .find((node) => node instanceof HTMLInputElement && isVisible(node))
+
+ if (!(input instanceof HTMLInputElement)) {
+ return false
+ }
+
+ const prototype = Object.getPrototypeOf(input)
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value')
+ if (!descriptor || typeof descriptor.set !== 'function') {
+ input.value = ${JSON.stringify(value)}
+ } else {
+ descriptor.set.call(input, ${JSON.stringify(value)})
+ }
+
+ input.dispatchEvent(new Event('input', { bubbles: true }))
+ input.dispatchEvent(new Event('change', { bubbles: true }))
+ return true
+ })()
+ `,
+ { userGesture: true },
+ )
+
+ if (!updated) {
+ throw new Error(`failed to set input value for selector "${selector}"`)
+ }
+}
+
+async function hoverFirstVisible(connection, selector) {
+ const hovered = await evaluate(
+ connection,
+ `
+ (() => {
+ const isVisible = (element) => {
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+ const style = window.getComputedStyle(element)
+ return style.display !== 'none' && style.visibility !== 'hidden' && element.getClientRects().length > 0
+ }
+
+ const element = Array.from(document.querySelectorAll(${JSON.stringify(selector)}))
+ .find((node) => isVisible(node))
+
+ if (!(element instanceof HTMLElement)) {
+ return false
+ }
+
+ element.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
+ element.dispatchEvent(new MouseEvent('mouseenter', { bubbles: true }))
+ return true
+ })()
+ `,
+ { userGesture: true },
+ )
+
+ if (!hovered) {
+ throw new Error(`failed to hover visible selector "${selector}"`)
+ }
+}
+
+function formatConsoleEntry(params) {
+ const text = (params.args ?? [])
+ .map((arg) => {
+ if (arg.value != null) {
+ return String(arg.value)
+ }
+ if (arg.unserializableValue != null) {
+ return arg.unserializableValue
+ }
+ if (arg.description != null) {
+ return arg.description
+ }
+ return arg.type ?? 'unknown'
+ })
+ .join(' ')
+
+ return {
+ type: params.type ?? 'log',
+ text,
+ }
+}
+
+function isIgnorableNetworkFailure(failure) {
+ return (
+ failure.canceled === true ||
+ failure.errorText === 'net::ERR_ABORTED' ||
+ failure.errorText === 'net::ERR_FAILED'
+ )
+}
+
+function isIgnorableConsoleError(text) {
+ return text.includes('Static function can not consume context like dynamic theme')
+}
+
+function printSummary(summary) {
+ console.log('CDP smoke completed successfully')
+ console.log(`browser: ${summary.browserVersion}`)
+ console.log(`title: ${summary.initialState.title}`)
+ console.log(
+ `capabilities: password=${summary.authCapabilities.password} email=${summary.authCapabilities.email_code} sms=${summary.authCapabilities.sms_code} passwordReset=${summary.authCapabilities.password_reset}`,
+ )
+ console.log(`tabs: ${summary.initialState.visibleTabs.join(', ')}`)
+ if (summary.forgotState) {
+ console.log(`forgot-password path: ${summary.forgotState.path}`)
+ } else {
+ console.log('forgot-password path: disabled')
+ }
+ console.log(
+ `protected dashboard redirect: ${summary.protectedRedirects.dashboard.path} (from=${summary.protectedRedirects.dashboard.redirectFrom})`,
+ )
+ console.log(
+ `protected users redirect: ${summary.protectedRedirects.users.path} (from=${summary.protectedRedirects.users.redirectFrom})`,
+ )
+ if (summary.authFlow) {
+ console.log(`pre-login users redirect from: ${summary.authFlow.preLoginRedirect.redirectFrom}`)
+ console.log(`login landing path: ${summary.authFlow.loginState.path}`)
+ console.log(`user detail title: ${summary.authFlow.userDetailState.title}`)
+ console.log(`assign roles title: ${summary.authFlow.assignRolesState.title}`)
+ console.log(`roles path: ${summary.authFlow.rolesState.path}`)
+ console.log(`permissions title: ${summary.authFlow.rolePermissionsState.title}`)
+ console.log(`dashboard path: ${summary.authFlow.dashboardState.path}`)
+ console.log(`logout path: ${summary.authFlow.logoutState.path}`)
+ console.log(
+ `post-logout dashboard redirect: ${summary.authFlow.postLogoutRedirects.dashboard.path} (from=${summary.authFlow.postLogoutRedirects.dashboard.redirectFrom})`,
+ )
+ console.log(
+ `post-logout users redirect: ${summary.authFlow.postLogoutRedirects.users.path} (from=${summary.authFlow.postLogoutRedirects.users.redirectFrom})`,
+ )
+ }
+ console.log('responsive:')
+ for (const viewport of summary.responsiveChecks) {
+ console.log(
+ ` - ${viewport.name}: innerWidth=${viewport.width}, bodyScrollWidth=${viewport.bodyScrollWidth}`,
+ )
+ }
+ console.log('load timings:')
+ for (const timing of summary.loadTimings) {
+ console.log(` - ${timing.name}: ${timing.ms}ms`)
+ }
+}
+
+function logDebug(message) {
+ if (DEBUG) {
+ console.log(`[debug] ${message}`)
+ }
+}
+
+function formatError(error) {
+ if (error instanceof Error) {
+ return error.message
+ }
+ return String(error)
+}
+
+await main().catch((error) => {
+ console.error(`CDP smoke failed: ${formatError(error)}`)
+ process.exitCode = 1
+})
diff --git a/frontend/admin/scripts/run-cdp-smoke.ps1 b/frontend/admin/scripts/run-cdp-smoke.ps1
new file mode 100644
index 0000000..51d824d
--- /dev/null
+++ b/frontend/admin/scripts/run-cdp-smoke.ps1
@@ -0,0 +1,397 @@
+param(
+ [int]$Port = 0,
+ [string[]]$Command = @('node', './scripts/run-cdp-smoke.mjs')
+)
+
+$ErrorActionPreference = 'Stop'
+
+if (-not $Command -or $Command.Count -eq 0) {
+ throw 'Command must not be empty'
+}
+
+function Test-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url
+ )
+
+ try {
+ $response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2
+ return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500
+ } catch {
+ return $false
+ }
+}
+
+function Wait-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url,
+ [Parameter(Mandatory = $true)][string]$Label,
+ [int]$RetryCount = 60,
+ [int]$DelayMs = 500
+ )
+
+ for ($i = 0; $i -lt $RetryCount; $i++) {
+ if (Test-UrlReady -Url $Url) {
+ return
+ }
+ Start-Sleep -Milliseconds $DelayMs
+ }
+
+ throw "$Label did not become ready: $Url"
+}
+
+function Get-FreeTcpPort {
+ $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
+ $listener.Start()
+ try {
+ return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
+ } finally {
+ $listener.Stop()
+ }
+}
+
+function Resolve-BrowserPath {
+ if ($env:E2E_BROWSER_PATH) {
+ return $env:E2E_BROWSER_PATH
+ }
+
+ if ($env:CHROME_HEADLESS_SHELL_PATH) {
+ return $env:CHROME_HEADLESS_SHELL_PATH
+ }
+
+ if ($env:PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
+ return $env:PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
+ }
+
+ $baseDir = Join-Path $env:LOCALAPPDATA 'ms-playwright'
+ $candidate = Get-ChildItem $baseDir -Directory -Filter 'chromium_headless_shell-*' |
+ Sort-Object Name -Descending |
+ Select-Object -First 1
+
+ if ($candidate) {
+ return (Join-Path $candidate.FullName 'chrome-headless-shell-win64\chrome-headless-shell.exe')
+ }
+
+ foreach ($fallback in @(
+ 'C:\Program Files\Google\Chrome\Application\chrome.exe',
+ 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe',
+ 'C:\Program Files\Microsoft\Edge\Application\msedge.exe',
+ 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe'
+ )) {
+ if (Test-Path $fallback) {
+ return $fallback
+ }
+ }
+
+ throw 'No compatible browser found; set E2E_BROWSER_PATH or CHROME_HEADLESS_SHELL_PATH explicitly if needed'
+}
+
+function Test-HeadlessShellBrowser {
+ param(
+ [Parameter(Mandatory = $true)][string]$BrowserPath
+ )
+
+ return [System.IO.Path]::GetFileName($BrowserPath).ToLowerInvariant().Contains('headless-shell')
+}
+
+function Get-BrowserArguments {
+ param(
+ [Parameter(Mandatory = $true)][string]$BrowserPath,
+ [Parameter(Mandatory = $true)][int]$Port,
+ [Parameter(Mandatory = $true)][string]$ProfileDir
+ )
+
+ $arguments = @(
+ "--remote-debugging-port=$Port",
+ "--user-data-dir=$ProfileDir",
+ '--no-sandbox'
+ )
+
+ if (Test-HeadlessShellBrowser -BrowserPath $BrowserPath) {
+ $arguments += '--single-process'
+ } else {
+ $arguments += @(
+ '--disable-dev-shm-usage',
+ '--disable-background-networking',
+ '--disable-background-timer-throttling',
+ '--disable-renderer-backgrounding',
+ '--disable-sync',
+ '--headless=new'
+ )
+ }
+
+ $arguments += 'about:blank'
+ return $arguments
+}
+
+function Get-BrowserProcessIds {
+ param(
+ [Parameter(Mandatory = $true)][string]$BrowserPath
+ )
+
+ $processName = [System.IO.Path]::GetFileNameWithoutExtension($BrowserPath)
+ try {
+ return @(Get-Process -Name $processName -ErrorAction Stop | Select-Object -ExpandProperty Id)
+ } catch {
+ return @()
+ }
+}
+
+function Get-BrowserProcessesByProfile {
+ param(
+ [Parameter(Mandatory = $true)][string]$BrowserPath,
+ [Parameter(Mandatory = $true)][string]$ProfileDir
+ )
+
+ $processFileName = [System.IO.Path]::GetFileName($BrowserPath)
+ $profileFragment = $ProfileDir.ToLowerInvariant()
+
+ try {
+ return @(
+ Get-CimInstance Win32_Process -Filter ("Name = '{0}'" -f $processFileName) -ErrorAction Stop |
+ Where-Object {
+ $commandLine = $_.CommandLine
+ $commandLine -and $commandLine.ToLowerInvariant().Contains($profileFragment)
+ }
+ )
+ } catch {
+ return @()
+ }
+}
+
+function Get-ChildProcessIds {
+ param(
+ [Parameter(Mandatory = $true)][int]$ParentId
+ )
+
+ $pending = [System.Collections.Generic.Queue[int]]::new()
+ $seen = [System.Collections.Generic.HashSet[int]]::new()
+ $pending.Enqueue($ParentId)
+
+ while ($pending.Count -gt 0) {
+ $currentParentId = $pending.Dequeue()
+
+ try {
+ $children = @(Get-CimInstance Win32_Process -Filter ("ParentProcessId = {0}" -f $currentParentId) -ErrorAction Stop)
+ } catch {
+ $children = @()
+ }
+
+ foreach ($child in $children) {
+ if ($seen.Add([int]$child.ProcessId)) {
+ $pending.Enqueue([int]$child.ProcessId)
+ }
+ }
+ }
+
+ return @($seen)
+}
+
+function Get-BrowserCleanupIds {
+ param(
+ [Parameter(Mandatory = $true)]$Handle
+ )
+
+ $ids = [System.Collections.Generic.HashSet[int]]::new()
+
+ if ($Handle.Process) {
+ $null = $ids.Add([int]$Handle.Process.Id)
+ foreach ($childId in Get-ChildProcessIds -ParentId $Handle.Process.Id) {
+ $null = $ids.Add([int]$childId)
+ }
+ }
+
+ foreach ($processInfo in Get-BrowserProcessesByProfile -BrowserPath $Handle.BrowserPath -ProfileDir $Handle.ProfileDir) {
+ $null = $ids.Add([int]$processInfo.ProcessId)
+ }
+
+ $liveIds = @()
+ foreach ($processId in $ids) {
+ try {
+ Get-Process -Id $processId -ErrorAction Stop | Out-Null
+ $liveIds += $processId
+ } catch {
+ # Process already exited.
+ }
+ }
+
+ return @($liveIds | Sort-Object -Unique)
+}
+
+function Start-BrowserProcess {
+ param(
+ [Parameter(Mandatory = $true)][string]$BrowserPath,
+ [Parameter(Mandatory = $true)][int]$Port,
+ [Parameter(Mandatory = $true)][string]$ProfileDir
+ )
+
+ $baselineIds = Get-BrowserProcessIds -BrowserPath $BrowserPath
+ $arguments = Get-BrowserArguments -BrowserPath $BrowserPath -Port $Port -ProfileDir $ProfileDir
+ $stdoutPath = Join-Path $ProfileDir 'browser-stdout.log'
+ $stderrPath = Join-Path $ProfileDir 'browser-stderr.log'
+ Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
+
+ $process = Start-Process `
+ -FilePath $BrowserPath `
+ -ArgumentList $arguments `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+
+ return [pscustomobject]@{
+ BrowserPath = $BrowserPath
+ BaselineIds = $baselineIds
+ ProfileDir = $ProfileDir
+ Process = $process
+ StdOut = $stdoutPath
+ StdErr = $stderrPath
+ }
+}
+
+function Show-BrowserLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ foreach ($path in @($Handle.StdOut, $Handle.StdErr)) {
+ if (-not [string]::IsNullOrWhiteSpace($path) -and (Test-Path $path)) {
+ Get-Content $path -ErrorAction SilentlyContinue
+ }
+ }
+}
+
+function Stop-BrowserProcess {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if ($Handle.Process -and -not $Handle.Process.HasExited) {
+ foreach ($cleanupCommand in @(
+ { param($id) taskkill /PID $id /T /F *> $null },
+ { param($id) Stop-Process -Id $id -Force -ErrorAction Stop }
+ )) {
+ try {
+ & $cleanupCommand $Handle.Process.Id
+ } catch {
+ # Ignore cleanup errors here; the residual PID check below is authoritative.
+ }
+ }
+ }
+
+ $residualIds = @()
+
+ for ($attempt = 0; $attempt -lt 12; $attempt++) {
+ $residualIds = @(Get-BrowserCleanupIds -Handle $Handle)
+
+ foreach ($processId in $residualIds) {
+ foreach ($cleanupCommand in @(
+ { param($id) taskkill /PID $id /T /F *> $null },
+ { param($id) Stop-Process -Id $id -Force -ErrorAction Stop }
+ )) {
+ try {
+ & $cleanupCommand $processId
+ } catch {
+ # Ignore per-process cleanup errors during retry loop.
+ }
+ }
+ }
+
+ Start-Sleep -Milliseconds 500
+ $residualIds = @(Get-BrowserCleanupIds -Handle $Handle)
+
+ if ($residualIds.Count -eq 0) {
+ break
+ }
+ }
+
+ if ($residualIds.Count -gt 0) {
+ throw "browser cleanup leaked PIDs: $($residualIds -join ', ')"
+ }
+}
+
+function Remove-BrowserLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ $paths = @($Handle.StdOut, $Handle.StdErr) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
+ if ($paths.Count -gt 0) {
+ Remove-Item $paths -Force -ErrorAction SilentlyContinue
+ }
+}
+
+$browserPath = Resolve-BrowserPath
+Write-Host "CDP browser: $browserPath"
+$Port = if ($Port -gt 0) { $Port } else { Get-FreeTcpPort }
+$profileRoot = Join-Path (Resolve-Path (Join-Path $PSScriptRoot '..')).Path '.cache\cdp-profiles'
+New-Item -ItemType Directory -Force $profileRoot | Out-Null
+$profileDir = Join-Path $profileRoot "pw-profile-cdp-smoke-win-$Port"
+$browserReadyUrl = "http://127.0.0.1:$Port/json/version"
+$browserCdpBaseUrl = "http://127.0.0.1:$Port"
+$browserHandle = $null
+
+try {
+ for ($attempt = 1; $attempt -le 2; $attempt++) {
+ Remove-Item -Recurse -Force $profileDir -ErrorAction SilentlyContinue
+ $browserHandle = Start-BrowserProcess -BrowserPath $browserPath -Port $Port -ProfileDir $profileDir
+
+ try {
+ Wait-UrlReady -Url $browserReadyUrl -Label "browser CDP endpoint (attempt $attempt)"
+ Write-Host "CDP endpoint ready: $browserReadyUrl"
+ break
+ } catch {
+ Show-BrowserLogs $browserHandle
+ Stop-BrowserProcess $browserHandle
+ Remove-BrowserLogs $browserHandle
+ $browserHandle = $null
+
+ if ($attempt -eq 2) {
+ throw
+ }
+ }
+ }
+
+ if (-not $env:E2E_COMMAND_TIMEOUT_MS) {
+ $env:E2E_COMMAND_TIMEOUT_MS = '120000'
+ }
+
+ $env:E2E_SKIP_BROWSER_LAUNCH = '1'
+ $env:E2E_CDP_PORT = "$Port"
+ $env:E2E_CDP_BASE_URL = $browserCdpBaseUrl
+ $env:E2E_PLAYWRIGHT_CDP_URL = $browserCdpBaseUrl
+ $env:E2E_EXTERNAL_CDP = '1'
+
+ $commandName = $Command[0]
+ $commandArgs = @()
+ if ($Command.Count -gt 1) {
+ $commandArgs = $Command[1..($Command.Count - 1)]
+ }
+
+ Write-Host "Launching command: $commandName $($commandArgs -join ' ')"
+ & $commandName @commandArgs
+ if ($LASTEXITCODE -ne 0) {
+ throw "command failed with exit code $LASTEXITCODE"
+ }
+} finally {
+ Stop-BrowserProcess $browserHandle
+ Remove-BrowserLogs $browserHandle
+ Remove-Item Env:E2E_SKIP_BROWSER_LAUNCH -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_CDP_PORT -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_CDP_BASE_URL -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_PLAYWRIGHT_CDP_URL -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_EXTERNAL_CDP -ErrorAction SilentlyContinue
+ Remove-Item -Recurse -Force $profileDir -ErrorAction SilentlyContinue
+}
diff --git a/frontend/admin/scripts/run-playwright-auth-e2e.ps1 b/frontend/admin/scripts/run-playwright-auth-e2e.ps1
new file mode 100644
index 0000000..1ed5034
--- /dev/null
+++ b/frontend/admin/scripts/run-playwright-auth-e2e.ps1
@@ -0,0 +1,297 @@
+param(
+ [string]$AdminUsername = 'e2e_admin',
+ [string]$AdminPassword = 'E2EAdmin@123456',
+ [string]$AdminEmail = 'e2e_admin@example.com',
+ [int]$BrowserPort = 0,
+ [int]$BackendPort = 0,
+ [int]$FrontendPort = 0
+)
+
+$ErrorActionPreference = 'Stop'
+
+$projectRoot = (Resolve-Path (Join-Path $PSScriptRoot '..\..\..')).Path
+$frontendRoot = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
+$tempCacheRoot = Join-Path $env:TEMP 'ums-e2e-cache'
+$goCacheDir = Join-Path $tempCacheRoot 'go-build'
+$goModCacheDir = Join-Path $tempCacheRoot 'gomod'
+$goPathDir = Join-Path $tempCacheRoot 'gopath'
+$serverExePath = Join-Path $env:TEMP ("ums-server-playwright-e2e-" + [guid]::NewGuid().ToString('N') + '.exe')
+$e2eRunRoot = Join-Path $env:TEMP ("ums-playwright-e2e-" + [guid]::NewGuid().ToString('N'))
+$e2eDataRoot = Join-Path $e2eRunRoot 'data'
+$e2eDbPath = Join-Path $e2eDataRoot 'user_management.e2e.db'
+$smtpCaptureFile = Join-Path $e2eRunRoot 'smtp-capture.jsonl'
+
+New-Item -ItemType Directory -Force $goCacheDir, $goModCacheDir, $goPathDir, $e2eDataRoot | Out-Null
+
+function Get-FreeTcpPort {
+ $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Loopback, 0)
+ $listener.Start()
+ try {
+ return ([System.Net.IPEndPoint]$listener.LocalEndpoint).Port
+ } finally {
+ $listener.Stop()
+ }
+}
+
+function Test-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url
+ )
+
+ try {
+ $response = Invoke-WebRequest $Url -UseBasicParsing -TimeoutSec 2
+ return $response.StatusCode -ge 200 -and $response.StatusCode -lt 500
+ } catch {
+ return $false
+ }
+}
+
+function Wait-UrlReady {
+ param(
+ [Parameter(Mandatory = $true)][string]$Url,
+ [Parameter(Mandatory = $true)][string]$Label,
+ [int]$RetryCount = 120,
+ [int]$DelayMs = 500
+ )
+
+ for ($i = 0; $i -lt $RetryCount; $i++) {
+ if (Test-UrlReady -Url $Url) {
+ return
+ }
+ Start-Sleep -Milliseconds $DelayMs
+ }
+
+ throw "$Label did not become ready: $Url"
+}
+
+function Start-ManagedProcess {
+ param(
+ [Parameter(Mandatory = $true)][string]$Name,
+ [Parameter(Mandatory = $true)][string]$FilePath,
+ [string[]]$ArgumentList = @(),
+ [Parameter(Mandatory = $true)][string]$WorkingDirectory
+ )
+
+ $stdoutPath = Join-Path $env:TEMP "$Name-stdout.log"
+ $stderrPath = Join-Path $env:TEMP "$Name-stderr.log"
+ Remove-Item $stdoutPath, $stderrPath -Force -ErrorAction SilentlyContinue
+
+ if ($ArgumentList -and $ArgumentList.Count -gt 0) {
+ $process = Start-Process `
+ -FilePath $FilePath `
+ -ArgumentList $ArgumentList `
+ -WorkingDirectory $WorkingDirectory `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+ } else {
+ $process = Start-Process `
+ -FilePath $FilePath `
+ -WorkingDirectory $WorkingDirectory `
+ -PassThru `
+ -WindowStyle Hidden `
+ -RedirectStandardOutput $stdoutPath `
+ -RedirectStandardError $stderrPath
+ }
+
+ return [pscustomobject]@{
+ Name = $Name
+ Process = $process
+ StdOut = $stdoutPath
+ StdErr = $stderrPath
+ }
+}
+
+function Stop-ManagedProcess {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if ($Handle.Process -and -not $Handle.Process.HasExited) {
+ try {
+ taskkill /PID $Handle.Process.Id /T /F *> $null
+ } catch {
+ Stop-Process -Id $Handle.Process.Id -Force -ErrorAction SilentlyContinue
+ }
+ }
+}
+
+function Show-ManagedProcessLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ if (Test-Path $Handle.StdOut) {
+ Get-Content $Handle.StdOut -ErrorAction SilentlyContinue
+ }
+ if (Test-Path $Handle.StdErr) {
+ Get-Content $Handle.StdErr -ErrorAction SilentlyContinue
+ }
+}
+
+function Remove-ManagedProcessLogs {
+ param(
+ [Parameter(Mandatory = $false)]$Handle
+ )
+
+ if (-not $Handle) {
+ return
+ }
+
+ Remove-Item $Handle.StdOut, $Handle.StdErr -Force -ErrorAction SilentlyContinue
+}
+
+$backendHandle = $null
+$frontendHandle = $null
+$smtpHandle = $null
+$selectedBackendPort = if ($BackendPort -gt 0) { $BackendPort } else { Get-FreeTcpPort }
+$selectedFrontendPort = if ($FrontendPort -gt 0) { $FrontendPort } else { Get-FreeTcpPort }
+$selectedSMTPPort = Get-FreeTcpPort
+$backendBaseUrl = "http://127.0.0.1:$selectedBackendPort"
+$frontendBaseUrl = "http://127.0.0.1:$selectedFrontendPort"
+
+try {
+ Push-Location $projectRoot
+ try {
+ $env:GOCACHE = $goCacheDir
+ $env:GOMODCACHE = $goModCacheDir
+ $env:GOPATH = $goPathDir
+ go build -o $serverExePath .\cmd\server\main.go
+ if ($LASTEXITCODE -ne 0) {
+ throw 'server build failed'
+ }
+ } finally {
+ Pop-Location
+ Remove-Item Env:GOCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOMODCACHE -ErrorAction SilentlyContinue
+ Remove-Item Env:GOPATH -ErrorAction SilentlyContinue
+ }
+
+ $env:UMS_SERVER_PORT = "$selectedBackendPort"
+ $env:UMS_DATABASE_SQLITE_PATH = $e2eDbPath
+$env:UMS_SERVER_MODE = 'debug'
+$env:UMS_PASSWORD_RESET_SITE_URL = $frontendBaseUrl
+$env:UMS_CORS_ALLOWED_ORIGINS = "$frontendBaseUrl,http://localhost:$selectedFrontendPort"
+ $env:UMS_LOGGING_OUTPUT = 'stdout'
+ $env:UMS_EMAIL_HOST = '127.0.0.1'
+ $env:UMS_EMAIL_PORT = "$selectedSMTPPort"
+ $env:UMS_EMAIL_FROM_EMAIL = 'noreply@test.local'
+ $env:UMS_EMAIL_FROM_NAME = 'UMS E2E'
+
+ Write-Host "playwright e2e backend: $backendBaseUrl"
+ Write-Host "playwright e2e frontend: $frontendBaseUrl"
+ Write-Host "playwright e2e smtp: 127.0.0.1:$selectedSMTPPort"
+ Write-Host "playwright e2e sqlite: $e2eDbPath"
+
+ $smtpHandle = Start-ManagedProcess `
+ -Name 'ums-smtp-capture' `
+ -FilePath 'node' `
+ -ArgumentList @((Join-Path $PSScriptRoot 'mock-smtp-capture.mjs'), '--port', "$selectedSMTPPort", '--output', $smtpCaptureFile) `
+ -WorkingDirectory $frontendRoot
+
+ Start-Sleep -Milliseconds 500
+ if ($smtpHandle.Process -and $smtpHandle.Process.HasExited) {
+ Show-ManagedProcessLogs $smtpHandle
+ throw 'smtp capture server failed to start'
+ }
+
+ $backendHandle = Start-ManagedProcess `
+ -Name 'ums-backend-playwright' `
+ -FilePath $serverExePath `
+ -WorkingDirectory $projectRoot
+
+ try {
+ Wait-UrlReady -Url "$backendBaseUrl/health" -Label 'backend'
+ } catch {
+ Show-ManagedProcessLogs $backendHandle
+ throw
+ }
+
+ $env:VITE_API_PROXY_TARGET = $backendBaseUrl
+ $env:VITE_API_BASE_URL = '/api/v1'
+ $frontendHandle = Start-ManagedProcess `
+ -Name 'ums-frontend-playwright' `
+ -FilePath 'npm.cmd' `
+ -ArgumentList @('run', 'dev', '--', '--host', '127.0.0.1', '--port', "$selectedFrontendPort") `
+ -WorkingDirectory $frontendRoot
+
+ try {
+ Wait-UrlReady -Url $frontendBaseUrl -Label 'frontend'
+ } catch {
+ Show-ManagedProcessLogs $frontendHandle
+ throw
+ }
+
+ $env:E2E_LOGIN_USERNAME = $AdminUsername
+ $env:E2E_LOGIN_PASSWORD = $AdminPassword
+ $env:E2E_LOGIN_EMAIL = $AdminEmail
+ $env:E2E_EXPECT_ADMIN_BOOTSTRAP = '1'
+ $env:E2E_EXTERNAL_WEB_SERVER = '1'
+ $env:E2E_BASE_URL = $frontendBaseUrl
+ $env:E2E_SMTP_CAPTURE_FILE = $smtpCaptureFile
+
+ Push-Location $frontendRoot
+ try {
+ $lastError = $null
+ for ($attempt = 1; $attempt -le 2; $attempt++) {
+ try {
+ & (Join-Path $PSScriptRoot 'run-cdp-smoke.ps1') `
+ -Port $BrowserPort `
+ -Command @('node', './scripts/run-playwright-cdp-e2e.mjs')
+ $lastError = $null
+ break
+ } catch {
+ $lastError = $_
+ if ($attempt -ge 2) {
+ throw
+ }
+ $retryReason = if ($_.Exception -and $_.Exception.Message) { $_.Exception.Message } else { $_ | Out-String }
+ Write-Host "playwright-cdp suite retry: restarting browser and rerunning attempt $($attempt + 1) :: $retryReason"
+ Start-Sleep -Seconds 1
+ }
+ }
+
+ if ($lastError) {
+ throw $lastError
+ }
+ } finally {
+ Pop-Location
+ Remove-Item Env:E2E_LOGIN_USERNAME -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_LOGIN_PASSWORD -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_LOGIN_EMAIL -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_EXPECT_ADMIN_BOOTSTRAP -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_EXTERNAL_WEB_SERVER -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_BASE_URL -ErrorAction SilentlyContinue
+ Remove-Item Env:E2E_SMTP_CAPTURE_FILE -ErrorAction SilentlyContinue
+ }
+} finally {
+ Stop-ManagedProcess $frontendHandle
+ Remove-ManagedProcessLogs $frontendHandle
+ Stop-ManagedProcess $backendHandle
+ Remove-ManagedProcessLogs $backendHandle
+ Stop-ManagedProcess $smtpHandle
+ Remove-ManagedProcessLogs $smtpHandle
+ Remove-Item Env:UMS_SERVER_PORT -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_DATABASE_SQLITE_PATH -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_SERVER_MODE -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_PASSWORD_RESET_SITE_URL -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_CORS_ALLOWED_ORIGINS -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_LOGGING_OUTPUT -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_EMAIL_HOST -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_EMAIL_PORT -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_EMAIL_FROM_EMAIL -ErrorAction SilentlyContinue
+ Remove-Item Env:UMS_EMAIL_FROM_NAME -ErrorAction SilentlyContinue
+ Remove-Item Env:VITE_API_PROXY_TARGET -ErrorAction SilentlyContinue
+ Remove-Item Env:VITE_API_BASE_URL -ErrorAction SilentlyContinue
+ Remove-Item $serverExePath -Force -ErrorAction SilentlyContinue
+ Remove-Item $e2eRunRoot -Recurse -Force -ErrorAction SilentlyContinue
+}
diff --git a/frontend/admin/scripts/run-playwright-cdp-e2e.mjs b/frontend/admin/scripts/run-playwright-cdp-e2e.mjs
new file mode 100644
index 0000000..74c5031
--- /dev/null
+++ b/frontend/admin/scripts/run-playwright-cdp-e2e.mjs
@@ -0,0 +1,1176 @@
+import process from 'node:process'
+import { access, mkdir, mkdtemp, readFile, readdir, rm } from 'node:fs/promises'
+import { constants as fsConstants } from 'node:fs'
+import { spawn } from 'node:child_process'
+import net from 'node:net'
+import { tmpdir } from 'node:os'
+import path from 'node:path'
+import { chromium, expect } from '@playwright/test'
+
+const TEXT = {
+ accessControl: '\u8bbf\u95ee\u63a7\u5236',
+ adminBootstrapTitle: '\u7cfb\u7edf\u5c1a\u672a\u521d\u59cb\u5316\u9996\u4e2a\u7ba1\u7406\u5458\u8d26\u53f7',
+ adminRoleName: '\u7ba1\u7406\u5458',
+ adminBootstrapAction: '\u521d\u59cb\u5316\u7ba1\u7406\u5458',
+ adminBootstrapPageTitle: '\u521d\u59cb\u5316\u9996\u4e2a\u7ba1\u7406\u5458\u8d26\u53f7',
+ appTitle: '\u7528\u6237\u7ba1\u7406\u7cfb\u7edf',
+ assignPermissions: '\u5206\u914d\u6743\u9650',
+ assignRoles: '\u5206\u914d\u89d2\u8272',
+ assignRolesAction: '\u89d2\u8272',
+ backToLogin: '\u8fd4\u56de\u767b\u5f55',
+ bootstrapAdminConfirmPasswordPlaceholder: '\u786e\u8ba4\u7ba1\u7406\u5458\u5bc6\u7801',
+ bootstrapAdminEmailPlaceholder: '\u7ba1\u7406\u5458\u90ae\u7bb1\uff08\u9009\u586b\uff09',
+ bootstrapAdminPasswordPlaceholder: '\u7ba1\u7406\u5458\u5bc6\u7801',
+ bootstrapAdminSubmit: '\u5b8c\u6210\u521d\u59cb\u5316\u5e76\u8fdb\u5165\u7cfb\u7edf',
+ bootstrapAdminUsernamePlaceholder: '\u7ba1\u7406\u5458\u7528\u6237\u540d',
+ confirmPasswordPlaceholder: '\u786e\u8ba4\u5bc6\u7801',
+ createAccount: '\u521b\u5efa\u8d26\u53f7',
+ createUser: '\u521b\u5efa\u7528\u6237',
+ createUserEmailPlaceholder: '\u90ae\u7bb1\u5730\u5740',
+ createUserPasswordPlaceholder: '\u8bf7\u8f93\u5165\u521d\u59cb\u5bc6\u7801',
+ createUserUsernamePlaceholder: '\u8bf7\u8f93\u5165\u7528\u6237\u540d',
+ createRole: '\u521b\u5efa\u89d2\u8272',
+ dashboard: '\u603b\u89c8',
+ emailCodeLogin: '\u90ae\u7bb1\u9a8c\u8bc1\u7801',
+ emailActivationSuccess: '\u90ae\u7bb1\u9a8c\u8bc1\u6210\u529f',
+ forgotPassword: '\u5fd8\u8bb0\u5bc6\u7801\uff1f',
+ loginAction: '\u767b\u5f55',
+ loginNow: '\u7acb\u5373\u767b\u5f55',
+ logout: '\u9000\u51fa\u767b\u5f55',
+ passwordPlaceholder: '\u5bc6\u7801',
+ permissionsAction: '\u6743\u9650',
+ permissionsHint: '\u9009\u62e9\u8981\u5206\u914d\u7ed9\u8be5\u89d2\u8272\u7684\u6743\u9650',
+ profile: '\u4e2a\u4eba\u8d44\u6599',
+ registerEmailPlaceholder: '\u90ae\u7bb1\u5730\u5740\uff08\u9009\u586b\uff09',
+ registerSuccess: '\u6ce8\u518c\u6210\u529f',
+ roleFilter: '\u89d2\u8272\u540d\u79f0/\u4ee3\u7801',
+ roles: '\u89d2\u8272\u7ba1\u7406',
+ smsCodeLogin: '\u77ed\u4fe1\u9a8c\u8bc1\u7801',
+ todaySuccessLogins: '\u4eca\u65e5\u6210\u529f\u767b\u5f55',
+ totalUsers: '\u7528\u6237\u603b\u6570',
+ userDetail: '\u7528\u6237\u8be6\u60c5',
+ userDetailAction: '\u8be6\u60c5',
+ userId: '\u7528\u6237 ID',
+ usernamePlaceholder: '\u7528\u6237\u540d',
+ users: '\u7528\u6237\u7ba1\u7406',
+ usersFilter: '\u7528\u6237\u540d/\u90ae\u7bb1/\u624b\u673a\u53f7',
+ welcomeLogin: '\u6b22\u8fce\u767b\u5f55',
+}
+
+const BASE_URL = (process.env.E2E_BASE_URL ?? 'http://127.0.0.1:3000').replace(/\/$/, '')
+const VIEWPORTS = [
+ { name: 'desktop', width: 1440, height: 960 },
+ { name: 'tablet', width: 820, height: 1180 },
+ { name: 'mobile', width: 390, height: 844 },
+]
+const IGNORED_CONSOLE_ERRORS = [
+ 'Static function can not consume context like dynamic theme',
+]
+const IGNORED_REQUEST_FAILURES = new Set([
+ 'net::ERR_ABORTED',
+ 'net::ERR_FAILED',
+])
+const DEBUG = process.env.E2E_DEBUG === '1'
+const STARTUP_TIMEOUT_MS = Number(process.env.E2E_STARTUP_TIMEOUT_MS ?? 30000)
+const SMTP_CAPTURE_FILE = (process.env.E2E_SMTP_CAPTURE_FILE ?? '').trim()
+const SESSION_PRESENCE_COOKIE_NAME = 'ums_session_present'
+
+let managedCdpUrl = null
+
+function appUrl(pathname) {
+ return new URL(pathname, `${BASE_URL}/`).toString()
+}
+
+function delay(ms) {
+ return new Promise((resolve) => setTimeout(resolve, ms))
+}
+
+function escapeRegex(value) {
+ return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
+}
+
+function requireEnv(name) {
+ const value = (process.env[name] ?? '').trim()
+ if (!value) {
+ throw new Error(`${name} is required.`)
+ }
+ return value
+}
+
+async function readCapturedMessages() {
+ if (!SMTP_CAPTURE_FILE) {
+ return []
+ }
+
+ try {
+ const content = await readFile(SMTP_CAPTURE_FILE, 'utf8')
+ return content
+ .split(/\r?\n/)
+ .filter(Boolean)
+ .flatMap((line) => {
+ try {
+ return [JSON.parse(line)]
+ } catch {
+ return []
+ }
+ })
+ } catch (error) {
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
+ return []
+ }
+ throw error
+ }
+}
+
+function normalizeEmail(value) {
+ return String(value ?? '')
+ .trim()
+ .replace(/^<|>$/g, '')
+ .toLowerCase()
+}
+
+function capturedMessageMatchesRecipient(message, email) {
+ const target = normalizeEmail(email)
+ if (!target) {
+ return false
+ }
+
+ const recipients = Array.isArray(message?.rcptTo) ? message.rcptTo : []
+ return recipients.some((candidate) => normalizeEmail(candidate) === target)
+}
+
+function extractActivationLink(message) {
+ const body = String(message?.data ?? '')
+ const match = body.match(/https?:\/\/[^\s"<]+\/activate-account\?token=[^"\s<]+/)
+ return match?.[0] ?? null
+}
+
+async function waitForActivationLink(email, timeoutMs = 20_000) {
+ const startedAt = Date.now()
+
+ while (Date.now() - startedAt < timeoutMs) {
+ const messages = await readCapturedMessages()
+ const matchedMessages = messages.filter((message) => capturedMessageMatchesRecipient(message, email))
+
+ for (let index = matchedMessages.length - 1; index >= 0; index -= 1) {
+ const activationLink = extractActivationLink(matchedMessages[index])
+ if (activationLink) {
+ return activationLink
+ }
+ }
+
+ await delay(250)
+ }
+
+ throw new Error(`Timed out waiting for activation email for ${email}.`)
+}
+
+function resolveCdpUrl() {
+ if (managedCdpUrl) {
+ return managedCdpUrl
+ }
+
+ const baseUrl = (process.env.E2E_PLAYWRIGHT_CDP_URL ?? process.env.E2E_CDP_BASE_URL ?? '').trim()
+ if (baseUrl) {
+ return baseUrl
+ }
+
+ const port = Number(process.env.E2E_CDP_PORT ?? 0)
+ if (port > 0) {
+ return `http://127.0.0.1:${port}`
+ }
+
+ throw new Error('E2E_PLAYWRIGHT_CDP_URL or E2E_CDP_PORT is required.')
+}
+
+function createSignals() {
+ return {
+ consoleErrors: [],
+ dialogs: [],
+ pageErrors: [],
+ popups: [],
+ requestFailures: [],
+ unauthorizedResponses: [],
+ windowGuardEvents: [],
+ }
+}
+
+function logDebug(message) {
+ if (DEBUG) {
+ console.log(`[debug] ${message}`)
+ }
+}
+
+function formatError(error) {
+ if (!error) {
+ return 'unknown error'
+ }
+
+ if (error instanceof Error) {
+ return error.message || error.name
+ }
+
+ return String(error)
+}
+
+function isRetryableTargetError(error) {
+ const message = formatError(error)
+ return (
+ message.includes('Target page, context or browser has been closed') ||
+ message.includes('Target closed') ||
+ message.includes('Browser has been closed')
+ )
+}
+
+async function assertFileExists(filePath) {
+ await access(filePath, fsConstants.F_OK)
+}
+
+function isHeadlessShellBrowser(browserPath) {
+ return path.basename(browserPath).toLowerCase().includes('headless-shell')
+}
+
+async function resolveManagedBrowserPath() {
+ const envCandidates = [
+ process.env.E2E_BROWSER_PATH,
+ process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
+ process.env.CHROME_HEADLESS_SHELL_PATH,
+ ]
+ .map((value) => (value ?? '').trim())
+ .filter(Boolean)
+
+ for (const candidate of envCandidates) {
+ await assertFileExists(candidate)
+ return candidate
+ }
+
+ for (const candidate of [
+ 'C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe',
+ 'C:\\Program Files (x86)\\Google\\Chrome\\Application\\chrome.exe',
+ 'C:\\Program Files\\Microsoft\\Edge\\Application\\msedge.exe',
+ 'C:\\Program Files (x86)\\Microsoft\\Edge\\Application\\msedge.exe',
+ ]) {
+ try {
+ await assertFileExists(candidate)
+ return candidate
+ } catch {
+ continue
+ }
+ }
+
+ const baseDir = path.join(process.env.LOCALAPPDATA ?? '', 'ms-playwright')
+ const candidates = []
+
+ try {
+ const entries = await readdir(baseDir, { withFileTypes: true })
+ for (const entry of entries) {
+ if (!entry.isDirectory() || !entry.name.startsWith('chromium_headless_shell-')) {
+ continue
+ }
+
+ candidates.push(
+ path.join(baseDir, entry.name, 'chrome-headless-shell-win64', 'chrome-headless-shell.exe'),
+ )
+ }
+ } catch {
+ throw new Error('failed to scan Playwright browser cache under LOCALAPPDATA')
+ }
+
+ candidates.sort().reverse()
+ for (const candidate of candidates) {
+ try {
+ await assertFileExists(candidate)
+ return candidate
+ } catch {
+ continue
+ }
+ }
+
+ throw new Error('No compatible browser found for Playwright CDP E2E.')
+}
+
+async function createManagedBrowserProfileDir(browserPath, port) {
+ if (!isHeadlessShellBrowser(browserPath)) {
+ return await mkdtemp(path.join(tmpdir(), 'pw-playwright-cdp-'))
+ }
+
+ const profileRoot = path.join(process.cwd(), '.cache', 'cdp-profiles')
+ await mkdir(profileRoot, { recursive: true })
+ return path.join(profileRoot, `pw-profile-playwright-cdp-${port}`)
+}
+
+function startManagedBrowser(browserPath, port, profileDir) {
+ const args = [`--remote-debugging-port=${port}`, `--user-data-dir=${profileDir}`, '--no-sandbox']
+
+ if (isHeadlessShellBrowser(browserPath)) {
+ args.push('--single-process')
+ } else {
+ args.push(
+ '--disable-dev-shm-usage',
+ '--disable-background-networking',
+ '--disable-background-timer-throttling',
+ '--disable-renderer-backgrounding',
+ '--disable-sync',
+ '--headless=new',
+ )
+ }
+
+ args.push('about:blank')
+
+ const browserProcess = spawn(browserPath, args, {
+ stdio: ['ignore', 'ignore', 'pipe'],
+ windowsHide: true,
+ })
+
+ let stderr = ''
+ browserProcess.stderr?.on('data', (chunk) => {
+ stderr += chunk.toString()
+ if (stderr.length > 4000) {
+ stderr = stderr.slice(-4000)
+ }
+ })
+
+ browserProcess.on('exit', (code, signal) => {
+ if (code !== 0 && signal == null) {
+ console.error(`managed browser exited unexpectedly with code ${code}`)
+ const details = stderr.trim()
+ if (details) {
+ console.error(details)
+ }
+ }
+ })
+
+ return browserProcess
+}
+
+async function killManagedBrowser(browserProcess) {
+ if (!browserProcess || browserProcess.exitCode != null || browserProcess.pid == null) {
+ return
+ }
+
+ await new Promise((resolve) => {
+ const killer = spawn('taskkill', ['/PID', String(browserProcess.pid), '/T', '/F'], {
+ stdio: 'ignore',
+ windowsHide: true,
+ })
+
+ killer.once('error', () => {
+ try {
+ browserProcess.kill('SIGKILL')
+ } catch {
+ // ignore
+ }
+ resolve()
+ })
+
+ killer.once('exit', () => resolve())
+ })
+}
+
+async function getFreePort() {
+ return await new Promise((resolve, reject) => {
+ const server = net.createServer()
+ server.unref()
+ server.on('error', reject)
+ server.listen(0, '127.0.0.1', () => {
+ const address = server.address()
+ if (address == null || typeof address === 'string') {
+ server.close(() => reject(new Error('failed to resolve a free port')))
+ return
+ }
+
+ server.close((error) => {
+ if (error) {
+ reject(error)
+ return
+ }
+ resolve(address.port)
+ })
+ })
+ })
+}
+
+async function waitForHttp(url, timeoutMs, label) {
+ const startedAt = Date.now()
+ let lastError = null
+
+ while (Date.now() - startedAt < timeoutMs) {
+ try {
+ const response = await fetch(url)
+ if (response.ok) {
+ return response
+ }
+ lastError = new Error(`${label} returned ${response.status}`)
+ } catch (error) {
+ lastError = error
+ }
+ await delay(250)
+ }
+
+ throw new Error(`timed out waiting for ${label}: ${formatError(lastError)}`)
+}
+
+async function waitForJson(url, timeoutMs, label = url) {
+ const response = await waitForHttp(url, timeoutMs, label)
+ return await response.json()
+}
+
+function isIgnoredConsoleError(text) {
+ if (text.includes('favicon') && text.includes('404')) {
+ return true
+ }
+
+ return IGNORED_CONSOLE_ERRORS.some((value) => text.includes(value))
+}
+
+function formatSignals(signals) {
+ const lines = []
+
+ if (signals.windowGuardEvents.length > 0) {
+ lines.push(`window-guard events:\n${signals.windowGuardEvents.join('\n')}`)
+ }
+ if (signals.dialogs.length > 0) {
+ lines.push(`native dialogs:\n${signals.dialogs.join('\n')}`)
+ }
+ if (signals.popups.length > 0) {
+ lines.push(`popup pages:\n${signals.popups.join('\n')}`)
+ }
+ if (signals.pageErrors.length > 0) {
+ lines.push(`page errors:\n${signals.pageErrors.join('\n\n')}`)
+ }
+ if (signals.consoleErrors.length > 0) {
+ lines.push(`console errors:\n${signals.consoleErrors.join('\n')}`)
+ }
+ if (signals.requestFailures.length > 0) {
+ lines.push(`request failures:\n${signals.requestFailures.join('\n')}`)
+ }
+ if (signals.unauthorizedResponses.length > 0) {
+ lines.push(`unauthorized responses:\n${signals.unauthorizedResponses.join('\n')}`)
+ }
+
+ return lines.join('\n\n')
+}
+
+function assertCleanSignals(signals) {
+ const output = formatSignals(signals)
+ if (output) {
+ throw new Error(output)
+ }
+}
+
+function attachSignalCollectors(page, signals) {
+ const onConsole = (message) => {
+ if (message.type() !== 'error') {
+ return
+ }
+
+ const text = message.text()
+ if (text.startsWith('[window-guard]')) {
+ signals.windowGuardEvents.push(text)
+ return
+ }
+
+ if (!isIgnoredConsoleError(text)) {
+ signals.consoleErrors.push(text)
+ }
+ }
+
+ const onDialog = (dialog) => {
+ signals.dialogs.push(`${dialog.type()}: ${dialog.message()}`)
+ void dialog.dismiss().catch(() => {})
+ }
+
+ const onPageError = (error) => {
+ signals.pageErrors.push(error.stack ?? error.message)
+ }
+
+ const onPopup = (popup) => {
+ signals.popups.push(popup.url() || 'about:blank')
+ void popup.close().catch(() => {})
+ }
+
+ const onRequestFailed = (request) => {
+ const failureText = request.failure()?.errorText ?? 'unknown failure'
+ if (!IGNORED_REQUEST_FAILURES.has(failureText)) {
+ signals.requestFailures.push(`${request.method()} ${request.url()} :: ${failureText}`)
+ }
+ }
+
+ const onResponse = (response) => {
+ if (response.status() === 401) {
+ signals.unauthorizedResponses.push(`${response.request().method()} ${response.url()}`)
+ }
+ }
+
+ page.on('console', onConsole)
+ page.on('dialog', onDialog)
+ page.on('pageerror', onPageError)
+ page.on('popup', onPopup)
+ page.on('requestfailed', onRequestFailed)
+ page.on('response', onResponse)
+
+ return () => {
+ page.off('console', onConsole)
+ page.off('dialog', onDialog)
+ page.off('pageerror', onPageError)
+ page.off('popup', onPopup)
+ page.off('requestfailed', onRequestFailed)
+ page.off('response', onResponse)
+ }
+}
+
+async function resetBrowserState(context, page) {
+ logDebug('resetting browser state')
+ await context.clearCookies()
+ await page.goto(appUrl('/login'), { waitUntil: 'domcontentloaded' })
+ await page.evaluate(() => {
+ localStorage.clear()
+ sessionStorage.clear()
+ })
+ await page.goto('about:blank')
+}
+
+async function openDevToolsPageTarget() {
+ const endpoints = [
+ `${resolveCdpUrl()}/json/new?about:blank`,
+ `${resolveCdpUrl()}/json/new?url=about:blank`,
+ ]
+
+ for (const endpoint of endpoints) {
+ for (const method of ['PUT', 'GET']) {
+ try {
+ await fetch(endpoint, { method })
+ return
+ } catch {
+ // try next variant
+ }
+ }
+ }
+}
+
+async function connectBrowserWithRetry() {
+ let lastError = null
+
+ for (let attempt = 1; attempt <= 3; attempt += 1) {
+ try {
+ return await chromium.connectOverCDP(resolveCdpUrl())
+ } catch (error) {
+ lastError = error
+ if (attempt >= 3) {
+ break
+ }
+ await delay(500)
+ }
+ }
+
+ throw lastError ?? new Error('Failed to connect to the Chromium CDP endpoint.')
+}
+
+async function ensurePersistentPage(browser, context) {
+ let page = context.pages().find((candidate) => !candidate.isClosed())
+ if (page) {
+ return page
+ }
+
+ try {
+ const session = await browser.newBrowserCDPSession()
+ try {
+ await session.send('Target.createTarget', { url: 'about:blank' })
+ } finally {
+ await session.detach().catch(() => {})
+ }
+ } catch {
+ // fall through to DevTools HTTP endpoint fallback
+ }
+
+ await openDevToolsPageTarget()
+
+ for (let attempt = 0; attempt < 50; attempt += 1) {
+ page = context.pages().find((candidate) => !candidate.isClosed())
+ if (page) {
+ return page
+ }
+ await delay(100)
+ }
+
+ return null
+}
+
+async function getProtectedRouteRedirect(page) {
+ return await page.evaluate(() => {
+ return {
+ path: window.location.pathname,
+ redirectFrom: history.state?.usr?.from?.pathname ?? null,
+ title: document.title,
+ }
+ })
+}
+
+async function clickSidebarMenu(page, label) {
+ const menuItems = page
+ .locator('.ant-layout-sider .ant-menu-item, .ant-drawer .ant-menu-item')
+ .filter({ hasText: label })
+
+ const count = await menuItems.count()
+ for (let index = 0; index < count; index += 1) {
+ const menuItem = menuItems.nth(index)
+ if (await menuItem.isVisible()) {
+ await forceClick(menuItem)
+ return
+ }
+ }
+
+ throw new Error(`No visible menu item found for ${label}.`)
+}
+
+async function expandSidebarGroup(page, label) {
+ const groups = page
+ .locator('.ant-layout-sider .ant-menu-submenu-title, .ant-drawer .ant-menu-submenu-title')
+ .filter({ hasText: label })
+
+ const count = await groups.count()
+ for (let index = 0; index < count; index += 1) {
+ const group = groups.nth(index)
+ if (await group.isVisible()) {
+ await forceClick(group)
+ return
+ }
+ }
+
+ throw new Error(`No visible menu group found for ${label}.`)
+}
+
+async function forceFillInput(locator, value) {
+ await expect(locator).toBeVisible()
+ await locator.evaluate((element, nextValue) => {
+ if (!(element instanceof HTMLInputElement)) {
+ throw new Error('Target element is not an input.')
+ }
+
+ element.focus()
+ const prototype = Object.getPrototypeOf(element)
+ const descriptor = Object.getOwnPropertyDescriptor(prototype, 'value')
+ if (descriptor?.set) {
+ descriptor.set.call(element, nextValue)
+ } else {
+ element.value = nextValue
+ }
+
+ element.dispatchEvent(new Event('input', { bubbles: true }))
+ element.dispatchEvent(new Event('change', { bubbles: true }))
+ }, value)
+}
+
+async function forceClick(locator) {
+ await expect(locator).toBeVisible()
+ await locator.evaluate((element) => {
+ if (!(element instanceof HTMLElement)) {
+ throw new Error('Target element is not clickable.')
+ }
+
+ element.scrollIntoView({ block: 'center', inline: 'center' })
+ const target =
+ element.closest(
+ 'button, a, [role="button"], [role="menuitem"], .ant-btn, .ant-menu-item, .ant-modal-close, .ant-drawer-close',
+ ) ?? element
+
+ target.dispatchEvent(new MouseEvent('mouseover', { bubbles: true }))
+ target.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))
+ target.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
+ target.dispatchEvent(new MouseEvent('click', { bubbles: true }))
+ })
+}
+
+async function readRefreshToken(page) {
+ return await page.evaluate((cookieName) => {
+ const target = `${cookieName}=`
+ const matched = document.cookie
+ .split(';')
+ .map((cookie) => cookie.trim())
+ .find((cookie) => cookie.startsWith(target))
+
+ return matched ? matched.slice(target.length) : null
+ }, SESSION_PRESENCE_COOKIE_NAME)
+}
+
+async function assertApiSuccessResponse(response, label) {
+ const responseBody = await response.text().catch(() => '')
+ if (!response.ok()) {
+ throw new Error(`${label} request failed: ${response.status()} ${responseBody}`)
+ }
+
+ let payload
+ try {
+ payload = JSON.parse(responseBody)
+ } catch (error) {
+ if (error instanceof SyntaxError) {
+ throw new Error(`${label} response is not valid JSON: ${responseBody}`)
+ }
+ throw error
+ }
+
+ if (payload?.code !== 0) {
+ throw new Error(`${label} business response failed: ${responseBody}`)
+ }
+
+ return payload
+}
+
+async function loginWithPassword(page, username, password, expectedUrlPattern) {
+ const usernameInput = page
+ .locator(`input[autocomplete="username"], input[placeholder="${TEXT.usernamePlaceholder}"]`)
+ .first()
+ const loginForm = usernameInput.locator('xpath=ancestor::form[1]')
+
+ await forceFillInput(usernameInput, username)
+ await forceFillInput(loginForm.locator('input[type="password"]').first(), password)
+ const loginResponsePromise = page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/login') && response.request().method() === 'POST'
+ }, { timeout: 5_000 }).catch(() => null)
+ await forceClick(loginForm.locator('button[type="submit"]').first())
+
+ const loginResponse = await loginResponsePromise
+ if (loginResponse) {
+ await assertApiSuccessResponse(loginResponse, 'password login')
+ }
+
+ if (expectedUrlPattern) {
+ await expect(page).toHaveURL(expectedUrlPattern, { timeout: 30 * 1000 })
+ }
+}
+
+async function loginFromLoginPage(page) {
+ const username = requireEnv('E2E_LOGIN_USERNAME')
+ const password = requireEnv('E2E_LOGIN_PASSWORD')
+
+ await page.goto(appUrl('/login'))
+ await expect(page).toHaveURL(/\/login$/)
+ await expect(page.getByRole('heading', { name: TEXT.welcomeLogin })).toBeVisible()
+
+ await loginWithPassword(page, username, password, /\/dashboard$/)
+
+ return { username, password }
+}
+
+async function verifyAdminBootstrapWorkflow(page) {
+ const username = requireEnv('E2E_LOGIN_USERNAME')
+ const password = requireEnv('E2E_LOGIN_PASSWORD')
+ const email = (process.env.E2E_LOGIN_EMAIL ?? `${username}@example.com`).trim()
+
+ const capabilitiesResponse = page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/capabilities') && response.request().method() === 'GET'
+ })
+
+ await page.goto(appUrl('/login'))
+ const capabilitiesPayload = await (await capabilitiesResponse).json()
+ expect(Boolean(capabilitiesPayload?.data?.admin_bootstrap_required)).toBe(true)
+
+ await expect(page.getByText(TEXT.adminBootstrapTitle)).toBeVisible()
+ await forceClick(page.getByRole('button', { name: TEXT.adminBootstrapAction }))
+ await expect(page).toHaveURL(/\/bootstrap-admin$/)
+ await expect(page.getByRole('heading', { name: TEXT.adminBootstrapPageTitle })).toBeVisible()
+
+ await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminUsernamePlaceholder}"]`).first(), username)
+ await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminEmailPlaceholder}"]`).first(), email)
+ await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminPasswordPlaceholder}"]`).first(), password)
+ await forceFillInput(page.locator(`input[placeholder="${TEXT.bootstrapAdminConfirmPasswordPlaceholder}"]`).first(), password)
+
+ const [bootstrapResponse] = await Promise.all([
+ page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/bootstrap-admin') && response.request().method() === 'POST'
+ }),
+ forceClick(page.getByRole('button', { name: TEXT.bootstrapAdminSubmit })),
+ ])
+ await assertApiSuccessResponse(bootstrapResponse, 'bootstrap admin')
+
+ await expect(page).toHaveURL(/\/dashboard$/, { timeout: 30 * 1000 })
+ await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible()
+
+ await forceClick(page.locator('[class*="userTrigger"]'))
+ await forceClick(page.getByText(TEXT.logout, { exact: true }))
+ await expect(page).toHaveURL(/\/login$/)
+ await expect(page.getByText(TEXT.adminBootstrapTitle)).toHaveCount(0)
+}
+
+async function verifyPublicRegistration(page) {
+ const username = `e2e_register_${Date.now()}`
+ const password = 'Register123!@#'
+
+ await page.goto(appUrl('/login'))
+ await expect(page.getByRole('link', { name: TEXT.createAccount })).toBeVisible()
+ await forceClick(page.getByRole('link', { name: TEXT.createAccount }))
+ await expect(page).toHaveURL(/\/register$/)
+ await expect(page.getByRole('heading', { name: TEXT.createAccount })).toBeVisible()
+
+ await forceFillInput(page.getByPlaceholder(TEXT.usernamePlaceholder), username)
+ await forceFillInput(page.locator(`input[placeholder="${TEXT.passwordPlaceholder}"]`).first(), password)
+ await forceFillInput(
+ page.locator(`input[placeholder="${TEXT.confirmPasswordPlaceholder}"]`).first(),
+ password,
+ )
+ const [registerResponse] = await Promise.all([
+ page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/register') && response.request().method() === 'POST'
+ }),
+ forceClick(page.getByRole('button', { name: TEXT.createAccount })),
+ ])
+ await assertApiSuccessResponse(registerResponse, 'register')
+
+ await expect(page.locator('.ant-result-title').filter({ hasText: TEXT.registerSuccess }).first()).toBeVisible({ timeout: 20 * 1000 })
+ await forceClick(page.getByRole('button', { name: TEXT.backToLogin }))
+ await expect(page).toHaveURL(/\/login$/)
+
+ await loginWithPassword(page, username, password, /\/profile$/)
+ await expect(page.locator('body')).toContainText(TEXT.profile)
+
+ await forceClick(page.locator('[class*="userTrigger"]'))
+ await forceClick(page.getByText(TEXT.logout, { exact: true }))
+ await expect(page).toHaveURL(/\/login$/)
+}
+
+async function verifyEmailActivationWorkflow(page) {
+ const username = `e2e_activate_${Date.now()}`
+ const email = `${username}@example.com`
+ const password = 'Register123!@#'
+
+ await page.goto(appUrl('/register'))
+ await expect(page).toHaveURL(/\/register$/)
+ await expect(page.getByRole('heading', { name: TEXT.createAccount })).toBeVisible()
+
+ await forceFillInput(page.getByPlaceholder(TEXT.usernamePlaceholder), username)
+ await forceFillInput(page.getByPlaceholder(TEXT.registerEmailPlaceholder), email)
+ await forceFillInput(page.locator(`input[placeholder="${TEXT.passwordPlaceholder}"]`).first(), password)
+ await forceFillInput(
+ page.locator(`input[placeholder="${TEXT.confirmPasswordPlaceholder}"]`).first(),
+ password,
+ )
+
+ const [registerResponse] = await Promise.all([
+ page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/register') && response.request().method() === 'POST'
+ }),
+ forceClick(page.getByRole('button', { name: TEXT.createAccount })),
+ ])
+ await assertApiSuccessResponse(registerResponse, 'register email activation')
+ await expect(page.locator('.ant-result-title').filter({ hasText: TEXT.registerSuccess }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ const activationLink = await waitForActivationLink(email)
+ const [activationResponse] = await Promise.all([
+ page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/activate') && response.request().method() === 'GET'
+ }),
+ page.goto(activationLink),
+ ])
+ await assertApiSuccessResponse(activationResponse, 'activate email')
+ await expect(page.locator('body')).toContainText(TEXT.emailActivationSuccess, { timeout: 20 * 1000 })
+ await forceClick(page.getByRole('button', { name: TEXT.loginNow }))
+ await expect(page).toHaveURL(/\/login$/)
+
+ await loginWithPassword(page, username, password, /\/profile$/)
+ await expect(page.locator('body')).toContainText(TEXT.profile)
+
+ await forceClick(page.locator('[class*="userTrigger"]'))
+ await forceClick(page.getByText(TEXT.logout, { exact: true }))
+ await expect(page).toHaveURL(/\/login$/)
+}
+
+async function runScenario(browser, context, name, fn) {
+ console.log(`START ${name}`)
+ let lastError = null
+
+ for (let attempt = 1; attempt <= 2; attempt += 1) {
+ const activeContext = browser.contexts()[0] ?? context
+ const page = await ensurePersistentPage(browser, activeContext)
+ if (!page) {
+ throw new Error('No persistent page is available in the Chromium CDP context.')
+ }
+
+ for (const extraPage of activeContext.pages()) {
+ if (extraPage === page) {
+ continue
+ }
+ await extraPage.close({ runBeforeUnload: false }).catch(() => {})
+ }
+
+ const signals = createSignals()
+ const detachSignals = attachSignalCollectors(page, signals)
+ const startedAt = Date.now()
+
+ try {
+ console.log(`STEP ${name} reset-state`)
+ await resetBrowserState(activeContext, page)
+ console.log(`STEP ${name} execute`)
+ await fn(page)
+ assertCleanSignals(signals)
+ console.log(`PASS ${name} (${Date.now() - startedAt}ms)`)
+ return
+ } catch (error) {
+ lastError = error
+ const signalOutput = formatSignals(signals)
+ if (signalOutput) {
+ console.error(`SIGNALS ${name}\n${signalOutput}`)
+ }
+
+ if (attempt >= 2 || !isRetryableTargetError(error)) {
+ throw error
+ }
+
+ console.warn(`RETRY ${name} attempt ${attempt + 1}: ${formatError(error)}`)
+ await delay(500)
+ } finally {
+ detachSignals()
+ if (!page.isClosed()) {
+ await page.goto('about:blank').catch(() => {})
+ }
+ }
+ }
+
+ throw lastError ?? new Error(`Scenario ${name} failed`)
+}
+
+async function verifyLoginSurface(page) {
+ console.log('STEP login-surface wait-capabilities')
+ const capabilitiesResponse = page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/auth/capabilities') && response.request().method() === 'GET'
+ })
+
+ console.log('STEP login-surface goto-login')
+ await page.goto(appUrl('/login'))
+ console.log('STEP login-surface capabilities-response')
+ const capabilitiesPayload = await (await capabilitiesResponse).json()
+ const capabilities = capabilitiesPayload?.data ?? {}
+
+ await expect(page).toHaveTitle(new RegExp(TEXT.appTitle))
+ await expect(page.locator('html')).toHaveAttribute('lang', 'zh-CN')
+ await expect(page.getByRole('heading', { name: TEXT.welcomeLogin })).toBeVisible()
+ await expect(page.getByPlaceholder(TEXT.usernamePlaceholder)).toBeVisible()
+ await expect(page.getByPlaceholder(TEXT.passwordPlaceholder)).toBeVisible()
+
+ if (capabilities.email_code) {
+ await expect(page.getByRole('tab', { name: TEXT.emailCodeLogin })).toBeVisible()
+ } else {
+ await expect(page.getByRole('tab', { name: TEXT.emailCodeLogin })).toHaveCount(0)
+ }
+
+ if (capabilities.sms_code) {
+ await expect(page.getByRole('tab', { name: TEXT.smsCodeLogin })).toBeVisible()
+ } else {
+ await expect(page.getByRole('tab', { name: TEXT.smsCodeLogin })).toHaveCount(0)
+ }
+
+ if (capabilities.password_reset) {
+ await expect(page.getByRole('link', { name: TEXT.forgotPassword })).toBeVisible()
+ } else {
+ await expect(page.getByRole('link', { name: TEXT.forgotPassword })).toHaveCount(0)
+ }
+
+ await page.goto(appUrl('/dashboard'))
+ await expect(page).toHaveURL(/\/login$/, { timeout: 10 * 1000 })
+ const dashboardRedirect = await getProtectedRouteRedirect(page)
+ expect(dashboardRedirect.path).toBe('/login')
+ expect(dashboardRedirect.redirectFrom).toBe('/dashboard')
+
+ await page.goto(appUrl('/users'))
+ await expect(page).toHaveURL(/\/login$/, { timeout: 10 * 1000 })
+ const usersRedirect = await getProtectedRouteRedirect(page)
+ expect(usersRedirect.path).toBe('/login')
+ expect(usersRedirect.redirectFrom).toBe('/users')
+}
+
+async function verifyAuthWorkflow(page) {
+ logDebug('verifyAuthWorkflow: login /login')
+ const credentials = await loginFromLoginPage(page)
+ const createdUsername = `e2e_user_${Date.now()}`
+ await page.goto(appUrl('/users'))
+ await expect(page).toHaveURL(/\/users$/)
+
+ expect(await readRefreshToken(page)).toBeTruthy()
+
+ const userRow = page.locator('tbody tr').filter({ hasText: credentials.username }).first()
+ await expect(userRow).toBeVisible({ timeout: 20 * 1000 })
+ await expect(page.getByPlaceholder(TEXT.usersFilter)).toBeVisible()
+
+ await forceClick(userRow.getByRole('button', { name: TEXT.userDetailAction }))
+ const userDetailTitle = page.locator('.ant-drawer-title')
+ await expect(userDetailTitle).toHaveText(TEXT.userDetail)
+ await expect(page.locator('.ant-drawer')).toContainText(TEXT.userId)
+ await expect(page.locator('.ant-drawer')).toContainText(credentials.username)
+ await page.goto(appUrl('/users'))
+ await expect(page.locator('tbody tr').filter({ hasText: credentials.username }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ await forceClick(userRow.getByRole('button', { name: TEXT.assignRolesAction }))
+ const assignRolesTitle = page.locator('.ant-modal-title')
+ await expect(assignRolesTitle).toContainText(TEXT.assignRoles)
+ await expect(page.locator('.ant-modal')).toContainText(credentials.username)
+ await page.goto(appUrl('/users'))
+ await expect(page.locator('tbody tr').filter({ hasText: credentials.username }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ await forceClick(page.getByRole('button', { name: TEXT.createUser }).first())
+ await expect(page.locator('.ant-modal-title')).toContainText(TEXT.createUser)
+ const createUserModal = page.locator('.ant-modal').last()
+ const createUserResponsePromise = page.waitForResponse((response) => {
+ return response.url().includes('/api/v1/users') && response.request().method() === 'POST'
+ })
+ await forceFillInput(
+ createUserModal.locator(`input[placeholder="${TEXT.createUserUsernamePlaceholder}"]`).first(),
+ createdUsername,
+ )
+ await forceFillInput(
+ createUserModal.locator(`input[placeholder="${TEXT.createUserPasswordPlaceholder}"]`).first(),
+ 'Pass123!@#',
+ )
+ await forceFillInput(
+ createUserModal.locator(`input[placeholder="${TEXT.createUserEmailPlaceholder}"]`).first(),
+ `${createdUsername}@example.com`,
+ )
+ await forceClick(createUserModal.locator('.ant-btn-primary').last())
+ const createUserResponse = await createUserResponsePromise
+ await assertApiSuccessResponse(createUserResponse, 'create user')
+ await expect(createUserModal).toHaveClass(/ant-zoom-leave/, { timeout: 20 * 1000 })
+ await page.goto(appUrl('/users'))
+ await expect(page).toHaveURL(/\/users$/)
+ await forceFillInput(page.getByPlaceholder(TEXT.usersFilter), createdUsername)
+ await expect(page.locator('tbody tr').filter({ hasText: createdUsername }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ await page.goto(appUrl('/roles'))
+ await expect(page).toHaveURL(/\/roles$/)
+ await expect(page.getByPlaceholder(TEXT.roleFilter)).toBeVisible()
+ await expect(page.getByRole('button', { name: TEXT.createRole })).toBeVisible()
+
+ const adminRoleRow = page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()
+ await expect(adminRoleRow).toBeVisible({ timeout: 20 * 1000 })
+ await forceClick(adminRoleRow.getByRole('button', { name: TEXT.permissionsAction }))
+
+ const assignPermissionsTitle = page.locator('.ant-modal-title')
+ await expect(assignPermissionsTitle).toContainText(TEXT.assignPermissions)
+ await expect(page.locator('.ant-modal')).toContainText(TEXT.adminRoleName)
+ await expect(page.locator('.ant-modal')).toContainText(TEXT.permissionsHint)
+ await page.goto(appUrl('/roles'))
+ await expect(page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ await page.goto(appUrl('/dashboard'))
+ await expect(page).toHaveURL(/\/dashboard$/)
+ await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible()
+ await expect(page.getByText(TEXT.totalUsers)).toBeVisible()
+
+ await forceClick(page.locator('[class*="userTrigger"]'))
+ await forceClick(page.getByText(TEXT.logout, { exact: true }))
+ await expect(page).toHaveURL(/\/login$/)
+ await expect(await readRefreshToken(page)).toBeNull()
+
+ await page.goto(appUrl('/dashboard'))
+ const postLogoutRedirect = await getProtectedRouteRedirect(page)
+ expect(postLogoutRedirect.path).toBe('/login')
+ expect(postLogoutRedirect.redirectFrom).toBe('/dashboard')
+}
+
+async function verifyResponsiveLogin(page) {
+ for (const viewport of VIEWPORTS) {
+ logDebug(`verifyResponsiveLogin: ${viewport.name}`)
+ await page.setViewportSize({ width: viewport.width, height: viewport.height })
+ await page.goto(appUrl('/login'))
+ await expect(page).toHaveTitle(new RegExp(TEXT.appTitle))
+ await expect(page.getByRole('heading', { name: TEXT.welcomeLogin })).toBeVisible()
+
+ const metrics = await page.evaluate(() => {
+ return {
+ innerWidth: window.innerWidth,
+ bodyScrollWidth: document.body.scrollWidth,
+ documentScrollWidth: document.documentElement.scrollWidth,
+ }
+ })
+
+ expect(metrics.bodyScrollWidth).toBeLessThanOrEqual(metrics.innerWidth + 24)
+ expect(metrics.documentScrollWidth).toBeLessThanOrEqual(metrics.innerWidth + 24)
+ }
+}
+
+async function verifyDesktopAndMobileNavigation(page) {
+ logDebug('verifyDesktopAndMobileNavigation: login /login')
+ const credentials = requireEnv('E2E_LOGIN_USERNAME')
+
+ await page.setViewportSize({ width: 1440, height: 960 })
+ await loginFromLoginPage(page)
+ await expect(page.getByText(TEXT.todaySuccessLogins)).toBeVisible()
+
+ await expandSidebarGroup(page, TEXT.accessControl)
+ await clickSidebarMenu(page, TEXT.users)
+ await expect(page).toHaveURL(/\/users$/)
+ await expect(page.locator('tbody tr').filter({ hasText: credentials }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ await expandSidebarGroup(page, TEXT.accessControl)
+ await clickSidebarMenu(page, TEXT.roles)
+ await expect(page).toHaveURL(/\/roles$/)
+ await expect(page.locator('tbody tr').filter({ hasText: TEXT.adminRoleName }).first()).toBeVisible({ timeout: 20 * 1000 })
+
+ await page.setViewportSize({ width: 390, height: 844 })
+ await expect
+ .poll(async () => await page.evaluate(() => window.innerWidth < 768))
+ .toBe(true)
+ await page.evaluate(() => window.dispatchEvent(new Event('resize')))
+ await expect
+ .poll(async () => await page.locator('.ant-layout-header .ant-btn').count())
+ .toBeGreaterThan(0)
+
+ const mobileMenuButton = page.locator('.ant-layout-header .ant-btn').first()
+ await expect(mobileMenuButton).toBeVisible()
+ await forceClick(mobileMenuButton)
+
+ await expect(page.locator('.ant-drawer-content')).toBeVisible({ timeout: 10 * 1000 })
+ const mobileDashboardItem = page.locator('.ant-drawer .ant-menu-item').filter({ hasText: TEXT.dashboard }).first()
+ await expect(mobileDashboardItem).toBeVisible()
+ await forceClick(mobileDashboardItem)
+ await expect(page).toHaveURL(/\/dashboard$/)
+ await page.goto(appUrl('/dashboard'))
+ await expect(page.locator('body')).toContainText(TEXT.todaySuccessLogins, { timeout: 10 * 1000 })
+}
+
+async function main() {
+ let browser = null
+ let managedBrowser = null
+ let managedProfileDir = null
+
+ if (process.env.E2E_MANAGED_BROWSER === '1') {
+ const browserPath = await resolveManagedBrowserPath()
+ const port = await getFreePort()
+ managedProfileDir = await createManagedBrowserProfileDir(browserPath, port)
+ managedBrowser = startManagedBrowser(browserPath, port, managedProfileDir)
+ managedCdpUrl = `http://127.0.0.1:${port}`
+ console.log(`LAUNCH playwright-cdp ${browserPath}`)
+ await waitForJson(`${managedCdpUrl}/json/version`, STARTUP_TIMEOUT_MS, 'managed browser CDP endpoint')
+ }
+
+ console.log('CONNECT playwright-cdp')
+ browser = await connectBrowserWithRetry()
+
+ try {
+ console.log('CONNECTED playwright-cdp')
+ const context = browser.contexts()[0]
+ if (!context) {
+ throw new Error('No persistent Chromium context is available through CDP.')
+ }
+
+ if (process.env.E2E_EXPECT_ADMIN_BOOTSTRAP === '1') {
+ await runScenario(browser, context, 'admin-bootstrap', verifyAdminBootstrapWorkflow)
+ }
+ await runScenario(browser, context, 'public-registration', verifyPublicRegistration)
+ await runScenario(browser, context, 'email-activation', verifyEmailActivationWorkflow)
+ await runScenario(browser, context, 'login-surface', verifyLoginSurface)
+ await runScenario(browser, context, 'auth-workflow', verifyAuthWorkflow)
+ await runScenario(browser, context, 'responsive-login', verifyResponsiveLogin)
+ await runScenario(browser, context, 'desktop-mobile-navigation', verifyDesktopAndMobileNavigation)
+ console.log('Playwright CDP E2E completed successfully')
+ } finally {
+ await browser?.close().catch(() => {})
+ await killManagedBrowser(managedBrowser)
+ if (managedProfileDir) {
+ await rm(managedProfileDir, { recursive: true, force: true }).catch(() => {})
+ }
+ managedCdpUrl = null
+ }
+}
+
+await main().catch((error) => {
+ console.error(error && error.stack ? error.stack : error)
+ process.exitCode = 1
+})
diff --git a/frontend/admin/scripts/run-vitest.mjs b/frontend/admin/scripts/run-vitest.mjs
new file mode 100644
index 0000000..64a11a8
--- /dev/null
+++ b/frontend/admin/scripts/run-vitest.mjs
@@ -0,0 +1,79 @@
+import path from 'node:path'
+import { fileURLToPath } from 'node:url'
+import react from '@vitejs/plugin-react'
+import { parseCLI, startVitest } from 'vitest/node'
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url))
+const root = path.resolve(__dirname, '..')
+
+const { filter, options } = parseCLI(['vitest', ...process.argv.slice(2)])
+const { coverage: coverageOptions, ...cliOptions } = options
+
+const baseCoverage = {
+ provider: 'v8',
+ reporter: ['text', 'json', 'html'],
+ include: ['src/**/*.{ts,tsx}'],
+ exclude: [
+ 'src/**/*.d.ts',
+ 'src/**/*.interface.ts',
+ 'src/test/**',
+ 'src/main.tsx',
+ 'src/vite-env.d.ts',
+ ],
+}
+
+function resolveCoverageConfig(option) {
+ if (!option) {
+ return {
+ ...baseCoverage,
+ enabled: false,
+ }
+ }
+
+ if (option === true) {
+ return {
+ ...baseCoverage,
+ enabled: true,
+ }
+ }
+
+ return {
+ ...baseCoverage,
+ ...option,
+ enabled: option.enabled ?? true,
+ }
+}
+
+const ctx = await startVitest(
+ 'test',
+ filter,
+ {
+ ...cliOptions,
+ root,
+ config: false,
+ globals: true,
+ environment: 'jsdom',
+ setupFiles: ['./src/test/setup.ts'],
+ include: ['src/**/*.{test,spec}.{js,ts,jsx,tsx}'],
+ coverage: resolveCoverageConfig(coverageOptions),
+ pool: cliOptions.pool ?? 'threads',
+ fileParallelism: cliOptions.fileParallelism ?? false,
+ maxWorkers: cliOptions.maxWorkers ?? 1,
+ testTimeout: cliOptions.testTimeout ?? 10000,
+ hookTimeout: cliOptions.hookTimeout ?? 10000,
+ clearMocks: true,
+ },
+ {
+ plugins: [react()],
+ resolve: {
+ preserveSymlinks: true,
+ alias: {
+ '@': path.resolve(root, 'src'),
+ },
+ },
+ },
+)
+
+if (!ctx?.shouldKeepServer()) {
+ await ctx?.exit()
+}
diff --git a/frontend/admin/src/app/App.test.tsx b/frontend/admin/src/app/App.test.tsx
new file mode 100644
index 0000000..324b0ce
--- /dev/null
+++ b/frontend/admin/src/app/App.test.tsx
@@ -0,0 +1,40 @@
+import type { ReactNode } from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+
+import App from './App'
+
+const routerProviderMock = vi.fn((props: unknown) => {
+ void props
+ return
+})
+const errorBoundaryMock = vi.fn(({ children }: { children: ReactNode }) => (
+ {children}
+))
+
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom')
+ return {
+ ...actual,
+ RouterProvider: (props: unknown) => routerProviderMock(props),
+ }
+})
+
+vi.mock('@/components/common', () => ({
+ ErrorBoundary: (props: { children: React.ReactNode }) => errorBoundaryMock(props),
+}))
+
+describe('App', () => {
+ it('renders the router provider inside the error boundary shell', () => {
+ render()
+
+ expect(screen.getByTestId('error-boundary')).toBeInTheDocument()
+ expect(screen.getByTestId('router-provider')).toBeInTheDocument()
+ expect(errorBoundaryMock).toHaveBeenCalledTimes(1)
+ expect(routerProviderMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ router: expect.any(Object),
+ }),
+ )
+ })
+})
diff --git a/frontend/admin/src/app/App.tsx b/frontend/admin/src/app/App.tsx
new file mode 100644
index 0000000..acf3a91
--- /dev/null
+++ b/frontend/admin/src/app/App.tsx
@@ -0,0 +1,38 @@
+/**
+ * Admin Frontend App Shell
+ *
+ * 项目:用户管理系统 Admin 后台
+ * 技术栈:React 18 + TypeScript + Vite + Ant Design 5
+ */
+
+import { Suspense } from 'react'
+import { RouterProvider } from 'react-router-dom'
+import { Spin } from 'antd'
+import { ErrorBoundary } from '@/components/common'
+import { router } from './router'
+
+const routeFallback = (
+
+
+
+)
+
+function App() {
+ return (
+
+
+
+
+
+ )
+}
+
+export default App
diff --git a/frontend/admin/src/app/RootLayout.test.tsx b/frontend/admin/src/app/RootLayout.test.tsx
new file mode 100644
index 0000000..50fcc74
--- /dev/null
+++ b/frontend/admin/src/app/RootLayout.test.tsx
@@ -0,0 +1,27 @@
+import type { ReactNode } from 'react'
+import { render, screen } from '@testing-library/react'
+import { describe, expect, it, vi } from 'vitest'
+
+import { RootLayout } from './RootLayout'
+
+const authProviderMock = vi.fn(({ children }: { children: ReactNode }) => (
+ {children}
+))
+
+vi.mock('react-router-dom', () => ({
+ Outlet: () => ,
+}))
+
+vi.mock('./providers/AuthProvider', () => ({
+ AuthProvider: (props: { children: ReactNode }) => authProviderMock(props),
+}))
+
+describe('RootLayout', () => {
+ it('wraps the route outlet with the auth provider', () => {
+ render()
+
+ expect(screen.getByTestId('auth-provider')).toBeInTheDocument()
+ expect(screen.getByTestId('root-outlet')).toBeInTheDocument()
+ expect(authProviderMock).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/frontend/admin/src/app/RootLayout.tsx b/frontend/admin/src/app/RootLayout.tsx
new file mode 100644
index 0000000..3bde131
--- /dev/null
+++ b/frontend/admin/src/app/RootLayout.tsx
@@ -0,0 +1,10 @@
+import { Outlet } from 'react-router-dom'
+import { AuthProvider } from './providers/AuthProvider'
+
+export function RootLayout() {
+ return (
+
+
+
+ )
+}
diff --git a/frontend/admin/src/app/bootstrap/installWindowGuards.test.ts b/frontend/admin/src/app/bootstrap/installWindowGuards.test.ts
new file mode 100644
index 0000000..a2285ab
--- /dev/null
+++ b/frontend/admin/src/app/bootstrap/installWindowGuards.test.ts
@@ -0,0 +1,67 @@
+import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
+
+import { installWindowGuards, restoreWindowGuardsForTest } from './installWindowGuards'
+
+describe('installWindowGuards', () => {
+ const logger = vi.fn()
+
+ beforeEach(() => {
+ logger.mockReset()
+ restoreWindowGuardsForTest()
+ })
+
+ afterEach(() => {
+ restoreWindowGuardsForTest()
+ })
+
+ it('blocks native dialogs and popup windows with structured logs', () => {
+ installWindowGuards(logger)
+
+ expect(window.alert('danger')).toBeUndefined()
+ expect(window.confirm('continue?')).toBe(false)
+ expect(window.prompt('name?', 'admin')).toBeNull()
+ expect(window.open('https://example.com', '_blank')).toBeNull()
+
+ expect(logger).toHaveBeenCalledTimes(4)
+ expect(logger.mock.calls[0][0]).toContain('native-alert-blocked')
+ expect(logger.mock.calls[1][0]).toContain('native-confirm-blocked')
+ expect(logger.mock.calls[2][0]).toContain('native-prompt-blocked')
+ expect(logger.mock.calls[3][0]).toContain('popup-blocked')
+ })
+
+ it('logs window errors and unhandled promise rejections', () => {
+ installWindowGuards(logger)
+
+ const runtimeError = new Error('boom')
+ window.dispatchEvent(new ErrorEvent('error', {
+ message: runtimeError.message,
+ filename: '/app.js',
+ lineno: 12,
+ colno: 34,
+ error: runtimeError,
+ }))
+
+ const rejectionEvent = new Event('unhandledrejection') as PromiseRejectionEvent
+ Object.defineProperty(rejectionEvent, 'promise', {
+ value: Promise.resolve(),
+ configurable: true,
+ })
+ Object.defineProperty(rejectionEvent, 'reason', {
+ value: new Error('rejected'),
+ configurable: true,
+ })
+ window.dispatchEvent(rejectionEvent)
+
+ expect(logger).toHaveBeenCalledTimes(2)
+ expect(logger.mock.calls[0][0]).toContain('[window-guard] error')
+ expect(logger.mock.calls[1][0]).toContain('[window-guard] unhandledrejection')
+ })
+
+ it('does not install twice', () => {
+ installWindowGuards(logger)
+ installWindowGuards(logger)
+
+ window.alert('once')
+ expect(logger).toHaveBeenCalledTimes(1)
+ })
+})
diff --git a/frontend/admin/src/app/bootstrap/installWindowGuards.ts b/frontend/admin/src/app/bootstrap/installWindowGuards.ts
new file mode 100644
index 0000000..27259de
--- /dev/null
+++ b/frontend/admin/src/app/bootstrap/installWindowGuards.ts
@@ -0,0 +1,144 @@
+type GuardLogger = (message?: unknown, ...optionalParams: unknown[]) => void
+
+type WindowGuardOriginals = {
+ alert: typeof window.alert
+ confirm: typeof window.confirm
+ prompt: typeof window.prompt
+ open: typeof window.open
+}
+
+type WindowGuardListeners = {
+ error: (event: ErrorEvent) => void
+ unhandledrejection: (event: PromiseRejectionEvent) => void
+}
+
+declare global {
+ interface Window {
+ __UMS_WINDOW_GUARDS_INSTALLED__?: boolean
+ __UMS_WINDOW_GUARDS_ORIGINALS__?: WindowGuardOriginals
+ __UMS_WINDOW_GUARDS_LISTENERS__?: WindowGuardListeners
+ }
+}
+
+function formatValue(value: unknown): string {
+ if (typeof value === 'string') {
+ return value
+ }
+
+ if (value instanceof Error) {
+ return value.stack || value.message
+ }
+
+ try {
+ return JSON.stringify(value)
+ } catch {
+ return String(value)
+ }
+}
+
+function reportWindowEvent(
+ logger: GuardLogger,
+ category: string,
+ payload: Record,
+) {
+ logger(`[window-guard] ${category}`, payload)
+}
+
+export function installWindowGuards(logger: GuardLogger = console.error) {
+ if (typeof window === 'undefined' || window.__UMS_WINDOW_GUARDS_INSTALLED__) {
+ return
+ }
+
+ window.__UMS_WINDOW_GUARDS_ORIGINALS__ = {
+ alert: window.alert.bind(window),
+ confirm: window.confirm.bind(window),
+ prompt: window.prompt.bind(window),
+ open: window.open.bind(window),
+ }
+
+ const onError = (event: ErrorEvent) => {
+ reportWindowEvent(logger, 'error', {
+ message: event.message,
+ source: event.filename,
+ line: event.lineno,
+ column: event.colno,
+ error: formatValue(event.error),
+ })
+ }
+
+ const onUnhandledRejection = (event: PromiseRejectionEvent) => {
+ reportWindowEvent(logger, 'unhandledrejection', {
+ reason: formatValue(event.reason),
+ })
+ }
+
+ window.__UMS_WINDOW_GUARDS_LISTENERS__ = {
+ error: onError,
+ unhandledrejection: onUnhandledRejection,
+ }
+
+ window.addEventListener('error', onError)
+ window.addEventListener('unhandledrejection', onUnhandledRejection)
+
+ window.alert = (message?: unknown) => {
+ reportWindowEvent(logger, 'native-alert-blocked', {
+ message: formatValue(message),
+ })
+ }
+
+ window.confirm = (message?: string) => {
+ reportWindowEvent(logger, 'native-confirm-blocked', {
+ message: formatValue(message),
+ fallback: false,
+ })
+ return false
+ }
+
+ window.prompt = (message?: string, defaultValue?: string) => {
+ reportWindowEvent(logger, 'native-prompt-blocked', {
+ message: formatValue(message),
+ defaultValue: formatValue(defaultValue ?? ''),
+ fallback: null,
+ })
+ return null
+ }
+
+ window.open = (url?: string | URL, target?: string, features?: string) => {
+ reportWindowEvent(logger, 'popup-blocked', {
+ url: typeof url === 'string' ? url : url?.toString() ?? '',
+ target: target ?? '',
+ features: features ?? '',
+ fallback: null,
+ })
+ return null
+ }
+
+ window.__UMS_WINDOW_GUARDS_INSTALLED__ = true
+}
+
+export function restoreWindowGuardsForTest() {
+ if (typeof window === 'undefined') {
+ return
+ }
+
+ const originals = window.__UMS_WINDOW_GUARDS_ORIGINALS__
+ const listeners = window.__UMS_WINDOW_GUARDS_LISTENERS__
+ if (!originals) {
+ window.__UMS_WINDOW_GUARDS_INSTALLED__ = false
+ return
+ }
+
+ if (listeners) {
+ window.removeEventListener('error', listeners.error)
+ window.removeEventListener('unhandledrejection', listeners.unhandledrejection)
+ delete window.__UMS_WINDOW_GUARDS_LISTENERS__
+ }
+
+ window.alert = originals.alert
+ window.confirm = originals.confirm
+ window.prompt = originals.prompt
+ window.open = originals.open
+
+ delete window.__UMS_WINDOW_GUARDS_ORIGINALS__
+ window.__UMS_WINDOW_GUARDS_INSTALLED__ = false
+}
diff --git a/frontend/admin/src/app/providers/AuthProvider.test.tsx b/frontend/admin/src/app/providers/AuthProvider.test.tsx
new file mode 100644
index 0000000..a96121b
--- /dev/null
+++ b/frontend/admin/src/app/providers/AuthProvider.test.tsx
@@ -0,0 +1,442 @@
+import { fireEvent, render, screen, waitFor } from '@testing-library/react'
+import { beforeEach, describe, expect, it, vi } from 'vitest'
+
+import type { Role, SessionUser, TokenBundle } from '@/types'
+import { useAuth } from './auth-context'
+import { AuthProvider } from './AuthProvider'
+
+let storedAccessToken: string | null = null
+let storedUser: SessionUser | null = null
+let storedRoles: Role[] = []
+
+const navigateMock = vi.fn()
+const getMock = vi.fn()
+const setRefreshTokenMock = vi.fn()
+const clearRefreshTokenMock = vi.fn()
+const hasSessionPresenceCookieMock = vi.fn()
+const setAccessTokenMock = vi.fn((token: string, expiresIn: number) => {
+ void expiresIn
+ storedAccessToken = token
+})
+const getCurrentUserMock = vi.fn(() => storedUser)
+const setCurrentUserMock = vi.fn((user: SessionUser) => {
+ storedUser = user
+})
+const getCurrentRolesMock = vi.fn(() => storedRoles)
+const setCurrentRolesMock = vi.fn((roles: Role[]) => {
+ storedRoles = roles
+})
+const clearSessionMock = vi.fn(() => {
+ storedAccessToken = null
+ storedUser = null
+ storedRoles = []
+})
+const isAuthenticatedMock = vi.fn(() => storedAccessToken !== null && storedUser !== null)
+const isAccessTokenExpiredMock = vi.fn()
+const initCSRFTokenMock = vi.fn()
+const clearCSRFTokenMock = vi.fn()
+const logoutRequestMock = vi.fn()
+const refreshSessionMock = vi.fn()
+
+vi.mock('react-router-dom', async () => {
+ const actual = await vi.importActual('react-router-dom')
+ return {
+ ...actual,
+ useNavigate: () => navigateMock,
+ }
+})
+
+vi.mock('@/lib/http', () => ({
+ get: (path: string) => getMock(path),
+}))
+
+vi.mock('@/lib/storage', () => ({
+ setRefreshToken: (token: string | null | undefined) => setRefreshTokenMock(token),
+ clearRefreshToken: () => clearRefreshTokenMock(),
+ hasSessionPresenceCookie: () => hasSessionPresenceCookieMock(),
+}))
+
+vi.mock('@/lib/http/auth-session', () => ({
+ setAccessToken: (token: string, expiresIn: number) => setAccessTokenMock(token, expiresIn),
+ getCurrentUser: () => getCurrentUserMock(),
+ setCurrentUser: (user: SessionUser) => setCurrentUserMock(user),
+ getCurrentRoles: () => getCurrentRolesMock(),
+ setCurrentRoles: (roles: Role[]) => setCurrentRolesMock(roles),
+ clearSession: () => clearSessionMock(),
+ isAuthenticated: () => isAuthenticatedMock(),
+ isAccessTokenExpired: () => isAccessTokenExpiredMock(),
+}))
+
+vi.mock('@/lib/http/csrf', () => ({
+ initCSRFToken: () => initCSRFTokenMock(),
+ clearCSRFToken: () => clearCSRFTokenMock(),
+}))
+
+vi.mock('@/services/auth', () => ({
+ logout: () => logoutRequestMock(),
+ refreshSession: () => refreshSessionMock(),
+}))
+
+const adminUser: SessionUser = {
+ id: 1,
+ username: 'admin',
+ email: 'admin@example.com',
+ phone: '13800138000',
+ nickname: 'Admin',
+ avatar: '',
+ status: 1,
+}
+
+const adminRoles: Role[] = [
+ {
+ id: 1,
+ name: 'Administrator',
+ code: 'admin',
+ description: 'System administrator',
+ is_system: true,
+ is_default: false,
+ status: 1,
+ },
+]
+
+const refreshedSession: TokenBundle = {
+ access_token: 'access-token',
+ refresh_token: 'refresh-token',
+ expires_in: 7200,
+ user: adminUser,
+}
+
+const loginSession: TokenBundle = {
+ access_token: 'login-access-token',
+ refresh_token: 'login-refresh-token',
+ expires_in: 3600,
+ user: adminUser,
+}
+
+const operatorUser: SessionUser = {
+ id: 2,
+ username: 'operator',
+ email: 'operator@example.com',
+ phone: '13900139000',
+ nickname: 'Operator',
+ avatar: '',
+ status: 1,
+}
+
+const operatorRoles: Role[] = [
+ {
+ id: 2,
+ name: 'Operator',
+ code: 'operator',
+ description: 'Operations user',
+ is_system: false,
+ is_default: false,
+ status: 1,
+ },
+]
+
+function Probe() {
+ const auth = useAuth()
+
+ return (
+
+ {String(auth.isLoading)}
+ {String(auth.isAuthenticated)}
+ {auth.user?.username ?? ''}
+ {auth.roles.map((role) => role.code).join(',')}
+
+
+
+
+ )
+}
+
+function renderAuthProvider() {
+ return render(
+
+
+ ,
+ )
+}
+
+async function waitForProviderIdle() {
+ await waitFor(() => {
+ expect(screen.getByTestId('loading')).toHaveTextContent('false')
+ })
+}
+
+describe('AuthProvider', () => {
+ beforeEach(() => {
+ window.history.pushState({}, '', '/')
+ storedAccessToken = null
+ storedUser = null
+ storedRoles = []
+
+ navigateMock.mockReset()
+ getMock.mockReset()
+ setRefreshTokenMock.mockReset()
+ clearRefreshTokenMock.mockReset()
+ hasSessionPresenceCookieMock.mockReset()
+ setAccessTokenMock.mockReset()
+ getCurrentUserMock.mockReset()
+ setCurrentUserMock.mockReset()
+ getCurrentRolesMock.mockReset()
+ setCurrentRolesMock.mockReset()
+ clearSessionMock.mockReset()
+ isAuthenticatedMock.mockReset()
+ isAccessTokenExpiredMock.mockReset()
+ initCSRFTokenMock.mockReset()
+ clearCSRFTokenMock.mockReset()
+ logoutRequestMock.mockReset()
+ isAccessTokenExpiredMock.mockReturnValue(true)
+ isAuthenticatedMock.mockImplementation(() => storedAccessToken !== null && storedUser !== null)
+ getCurrentUserMock.mockImplementation(() => storedUser)
+ setCurrentUserMock.mockImplementation((user: SessionUser) => {
+ storedUser = user
+ })
+ getCurrentRolesMock.mockImplementation(() => storedRoles)
+ setCurrentRolesMock.mockImplementation((roles: Role[]) => {
+ storedRoles = roles
+ })
+ setAccessTokenMock.mockImplementation((token: string, expiresIn: number) => {
+ void expiresIn
+ storedAccessToken = token
+ })
+ clearSessionMock.mockImplementation(() => {
+ storedAccessToken = null
+ storedUser = null
+ storedRoles = []
+ })
+ hasSessionPresenceCookieMock.mockReturnValue(false)
+ initCSRFTokenMock.mockResolvedValue(undefined)
+ clearCSRFTokenMock.mockReturnValue(undefined)
+ logoutRequestMock.mockResolvedValue(undefined)
+ refreshSessionMock.mockReset()
+ })
+
+ it('reuses an in-memory authenticated session when the access token is still valid', async () => {
+ storedAccessToken = 'cached-access-token'
+ storedUser = adminUser
+ storedRoles = adminRoles
+ isAccessTokenExpiredMock.mockReturnValue(false)
+
+ renderAuthProvider()
+ await waitForProviderIdle()
+
+ expect(refreshSessionMock).not.toHaveBeenCalled()
+ expect(navigateMock).not.toHaveBeenCalled()
+ expect(clearRefreshTokenMock).not.toHaveBeenCalled()
+ expect(clearSessionMock).not.toHaveBeenCalled()
+ expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('true')
+ expect(screen.getByTestId('username')).toHaveTextContent('admin')
+ expect(screen.getByTestId('roles')).toHaveTextContent('admin')
+ })
+
+ it('clears the local session when auth state has no current user and no backend session cookie exists', async () => {
+ storedAccessToken = 'dangling-access-token'
+ isAuthenticatedMock.mockReturnValue(true)
+ isAccessTokenExpiredMock.mockReturnValue(false)
+ getCurrentUserMock.mockReturnValue(null)
+
+ renderAuthProvider()
+ await waitForProviderIdle()
+
+ expect(refreshSessionMock).not.toHaveBeenCalled()
+ expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
+ expect(clearSessionMock).toHaveBeenCalledTimes(1)
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
+ expect(screen.getByTestId('username').textContent).toBe('')
+ })
+
+ it('restores the session by refreshing through the backend cookie flow', async () => {
+ hasSessionPresenceCookieMock.mockReturnValue(true)
+ refreshSessionMock.mockResolvedValue(refreshedSession)
+ getMock.mockResolvedValue(adminRoles)
+
+ renderAuthProvider()
+
+ await waitFor(() => {
+ expect(refreshSessionMock).toHaveBeenCalledTimes(1)
+ })
+
+ await waitForProviderIdle()
+
+ expect(setAccessTokenMock).toHaveBeenCalledWith('access-token', 7200)
+ expect(setRefreshTokenMock).toHaveBeenCalledWith('refresh-token')
+ expect(setCurrentUserMock).toHaveBeenCalledWith(adminUser)
+ expect(setCurrentRolesMock).toHaveBeenCalledWith(adminRoles)
+ expect(getMock).toHaveBeenCalledWith('/users/1/roles')
+ expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
+ expect(navigateMock).not.toHaveBeenCalled()
+ expect(screen.getByTestId('username')).toHaveTextContent('admin')
+ expect(screen.getByTestId('roles')).toHaveTextContent('admin')
+ })
+
+ it('restores the session with empty roles when the role lookup fails after refresh', async () => {
+ hasSessionPresenceCookieMock.mockReturnValue(true)
+ refreshSessionMock.mockResolvedValue(refreshedSession)
+ getMock.mockRejectedValue(new Error('roles lookup failed'))
+
+ renderAuthProvider()
+ await waitForProviderIdle()
+
+ expect(setAccessTokenMock).toHaveBeenCalledWith('access-token', 7200)
+ expect(setRefreshTokenMock).toHaveBeenCalledWith('refresh-token')
+ expect(setCurrentUserMock).toHaveBeenCalledWith(adminUser)
+ expect(setCurrentRolesMock).toHaveBeenCalledWith([])
+ expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('true')
+ expect(screen.getByTestId('username')).toHaveTextContent('admin')
+ expect(screen.getByTestId('roles').textContent).toBe('')
+ })
+
+ it('clears local state when refresh fails against the backend cookie flow', async () => {
+ hasSessionPresenceCookieMock.mockReturnValue(true)
+ refreshSessionMock.mockRejectedValue(new Error('missing refresh cookie'))
+
+ renderAuthProvider()
+
+ await waitFor(() => {
+ expect(refreshSessionMock).toHaveBeenCalledTimes(1)
+ })
+
+ await waitForProviderIdle()
+
+ expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
+ expect(clearSessionMock).toHaveBeenCalledTimes(1)
+ expect(navigateMock).not.toHaveBeenCalled()
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
+ })
+
+ it('persists tokens, user, roles, and csrf state after login success', async () => {
+ renderAuthProvider()
+ await waitForProviderIdle()
+ vi.clearAllMocks()
+
+ getMock.mockResolvedValue(adminRoles)
+ fireEvent.click(screen.getByRole('button', { name: 'login-success' }))
+
+ await waitFor(() => {
+ expect(setAccessTokenMock).toHaveBeenCalledWith('login-access-token', 3600)
+ })
+
+ expect(setRefreshTokenMock).toHaveBeenCalledWith('login-refresh-token')
+ expect(setCurrentUserMock).toHaveBeenCalledWith(adminUser)
+ expect(setCurrentRolesMock).toHaveBeenCalledWith(adminRoles)
+ expect(getMock).toHaveBeenCalledWith('/users/1/roles')
+ expect(initCSRFTokenMock).toHaveBeenCalledTimes(1)
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('true')
+ expect(screen.getByTestId('username')).toHaveTextContent('admin')
+ expect(screen.getByTestId('roles')).toHaveTextContent('admin')
+ })
+
+ it('refreshes the current user and roles from the backend', async () => {
+ renderAuthProvider()
+ await waitForProviderIdle()
+ vi.clearAllMocks()
+
+ getMock.mockImplementation((path: string) => {
+ if (path === '/auth/userinfo') {
+ return Promise.resolve(operatorUser)
+ }
+ if (path === '/users/2/roles') {
+ return Promise.resolve(operatorRoles)
+ }
+ return Promise.reject(new Error(`Unexpected path: ${path}`))
+ })
+
+ fireEvent.click(screen.getByRole('button', { name: 'refresh-user' }))
+
+ await waitFor(() => {
+ expect(setCurrentUserMock).toHaveBeenCalledWith(operatorUser)
+ })
+
+ expect(setCurrentRolesMock).toHaveBeenCalledWith(operatorRoles)
+ expect(screen.getByTestId('username')).toHaveTextContent('operator')
+ expect(screen.getByTestId('roles')).toHaveTextContent('operator')
+ })
+
+ it('logs refreshUser failures without corrupting the current auth state', async () => {
+ storedAccessToken = 'cached-access-token'
+ storedUser = adminUser
+ storedRoles = adminRoles
+ isAccessTokenExpiredMock.mockReturnValue(false)
+
+ renderAuthProvider()
+ await waitForProviderIdle()
+ vi.clearAllMocks()
+
+ const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
+ getMock.mockRejectedValue(new Error('userinfo failed'))
+
+ fireEvent.click(screen.getByRole('button', { name: 'refresh-user' }))
+
+ await waitFor(() => {
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
+ 'Failed to refresh user info:',
+ expect.any(Error),
+ )
+ })
+
+ expect(screen.getByTestId('username')).toHaveTextContent('admin')
+ expect(screen.getByTestId('roles')).toHaveTextContent('admin')
+ consoleErrorSpy.mockRestore()
+ })
+
+ it('clears the local session and navigates to login when logout succeeds', async () => {
+ storedAccessToken = 'cached-access-token'
+ storedUser = adminUser
+ storedRoles = adminRoles
+ isAccessTokenExpiredMock.mockReturnValue(false)
+
+ renderAuthProvider()
+ await waitForProviderIdle()
+ vi.clearAllMocks()
+
+ fireEvent.click(screen.getByRole('button', { name: 'logout' }))
+
+ await waitFor(() => {
+ expect(logoutRequestMock).toHaveBeenCalledTimes(1)
+ })
+
+ expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
+ expect(clearSessionMock).toHaveBeenCalledTimes(1)
+ expect(clearCSRFTokenMock).toHaveBeenCalledTimes(1)
+ expect(navigateMock).toHaveBeenCalledWith('/login')
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
+ expect(screen.getByTestId('username').textContent).toBe('')
+ })
+
+ it('clears the local session and navigates to login when logout fails', async () => {
+ storedAccessToken = 'cached-access-token'
+ storedUser = adminUser
+ storedRoles = adminRoles
+ isAccessTokenExpiredMock.mockReturnValue(false)
+ logoutRequestMock.mockRejectedValue(new Error('logout failed'))
+
+ renderAuthProvider()
+ await waitForProviderIdle()
+ vi.clearAllMocks()
+
+ logoutRequestMock.mockRejectedValue(new Error('logout failed'))
+ fireEvent.click(screen.getByRole('button', { name: 'logout' }))
+
+ await waitFor(() => {
+ expect(logoutRequestMock).toHaveBeenCalledTimes(1)
+ })
+
+ expect(clearRefreshTokenMock).toHaveBeenCalledTimes(1)
+ expect(clearSessionMock).toHaveBeenCalledTimes(1)
+ expect(clearCSRFTokenMock).toHaveBeenCalledTimes(1)
+ expect(navigateMock).toHaveBeenCalledWith('/login')
+ expect(screen.getByTestId('authenticated')).toHaveTextContent('false')
+ expect(screen.getByTestId('username').textContent).toBe('')
+ })
+})
diff --git a/frontend/admin/src/app/providers/AuthProvider.tsx b/frontend/admin/src/app/providers/AuthProvider.tsx
new file mode 100644
index 0000000..64a57f1
--- /dev/null
+++ b/frontend/admin/src/app/providers/AuthProvider.tsx
@@ -0,0 +1,201 @@
+/**
+ * AuthProvider - 全局会话上下文
+ *
+ * 提供:
+ * - 会话状态(user, roles, isAdmin)
+ * - 登录/登出方法
+ * - 会话恢复(启动时自动刷新)
+ */
+
+import {
+ useEffect,
+ useState,
+ useCallback,
+ type ReactNode,
+} from 'react'
+import { useNavigate } from 'react-router-dom'
+import type { SessionUser, Role, TokenBundle } from '@/types'
+import { get } from '@/lib/http'
+import {
+ setRefreshToken,
+ clearRefreshToken,
+ hasSessionPresenceCookie,
+} from '@/lib/storage'
+import {
+ setAccessToken,
+ getCurrentUser,
+ setCurrentUser,
+ getCurrentRoles,
+ setCurrentRoles,
+ clearSession,
+ isAuthenticated,
+ isAccessTokenExpired,
+} from '@/lib/http/auth-session'
+import { initCSRFToken, clearCSRFToken } from '@/lib/http/csrf'
+import { logout as logoutRequest, refreshSession } from '@/services/auth'
+import { AuthContext, type AuthContextValue } from './auth-context'
+
+// ==================== Provider ====================
+
+interface AuthProviderProps {
+ children: ReactNode
+}
+
+export function AuthProvider({ children }: AuthProviderProps) {
+ const [user, setUser] = useState(getCurrentUser())
+ const [roles, setRoles] = useState(getCurrentRoles())
+ const [isLoading, setIsLoading] = useState(true)
+ const navigate = useNavigate()
+ const effectiveUser = user ?? getCurrentUser()
+ const effectiveRoles = roles.length > 0 ? roles : getCurrentRoles()
+
+ // 判断是否为管理员
+ const isAdmin = effectiveRoles.some((role) => role.code === 'admin')
+
+ /**
+ * 获取用户角色
+ */
+ const fetchUserRoles = useCallback(async (userId: number): Promise => {
+ try {
+ const result = await get(`/users/${userId}/roles`)
+ return result
+ } catch {
+ return []
+ }
+ }, [])
+
+ /**
+ * 登录成功回调
+ */
+ const onLoginSuccess = useCallback(async (tokenBundle: TokenBundle) => {
+ // 保存 tokens
+ setAccessToken(tokenBundle.access_token, tokenBundle.expires_in)
+ setRefreshToken(tokenBundle.refresh_token)
+
+ // 保存用户信息
+ setCurrentUser(tokenBundle.user)
+ setUser(tokenBundle.user)
+
+ // 获取角色
+ const userRoles = await fetchUserRoles(tokenBundle.user.id)
+ setCurrentRoles(userRoles)
+ setRoles(userRoles)
+
+ // 初始化 CSRF Token
+ await initCSRFToken()
+ }, [fetchUserRoles])
+
+ /**
+ * 刷新用户信息
+ */
+ const refreshUser = useCallback(async () => {
+ try {
+ const userInfo = await get('/auth/userinfo')
+ setCurrentUser(userInfo)
+ setUser(userInfo)
+
+ const userRoles = await fetchUserRoles(userInfo.id)
+ setCurrentRoles(userRoles)
+ setRoles(userRoles)
+ } catch {
+ // 刷新失败,清除会话
+ setUser(null)
+ setRoles([])
+ }
+ }, [fetchUserRoles])
+
+ /**
+ * 登出
+ */
+ const logout = useCallback(async () => {
+ try {
+ await logoutRequest()
+ } catch {
+ // 忽略登出请求错误
+ } finally {
+ // 无论请求成功与否,都清除本地会话和 CSRF Token
+ clearRefreshToken()
+ clearSession()
+ clearCSRFToken()
+ setUser(null)
+ setRoles([])
+ navigate('/login')
+ }
+ }, [navigate])
+
+ /**
+ * 会话恢复(应用启动时,只运行一次)
+ */
+ useEffect(() => {
+ const restoreSession = async () => {
+ // 如果已有 access_token 且未过期,直接使用
+ if (isAuthenticated() && !isAccessTokenExpired()) {
+ const currentUser = getCurrentUser()
+ const currentRoles = getCurrentRoles()
+
+ if (currentUser) {
+ setUser(currentUser)
+ setRoles(currentRoles)
+ await initCSRFToken()
+ setIsLoading(false)
+ return
+ }
+ }
+
+ if (!hasSessionPresenceCookie()) {
+ clearRefreshToken()
+ clearSession()
+ setUser(null)
+ setRoles([])
+ setIsLoading(false)
+ return
+ }
+
+ try {
+ const result = await refreshSession()
+
+ // 保存 tokens
+ setAccessToken(result.access_token, result.expires_in)
+ setRefreshToken(result.refresh_token)
+
+ // 保存用户信息
+ setCurrentUser(result.user)
+ setUser(result.user)
+
+ // 获取角色
+ const userRoles = await fetchUserRoles(result.user.id)
+ setCurrentRoles(userRoles)
+ setRoles(userRoles)
+ await initCSRFToken()
+ } catch {
+ // 刷新失败,清除会话
+ clearRefreshToken()
+ clearSession()
+ setUser(null)
+ setRoles([])
+ }
+
+ setIsLoading(false)
+ }
+
+ restoreSession()
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []) // 只在挂载时运行一次,不依赖 location.pathname
+
+ const value: AuthContextValue = {
+ user: effectiveUser,
+ roles: effectiveRoles,
+ isAdmin,
+ isAuthenticated: effectiveUser !== null && isAuthenticated(),
+ isLoading,
+ onLoginSuccess,
+ logout,
+ refreshUser,
+ }
+
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/admin/src/app/providers/ThemeProvider.test.tsx b/frontend/admin/src/app/providers/ThemeProvider.test.tsx
new file mode 100644
index 0000000..0521236
--- /dev/null
+++ b/frontend/admin/src/app/providers/ThemeProvider.test.tsx
@@ -0,0 +1,65 @@
+import type { ReactNode } from 'react'
+import { render, screen } from '@testing-library/react'
+import zhCN from 'antd/locale/zh_CN'
+import { describe, expect, it, vi } from 'vitest'
+
+import { ThemeProvider } from './ThemeProvider'
+
+const configProviderMock = vi.fn(
+ ({ children }: { children: ReactNode }) => {children}
,
+)
+
+vi.mock('antd', async () => {
+ const actual = await vi.importActual('antd')
+ return {
+ ...actual,
+ ConfigProvider: (props: { children: ReactNode; theme: unknown; locale: unknown }) => configProviderMock(props),
+ }
+})
+
+describe('ThemeProvider', () => {
+ it('passes the theme tokens and locale to ConfigProvider', () => {
+ render(
+
+ theme child
+ ,
+ )
+
+ expect(screen.getByTestId('config-provider')).toBeInTheDocument()
+ expect(screen.getByText('theme child')).toBeInTheDocument()
+ expect(configProviderMock).toHaveBeenCalledWith(
+ expect.objectContaining({
+ locale: zhCN,
+ theme: expect.objectContaining({
+ token: expect.objectContaining({
+ colorPrimary: '#0e5a6a',
+ colorSuccess: '#217a5b',
+ colorWarning: '#b7791f',
+ colorError: '#b33a3a',
+ colorInfo: '#2d6a9f',
+ fontFamily: '"IBM Plex Sans", "PingFang SC", "Microsoft YaHei", sans-serif',
+ borderRadius: 10,
+ }),
+ components: expect.objectContaining({
+ Table: expect.objectContaining({
+ headerBg: '#f8f5ef',
+ borderColor: '#d6d0c3',
+ }),
+ Card: expect.objectContaining({
+ borderRadiusLG: 16,
+ }),
+ Button: expect.objectContaining({
+ controlHeightLG: 44,
+ }),
+ Input: expect.objectContaining({
+ controlHeight: 36,
+ }),
+ Select: expect.objectContaining({
+ controlHeight: 36,
+ }),
+ }),
+ }),
+ }),
+ )
+ })
+})
diff --git a/frontend/admin/src/app/providers/ThemeProvider.tsx b/frontend/admin/src/app/providers/ThemeProvider.tsx
new file mode 100644
index 0000000..4027e68
--- /dev/null
+++ b/frontend/admin/src/app/providers/ThemeProvider.tsx
@@ -0,0 +1,89 @@
+/**
+ * 主题配置 Provider
+ * 使用 Ant Design 5 的 ConfigProvider 覆盖主题 Token
+ */
+
+import { ConfigProvider, type ThemeConfig } from 'antd'
+import zhCN from 'antd/locale/zh_CN'
+import type { ReactNode } from 'react'
+
+/**
+ * Ant Design 主题配置
+ * 基于 Mineral Console 视觉方向
+ */
+const themeConfig: ThemeConfig = {
+ token: {
+ // 主色
+ colorPrimary: '#0e5a6a',
+ colorPrimaryHover: '#0a4b59',
+ colorPrimaryActive: '#083d4a',
+
+ // 成功色
+ colorSuccess: '#217a5b',
+
+ // 警告色
+ colorWarning: '#b7791f',
+
+ // 错误色
+ colorError: '#b33a3a',
+
+ // 信息色
+ colorInfo: '#2d6a9f',
+
+ // 字体
+ fontFamily: '"IBM Plex Sans", "PingFang SC", "Microsoft YaHei", sans-serif',
+ fontSize: 14,
+
+ // 圆角
+ borderRadius: 10,
+ borderRadiusLG: 16,
+ borderRadiusSM: 6,
+
+ // 链接
+ colorLink: '#0e5a6a',
+ colorLinkHover: '#0a4b59',
+ colorLinkActive: '#083d4a',
+ },
+ components: {
+ // 表格组件定制
+ Table: {
+ headerBg: '#f8f5ef',
+ borderColor: '#d6d0c3',
+ rowHoverBg: 'rgba(14, 90, 106, 0.04)',
+ },
+ // 卡片组件定制
+ Card: {
+ borderRadiusLG: 16,
+ boxShadowTertiary: '0 10px 30px rgba(23, 33, 43, 0.06)',
+ },
+ // 按钮组件定制
+ Button: {
+ borderRadius: 10,
+ controlHeight: 36,
+ controlHeightLG: 44,
+ controlHeightSM: 28,
+ },
+ // 输入框组件定制
+ Input: {
+ borderRadius: 10,
+ controlHeight: 36,
+ },
+ // 选择器组件定制
+ Select: {
+ borderRadius: 10,
+ controlHeight: 36,
+ },
+ },
+}
+
+interface ThemeProviderProps {
+ children: ReactNode
+}
+
+export function ThemeProvider({ children }: ThemeProviderProps) {
+ return (
+
+ {children}
+
+ )
+}
diff --git a/frontend/admin/src/app/providers/auth-context.ts b/frontend/admin/src/app/providers/auth-context.ts
new file mode 100644
index 0000000..6237751
--- /dev/null
+++ b/frontend/admin/src/app/providers/auth-context.ts
@@ -0,0 +1,24 @@
+import { createContext, useContext } from 'react'
+import type { Role, SessionUser, TokenBundle } from '@/types'
+
+export interface AuthContextValue {
+ user: SessionUser | null
+ roles: Role[]
+ isAdmin: boolean
+ isAuthenticated: boolean
+ isLoading: boolean
+ onLoginSuccess: (tokenBundle: TokenBundle) => Promise
+ logout: () => Promise
+ refreshUser: () => Promise
+}
+
+export const AuthContext = createContext(null)
+
+export function useAuth(): AuthContextValue {
+ const context = useContext(AuthContext)
+ if (!context) {
+ throw new Error('useAuth must be used within an AuthProvider')
+ }
+
+ return context
+}
diff --git a/frontend/admin/src/app/router.test.tsx b/frontend/admin/src/app/router.test.tsx
new file mode 100644
index 0000000..e81bad3
--- /dev/null
+++ b/frontend/admin/src/app/router.test.tsx
@@ -0,0 +1,210 @@
+import { createElement, type ComponentType, type ReactElement, type ReactNode } from 'react'
+import { afterEach, describe, expect, it, vi } from 'vitest'
+import { Navigate, type RouteObject } from 'react-router-dom'
+
+type PageDefinition = {
+ exportName: string
+ routePath: string
+ requireAdmin?: boolean
+}
+
+type RouterFixture = {
+ lazyPage: typeof import('./router').lazyPage
+ router: { routes: RouteObject[] }
+}
+
+type LazyType = {
+ _init: (payload: unknown) => unknown
+ _payload: unknown
+}
+
+const publicPages: PageDefinition[] = [
+ { routePath: '/login', exportName: 'LoginPage' },
+ { routePath: '/register', exportName: 'RegisterPage' },
+ { routePath: '/bootstrap-admin', exportName: 'BootstrapAdminPage' },
+ { routePath: '/activate-account', exportName: 'ActivateAccountPage' },
+ { routePath: '/login/oauth/callback', exportName: 'OAuthCallbackPage' },
+ { routePath: '/forgot-password', exportName: 'ForgotPasswordPage' },
+ { routePath: '/reset-password', exportName: 'ResetPasswordPage' },
+]
+
+const protectedPages: PageDefinition[] = [
+ { routePath: 'dashboard', exportName: 'DashboardPage', requireAdmin: true },
+ { routePath: 'users', exportName: 'UsersPage', requireAdmin: true },
+ { routePath: 'roles', exportName: 'RolesPage', requireAdmin: true },
+ { routePath: 'permissions', exportName: 'PermissionsPage', requireAdmin: true },
+ { routePath: 'logs/login', exportName: 'LoginLogsPage', requireAdmin: true },
+ { routePath: 'logs/operation', exportName: 'OperationLogsPage', requireAdmin: true },
+ { routePath: 'webhooks', exportName: 'WebhooksPage' },
+ { routePath: 'import-export', exportName: 'ImportExportPage', requireAdmin: true },
+ { routePath: 'profile', exportName: 'ProfilePage' },
+ { routePath: 'profile/security', exportName: 'ProfileSecurityPage' },
+]
+
+const resetModulePaths = ['./router', 'react-router-dom']
+
+afterEach(() => {
+ vi.clearAllMocks()
+ for (const modulePath of resetModulePaths) {
+ vi.doUnmock(modulePath)
+ }
+ vi.resetModules()
+})
+
+function asRouteObject(route: unknown): RouteObject {
+ return route as RouteObject
+}
+
+function asElement(node: ReactNode | undefined): ReactElement<{ children?: ReactNode }> | null {
+ return node && typeof node === 'object' && 'type' in node ? (node as ReactElement<{ children?: ReactNode }>) : null
+}
+
+function asLazyType(type: unknown): LazyType {
+ return type as LazyType
+}
+
+function getComponentName(type: unknown): string | undefined {
+ return typeof type === 'function' ? type.name : undefined
+}
+
+function getRouteByPath(routes: RouteObject[], path: string): RouteObject {
+ const route = routes.find((candidate) => candidate.path === path)
+ expect(route).toBeDefined()
+ return route as RouteObject
+}
+
+function expectLazyElement(node: ReactNode | undefined): LazyType {
+ const element = asElement(node)
+ expect(element).not.toBeNull()
+
+ const lazyType = asLazyType(element?.type)
+ expect(typeof lazyType).toBe('object')
+ expect(typeof lazyType._init).toBe('function')
+
+ return lazyType
+}
+
+async function resolveLazyType(lazyType: LazyType): Promise> {
+ for (;;) {
+ try {
+ return lazyType._init(lazyType._payload) as ComponentType