From aafc6bcf033e170074f1090404cdb566252a3287 Mon Sep 17 00:00:00 2001 From: qq143838 Date: Fri, 28 Aug 2026 05:21:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E5=8C=96=E6=B8=B8=E6=88=8F?= =?UTF-8?q?=E8=BF=90=E8=90=A5=E5=90=8E=E5=8F=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 1 + .gitignore | 8 + .oxfmtrc.json | 14 + .oxlintrc.json | 49 + .storybook/main.ts | 25 + .storybook/preview-head.html | 6 + .storybook/preview.tsx | 68 + AGENTS.md | 18 + README.md | 91 + index.html | 14 + login.html | 14 + package-lock.json | 8508 +++++++++++++++++ package.json | 76 + public/mockServiceWorker.js | 361 + src/api/QueryProvider.tsx | 8 + src/api/http-init.ts | 211 + src/api/queries/useGameAccountsQuery.ts | 80 + src/api/queries/useGameAnnouncementsQuery.ts | 123 + src/api/queries/useGameCdksQuery.ts | 109 + src/api/queries/useGameOrdersQuery.ts | 117 + src/api/queries/useGameOverviewQuery.ts | 39 + src/api/queries/useGamePlayersQuery.ts | 115 + src/api/queries/useGameServersQuery.ts | 125 + src/api/queryKeys.ts | 29 + src/components/form/rhf/FormField.stories.tsx | 171 + src/components/form/rhf/FormField.tsx | 98 + src/components/form/rhf/index.ts | 5 + .../form/rhf/normalizeAntdOnChange.ts | 10 + src/components/form/rhf/rhfZodValidate.ts | 9 + src/components/form/rhf/toDotted.ts | 5 + src/components/form/rhf/useZodForm.ts | 20 + src/components/viz/Sparkline.css | 113 + src/components/viz/Sparkline.stories.tsx | 64 + src/components/viz/Sparkline.tsx | 611 ++ src/components/viz/index.ts | 1 + src/entries/login.tsx | 37 + src/env.d.ts | 5 + src/hooks/usePageTitle.ts | 22 + src/hooks/useTheme.tsx | 188 + src/i18n/locales/ar-EG.json | 18 + src/i18n/locales/en-US.json | 18 + src/i18n/locales/es-ES.json | 18 + src/i18n/locales/fa-IR.json | 18 + src/i18n/locales/id-ID.json | 18 + src/i18n/locales/ja-JP.json | 18 + src/i18n/locales/pt-BR.json | 18 + src/i18n/locales/ru-RU.json | 18 + src/i18n/locales/tr-TR.json | 18 + src/i18n/locales/uk-UA.json | 18 + src/i18n/locales/vi-VN.json | 18 + src/i18n/locales/zh-CN.json | 18 + src/i18n/locales/zh-TW.json | 18 + src/i18n/react.ts | 45 + src/layouts/AppSidebar.css | 170 + src/layouts/AppSidebar.tsx | 148 + src/layouts/PanelLayout.tsx | 8 + src/layouts/WorkspaceTabs.css | 215 + src/layouts/WorkspaceTabs.tsx | 329 + src/main.tsx | 36 + src/mocks/browser.ts | 5 + src/mocks/handlers.ts | 1575 +++ src/mocks/index.ts | 9 + src/pages/accounts/AccountsPage.css | 447 + src/pages/accounts/AccountsPage.tsx | 634 ++ src/pages/announcements/AnnouncementsPage.css | 511 + src/pages/announcements/AnnouncementsPage.tsx | 793 ++ src/pages/cdks/CdksPage.css | 572 ++ src/pages/cdks/CdksPage.tsx | 814 ++ src/pages/login/LoginPage.css | 413 + src/pages/login/LoginPage.tsx | 257 + src/pages/orders/OrdersPage.css | 510 + src/pages/orders/OrdersPage.tsx | 696 ++ src/pages/overview/OverviewPage.css | 345 + src/pages/overview/OverviewPage.tsx | 435 + src/pages/players/PlayersPage.css | 500 + src/pages/players/PlayersPage.tsx | 774 ++ src/pages/servers/ServersPage.css | 514 + src/pages/servers/ServersPage.tsx | 814 ++ src/queryClient.ts | 14 + src/routes.tsx | 61 + src/schemas/gameAccount.ts | 48 + src/schemas/gameAnnouncement.ts | 91 + src/schemas/gameCdk.ts | 101 + src/schemas/gameOrder.ts | 101 + src/schemas/gameOverview.ts | 60 + src/schemas/gamePlayer.ts | 79 + src/schemas/gameServer.ts | 77 + src/schemas/login.ts | 15 + src/styles/global.css | 43 + src/test/app-sidebar.test.tsx | 44 + src/test/game-account-api.test.ts | 44 + src/test/game-accounts-page.test.tsx | 255 + src/test/game-announcement-api.test.ts | 123 + src/test/game-announcements-page.test.tsx | 238 + src/test/game-cdk-api.test.ts | 106 + src/test/game-cdks-page.test.tsx | 270 + src/test/game-order-api.test.ts | 120 + src/test/game-orders-page.test.tsx | 243 + src/test/game-overview-page.test.tsx | 94 + src/test/game-player-api.test.ts | 114 + src/test/game-players-page.test.tsx | 232 + src/test/game-server-api.test.ts | 117 + src/test/game-servers-page.test.tsx | 271 + src/test/http-init-msw.test.ts | 91 + src/test/http-init.test.tsx | 295 + src/test/httpUtil.test.ts | 111 + src/test/i18n-locales.test.ts | 43 + src/test/msw/handlers.ts | 7 + src/test/msw/server.ts | 5 + src/test/rhf-form-field.test.tsx | 96 + src/test/setup.components.ts | 100 + src/test/setup.msw.ts | 7 + src/test/setup.ts | 8 + src/test/storybook-theme.test.tsx | 57 + src/test/test-utils.tsx | 64 + src/test/workspace-tabs.test.tsx | 140 + src/test/zodValidate.test.ts | 51 + src/utils/index.ts | 193 + src/utils/messageBus.ts | 12 + src/utils/zodValidate.ts | 24 + tsconfig.json | 29 + vite.config.js | 152 + vitest.config.ts | 58 + vitest.shims.d.ts | 1 + 124 files changed, 27876 insertions(+) create mode 100644 .env.development create mode 100644 .gitignore create mode 100644 .oxfmtrc.json create mode 100644 .oxlintrc.json create mode 100644 .storybook/main.ts create mode 100644 .storybook/preview-head.html create mode 100644 .storybook/preview.tsx create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 index.html create mode 100644 login.html create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 public/mockServiceWorker.js create mode 100644 src/api/QueryProvider.tsx create mode 100644 src/api/http-init.ts create mode 100644 src/api/queries/useGameAccountsQuery.ts create mode 100644 src/api/queries/useGameAnnouncementsQuery.ts create mode 100644 src/api/queries/useGameCdksQuery.ts create mode 100644 src/api/queries/useGameOrdersQuery.ts create mode 100644 src/api/queries/useGameOverviewQuery.ts create mode 100644 src/api/queries/useGamePlayersQuery.ts create mode 100644 src/api/queries/useGameServersQuery.ts create mode 100644 src/api/queryKeys.ts create mode 100644 src/components/form/rhf/FormField.stories.tsx create mode 100644 src/components/form/rhf/FormField.tsx create mode 100644 src/components/form/rhf/index.ts create mode 100644 src/components/form/rhf/normalizeAntdOnChange.ts create mode 100644 src/components/form/rhf/rhfZodValidate.ts create mode 100644 src/components/form/rhf/toDotted.ts create mode 100644 src/components/form/rhf/useZodForm.ts create mode 100644 src/components/viz/Sparkline.css create mode 100644 src/components/viz/Sparkline.stories.tsx create mode 100644 src/components/viz/Sparkline.tsx create mode 100644 src/components/viz/index.ts create mode 100644 src/entries/login.tsx create mode 100644 src/env.d.ts create mode 100644 src/hooks/usePageTitle.ts create mode 100644 src/hooks/useTheme.tsx create mode 100644 src/i18n/locales/ar-EG.json create mode 100644 src/i18n/locales/en-US.json create mode 100644 src/i18n/locales/es-ES.json create mode 100644 src/i18n/locales/fa-IR.json create mode 100644 src/i18n/locales/id-ID.json create mode 100644 src/i18n/locales/ja-JP.json create mode 100644 src/i18n/locales/pt-BR.json create mode 100644 src/i18n/locales/ru-RU.json create mode 100644 src/i18n/locales/tr-TR.json create mode 100644 src/i18n/locales/uk-UA.json create mode 100644 src/i18n/locales/vi-VN.json create mode 100644 src/i18n/locales/zh-CN.json create mode 100644 src/i18n/locales/zh-TW.json create mode 100644 src/i18n/react.ts create mode 100644 src/layouts/AppSidebar.css create mode 100644 src/layouts/AppSidebar.tsx create mode 100644 src/layouts/PanelLayout.tsx create mode 100644 src/layouts/WorkspaceTabs.css create mode 100644 src/layouts/WorkspaceTabs.tsx create mode 100644 src/main.tsx create mode 100644 src/mocks/browser.ts create mode 100644 src/mocks/handlers.ts create mode 100644 src/mocks/index.ts create mode 100644 src/pages/accounts/AccountsPage.css create mode 100644 src/pages/accounts/AccountsPage.tsx create mode 100644 src/pages/announcements/AnnouncementsPage.css create mode 100644 src/pages/announcements/AnnouncementsPage.tsx create mode 100644 src/pages/cdks/CdksPage.css create mode 100644 src/pages/cdks/CdksPage.tsx create mode 100644 src/pages/login/LoginPage.css create mode 100644 src/pages/login/LoginPage.tsx create mode 100644 src/pages/orders/OrdersPage.css create mode 100644 src/pages/orders/OrdersPage.tsx create mode 100644 src/pages/overview/OverviewPage.css create mode 100644 src/pages/overview/OverviewPage.tsx create mode 100644 src/pages/players/PlayersPage.css create mode 100644 src/pages/players/PlayersPage.tsx create mode 100644 src/pages/servers/ServersPage.css create mode 100644 src/pages/servers/ServersPage.tsx create mode 100644 src/queryClient.ts create mode 100644 src/routes.tsx create mode 100644 src/schemas/gameAccount.ts create mode 100644 src/schemas/gameAnnouncement.ts create mode 100644 src/schemas/gameCdk.ts create mode 100644 src/schemas/gameOrder.ts create mode 100644 src/schemas/gameOverview.ts create mode 100644 src/schemas/gamePlayer.ts create mode 100644 src/schemas/gameServer.ts create mode 100644 src/schemas/login.ts create mode 100644 src/styles/global.css create mode 100644 src/test/app-sidebar.test.tsx create mode 100644 src/test/game-account-api.test.ts create mode 100644 src/test/game-accounts-page.test.tsx create mode 100644 src/test/game-announcement-api.test.ts create mode 100644 src/test/game-announcements-page.test.tsx create mode 100644 src/test/game-cdk-api.test.ts create mode 100644 src/test/game-cdks-page.test.tsx create mode 100644 src/test/game-order-api.test.ts create mode 100644 src/test/game-orders-page.test.tsx create mode 100644 src/test/game-overview-page.test.tsx create mode 100644 src/test/game-player-api.test.ts create mode 100644 src/test/game-players-page.test.tsx create mode 100644 src/test/game-server-api.test.ts create mode 100644 src/test/game-servers-page.test.tsx create mode 100644 src/test/http-init-msw.test.ts create mode 100644 src/test/http-init.test.tsx create mode 100644 src/test/httpUtil.test.ts create mode 100644 src/test/i18n-locales.test.ts create mode 100644 src/test/msw/handlers.ts create mode 100644 src/test/msw/server.ts create mode 100644 src/test/rhf-form-field.test.tsx create mode 100644 src/test/setup.components.ts create mode 100644 src/test/setup.msw.ts create mode 100644 src/test/setup.ts create mode 100644 src/test/storybook-theme.test.tsx create mode 100644 src/test/test-utils.tsx create mode 100644 src/test/workspace-tabs.test.tsx create mode 100644 src/test/zodValidate.test.ts create mode 100644 src/utils/index.ts create mode 100644 src/utils/messageBus.ts create mode 100644 src/utils/zodValidate.ts create mode 100644 tsconfig.json create mode 100644 vite.config.js create mode 100644 vitest.config.ts create mode 100644 vitest.shims.d.ts diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..32f67cf --- /dev/null +++ b/.env.development @@ -0,0 +1 @@ +VITE_MOCK_API=true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eafe9be --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +node_modules/ +dist/ +.vite/ +*.log +*.zip +*.tsbuildinfo +coverage/ +storybook-static/ diff --git a/.oxfmtrc.json b/.oxfmtrc.json new file mode 100644 index 0000000..3d73d55 --- /dev/null +++ b/.oxfmtrc.json @@ -0,0 +1,14 @@ +{ + "$schema": "./node_modules/oxfmt/configuration_schema.json", + "semi": true, + "singleQuote": true, + "trailingComma": "all", + "printWidth": 100, + "tabWidth": 2, + "ignorePatterns": [ + "node_modules", + "src/generated", + "public", + "tools/oxlint/__fixtures__" + ] +} diff --git a/.oxlintrc.json b/.oxlintrc.json new file mode 100644 index 0000000..91a3400 --- /dev/null +++ b/.oxlintrc.json @@ -0,0 +1,49 @@ +{ + "$schema": "./node_modules/oxlint/configuration_schema.json", + "ignorePatterns": [ + "node_modules/**" + ], + "plugins": [ + "typescript", + "react", + "jsx-a11y" + ], + "categories": { + "correctness": "error" + }, + "env": { + "browser": true, + "es2022": true + }, + "rules": { + "typescript/no-explicit-any": "error", + "typescript/no-unused-vars": [ + "warn", + { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_", + "caughtErrorsIgnorePattern": "^_" + } + ], + "typescript/ban-ts-comment": "error", + "typescript/no-empty-object-type": "error", + "typescript/no-namespace": "error", + "typescript/no-require-imports": "error", + "typescript/no-this-alias": "error", + "typescript/no-unsafe-function-type": "error", + "typescript/no-unused-expressions": "warn", + "typescript/no-wrapper-object-types": "error", + "typescript/prefer-as-const": "error", + "typescript/triple-slash-reference": "error", + "no-empty": [ + "error", + { + "allowEmptyCatch": true + } + ], + "react-hooks/rules-of-hooks": "error", + "react-hooks/exhaustive-deps": "error", + "jsx-a11y/no-autofocus": "off", + "jsx-a11y/prefer-tag-over-role": "off" + } +} diff --git a/.storybook/main.ts b/.storybook/main.ts new file mode 100644 index 0000000..1cd2ea7 --- /dev/null +++ b/.storybook/main.ts @@ -0,0 +1,25 @@ +import type { StorybookConfig } from '@storybook/react-vite'; + +const config: StorybookConfig = { + framework: { + name: '@storybook/react-vite', + options: {}, + }, + stories: ['../src/**/*.stories.@(ts|tsx)'], + addons: ['@storybook/addon-docs', '@storybook/addon-a11y', '@storybook/addon-vitest'], + viteFinal: (viteConfig) => { + if (viteConfig.build) { + viteConfig.build.outDir = undefined; + viteConfig.build.emptyOutDir = false; + if (viteConfig.build.rollupOptions) { + viteConfig.build.rollupOptions.input = undefined; + } + } + if (viteConfig.experimental) { + viteConfig.experimental.renderBuiltUrl = undefined; + } + return viteConfig; + }, +}; + +export default config; diff --git a/.storybook/preview-head.html b/.storybook/preview-head.html new file mode 100644 index 0000000..3cfb32a --- /dev/null +++ b/.storybook/preview-head.html @@ -0,0 +1,6 @@ + diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx new file mode 100644 index 0000000..675defb --- /dev/null +++ b/.storybook/preview.tsx @@ -0,0 +1,68 @@ +import { useLayoutEffect } from 'react'; +import type { Decorator, Preview } from '@storybook/react-vite'; +import { ConfigProvider } from 'antd'; +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import { buildAntdThemeConfig } from '@/hooks/useTheme'; +import enUS from '../src/i18n/locales/en-US.json'; + +if (!i18next.isInitialized) { + void i18next.use(initReactI18next).init({ + lng: 'en-US', + fallbackLng: 'en-US', + resources: { 'en-US': { translation: enUS } }, + interpolation: { escapeValue: false, prefix: '{', suffix: '}' }, + returnNull: false, + }); +} + +export const withTheme: Decorator = function ThemeDecorator(Story, context) { + const dark = context.globals.theme === 'dark'; + useLayoutEffect(() => { + document.body.classList.remove('dark', 'light'); + document.body.classList.add(dark ? 'dark' : 'light'); + document.documentElement.removeAttribute('data-theme'); + }, [dark]); + return ( + +
+ +
+
+ ); +}; + +const preview: Preview = { + decorators: [withTheme], + globalTypes: { + theme: { + description: 'Ant Design theme', + defaultValue: 'light', + toolbar: { + title: 'Theme', + icon: 'circlehollow', + items: [ + { value: 'light', title: 'Light' }, + { value: 'dark', title: 'Dark' }, + ], + dynamicTitle: true, + }, + }, + }, + parameters: { + controls: { + expanded: true, + sort: 'requiredFirst', + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + a11y: { + test: 'error', + }, + }, +}; + +export default preview; diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..bf31122 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,18 @@ +# Admin UI Agent Instructions + +## Collaboration Rules + +1. 禁止启用任何子代理。所有分析、开发、测试和验证工作必须由当前主代理直接完成。 + +## Git 提交规则 + +1. 所有 Git 提交信息必须使用中文,禁止使用英文提交信息。 +2. 推送前必须检查本次提交信息,确认其准确描述修改内容且为中文。 + +## Display Formatting Rules + +1. 所有用户界面中的可见数字禁止使用千位分隔符。玩家数量、订单数量、金额、统计指标、图表坐标、图表 Tooltip 和表格数据都必须显示为连续数字,例如使用 `10213`,禁止显示为 `10,213`。 + +## Layout Rules + +1. 桌面端运营页面必须占满侧边栏右侧的可用宽度,只保留正常页面内边距。禁止使用任意固定 `max-width` 将主内容区居中收窄。 diff --git a/README.md b/README.md new file mode 100644 index 0000000..599c45e --- /dev/null +++ b/README.md @@ -0,0 +1,91 @@ +# 游戏运营后台 + +可复用的游戏服务端运营后台前端模板,技术栈为 React、TypeScript、Ant Design、TanStack Query 和 Vite。 + +当前包含: + +- 登录页 +- 总览 +- 账号管理 +- 区服管理 +- 玩家管理 +- CDK管理 +- 公告管理 +- 订单管理 + +## 开发 + +```sh +npm install +npm run dev +``` + +开发服务器监听 `0.0.0.0:19999`: + +- 登录页:`http://localhost:19999/` +- 后台:`http://localhost:19999/panel/` + +`.env.development` 默认启用 MSW Mock API,因此不依赖后端即可完成页面开发和验收。 + +## 配置 + +| 变量 | 用途 | +|---|---| +| `VITE_MOCK_API` | 开发环境是否启用 Mock API | +| `VITE_ADMIN_BASE_PATH` | 后台部署路径前缀,默认 `/` | +| `VITE_BACKEND_TARGET` | 可选的开发代理后端地址 | +| `VITE_SOURCEMAP` | 生产构建是否输出 sourcemap | + +生产环境可由服务端在 HTML 中提前注入 `window.ADMIN_UI_BASE_PATH`。路由固定挂载在该前缀下的 `/panel`,适合由 Go 的 `embed.FS` 或其他静态资源方案直接嵌入。 + +## API 约定 + +现有页面通过以下前缀访问业务接口: + +```text +/panel/api/game/overview +/panel/api/game/accounts +/panel/api/game/servers +/panel/api/game/players +/panel/api/game/cdks +/panel/api/game/announcements +/panel/api/game/orders +``` + +接口响应使用统一结构: + +```json +{ + "success": true, + "msg": "", + "obj": {} +} +``` + +开发阶段的对应实现位于 `src/mocks/handlers.ts`,接入真实服务端时可保持页面和 Query 层不变。 + +## 命令 + +```sh +npm run typecheck +npm run lint +npm run format:check +npm run test +npm run build +``` + +## 目录 + +```text +src/ + api/ 请求封装和游戏业务 Query + components/ 登录表单与图表组件 + hooks/ 主题和页面标题 + i18n/ 登录页多语言 + layouts/ 左侧菜单和顶部标签栏 + mocks/ 开发环境 Mock API + pages/ 当前运营后台页面 + schemas/ 游戏业务 Zod Schema + styles/ 全局布局样式 + test/ 当前功能回归测试 +``` diff --git a/index.html b/index.html new file mode 100644 index 0000000..4a40399 --- /dev/null +++ b/index.html @@ -0,0 +1,14 @@ + + + + + + + 游戏运营后台 + + +
+
+ + + diff --git a/login.html b/login.html new file mode 100644 index 0000000..051a840 --- /dev/null +++ b/login.html @@ -0,0 +1,14 @@ + + + + + + + 游戏运营后台 + + +
+
+ + + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..efc77e3 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,8508 @@ +{ + "name": "admin-ui", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "admin-ui", + "version": "1.0.0", + "dependencies": { + "@ant-design/icons": "^6.3.2", + "@hookform/resolvers": "^5.9.1", + "@tanstack/react-query": "^5.102.2", + "antd": "^6.6.1", + "dayjs": "^1.11.23", + "i18next": "^26.4.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-hook-form": "^7.86.0", + "react-i18next": "^17.0.12", + "react-router": "^8.3.0", + "uplot": "^1.6.32", + "zod": "^4.4.3" + }, + "devDependencies": { + "@storybook/addon-a11y": "^10.5.10", + "@storybook/addon-docs": "^10.5.10", + "@storybook/addon-vitest": "^10.5.10", + "@storybook/react-vite": "^10.5.10", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@vitejs/plugin-react": "^6.1.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/coverage-v8": "^4.1.11", + "jsdom": "^30.0.1", + "lint-staged": "^17.3.0", + "msw": "^2.15.0", + "oxfmt": "0.64.0", + "oxlint": "1.79.0", + "oxlint-tsgolint": "^7.0.2001", + "playwright": "^1.62.1", + "storybook": "^10.5.10", + "typescript": "7.0.2", + "vite": "8.2.2", + "vitest": "^4.1.11" + }, + "engines": { + "node": ">=24.0.0", + "npm": ">=10.0.0" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ant-design/colors": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/@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": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.1.2.tgz", + "integrity": "sha512-2Hy8BnCEH31xPeSLbhhB2ctCPXE2ZnASdi+KbSeS79BNbUhL9hAEe20SkUk+BR8aKTmqb6+FKFruk7w8z0VoRQ==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@emotion/hash": "^0.8.0", + "@emotion/unitless": "^0.7.5", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1", + "csstype": "^3.1.3", + "stylis": "^4.3.4" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@ant-design/cssinjs-utils": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.1.2.tgz", + "integrity": "sha512-5fTHQ158jJJ5dC/ECeyIdZUzKxE/mpEMRZxthyG1sw/AKRHKgJBg00Yi6ACVXgycdje7KahRNvNET/uBccwCnA==", + "license": "MIT", + "dependencies": { + "@ant-design/cssinjs": "^2.1.2", + "@babel/runtime": "^7.23.2", + "@rc-component/util": "^1.4.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/@ant-design/fast-color": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@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.3.2", + "resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.3.2.tgz", + "integrity": "sha512-B6O5a5XJ4wjtNOfZejXYwHW5zvKV5gYkjGf11dHGLEbKn0ABDGndo41+gfIiXyTFhvESj4XTotuud33mUFid0g==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/icons-svg": "^4.5.0", + "@rc-component/util": "^1.11.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.5.0", + "resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.5.0.tgz", + "integrity": "sha512-1BTUFyKPTBZ53MuTP8s0k5SFEXL7o3VHEOwLgzaoWKwnBeqIcqUtVshc4SKzhI6uACfqhJqBwBUE9FsWR3uULA==", + "license": "MIT" + }, + "node_modules/@ant-design/react-slick": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz", + "integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.28.4", + "clsx": "^2.1.1", + "json2mq": "^0.2.0", + "throttle-debounce": "^5.0.0" + }, + "peerDependencies": { + "react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@asamuzakjp/css-color": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.7.tgz", + "integrity": "sha512-vC/bk1Lz7Tn/EfU9/apOTBk80/8dyGyWMowPoV1tJ52muDGsDqt2HPT2klrFUiY60MQmQv9q8yIht15JnBgDGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^3.3.0", + "@csstools/css-color-parser": "^4.1.10", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", + "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2" + }, + "engines": { + "node": "^22.13.0 || >=24.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", + "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-compilation-targets": "^7.29.7", + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helpers": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7", + "@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/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.8.tgz", + "integrity": "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.8", + "@babel/types": "^7.29.8", + "@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.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/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/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.8.tgz", + "integrity": "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.8.tgz", + "integrity": "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.8", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.8", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.8", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" + } + }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.1.tgz", + "integrity": "sha512-gLNsunvwf3mCi5u5o46/Z/JcJMnhbHSaZ69rkgPzNM3J4s8hWwpPUQB6/tt0EDFyCiWzxANlx+2LJwpYj4zS1w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", + "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.2.0.tgz", + "integrity": "sha512-5+5LEmFuY1AjXdYhmgjTJogtQnP1evJ1zrBZGUNZ0thkpwnnmKxcHdAMn/OtFjAb25zA+jKDVYVRl+5G7rjv1A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.1", + "@csstools/css-calc": "^3.3.0" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.8.tgz", + "integrity": "sha512-CpMLjAvwQg3BL5S0IeqsZNMH7EQrEWi0kLKOC13ZBF0ZwERiLWlibNPJr8G1kdU3Ms/r2KiNrF81pUh2HwAHdg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emotion/hash": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", + "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==", + "license": "MIT" + }, + "node_modules/@emotion/unitless": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", + "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==", + "license": "MIT" + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.9.1", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.9.1.tgz", + "integrity": "sha512-7b7vsbraJxKgjVSA1Nur9tLwj539WGJUBLA7QNvXnFoT2pM5Z7G+6rlukk4B2/QrTZy6huRtH6wKeESPKuIr6w==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "@sinclair/typebox": ">=0.25.24", + "@standard-schema/spec": "^1.0.0", + "@typeschema/main": ">=0.13.7", + "@vinejs/vine": "^2.0.0 || ^3.0.0 || ^4.0.0", + "ajv": "^8.12.0", + "ajv-errors": "^3.0.0", + "ajv-formats": "^2.1.1", + "arktype": "^2.0.0", + "ata-validator": "^1.2.0", + "class-transformer": ">=0.4.0", + "class-validator": ">=0.12.0", + "computed-types": "^1.0.0", + "effect": "^3.10.3", + "fluentvalidation-ts": "^3.0.0", + "fp-ts": "^2.7.0", + "io-ts": "^2.0.0", + "joi": "^17.0.0 || ^18.0.0", + "nope-validator": ">=0.12.0", + "react-hook-form": "^7.55.0", + "superstruct": ">=0.12.0", + "typanion": "^3.3.2", + "valibot": ">=0.31.0 || ^1.0.0-beta.4 || ^1.0.0-rc", + "vest": ">=6.0.0", + "yup": "^1.0.0", + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@sinclair/typebox": { + "optional": true + }, + "@standard-schema/spec": { + "optional": true + }, + "@typeschema/main": { + "optional": true + }, + "@vinejs/vine": { + "optional": true + }, + "ajv": { + "optional": true + }, + "ajv-errors": { + "optional": true + }, + "ajv-formats": { + "optional": true + }, + "arktype": { + "optional": true + }, + "ata-validator": { + "optional": true + }, + "class-transformer": { + "optional": true + }, + "class-validator": { + "optional": true + }, + "computed-types": { + "optional": true + }, + "effect": { + "optional": true + }, + "fluentvalidation-ts": { + "optional": true + }, + "fp-ts": { + "optional": true + }, + "io-ts": { + "optional": true + }, + "joi": { + "optional": true + }, + "nope-validator": { + "optional": true + }, + "superstruct": { + "optional": true + }, + "typanion": { + "optional": true + }, + "valibot": { + "optional": true + }, + "vest": { + "optional": true + }, + "yup": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@inquirer/ansi": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/ansi/-/ansi-2.0.7.tgz", + "integrity": "sha512-3eTuUO1vH2cZm2ZKHeQxnOqlTi9EfZDGgIe3BL3I4u+rJHocr9Fz86M4fjYABPvFnQG/gGK551HqDiIcETwU6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/confirm": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-6.1.1.tgz", + "integrity": "sha512-eb8DBZcz/2qHWQda4rk2JiQk5h9QV/cVHi1yjt0f69WFZMRFn0sJTye3EAP8icut8UDMjQPsaH5KbcOogefrFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^11.2.1", + "@inquirer/type": "^4.0.7" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/core": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-11.2.1.tgz", + "integrity": "sha512-Qd6GJT1yVyrZZCfN8W2qKF5ApmqryXRhRKCuip8h01x2w/esJQ2XIYc6f9abMIHgKQdBfFTSOdbHRLAhuM09UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^2.0.7", + "@inquirer/figures": "^2.0.7", + "@inquirer/type": "^4.0.7", + "cli-width": "^4.1.0", + "fast-wrap-ansi": "^0.2.0", + "mute-stream": "^3.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@inquirer/figures": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-2.0.7.tgz", + "integrity": "sha512-aJ8TBPOGB6f/2qziPfElISTCEd5XOYTFckA2SGjhNmiKzfK/u4ot3v0DUzGVdUnKjN10EqnnEPck36BkyfLnJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + } + }, + "node_modules/@inquirer/type": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-4.0.7.tgz", + "integrity": "sha512-t28inv14nMQ1PhKpsJPY+kEs/c00qzeCOS2gTNRyTjG5d6qsVA2fItxW4hkvGZ5lvanGLdtCzVIx5dwdRpN1+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=23.5.0 || ^22.13.0 || ^20.17.0" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@joshwooding/vite-plugin-react-docgen-typescript": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@joshwooding/vite-plugin-react-docgen-typescript/-/vite-plugin-react-docgen-typescript-0.7.0.tgz", + "integrity": "sha512-qvsTEwEFefhdirGOPnu9Wp6ChfIwy2dBCRuETU3uE+4cC+PFoxMSiiEhxk4lOluA34eARHA0OxqsEUYDqRMgeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "^13.0.1", + "react-docgen-typescript": "^2.2.2" + }, + "peerDependencies": { + "typescript": ">= 4.3.x", + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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.npmjs.org/@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/@mdx-js/react": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", + "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mdx": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=16", + "react": ">=16" + } + }, + "node_modules/@mswjs/interceptors": { + "version": "0.41.9", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.9.tgz", + "integrity": "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-draft/deferred-promise": "^2.2.0", + "@open-draft/logger": "^0.3.0", + "@open-draft/until": "^2.0.0", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "strict-event-emitter": "^0.5.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@mswjs/interceptors/node_modules/@open-draft/deferred-promise": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", + "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.3.tgz", + "integrity": "sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@noble/hashes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-2.3.0.tgz", + "integrity": "sha512-oN+QwyX7VSHotibwubG3kpzbwKrfnyR6OOO+3Nk/53ADL7FmgHHz4TgrbaYKvvOw09u6QTx0oiH1cNCIOuN0CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@open-draft/deferred-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-3.0.0.tgz", + "integrity": "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-draft/logger": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz", + "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-node-process": "^1.2.0", + "outvariant": "^1.4.0" + } + }, + "node_modules/@open-draft/until": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz", + "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-parser/binding-android-arm-eabi": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm-eabi/-/binding-android-arm-eabi-0.127.0.tgz", + "integrity": "sha512-0LC7ye4hvqbIKxAzThzvswgHLFu2AURKzYLeSVvLdu2TBOYWQDmHnTqPLeA597BcUCxiLqLsS4CJ5uoI5WYWCQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-android-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-android-arm64/-/binding-android-arm64-0.127.0.tgz", + "integrity": "sha512-b5jtVTH6AU5CJXHNdj7Jj9IEiR9yVjjnwHzPJhGyHGPdcsZSzBCkS9GBbV33niRMvKthDwQRFRJfI4a+k4PvYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.127.0.tgz", + "integrity": "sha512-obCE8B7ISKkJidjlhv9xRGJPOSDG2Yu6PRga9Ruaz35uintHxbp1Ki/Yc71wx4rj3Edrm0a1kzG1TAwit0wFpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-darwin-x64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-x64/-/binding-darwin-x64-0.127.0.tgz", + "integrity": "sha512-JL6Xb5IwPQT8rUzlpsX7E+AgfcdNklXNPFp8pjCQQ5MQOQo5rtEB2ui+3Hgg9Sn7Y9Egj6YOLLiHhLpdAe12Aw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-freebsd-x64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-freebsd-x64/-/binding-freebsd-x64-0.127.0.tgz", + "integrity": "sha512-SDQ/3MQFw58fqQz3Z1PhSKFF3JoCF4gmlNjziDm8X02tTahCw0qJbd7FGPDKw1i4VTBZene9JPyC3mHtSvi+wA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-gnueabihf": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.127.0.tgz", + "integrity": "sha512-Av+D1MIqzV0YMGPT9we2SIZaMKD7Cxs4CvXSx/yxaWHewZjYEjScpOf5igc8IILASViw4WTnjlwUdI1KzVtDHQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm-musleabihf": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.127.0.tgz", + "integrity": "sha512-Cs2fdJ8cPpFdeebj6p4dag8A4+56hPvZ0AhQQzlaLswGz1tz7bXt1nETLeorrM9+AMcWFFkqxcXwDGfTVidY8g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.127.0.tgz", + "integrity": "sha512-qdOfTcT6SY8gsJrrV92uyEUyjqMGPpIB5JZUG6QN5dukYd+7/j0kX6MwK1DgQj39jtUYixxPiaRUiEN1+0CXgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-arm64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.127.0.tgz", + "integrity": "sha512-EoTCZneNFU/P2qrpEM+RHmQwt+CvDkyGESG6qhr7KaegXLZwePfbrkCDfAk8/rhxbDUVGsZILX+2tqPzFtoFWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-ppc64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.127.0.tgz", + "integrity": "sha512-zALjmZYgxFLHjXeudcDF0xFGNydTAtkAeXAr2EuC17ywCyFxcmQra4w0BMde0Yi/re4Bi4iwEoEXtYN7l6eBLQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.127.0.tgz", + "integrity": "sha512-fPP8M6zQLS7Jz7o9d5ArUSuAuSK3e+WCYVrCpdzeCOejidtZExJ9tjhDrAd3HEPqARBCPmdpqxESPFqy44vkBQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-riscv64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.127.0.tgz", + "integrity": "sha512-7IcC4Ao02oGpfnjt+X/oF4U2mllo2qoSkw5xxiXNKL9MCTsTiAC6616beOuehdxGcnz1bRoPC1RQ2f1GQDdN+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-s390x-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.127.0.tgz", + "integrity": "sha512-pbXIhiNFHoqWeqDNLiJ9JkpHz1IM9k4DXa66x+1GTWMG7iLxtkXgE53iiuKSXwmk3zIYmaPVfBvgcAhS583K4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-gnu": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.127.0.tgz", + "integrity": "sha512-MYCguB9RvBvlSd6gbuNI7QwiLoCCAlGnlRJFPrzLI6U1/9wkC/WK6LtBAUln55H1Ctqw45PWmqrobKoMhsYQzQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-linux-x64-musl": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-linux-x64-musl/-/binding-linux-x64-musl-0.127.0.tgz", + "integrity": "sha512-5eY0B/bxf1xIUxb4NOTvOI3KWtBQfPWYyKAzgcrCt0mDibSZygVpO1Pz8bkeiSZ5Jj9+M09dkggG3H8I5d0Uyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-openharmony-arm64": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-openharmony-arm64/-/binding-openharmony-arm64-0.127.0.tgz", + "integrity": "sha512-Gld0ajrFTUXNtdw20fVBuTQx66FA75nIVg+//pPfR3sXkuABB4mTBhl3r9JNzrJpgW//qiwxf0nWXUWGJSL3UQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-wasm32-wasi": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-wasm32-wasi/-/binding-wasm32-wasi-0.127.0.tgz", + "integrity": "sha512-T6KVD7rhLzFlwGRXMnxUFfkCZD8FHnb968wVXW1mXzgRFc5RNXOBY2mPPDZ77x5Ln76ltLMgtPg0cOkU1NSrEQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.9.2", + "@emnapi/runtime": "1.9.2", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-arm64-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.127.0.tgz", + "integrity": "sha512-Ujvw4X+LD1CCGULcsQcvb4YNVoBGqt+JHgNNzGGaCImELiZLk477ifUH53gIbE7EKd933NdTi25JWEr9K2HwXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-ia32-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.127.0.tgz", + "integrity": "sha512-0cwxKO7KHQQQfo4Uf4B2SQrhgm+cJaP9OvFFhx52Tkg4bezsacu83GB2/In5bC415Ueeym+kXdnge/57rbSfTw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-parser/binding-win32-x64-msvc": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-parser/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.127.0.tgz", + "integrity": "sha512-rOrnSQSCbhI2kowr9XxE7m9a8oQXnBHjnS6j95LxxAnEZ0+Fz20WlRXG4ondQb+ejjt2KOsa65sE6++L6kUd+w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@oxc-resolver/binding-android-arm-eabi": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.21.2.tgz", + "integrity": "sha512-xQoCRv+gKax9KTdwdaQNnAFOai8neay7g3jExDIORzhbrejwGSJaZNTdOJHR5ziLg2joMxOCMOFMo4zuxza2uQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-android-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm64/-/binding-android-arm64-11.21.2.tgz", + "integrity": "sha512-HF5oiE2L05yInPYCFD/4uxSrEZW4SuIfn99Y6L1xnJnzl066JR+MJs2rIdstw8A2MPlAKH+13dpFPNycjqzvGg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-arm64/-/binding-darwin-arm64-11.21.2.tgz", + "integrity": "sha512-UX4u49CVCAD8QZNELaW8eMGgMAGwFWYEPbvNsh+3r/gs4NX3KfpiACMVwRQT0EuH3uat9hM5Zl+Ppm9pJD8tgg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-darwin-x64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-darwin-x64/-/binding-darwin-x64-11.21.2.tgz", + "integrity": "sha512-J9xPx7YBkrRmJ+xl561ztnMWEc1aOyjEIxBiGX1dVb3u7bGSnfObfcZk+Pd+uM0HZAPNsQ1xvD8j52A/uOSqNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxc-resolver/binding-freebsd-x64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-freebsd-x64/-/binding-freebsd-x64-11.21.2.tgz", + "integrity": "sha512-fRlt7OvSaQkWj6+EDTVxawVxOlqJB2QSnBfkeCyK4RTvsGctbw3BiH2Tb7DzMs7bikc4BRBpvWP5zF9K8b54Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-gnueabihf": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-11.21.2.tgz", + "integrity": "sha512-gK+vPUcPQITkGwBKpZGrcDHSlU6eDGl7AQacxS2CEKAZIBHWkOVFeJwLZ4tYnA1acJqRM5lt7yYwPCVqGHIJ7A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm-musleabihf": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-11.21.2.tgz", + "integrity": "sha512-L/Rgas7SrOKy/z7IH+HxSFRqVO4PuLDKLEGKvnhoCBBq3UJ0YzGBou3qMzaAGgkJwsIvX2pBF+7ojzfUhLiZxQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-11.21.2.tgz", + "integrity": "sha512-CUEYvlX1Fk7E9kUMzuswru1J9HLxMwnpDeQGjQuI4ZH+iNCoa2X9T+pvyzrbsgl7WnIeTFTlNlHsRfVdf5g9/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-arm64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-arm64-musl/-/binding-linux-arm64-musl-11.21.2.tgz", + "integrity": "sha512-ViN1ZibQyxwC67GpoP33oo+S9UyUnkog13vzQb9+v9bCNvrVzJuk0MRdacjtv/9xfRMVF/eqHFyl8YOeykI10Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-ppc64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-11.21.2.tgz", + "integrity": "sha512-CU1sCqWnhGqYD5I1HedHk5pujrn7ssDkNB/AEQd/pd3D/EojVSgJUlpbafwIbyhia3PgIfkvdFpRPWSALzVumA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-11.21.2.tgz", + "integrity": "sha512-jWVyZtIHca4Gb96x7dag+y69vlei7ffjrsveLkmf2ZhqEAz6ZSBnY1GWvgZUaZlwZP62A3xD092BW97Q5VGc+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-riscv64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-11.21.2.tgz", + "integrity": "sha512-LF29obFqNgBUgDX7rmUK7M4D0JQG5LxhYzn3xXmECcHU9aQAdWG7NiY052qybtesEdwHQXKNTWYQ7mTsybNvWg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-s390x-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-11.21.2.tgz", + "integrity": "sha512-+NYcm+cCHBbtdQQ3A4phQTSuVRYnNHz7wrl9XRAPEovcdoqi0mb1K5ZOl+jN54ZD+q1zz3V0vltbFJmzecJKmw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-gnu": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-gnu/-/binding-linux-x64-gnu-11.21.2.tgz", + "integrity": "sha512-UQqZDdG2r2HhAOsZEgufkIWHPQ886IUyuJQkoZByvzhW8j51R4UNzGBJFkTiTnLhQnggwJRdJgFWK4uY6ZVIMw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-linux-x64-musl": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-linux-x64-musl/-/binding-linux-x64-musl-11.21.2.tgz", + "integrity": "sha512-3Q9PMRjalWkT6NZ4jfujuqTCFwoWErg3y3BnOgb544B8IMw4PktiwWOigMfOHNRLMghZeJ7hpfpZf4CP7rV7Og==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxc-resolver/binding-openharmony-arm64": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-openharmony-arm64/-/binding-openharmony-arm64-11.21.2.tgz", + "integrity": "sha512-Eljeq3ndtyKhM+Es8LITi4Zl2htzuRZcrPMF3kMCsrILztvU6AjZ3FuEhHHKobPUB9rMpzLpJP5bDqXI7r+iog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-wasm32-wasi/-/binding-wasm32-wasi-11.21.2.tgz", + "integrity": "sha512-HGDbNsIywqc4LxU38+CTJNnB/6BF7rheWOJ259b4eE0aEnelYblC8x+1tEd63bp31fYne63RQz9Jb4yVnC7Yig==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.0", + "@emnapi/runtime": "1.11.0", + "@napi-rs/wasm-runtime": "^1.1.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.0.tgz", + "integrity": "sha512-l9Oo58x0HOP5znGzVhYW9U3e5wVuA4LAZU2AGezTmkhO1CgQRFDhDg4nneHsu/t3WniXg9QrG2nIXL/ZS8ln8Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.0.tgz", + "integrity": "sha512-55coeOFKHv1ywEcUXJtWU5f+Jr/W5tZDvZig8DLKSwUN1JpROQ4rk/SNOQiFWmaR/VKF4zuFyW1B8JduOSv6Pg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@oxc-resolver/binding-win32-arm64-msvc": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-11.21.2.tgz", + "integrity": "sha512-iWx25CBEgH49iE9q5coEGI/jb1jl5kkCY9z6U5Og67xCkQ/WFMDc2J5U78+AE91SUxM2NqSLhJC8/PLfWnImww==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxc-resolver/binding-win32-x64-msvc": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-win32-x64-msvc/-/binding-win32-x64-msvc-11.21.2.tgz", + "integrity": "sha512-VPoCAhKvCQTlG7vxqaBXcmuvbh77BfnGXj8g0pbvVXpm1F/R8rDVwqIWcEbMzrI1JvJlm8v7T9uMVQb6UctMRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxfmt/binding-android-arm-eabi": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm-eabi/-/binding-android-arm-eabi-0.64.0.tgz", + "integrity": "sha512-o6uzh/jTOQeAY5TdkAeXdqv7MBRcPxiRA08zrcBtkKj5cSu/FMu0Hl7Q6Fi1KCKyCWZ6lJVjBzdsJvsKltUsGQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-android-arm64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-android-arm64/-/binding-android-arm64-0.64.0.tgz", + "integrity": "sha512-jRGSUeeP7p3Gynw2YaCVtjBIA6ZxY6bEB/ES5i54OhqmRTyuVg7ZgstEtzgq6GOAJd+2QZ5pvf+bFfmW5Mp9cw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-arm64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-arm64/-/binding-darwin-arm64-0.64.0.tgz", + "integrity": "sha512-JINwtU2lW7nOFSqi+H2qplipNUqah9Gc1jgGmB82kTD4UnZrZIVxCJ9qEmFiKfjNq27gYLFhrUb0to86aCwMjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-darwin-x64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-darwin-x64/-/binding-darwin-x64-0.64.0.tgz", + "integrity": "sha512-gCmuswrgrOSajV4HCRFkVCGIruPq8bjYuPYgSE2WQB3mD6XrdyZ3JMSRZCkQ8zCxOyGWriBo6QoZ5nmMHQ1BfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-freebsd-x64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-freebsd-x64/-/binding-freebsd-x64-0.64.0.tgz", + "integrity": "sha512-Ab8g7a38pT0MMImjh7anRSTve6buWBIlcXIFBYa5xl4s6UxEgKSc2xOOhbGtLwvXnEi2PsEDGoJh3oUU7xkehQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-gnueabihf": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-0.64.0.tgz", + "integrity": "sha512-BgvS3CoQ+Xy2deoZqEN8JVKabcCZi2RxA3yant8G9OAv9KuPJ9TCjHkqigzdHUVwErZxEP5d2bzLIEyKYyBDLg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm-musleabihf": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-0.64.0.tgz", + "integrity": "sha512-QXpNxwoMj0YvnceCNZadNSden3bIcnvjn/sDp/rwZhRoZoZYGpHvtPyhGsdJz9uvT9GkaMW7SsLddurU56dt8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-0.64.0.tgz", + "integrity": "sha512-BBgH3I1ppDsI5pZ4Pdhw0ceYxwVCfbU/bZEBCeZ6caRS9x0ZabErxubP7riGUn11PXZBhe8DYdjkDKP1FlVQ5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-arm64-musl": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-arm64-musl/-/binding-linux-arm64-musl-0.64.0.tgz", + "integrity": "sha512-v19HSjC/BGXdt26qEvKZtwAHgGmQ2Agcap2kQP+KIqoRZqivVzYth3ui2dJA1i+6/fjpjga85lIOaJJjQ/bOOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-ppc64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-0.64.0.tgz", + "integrity": "sha512-PElLnOo4xFTBZrxPhgTIj0eHqZXwEBQoNWtb7facUV170T0B0FRET0iNbb3LUeLWTybkUW+vsdyv4ihOdyXGyw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-0.64.0.tgz", + "integrity": "sha512-Qzsg15n4F5CH+MorcRW4MkAEMiLzXmeG+DiDSbP/bBTqCmWOH3K9DHryNrve+JHlV0txS+B6Z9P5Xz+cmWeL+g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-riscv64-musl": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-0.64.0.tgz", + "integrity": "sha512-/GZ358wnQ/Ez4UVnCcZIi56JkY0sOdZ+B108pqXKqZz3jLS59F4KEAB1Qv3fRlObrFEk+3L2vUQ/xoPx+3vjXw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-s390x-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-0.64.0.tgz", + "integrity": "sha512-/C9We3DXegowfLXtVCYHeNiU9azwCDr5cQkEtCVlc74vyn+lLQSPApJ1CZmxAduqeq/Oi3gQ+IVptyhCaTMtkQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-gnu": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-gnu/-/binding-linux-x64-gnu-0.64.0.tgz", + "integrity": "sha512-91KM2CeRWscIEHlj1NsW2WSnzGeq1Ehq+39bfDowTdkn+fcvK/x4Y1RcyqT7glyBjZio0ldkeCG6Usj3v7ASog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-linux-x64-musl": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-linux-x64-musl/-/binding-linux-x64-musl-0.64.0.tgz", + "integrity": "sha512-gw7uEk9I+7zoT1EYLra1eWArIzNcz8e3jkv+Noo2+o2T7wPvsNSQbfoa4DSfZlvn1i6mJ05RiZ4/omaXPDNhQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-openharmony-arm64": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-openharmony-arm64/-/binding-openharmony-arm64-0.64.0.tgz", + "integrity": "sha512-HYHFf616FHSPSO07c09mjmXBfQ73wIVM3m0txOiooa5XZkGoxFd6B14PVj0LB0DXIqJ6wAO/dDR/NX/5UUaqnw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-arm64-msvc": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-0.64.0.tgz", + "integrity": "sha512-uQjFp081IZSWD6VAofX2iO2z01awAdHmfC+NrieWIPKrT2hZKQDyq/U18M7ifC0sm0Wz8aHY/p6+FDYIzs/CrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-ia32-msvc": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-0.64.0.tgz", + "integrity": "sha512-lNM6byTAQ881jugzFu8juJTbNRgsUTlswMA6pJmwi1XDvmIqnnb49lcUAs5gz94fCJLrVN+/X3s3jOKqx23WIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxfmt/binding-win32-x64-msvc": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.64.0.tgz", + "integrity": "sha512-BtmbtL/QjMtF1a6C3CqoDluH2IfB6fJt62E+B9RFfUPtFk4Iz9PFS6+y/SzzOvSxc7aUk2Kphwg7Dh8lMbwu6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint-tsgolint/darwin-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2001.tgz", + "integrity": "sha512-CUJEdbSZ54+Xy9OXqOhWLTKZKV0BBiV7C2i/ygyVmXtkUNXx5YCzN8DpSSshTAKktoL7S+tnQ/ftFG/i7X896w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/darwin-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2001.tgz", + "integrity": "sha512-pXfBb5BqONCcgrXQNUZWXgiYmRSWJzd97S8i41VVOh6ut0tyo+cJ5FKFpczDHxiVNfj/3e7c9B4MtztNdpIVCw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@oxlint-tsgolint/linux-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2001.tgz", + "integrity": "sha512-roP7zujb/QDPzDwEKsFFpzNHHy91/Y7oX9vQXk78ekyZtcQj1QXDIMH33gjDdHBfRl4K9pZ36xhRgrP4Zr+R8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/linux-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2001.tgz", + "integrity": "sha512-UDezNqdECVmngu2TPnjaS1YoAmcTaBoI5lV9vk3VahBxoi+I5r9k3iJTT7qZoYWOXTD/7T7bNcwRgrocR6BscQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@oxlint-tsgolint/win32-arm64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2001.tgz", + "integrity": "sha512-uJZhqB6pdXLuN+AD1F5082byyQti/NPmJA77GtcFlmT2HzRelqbNls3SaIqxpjdFgvSBF9g0yOKGBkGFg7kX8Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint-tsgolint/win32-x64": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2001.tgz", + "integrity": "sha512-FkDRm8hx9OwzGQqyWG1tO5QrTLRApff9DzSgpz9QZau37BR8d1VYKOxMLGf6shPZntJFoTwIIJYT68VndYDCog==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.79.0.tgz", + "integrity": "sha512-TebFaaMklO/RXzTv7PucaCq9l3X6D1gA+C8H6K4njtjFOV+zWE9MKLpulcJZN9bzytbUbQIY0mZuz12nQ5Kv4Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-android-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.79.0.tgz", + "integrity": "sha512-KqqnOtAVgNsPPF0YSodkFZA1O80jcKoCZCTu3bgsszxA+MrMP9TLzfXitKjEj1FmrPprKDMdRDMmY3weESO9sg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.79.0.tgz", + "integrity": "sha512-BVC2nsMzqQzRDPc5RhixkZ+m1p7iH4bxRRvqkbwDXX0PlQKm1BPy8J8cRjnAFafOq2QzI+BfO3vE8w2GZ3CBag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-darwin-x64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.79.0.tgz", + "integrity": "sha512-p6Lm+snmhGuLKL1+CpCV8L6ijkE/qJzK2H2jG9+eKJT0n31RbY4FLsdhexekgP3bLpw4Kgde+9DZuDZQ4yIInA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.79.0.tgz", + "integrity": "sha512-qDMm0dXZnoHyRqSL4N4xUq82T4sqK5cbKSjvd/dF/YbMUXc2R1wEPf+vmA5S0qUmi0nwXfNbjXBtZaIqzQLIMg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.79.0.tgz", + "integrity": "sha512-2od7s0nuKPzqyUZAWk9KkCyGg7eI9dwFPZg+20lB15fKFkVZ0c9ZFxqPfiBAyDTlTkh9stPI0t+JlPCqMbItVA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.79.0.tgz", + "integrity": "sha512-ZOQUjkzDnvlhSE3+tWC3YXx94MMl+sYMlwH+u1+YGApGHOJP/YAc8ZBRFOXZ6eOBmxtXAWuS/fBcdZr8qqNO1A==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.79.0.tgz", + "integrity": "sha512-lu158FR4nGqGeRS3BQvtG85wRgU/Fy4MD5Cxp1hzJXizGiLo6u2742wJSCDKh8cFcZntvX7fcxlq4mMmfryH1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.79.0.tgz", + "integrity": "sha512-mbpKQeE2aflTjddaHK7MP8KP/OFbUM++lt5M635ENM8IyIdK0jm2t9pb+2v9mVVIvhF6TqA4l7F79Pll1mi+uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.79.0.tgz", + "integrity": "sha512-WpGNua7gaxaHnpSDeog2ji8IDHn/QLPl9LPzwkR/FvVv58vT5BcXjRXnU+wbu3N75cpeha8CdC7ho/U2OIsB4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.79.0.tgz", + "integrity": "sha512-tK1E93A5LVzISg4ngpKJnfTs7EqtIUceGI7MQ4GyDjJiLi8wPCkEyKlj2xkyKWZ1yzkDJyLHTBJ5/iFWRdnJvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.79.0.tgz", + "integrity": "sha512-qhQvUIrngXivA2A9pQ+xPCychztn/5qUv7yS3gDwXv3w7Rag+eTeeXWmRyx+t7XsW5x6LuY/8AsTq36UgFIblg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.79.0.tgz", + "integrity": "sha512-sv6AaVgU/eE6u+6WFiQVDcPPwTxP6IJMSB9k701W2r/r6Tx465e8vPvVyRxquNH4Vy6KwRNu90mVbxXJN8+5gg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.79.0.tgz", + "integrity": "sha512-iFZL02deziHslb3jEX9KdqlAkYoo4fGyotchKDzdfK1f5mxlIBeiQeHhvK3iFpuEJSB4ma/qeFn9oxPiwnhUPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.79.0.tgz", + "integrity": "sha512-3DtZR2raqObnh7wXZoFYFd0Fw7skBvcb3f7A+/lkEiDuh8hrE6vv9b/62Qxao1a9/OeHLw/FcXlXzgsW9wTRFg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.79.0.tgz", + "integrity": "sha512-Oatt4GuA1WJkqzk2ozx4HrWROOi7opV3AKDw/U8qDIqeTqzsjn5K2x3REJMNjU3/KU/Bkq96Zi3CknaiDTaC/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.79.0.tgz", + "integrity": "sha512-NAgZr9Qp8nIA9rpo0JEvwiabTF/2UVqBNnupBG9X4kxXcQoScJUTi+qHhvabb9s/thgj5wQ4XcIaJvb+ZMgoKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.79.0.tgz", + "integrity": "sha512-+KyXjIvcpaXmWW/j9NNY5yWjrIVxaX18VyIheQy3jwc2GSYgpCr7MGI/HxIGQ/shAL5IWEKbhsqoMpAO5Stiog==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.79.0.tgz", + "integrity": "sha512-mEelcCMMBS57sIXh2veGMNy+pQwuGtcMxHxGIZWQ5Ba9pJ5jCCUFOZB9E2JhBaxGsURe+WGe0zJp4RVre52gpQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rc-component/async-validator": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-6.0.0.tgz", + "integrity": "sha512-D3AGQwdyE58gmvx6waVSXJ80JGO+IY5L2O8HDnSOex7JNlzB3GuN/4hyHNTdhy2qtOhkpbIjmeAN3tL993wKbA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.24.4" + }, + "engines": { + "node": ">=14.x" + } + }, + "node_modules/@rc-component/cascader": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.22.0.tgz", + "integrity": "sha512-SffrA57aS9oub3VuI7ajPhJTPtaNxngSvtRhD40Rd8dwJ5vfWPSrVanWgeepdWFGBt7EHftIK5RUU0u3rCTwWw==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.10.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/checkbox": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-2.0.0.tgz", + "integrity": "sha512-3CXGPpAR9gsPKeO2N78HAPOzU30UdemD6HGJoWVJOpa6WleaGB5kzZj3v6bdTZab31YuWgY/RxV3VKPctn0DwQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/collapse": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.2.0.tgz", + "integrity": "sha512-ZRYSKSS39qsFx93p26bde7JUZJshsUBEQRlRXPuJYlAiNX0vyYlF5TsAm8JZN3LcF8XvKikdzPbgAtXSbkLUkw==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.10.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/color-picker": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.1.1.tgz", + "integrity": "sha512-OHaCHLHszCegdXmIq2ZRIZBN/EtpT6Wm8SG/gpzLATHbVKc/avvuKi+zlOuk05FTWvgaMmpxAko44uRJ3M+2pg==", + "license": "MIT", + "dependencies": { + "@ant-design/fast-color": "^3.0.1", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/context": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.2.tgz", + "integrity": "sha512-uiGpAlblCNlziHPwj4S4Iy/oemeuz/hR03mbiEjTCXwsqOIN3BOzsRMyDwpyO5Fm0vIEEJRUf9ZtbRLbhksuTA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dialog": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.10.0.tgz", + "integrity": "sha512-eDukNlz9vNszAGv7i3zKXdxEd3wgVmNxuJijYt8zvTh17QwTu8KK/bdURRd/lU4qaMzhO1HKKmMrwOnkaw0BvQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.1.0", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/drawer": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.4.2.tgz", + "integrity": "sha512-1ib+fZEp6FBu+YvcIktm+nCQ+Q+qIpwpoaJH6opGr4ofh2QMq+qdr5DLC4oCf5qf3pcWX9lUWPYX652k4ini8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.1.3", + "@rc-component/util": "^1.9.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/dropdown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.3.tgz", + "integrity": "sha512-YTST/N6kpqpDz3IMuM/PSSZnrDpSOA6dgHv12gPA90ZTSLv2CoqkZ0+9NtwTY6BeO7dstPblSic2QJg7dSFy/g==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.11.0", + "react-dom": ">=16.11.0" + } + }, + "node_modules/@rc-component/form": { + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.8.6.tgz", + "integrity": "sha512-1EXVsSKPZC6kGrIqxVck7kAWM6T65b6et/n5wIcyiaIa41VrDshD0nVLHoBDEIZr2ATQsOP6icb4XQkNFMLBAw==", + "license": "MIT", + "dependencies": { + "@rc-component/async-validator": "^6.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/image": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.10.0.tgz", + "integrity": "sha512-BjeZCRQ+hw+4WAhvrw8rJvy5fckA2xpf/X2XQEOABUHvLTNB9inB98X3Mp54jYQ7g10DfWERQWHXeC4ylxp1Uw==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/portal": "^2.1.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/input": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.3.1.tgz", + "integrity": "sha512-iFvTUT9W+JC/MSin2aGAk8NqsVlTzcExNC9DZariON1IWirju9NoNeEk47an4Q8iHazkoVI/y1LnDi88+CPcig==", + "license": "MIT", + "dependencies": { + "@rc-component/resize-observer": "^1.1.1", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/input-number": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz", + "integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==", + "license": "MIT", + "dependencies": { + "@rc-component/mini-decimal": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/listy": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@rc-component/listy/-/listy-1.2.3.tgz", + "integrity": "sha512-IXiMjV5s0rczLBlfh7G5nB4M3365mrEeedjwKtf5I+Ns3PqRUsebR2h5u8CeFarsVfLUPC2I5p0h09TNoOWyvQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/portal": "^2.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.3.1", + "@rc-component/virtual-list": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/mentions": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.11.0.tgz", + "integrity": "sha512-IC2qXuEBMFHxPIXEFfYWj6Sr7UiDZnOqJHCYQBbwPzopBJOPZIR6mV9U4QH1bYQRlKYlYnIsajWDMgVGgWQyWQ==", + "license": "MIT", + "dependencies": { + "@rc-component/input": "~1.3.1", + "@rc-component/menu": "~1.4.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/menu": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.4.1.tgz", + "integrity": "sha512-3GsVRoQ4cnF/AoIQ4P+Z1haBfgfBPQfLT1RJY3Nu4DzOnheTslfCiGSPj7bv/cLj5sW5pHqN25dDXGP3JELAlQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mini-decimal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.4.tgz", + "integrity": "sha512-xiuXcaCwyOWpD8a8scdExFl+bntNphAW8XeenL1ig2en0AAZY0Pcp4pC0dI22qJ+NvxKn9RoNIoRdqYU3BLH4w==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.0" + }, + "engines": { + "node": ">=8.x" + } + }, + "node_modules/@rc-component/motion": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.3.3.tgz", + "integrity": "sha512-Xh3IszxvlSv3/PLYFyC2UZi9LNB83yOnkB/LNmRzaypZLvkhqUIPS7MQpGZcCMWrNsXV2p6YTSWbSGvFpEle9A==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/mutate-observer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz", + "integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/notification": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-2.0.7.tgz", + "integrity": "sha512-nqZzpf6BPdaj+3ILx7si79LLmqPKyUmQoXa+/9gg0SkH0v1DbD66oJgRMSBEVnd/zUT3D4gwxWIHUKebYf2ZXQ==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/overflow": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.1.tgz", + "integrity": "sha512-syfmgAABaHCnCDzPwHZ/2tuvIcpOO3jefYZMmfkN+pmo8HKTzsfhS57vxo4ksPdN0By+uWVJhJWNFozNBxi2eA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/pagination": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.4.0.tgz", + "integrity": "sha512-CW1g7P9V8u+e8JQdUsl2RWg+GCsoee0mtJjZUCCxn/vb3jzOwDKm6hAdwddHCVBfWJ58eGUBZz3IvnU8rRktjw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/picker": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.12.0.tgz", + "integrity": "sha512-0FZGgDiDZFMm2hcfGv4jyjm7yWIj2MiwfeTzLL0vWkO+8cCSKdiM9XhWpkn1aAsoI2EwUy48Oz2cGfvL8ZKrfw==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/trigger": "^3.6.15", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.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-component/portal": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.2.1.tgz", + "integrity": "sha512-ck+r1kW/JSv0wxPji3KN2ss9K6Z0qqwusw/mf/0JobXhZ8hC2ejZwCJObW/SvDi0uhA0VzmCnx0CaCci95tcmA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=12.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/progress": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz", + "integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/qrcode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-2.0.0.tgz", + "integrity": "sha512-aAv3QhPP1xyafuTZOxub6a54pCeBnN3IwQkpETrBtthq4BL5IgxnCbuoBWPDpdLw1y1j6BgBUCAKV92+yX06Dw==", + "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/rate": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz", + "integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/resize-observer": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.1.2.tgz", + "integrity": "sha512-t/Bb0W8uvL4PYKAB3YcChC+DlHh0Wt5kM7q/J+0qpVEUMLe7Hk5zuvc9km0hMnTFPSx5Z7Wu/fzCLN6erVLE8Q==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.0" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/segmented": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.3.0.tgz", + "integrity": "sha512-5J/bJ01mbDnoA6P/FW8SxUvKn+OgUSTZJPzCNnTBntG50tzoP7DydGhqxp7ggZXZls7me3mc2EQDXakU3iTVFg==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.11.1", + "@rc-component/motion": "^1.1.4", + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.0.0", + "react-dom": ">=16.0.0" + } + }, + "node_modules/@rc-component/select": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.10.1.tgz", + "integrity": "sha512-H+yQsl+qED9NilQ3g6zdpsMwUgwVjrcMTkNHAWRVU/MoNCYgTbDgU+MIMgZDK+rVdd2JUfI/MkysMcZZ0cyQKw==", + "license": "MIT", + "dependencies": { + "@rc-component/overflow": "^1.0.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/slider": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.1.1.tgz", + "integrity": "sha512-LSzgWGYDgeCDgR4r1XlU29gbYws6HpLnvJd/uMhLeW/vQgxldeR+Wb4uzHDCHiYEbr1bnEHWdjkPxjJRHxuiig==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/steps": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz", + "integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.2.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/switch": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz", + "integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.3.0", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/table": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.11.1.tgz", + "integrity": "sha512-OWdS6DMmeWb7bJBGqPxYZpQbzBlBiXZUu2sqo6Ii7Sjs9GeK1IsrXrWk26SL2c6KEseabswdxrRj7WUm9LdECw==", + "license": "MIT", + "dependencies": { + "@rc-component/context": "^2.0.1", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.0.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tabs": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.12.0.tgz", + "integrity": "sha512-XL7Kqy5fnUE2WTlO1/fCGrrfNlGFebdr7JseGkEIjzcVMAtIFQJ8sqCSOmxcXstjU6fonD/4rnhZHxj7sDTajQ==", + "license": "MIT", + "dependencies": { + "@rc-component/dropdown": "~1.0.0", + "@rc-component/menu": "~1.4.0", + "@rc-component/motion": "^1.1.3", + "@rc-component/resize-observer": "^1.0.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tooltip": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.5.0.tgz", + "integrity": "sha512-agQ/+mBqrEQfTX4D3KhQ7j+ZbX4/VHjoJ7Noa2wIdZ1/FbQTOd7Sn92rp+jtCoqAVTLUgSOydePIgZ204gi2EQ==", + "license": "MIT", + "dependencies": { + "@rc-component/trigger": "^3.10.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/tour": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.4.0.tgz", + "integrity": "sha512-aui4r4TqmTzwaBgcQxHYep8kM8PTjZFufjokObpy35KfFeZ0k9ArquWFZqegQlH24P14t+F0qO0mGTgzlav1yg==", + "license": "MIT", + "dependencies": { + "@rc-component/portal": "^2.2.0", + "@rc-component/trigger": "^3.0.0", + "@rc-component/util": "^1.7.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/tree": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.4.0.tgz", + "integrity": "sha512-dGsJGDJQedA0BqqVgj3F8BvHXTSZijyhTXdbAdkcx8lynzZkty/CV3Z3LOm/fxz+BCfl3dfGiAQpb7Q5XNvl0Q==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.0.0", + "@rc-component/util": "^1.11.1", + "@rc-component/virtual-list": "^1.2.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=10.x" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/tree-select": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.16.1.tgz", + "integrity": "sha512-a1Oi6EJhqAhdOxxupdJi6fP0RPHMKn5TcfkX2+llaQ4lF4nwfH7b6SCHcnsybaa2s+pk1yZYwVyeOYkDnEBRdg==", + "license": "MIT", + "dependencies": { + "@rc-component/select": "~1.10.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": "*", + "react-dom": "*" + } + }, + "node_modules/@rc-component/trigger": { + "version": "3.10.1", + "resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.10.1.tgz", + "integrity": "sha512-mXlDN0IXdtV8Yqqm8195ECCyrbmfvvfKvwVvSlH0+qvKD6BUF8gRhEjSy0FOcD1+CcDRHgTiX99LoxfQrmh3Cw==", + "license": "MIT", + "dependencies": { + "@rc-component/motion": "^1.3.3", + "@rc-component/portal": "^2.2.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/upload": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.1.tgz", + "integrity": "sha512-GvYWSKeaJTOxxC5p6+nOSadzfvXA1h8C/iHFPFZX+szH3JUXrvs+DLiW8YUTBgvMh8m63mJeHrlYlJzAlg+pDA==", + "license": "MIT", + "dependencies": { + "@rc-component/util": "^1.11.1", + "clsx": "^2.1.1" + }, + "peerDependencies": { + "react": ">=16.9.0", + "react-dom": ">=16.9.0" + } + }, + "node_modules/@rc-component/util": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.12.0.tgz", + "integrity": "sha512-AEjPL8JVdohIITaiXokyjL9WQ6tKWWjAYK9QU16tGNE9JaQABBQy+hA4H2Lup5MgXy9yY3iLrbZJheuU13hTdQ==", + "license": "MIT", + "dependencies": { + "is-mobile": "^5.0.0", + "react-is": "^19.2.7" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.5.1.tgz", + "integrity": "sha512-boqHxdtyWC88u8quYgEO49bcBy5fzRiOcnBge+N4nLzs2k8hUQ/yw7JE9dM6yCBE4jSm5YSHVCVMS+suBuJGKA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^8.0.0", + "@rc-component/resize-observer": "^1.0.1", + "@rc-component/util": "^1.4.0", + "clsx": "^2.1.1" + }, + "engines": { + "node": ">=8.x" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/@rc-component/virtual-list/node_modules/@babel/runtime": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-8.0.0.tgz", + "integrity": "sha512-sL6cvO2IfkSu/iU+zs2S/w01B7A8V7suXSIKEN4hPFFdZoiPGxrj5pAG0lCaqLWiEIrjKzdznIWuaLcxPR53qw==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.5.tgz", + "integrity": "sha512-DLe/i+l8ynIBY7XEQ191TeZvCoowIGa18R+dIV30GW7DiOtp74i/xX8hs8GUjW5ARV7VZuie3d6AumSmCwbeRA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.5.tgz", + "integrity": "sha512-zXcwKlQApYAOELHd8PwKDFkagYF9Wy4e0RJ+0qnzl9Pjnpj75TEG8ufv40p2J7kCEfwZAsNiuzRIyNNMWT38ig==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.5.tgz", + "integrity": "sha512-dK4QakI42nzWgJT5sm4y4y/O//D4OxM75/cH28RLV+nzIN9AY+YsbuUVrUTjlLjXR6vpyxFbSsbmNuJ6BP9sww==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.5.tgz", + "integrity": "sha512-fqSALaUu1Wjd1nK2uW2kJDWdLCc8lx1IcY+MTY26Aurfdx19anlzhqXOgCFbBFQnlFDTn4TC1/7Nz4Bl2mLP3A==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.5.tgz", + "integrity": "sha512-/vCnNxlkxs9tKxNDcyWUePpJ/PgTzxIaVhoM5SmG8UV+GR/IcPam4VYxi7GIMo7PSDuNqlJqvprqii9NqqVCMw==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.5.tgz", + "integrity": "sha512-abk0NLA519LxRCszmbE0jYKuQ9YPocOXTiOXOo6Yr+YAT95VH+PtqYAjOJvGKt3viEd/x4qzabAlwd5bHOOARg==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.5.tgz", + "integrity": "sha512-Y7eALiJ8lr0M2HH103Js+g7V34wf6snlpZLAsHI90uLhr3PVlNsbFVAXJC9d/V6BnPyKtpSwI+NcB/RLxsQxuA==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.5.tgz", + "integrity": "sha512-xMvZgnbZg4YVnR/AX2b3oOPDTFYJvUVaJg5FedA/LuvexAtXibZQej4cnTkw3rjsJ/ggUROB64TdtETiim+FYA==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.5.tgz", + "integrity": "sha512-GRjeqTUDHTo5GwntsLaAMcBahG3nlpjftXWZLN73HiYQlhwEowvarFgQnRnQZtIp4keXX7quXFbG38uPZBa2EA==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.5.tgz", + "integrity": "sha512-vLNTR45F2Uwc8AufkNXPmB4VliaXs+FvcheEogIzOXzO4l+LzieXF5A/TWxLy5HtqpsRCHUfd0lPVrrdgXdLHQ==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.5.tgz", + "integrity": "sha512-Mgj59/HTuYeK9Gz2MA+mBWKnHsAgkBSec15ZMb1st3oIfFbX7gCjOae7GydHhzcyQi9Z/7M1QuN9bR3oFqF0jQ==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.5.tgz", + "integrity": "sha512-mY8AP0/ichsbhAxGnLa3d3+MwV0EfgrPND2bplI3Ym8T6R2pJ0N87bvrKVwNXmdy3jnr6eQBecdqx/HMknBmpA==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.5.tgz", + "integrity": "sha512-8SLssA2oweAxyRgDp789ACfRb/3P+zNRJpzZxSizxF9m8NUDQ4+3xjo8ttjhVGGw6Qxb70oZiEtIjaKikCO7Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.5.tgz", + "integrity": "sha512-vGbruD5zquhoc8D9SViXgN2FBJtNdTyQ4DtG+SWiEGlJiAzoKcZ2xp+xuXCffhubVdt0NJlTZqkeRuERy7g8Cw==", + "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.2.5", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.5.tgz", + "integrity": "sha512-e/SXpgISz+IoqVcSSI0rx/d/he8zqLex+/rCWpnHpmVfmPIUjag9H6P7zotf0gJHwPUhQxZ/mF8tr6acebT9yw==", + "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.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/pluginutils": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.4.0.tgz", + "integrity": "sha512-MfPp06CjRLfXQ3wY0R8vJDYBy/MvVcc9OulEfR0B8Iv9ko+GCNaRZ+EpJYFl27LhKsZK0o420sYCRHCjfCgeUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "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==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "license": "MIT" + }, + "node_modules/@storybook/addon-a11y": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-a11y/-/addon-a11y-10.5.10.tgz", + "integrity": "sha512-RpRQV5xUbrl6hCiNrd5FSMIo6pnRZ0VZxWvEW/ASLcreGkKUW5jl2AeLCe5YROE2i80s/dU+6VPzOYKrwWNFbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "axe-core": "^4.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.5.10" + } + }, + "node_modules/@storybook/addon-docs": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-docs/-/addon-docs-10.5.10.tgz", + "integrity": "sha512-06JoK3/a7FWI/6GzuidJP9iHp1/Vejboe6lzS1jW+d8ItpecriBt+oXh1VNmUM7i7PjI6pZnet+j51QnLyeOoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdx-js/react": "^3.0.0", + "@storybook/csf-plugin": "10.5.10", + "@storybook/icons": "^2.0.2", + "@storybook/react-dom-shim": "10.5.10", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.10" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@storybook/addon-vitest": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/addon-vitest/-/addon-vitest-10.5.10.tgz", + "integrity": "sha512-JNQ9DSkLfxC8qqytBCej91zBExIZ7z97B410U2zgfQPki4HkI9Ffz97a15f5yhVZ79ppIrZ/ssI+WcyWn0ykXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^2.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@vitest/browser": "^3.0.0 || ^4.0.0", + "@vitest/browser-playwright": "^4.0.0", + "@vitest/runner": "^3.0.0 || ^4.0.0", + "storybook": "^10.5.10", + "vitest": "^3.0.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/runner": { + "optional": true + }, + "vitest": { + "optional": true + } + } + }, + "node_modules/@storybook/builder-vite": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/builder-vite/-/builder-vite-10.5.10.tgz", + "integrity": "sha512-O4GgIP0tKLRueom3EmU3OaBUHKjNYj+jkOvmTIkn3PYTiWVkCuHqSKEs4ADvRyaQuLH+peHhFe4JtkNC9KbtrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/csf-plugin": "10.5.10", + "ts-dedent": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "storybook": "^10.5.10", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@storybook/csf-plugin": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/csf-plugin/-/csf-plugin-10.5.10.tgz", + "integrity": "sha512-TaCLBrqVEr767+w58QDotDUiCTuE5cyRJuRcDlsKQyUIyGBv+lYD3lu8wBiVCYxgIjB/gu9HmqiC+0yx1rHzaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "unplugin": "^2.3.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "esbuild": "*", + "rollup": "*", + "storybook": "^10.5.10", + "vite": "*", + "webpack": "*" + }, + "peerDependenciesMeta": { + "esbuild": { + "optional": true + }, + "rollup": { + "optional": true + }, + "vite": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/@storybook/global": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@storybook/global/-/global-5.0.0.tgz", + "integrity": "sha512-FcOqPAXACP0I3oJ/ws6/rrPT9WGhu915Cg8D02a9YxLo0DE9zI+a9A5gRGvmQ09fiWPukqI8ZAEoQEdWUKMQdQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@storybook/icons": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@storybook/icons/-/icons-2.1.0.tgz", + "integrity": "sha512-Fxh9vYpX9bQqFeHRiY8h2ApeRGDzRSMLwJwNZ/AIRqnyOKHxRKL+yFe+ctEkVJmuptRE9u1Hrn8ZZNHyfDKKNg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/@storybook/react": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react/-/react-10.5.10.tgz", + "integrity": "sha512-4MBV5e1SXIMfPynLHzr+Mp0dwGv/FW1bklWAsS4ynBOAbC98W9p/I9vqBnUctsvE3BJkhzHQQyPwMHL5tTcHVA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/react-dom-shim": "10.5.10", + "react-docgen": "^8.0.2", + "react-docgen-typescript": "^2.2.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.10", + "typescript": ">= 4.9.x" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@storybook/react-dom-shim": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react-dom-shim/-/react-dom-shim-10.5.10.tgz", + "integrity": "sha512-rbu62ILo/VE3iXKmu+kWXFpD1H1Lwi0f19q/x7JnDsD2dxKS9w5znLEqPIq2qxpzi/wjjIb2iUP1cRG1d/9W5A==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "@types/react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.10" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@storybook/react-vite": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/@storybook/react-vite/-/react-vite-10.5.10.tgz", + "integrity": "sha512-xOztxefUnqKeuyvcnjspqmlDnER4cExL+liltrpdXLPJVqfFNr9lgM49FyEPajzsUVG9W/vHJWjbaQGGu1UsYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@joshwooding/vite-plugin-react-docgen-typescript": "^0.7.0", + "@rollup/pluginutils": "^5.0.2", + "@storybook/builder-vite": "10.5.10", + "@storybook/react": "10.5.10", + "empathic": "^2.0.0", + "magic-string": "^0.30.0", + "react-docgen": "^8.0.2", + "resolve": "^1.22.8", + "tsconfig-paths": "^4.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "storybook": "^10.5.10", + "typescript": ">= 4.9.x", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@tanstack/query-core": { + "version": "5.102.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.102.2.tgz", + "integrity": "sha512-zQ5794PXBlV5Wl7N23SR1/Ss+wPE/h2Ye4XlKpm3omN8i8b/Fd4DAdqpLS2AXj5M/RMObt3k5qy3E7kzt/AvBA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.102.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.102.2.tgz", + "integrity": "sha512-KxU8ZyOEuJ81eTSgXa8GQbk/jO/rz0elYtNKt3VMtM2pRjeO8ADIu7sqqmEjFKJKqco9h2N1ojIDuHs6VfDItQ==", + "license": "MIT", + "dependencies": { + "@tanstack/query-core": "5.102.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "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.npmjs.org/@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.npmjs.org/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.npmjs.org/@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.5", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.5.tgz", + "integrity": "sha512-FhqjldLTpteueBaKflhNFlMT3+PM0O5fiBUivht6b9CZ1eesJyy7+g3Jr7XwJzt/Hip3ZG5hWwK1MX1FuDiE4w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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/doctrine": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@types/doctrine/-/doctrine-0.0.9.tgz", + "integrity": "sha512-eOIHzCUSH7SMfonMG1LsC2f8vxBFtho6NGBznK41R84YzPuvSBzrhEps33IsQiOW9+VL6NQ9DbjQJznk/S4uRA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mdx": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "26.2.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-26.2.0.tgz", + "integrity": "sha512-5IviulTZeRNp2vAJ514cc/HUlY5nZ9fCbq9DMyC52BrhFZACo3nI0R7qBxhQmo/d27NFe96ur/b7Wwxklda+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~8.3.0" + } + }, + "node_modules/@types/react": { + "version": "19.2.18", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.18.tgz", + "integrity": "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.5.tgz", + "integrity": "sha512-fMPwH9v7r/pp43yUd2/Mbiex5KouJwwR3dzHkhLREUC6764VyDsqxhAxv6OFEYR1RhjOyD1naqba8ECDBe7ZQg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@types/resolve": { + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.6.tgz", + "integrity": "sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/set-cookie-parser": { + "version": "2.4.10", + "resolved": "https://registry.npmjs.org/@types/set-cookie-parser/-/set-cookie-parser-2.4.10.tgz", + "integrity": "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript/typescript-aix-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", + "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", + "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-darwin-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", + "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", + "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-freebsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", + "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", + "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", + "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-loong64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", + "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-mips64el": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", + "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-ppc64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", + "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-riscv64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", + "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-s390x": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", + "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-linux-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", + "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", + "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-netbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", + "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", + "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-openbsd-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", + "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-sunos-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", + "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-arm64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", + "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@typescript/typescript-win32-x64": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", + "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.1.0.tgz", + "integrity": "sha512-qd2BzUBehkov86WFhg0JkEFEYyCLG9uPCe6qWTY/kRlss9OvJrOF2UbIWT7p+8IzZHkEu0DNGHc4HSv+JdDLsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "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", + "oxc-transform-react": "^0.145.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + }, + "oxc-transform-react": { + "optional": true + } + } + }, + "node_modules/@vitest/browser": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.11.tgz", + "integrity": "sha512-bwMovvAeuTFOK5kIFevw4VEf+1gVEICv4SYK4k3knJOxl6b1zEWud8mYKD73e1B0odAn174h1MofURy2TPWf3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.11" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.11.tgz", + "integrity": "sha512-riLBxPqwnJ0lWs2DN2WeUfYeKLoAjbP2Xx8cLQdSddzMi20sksIa6K2mPz79DyMZKKVKH2ksOC2yJvtNcZg8cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.11", + "@vitest/mocker": "4.1.11", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/coverage-v8": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^1.0.2", + "@vitest/utils": "4.1.11", + "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.11", + "vitest": "4.1.11" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/expect/node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.11", + "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/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.11", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.11", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@webcontainer/env": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@webcontainer/env/-/env-1.1.1.tgz", + "integrity": "sha512-6aN99yL695Hi9SuIk1oC88l9o0gmxL1nGWWQ/kNy81HigJ0FoaoTXpytCj6ItzgyCEwA9kF1wixsTuv5cjsgng==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/antd": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/antd/-/antd-6.6.1.tgz", + "integrity": "sha512-QHIHYoUk9N9nJy1T9fyxWKjY0qApdTEDd/6lzqYng8Uryv9FejNmbhKvYF7obGqB+TuLXQsPVF7fOVgyzM1KrQ==", + "license": "MIT", + "dependencies": { + "@ant-design/colors": "^8.0.1", + "@ant-design/cssinjs": "^2.1.2", + "@ant-design/cssinjs-utils": "^2.1.2", + "@ant-design/fast-color": "^3.0.1", + "@ant-design/icons": "^6.3.2", + "@ant-design/react-slick": "~2.0.0", + "@babel/runtime": "^7.29.2", + "@rc-component/cascader": "~1.22.0", + "@rc-component/checkbox": "~2.0.0", + "@rc-component/collapse": "~1.2.0", + "@rc-component/color-picker": "~3.1.1", + "@rc-component/dialog": "~1.10.0", + "@rc-component/drawer": "~1.4.2", + "@rc-component/dropdown": "~1.0.3", + "@rc-component/form": "~1.8.6", + "@rc-component/image": "~1.10.0", + "@rc-component/input": "~1.3.1", + "@rc-component/input-number": "~1.6.2", + "@rc-component/listy": "~1.2.3", + "@rc-component/mentions": "~1.11.0", + "@rc-component/menu": "~1.4.1", + "@rc-component/motion": "^1.3.3", + "@rc-component/mutate-observer": "^2.0.1", + "@rc-component/notification": "~2.0.7", + "@rc-component/pagination": "~1.4.0", + "@rc-component/picker": "~1.12.0", + "@rc-component/progress": "~1.0.2", + "@rc-component/qrcode": "~2.0.0", + "@rc-component/rate": "~1.0.1", + "@rc-component/resize-observer": "^1.1.2", + "@rc-component/segmented": "~1.3.0", + "@rc-component/select": "~1.10.1", + "@rc-component/slider": "~1.1.1", + "@rc-component/steps": "~1.2.2", + "@rc-component/switch": "~1.0.3", + "@rc-component/table": "~1.11.1", + "@rc-component/tabs": "~1.12.0", + "@rc-component/tooltip": "~1.5.0", + "@rc-component/tour": "~2.4.0", + "@rc-component/tree": "~1.4.0", + "@rc-component/tree-select": "~1.16.1", + "@rc-component/trigger": "^3.10.1", + "@rc-component/upload": "~1.1.1", + "@rc-component/util": "^1.12.0", + "clsx": "^2.1.1", + "dayjs": "^1.11.11", + "scroll-into-view-if-needed": "^3.1.0", + "throttle-debounce": "^5.0.2" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ant-design" + }, + "peerDependencies": { + "react": ">=18.0.0", + "react-dom": ">=18.0.0" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/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-types": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.16.1.tgz", + "integrity": "sha512-6t10qk83GOG8p0vKmaCr8eiilZwO171AvbROMtvvNiwrTly62t+7XkA8RdIIVbpMhCASAsxgAzdRSwh6nw/5Dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ast-v8-to-istanbul": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.5.tgz", + "integrity": "sha512-UPAgKJFSEGMWSDr3LX4tqnAb4f7KGT8O40Tyx8wbYmmZ/yn58lNCm8h3svs3eXgiGd5AXxz8NDOvXWvicq+rJA==", + "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/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz", + "integrity": "sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "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/baseline-browser-mapping": { + "version": "2.11.18", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.18.tgz", + "integrity": "sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/browserslist": { + "version": "4.28.8", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", + "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "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.11.12", + "caniuse-lite": "^1.0.30001809", + "electron-to-chromium": "^1.5.402", + "node-releases": "^2.0.53", + "update-browserslist-db": "^1.3.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001809", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001809.tgz", + "integrity": "sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==", + "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": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz", + "integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==", + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-3.1.1.tgz", + "integrity": "sha512-UaXxwISYJPTr9hwQxMFYZ7kNhSXboMXP+Z3TRX6f1/NyaGPfuNUZOWP1pUEb75B2HjfklIYLVRfWiFZJyC6Npg==", + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/data-urls/node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.23", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.23.tgz", + "integrity": "sha512-QDTCU0M0MxR3hQfnlDJfwekQiaanm1ubOD231u73WBckQ/fsamwRLiE2GBz6D3a/xF1NgfiDLJjXBa1hYOYTtQ==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/default-browser": { + "version": "5.5.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.1.tgz", + "integrity": "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", + "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/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.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.413", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.413.tgz", + "integrity": "sha512-F1XPKvt7HVfly5WND90ec16nFsdr4g5x/cVUP3EqjeyXynupabGDqpMa84wwvuYGDnldXLBz6DLXyZXWO9TPvw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/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.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/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/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/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/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/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/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graphql": { + "version": "16.14.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.14.2.tgz", + "integrity": "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/headers-polyfill": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-5.0.1.tgz", + "integrity": "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/set-cookie-parser": "^2.4.10", + "set-cookie-parser": "^3.0.1" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/html-parse-stringify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-4.0.1.tgz", + "integrity": "sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==", + "license": "MIT", + "funding": { + "url": "https://locize.com" + } + }, + "node_modules/i18next": { + "version": "26.4.0", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.0.tgz", + "integrity": "sha512-rsmK5bFqsD1AetSFSIa43wtNR4WpvvH4p0tLEsTxkC7QTrfdFm06nbQ95bh8Og4wwaCnUEcm9DVYL2cgxitiQg==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-mobile": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz", + "integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==", + "license": "MIT" + }, + "node_modules/is-node-process": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", + "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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/is-wsl": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", + "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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.npmjs.org/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.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdom": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", + "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^6.0.5", + "@asamuzakjp/dom-selector": "^8.3.0", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.7", + "@exodus/bytes": "^1.15.1", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.5.2", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.2", + "undici": "^8.9.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^17.1.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + }, + "peerDependencies": { + "canvas": "^3.2.3" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json2mq": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/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/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "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.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "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.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "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/lint-staged": { + "version": "17.3.0", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-17.3.0.tgz", + "integrity": "sha512-woZS3vNe3UKqBaLPvbLOtKRY4tLANpWQhom12MGWqC8Mh1lCOO+WgSwmX2amjJAqTY9BkXYW87fCUH5H9Ph6xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^4.0.5", + "string-argv": "^0.3.2", + "tinyexec": "^1.2.4" + }, + "bin": { + "lint-staged": "bin/lint-staged.js" + }, + "engines": { + "node": ">=22.22.1" + }, + "funding": { + "url": "https://opencollective.com/lint-staged" + }, + "optionalDependencies": { + "yaml": "^2.9.0" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "11.5.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", + "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "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.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.4.tgz", + "integrity": "sha512-llBEhWm1SacoRwgHUoQJYtwp4PBLF4faQi5TCpIGyGs9n4y5+juI0tDgyKIfpqxckRHaHzouUEph3THklWh03w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/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/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/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": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/msw": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.15.0.tgz", + "integrity": "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "@inquirer/confirm": "^6.0.11", + "@mswjs/interceptors": "^0.41.3", + "@open-draft/deferred-promise": "^3.0.0", + "@types/statuses": "^2.0.6", + "cookie": "^1.1.1", + "graphql": "^16.13.2", + "headers-polyfill": "^5.0.1", + "is-node-process": "^1.2.0", + "outvariant": "^1.4.3", + "path-to-regexp": "^6.3.0", + "picocolors": "^1.1.1", + "rettime": "^0.11.11", + "statuses": "^2.0.2", + "strict-event-emitter": "^0.5.1", + "tough-cookie": "^6.0.1", + "type-fest": "^5.5.0", + "until-async": "^3.0.2", + "yargs": "^17.7.2" + }, + "bin": { + "msw": "cli/index.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/mswjs" + }, + "peerDependencies": { + "typescript": ">= 4.8.x" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/mute-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-3.0.0.tgz", + "integrity": "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "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/node-releases": { + "version": "2.0.53", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", + "integrity": "sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/obug": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.4.tgz", + "integrity": "sha512-4a+OsYv9UktOJKE+l1A4OufDgdRF9PifWj+tJnHURo/P+WOxpG4GzUFL9qCalmWauao6ogiG+QvnCovwPoyAWA==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/open": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", + "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "wsl-utils": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/outvariant": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz", + "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==", + "dev": true, + "license": "MIT" + }, + "node_modules/oxc-parser": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/oxc-parser/-/oxc-parser-0.127.0.tgz", + "integrity": "sha512-bkgD4qHlN7WxLdX8bLXdaU54TtQtAIg/ZBAfm0aje/mo3MRDo3P0hZSgr4U7O3xfX+fQmR5AP04JS/TGcZLcFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "^0.127.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-parser/binding-android-arm-eabi": "0.127.0", + "@oxc-parser/binding-android-arm64": "0.127.0", + "@oxc-parser/binding-darwin-arm64": "0.127.0", + "@oxc-parser/binding-darwin-x64": "0.127.0", + "@oxc-parser/binding-freebsd-x64": "0.127.0", + "@oxc-parser/binding-linux-arm-gnueabihf": "0.127.0", + "@oxc-parser/binding-linux-arm-musleabihf": "0.127.0", + "@oxc-parser/binding-linux-arm64-gnu": "0.127.0", + "@oxc-parser/binding-linux-arm64-musl": "0.127.0", + "@oxc-parser/binding-linux-ppc64-gnu": "0.127.0", + "@oxc-parser/binding-linux-riscv64-gnu": "0.127.0", + "@oxc-parser/binding-linux-riscv64-musl": "0.127.0", + "@oxc-parser/binding-linux-s390x-gnu": "0.127.0", + "@oxc-parser/binding-linux-x64-gnu": "0.127.0", + "@oxc-parser/binding-linux-x64-musl": "0.127.0", + "@oxc-parser/binding-openharmony-arm64": "0.127.0", + "@oxc-parser/binding-wasm32-wasi": "0.127.0", + "@oxc-parser/binding-win32-arm64-msvc": "0.127.0", + "@oxc-parser/binding-win32-ia32-msvc": "0.127.0", + "@oxc-parser/binding-win32-x64-msvc": "0.127.0" + } + }, + "node_modules/oxc-resolver": { + "version": "11.21.2", + "resolved": "https://registry.npmjs.org/oxc-resolver/-/oxc-resolver-11.21.2.tgz", + "integrity": "sha512-w5tLwYN3Zo24w5EeWJjJWZOwhYqTtC8PS2B1tIt7BZUuqTIcU07sQValbDw+rq7+AuAGzOHklgK+ifsy4lpXfw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxc-resolver/binding-android-arm-eabi": "11.21.2", + "@oxc-resolver/binding-android-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-arm64": "11.21.2", + "@oxc-resolver/binding-darwin-x64": "11.21.2", + "@oxc-resolver/binding-freebsd-x64": "11.21.2", + "@oxc-resolver/binding-linux-arm-gnueabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm-musleabihf": "11.21.2", + "@oxc-resolver/binding-linux-arm64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-arm64-musl": "11.21.2", + "@oxc-resolver/binding-linux-ppc64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-riscv64-musl": "11.21.2", + "@oxc-resolver/binding-linux-s390x-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-gnu": "11.21.2", + "@oxc-resolver/binding-linux-x64-musl": "11.21.2", + "@oxc-resolver/binding-openharmony-arm64": "11.21.2", + "@oxc-resolver/binding-wasm32-wasi": "11.21.2", + "@oxc-resolver/binding-win32-arm64-msvc": "11.21.2", + "@oxc-resolver/binding-win32-x64-msvc": "11.21.2" + } + }, + "node_modules/oxfmt": { + "version": "0.64.0", + "resolved": "https://registry.npmjs.org/oxfmt/-/oxfmt-0.64.0.tgz", + "integrity": "sha512-XZ4GFBN/PLbXKq+0zrgpQfPKYuJlUuj+nzZJY7UpIbFMNyefNLCdN9EwViycNqnYcv0wrn0jXcQLlqJp8RCKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinypool": "2.1.0" + }, + "bin": { + "oxfmt": "bin/oxfmt" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxfmt/binding-android-arm-eabi": "0.64.0", + "@oxfmt/binding-android-arm64": "0.64.0", + "@oxfmt/binding-darwin-arm64": "0.64.0", + "@oxfmt/binding-darwin-x64": "0.64.0", + "@oxfmt/binding-freebsd-x64": "0.64.0", + "@oxfmt/binding-linux-arm-gnueabihf": "0.64.0", + "@oxfmt/binding-linux-arm-musleabihf": "0.64.0", + "@oxfmt/binding-linux-arm64-gnu": "0.64.0", + "@oxfmt/binding-linux-arm64-musl": "0.64.0", + "@oxfmt/binding-linux-ppc64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-gnu": "0.64.0", + "@oxfmt/binding-linux-riscv64-musl": "0.64.0", + "@oxfmt/binding-linux-s390x-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-gnu": "0.64.0", + "@oxfmt/binding-linux-x64-musl": "0.64.0", + "@oxfmt/binding-openharmony-arm64": "0.64.0", + "@oxfmt/binding-win32-arm64-msvc": "0.64.0", + "@oxfmt/binding-win32-ia32-msvc": "0.64.0", + "@oxfmt/binding-win32-x64-msvc": "0.64.0" + }, + "peerDependencies": { + "svelte": "^5.0.0", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "svelte": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint": { + "version": "1.79.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.79.0.tgz", + "integrity": "sha512-hVJ9hq9m2unPS+Of4eJJgCPdIeCC+3DHEUX3tkmrPJr3OK2hz7PhXwgC+ZP71ZcYu8cCDEtQrqLxWNvxBppBVg==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.79.0", + "@oxlint/binding-android-arm64": "1.79.0", + "@oxlint/binding-darwin-arm64": "1.79.0", + "@oxlint/binding-darwin-x64": "1.79.0", + "@oxlint/binding-freebsd-x64": "1.79.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.79.0", + "@oxlint/binding-linux-arm-musleabihf": "1.79.0", + "@oxlint/binding-linux-arm64-gnu": "1.79.0", + "@oxlint/binding-linux-arm64-musl": "1.79.0", + "@oxlint/binding-linux-ppc64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-gnu": "1.79.0", + "@oxlint/binding-linux-riscv64-musl": "1.79.0", + "@oxlint/binding-linux-s390x-gnu": "1.79.0", + "@oxlint/binding-linux-x64-gnu": "1.79.0", + "@oxlint/binding-linux-x64-musl": "1.79.0", + "@oxlint/binding-openharmony-arm64": "1.79.0", + "@oxlint/binding-win32-arm64-msvc": "1.79.0", + "@oxlint/binding-win32-ia32-msvc": "1.79.0", + "@oxlint/binding-win32-x64-msvc": "1.79.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/oxlint-tsgolint": { + "version": "7.0.2001", + "resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2001.tgz", + "integrity": "sha512-KjK/XLcXr1DSyonKhsuFqJRiuKqcyG9j3LJ8nkOsrLzGvodBPqzHOKauy10asLMDI0sUpvb+1sxlzff3udZvfg==", + "dev": true, + "license": "MIT", + "bin": { + "tsgolint": "bin/tsgolint.js" + }, + "optionalDependencies": { + "@oxlint-tsgolint/darwin-arm64": "7.0.2001", + "@oxlint-tsgolint/darwin-x64": "7.0.2001", + "@oxlint-tsgolint/linux-arm64": "7.0.2001", + "@oxlint-tsgolint/linux-x64": "7.0.2001", + "@oxlint-tsgolint/win32-arm64": "7.0.2001", + "@oxlint-tsgolint/win32-x64": "7.0.2001" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "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/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.62.1.tgz", + "integrity": "sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.62.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.62.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.62.1.tgz", + "integrity": "sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "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.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "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/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/react": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", + "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-docgen": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-8.0.3.tgz", + "integrity": "sha512-aEZ9qP+/M+58x2qgfSFEWH1BxLyHe5+qkLNJOZQb5iGS017jpbRnoKhNRrXPeA6RfBrZO5wZrT9DMC1UqE1f1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.2", + "@types/babel__core": "^7.20.5", + "@types/babel__traverse": "^7.20.7", + "@types/doctrine": "^0.0.9", + "@types/resolve": "^1.20.2", + "doctrine": "^3.0.0", + "resolve": "^1.22.1", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.9.0 || >=22" + } + }, + "node_modules/react-docgen-typescript": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/react-docgen-typescript/-/react-docgen-typescript-2.4.0.tgz", + "integrity": "sha512-ZtAp5XTO5HRzQctjPU0ybY0RRCQO19X/8fxn3w7y2VVTUbGHDKULPTL4ky3vB05euSgG5NpALhEhDPvQ56wvXg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typescript": ">= 4.3.x" + } + }, + "node_modules/react-dom": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", + "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.8" + } + }, + "node_modules/react-hook-form": { + "version": "7.86.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.86.0.tgz", + "integrity": "sha512-4kbWJrh5jPZt1+YqVcXcGKffGcXV/XVbozknLh0Yjh0KhpoAkus21TAQhzRYqNwFkkObmnSvRlZZ3GT+ehoIrA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-i18next": { + "version": "17.0.12", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.12.tgz", + "integrity": "sha512-lFWPEGkxQ6RhusdUkysFBD58VHfSSzvHBzqMgN0SvfVpdQGfwtNkStTqdy08/sJd7s807qqutgx93fRpD0DJ3Q==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.29.7", + "html-parse-stringify": "^4.0.1", + "use-sync-external-store": "^1.6.0" + }, + "peerDependencies": { + "i18next": ">= 26.2.0", + "react": ">= 16.8.0", + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + }, + "react-native": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/react-is": { + "version": "19.2.8", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", + "integrity": "sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==", + "license": "MIT" + }, + "node_modules/react-router": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-8.3.0.tgz", + "integrity": "sha512-qyPMvW83jGIct3yiieisxdk9M745anqhpIMKN5m1t6yBMfgVPpt77aHOqs5fUlEJRMCGffg9BaQLH9oPVOL7xQ==", + "license": "MIT", + "dependencies": { + "cookie-es": "^3.1.1" + }, + "engines": { + "node": ">=22.22.0" + }, + "peerDependencies": { + "react": ">=19.2.7", + "react-dom": ">=19.2.7" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/recast": { + "version": "0.23.21", + "resolved": "https://registry.npmjs.org/recast/-/recast-0.23.21.tgz", + "integrity": "sha512-mFAyJq9vUbSTARLZUvAEf1z3YxlvAwswbmxMx2mPA/MSm4KmpwvwvhsH/NIrZhyOuwD60Lzyw2qh83uCbgTPYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.16.1", + "esprima": "~4.0.0", + "source-map": "~0.6.1", + "tiny-invariant": "^1.3.3", + "tslib": "^2.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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/redent/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/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/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/rettime": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.11.11.tgz", + "integrity": "sha512-ILJRqVWBCTlg9r42fFgwVZx1gnFAcQF8mRoMkbgQfIrjEDf9nbBFDFx00oloOa+Q869FUtaYDXZvEfnecQSCoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.5.tgz", + "integrity": "sha512-VD2IE5PUG4Oj8zz2VGykiYd5wbnjdIiSsNQb8Qu5B+noEp+A78mu2iVvpp27g8es14Tk9rofNs5Tku9iQCS4fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.146.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.5", + "@rolldown/binding-android-arm64": "1.2.5", + "@rolldown/binding-darwin-arm64": "1.2.5", + "@rolldown/binding-darwin-x64": "1.2.5", + "@rolldown/binding-freebsd-x64": "1.2.5", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.5", + "@rolldown/binding-linux-arm64-gnu": "1.2.5", + "@rolldown/binding-linux-arm64-musl": "1.2.5", + "@rolldown/binding-linux-ppc64-gnu": "1.2.5", + "@rolldown/binding-linux-s390x-gnu": "1.2.5", + "@rolldown/binding-linux-x64-gnu": "1.2.5", + "@rolldown/binding-linux-x64-musl": "1.2.5", + "@rolldown/binding-openharmony-arm64": "1.2.5", + "@rolldown/binding-win32-arm64-msvc": "1.2.5", + "@rolldown/binding-win32-x64-msvc": "1.2.5" + } + }, + "node_modules/rolldown/node_modules/@oxc-project/types": { + "version": "0.146.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.146.0.tgz", + "integrity": "sha512-XC0QsnnhVe7sLIWmYmdPw7x5P0h4W8vUU3Nv1ySgWXtvCz8NizoAEpGXA0sOYoJQV2Rl13LgURAHQ5cI5ILCSA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/run-applescript": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", + "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/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.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/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": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.2.tgz", + "integrity": "sha512-5/r/lTwbJ3zQ+qwdUFZYeRNqda7P5HD8zQKqlSjdGt1/S0cjLAphHusj4Y58ahDtWn/g32xrIS58/ikOvwl0Lw==", + "dev": true, + "license": "MIT" + }, + "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/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sirv": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/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/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/std-env": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", + "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "dev": true, + "license": "MIT" + }, + "node_modules/storybook": { + "version": "10.5.10", + "resolved": "https://registry.npmjs.org/storybook/-/storybook-10.5.10.tgz", + "integrity": "sha512-Rz8k9ejFHsi7lbtJTaxZlhCUz4GkbJIKEoKDjXeLfr/ZhXip73E6keKxW0KH8iGeKiCqHAbJCV4YIQrxTOLiig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@storybook/global": "^5.0.0", + "@storybook/icons": "^2.0.2", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "6.9.1", + "@testing-library/user-event": "^14.6.1", + "@vitest/expect": "3.2.4", + "@vitest/spy": "3.2.4", + "@webcontainer/env": "^1.1.1", + "esbuild": "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0 || ^0.28.0", + "jsonc-parser": "^3.3.1", + "open": "^10.2.0", + "oxc-parser": "^0.127.0", + "oxc-resolver": "11.21.2", + "recast": "^0.23.5", + "semver": "^7.7.3", + "use-sync-external-store": "^1.5.0", + "ws": "^8.21.1" + }, + "bin": { + "storybook": "dist/bin/dispatcher.js" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/storybook" + }, + "peerDependencies": { + "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "prettier": "^2 || ^3", + "vite-plus": "^0.1.15 || ^0.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "prettier": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, + "node_modules/storybook/node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/strict-event-emitter": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", + "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/string-argv": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.2.tgz", + "integrity": "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.19" + } + }, + "node_modules/string-convert": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz", + "integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/strip-indent": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.1.1.tgz", + "integrity": "sha512-SlyRoSkdh1dYP0PzclLE7r0M9sgbFKKMFXpFRUMNuKhQSbC6VQIGzq3E0qsfvGJaUFJPGv6Ws1NZ/haTAjfbMA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/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/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/throttle-debounce": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz", + "integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==", + "license": "MIT", + "engines": { + "node": ">=12.22" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "dev": true, + "license": "MIT" + }, + "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.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-2.1.0.tgz", + "integrity": "sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.0.0 || >=22.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.1.tgz", + "integrity": "sha512-yau8yJdTt989Mm0Bd/236QnzEiPf2xLLTqUZRUJOo/3CB078LSwzei343DgtJVmfJKJE3TMINY1u42SQsP6mXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", + "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.10" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", + "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "dev": true, + "license": "MIT" + }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/tough-cookie": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", + "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-dedent": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.3.0.tgz", + "integrity": "sha512-JfJeIHke7y2egdGGgRAvpCwYFUsHlM2gPcrVOxFkznt/4uzQ7HFmvE63iFHVLBJNDuyDOQgijDK/tXH/f6Msjg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, + "node_modules/tsconfig-paths": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "json5": "^2.2.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "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" + }, + "node_modules/type-fest": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.8.0.tgz", + "integrity": "sha512-YGYEVz3Fm5iy/AybuA0oyNFq7H4CgQNfRp/qfe8nurE1kuCeNm3/vfm9X4Mtl+qLyaKJUh5xrFZwogr41SMjYA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", + "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc" + }, + "engines": { + "node": ">=16.20.0" + }, + "optionalDependencies": { + "@typescript/typescript-aix-ppc64": "7.0.2", + "@typescript/typescript-darwin-arm64": "7.0.2", + "@typescript/typescript-darwin-x64": "7.0.2", + "@typescript/typescript-freebsd-arm64": "7.0.2", + "@typescript/typescript-freebsd-x64": "7.0.2", + "@typescript/typescript-linux-arm": "7.0.2", + "@typescript/typescript-linux-arm64": "7.0.2", + "@typescript/typescript-linux-loong64": "7.0.2", + "@typescript/typescript-linux-mips64el": "7.0.2", + "@typescript/typescript-linux-ppc64": "7.0.2", + "@typescript/typescript-linux-riscv64": "7.0.2", + "@typescript/typescript-linux-s390x": "7.0.2", + "@typescript/typescript-linux-x64": "7.0.2", + "@typescript/typescript-netbsd-arm64": "7.0.2", + "@typescript/typescript-netbsd-x64": "7.0.2", + "@typescript/typescript-openbsd-arm64": "7.0.2", + "@typescript/typescript-openbsd-x64": "7.0.2", + "@typescript/typescript-sunos-x64": "7.0.2", + "@typescript/typescript-win32-arm64": "7.0.2", + "@typescript/typescript-win32-x64": "7.0.2" + } + }, + "node_modules/undici": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", + "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", + "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unplugin": { + "version": "2.3.11", + "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", + "integrity": "sha512-5uKD0nqiYVzlmCRs01Fhs2BdkEgBS3SAVP6ndrBsuK42iC2+JHyxM05Rm9G8+5mkmRtzMZGY8Ct5+mliZxU/Ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/remapping": "^2.3.5", + "acorn": "^8.15.0", + "picomatch": "^4.0.3", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=18.12.0" + } + }, + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/kettanaito" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.1.tgz", + "integrity": "sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==", + "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/uplot": { + "version": "1.6.32", + "resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.32.tgz", + "integrity": "sha512-KIMVnG68zvu5XXUbC4LQEPnhwOxBuLyW1AHtpm6IKTXImkbLgkMy+jabjLgSLMasNuGGzQm/ep3tOkyTxpiQIw==", + "license": "MIT" + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/vite": { + "version": "8.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.2.tgz", + "integrity": "sha512-cFKLV/PRgAUlIRm5WjMjJ86jrftzpqcgH+Us+DS8mI3CDNiH30Whrz8uHL3+MOLPAgqbMBAqWdAHAphOAM+z/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.5", + "postcss": "^8.5.26", + "rolldown": "~1.2.4", + "tinyglobby": "^0.2.17" + }, + "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.4.0 || ^0.5.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "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/vite/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/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/vitest": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "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.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", + "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/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/vitest/node_modules/@vitest/expect": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest/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/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", + "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.15.1", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^22.14.0 || >=24.0.0" + } + }, + "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/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/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/ws": { + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "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/wsl-utils": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", + "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/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.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "optional": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.3", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz", + "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..c8f467e --- /dev/null +++ b/package.json @@ -0,0 +1,76 @@ +{ + "name": "admin-ui", + "version": "1.0.0", + "private": true, + "description": "Reusable game operations administration UI.", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview --host 0.0.0.0 --port 19999", + "lint": "oxlint src .storybook vite.config.js vitest.config.ts", + "lint:fix": "oxlint --fix src .storybook vite.config.js vitest.config.ts", + "lint:deprecated": "oxlint --type-aware -A all -D typescript/no-deprecated src", + "format": "oxfmt src .storybook vite.config.js vitest.config.ts", + "format:check": "oxfmt --check src .storybook vite.config.js vitest.config.ts", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build" + }, + "dependencies": { + "@ant-design/icons": "^6.3.2", + "@hookform/resolvers": "^5.9.1", + "@tanstack/react-query": "^5.102.2", + "antd": "^6.6.1", + "dayjs": "^1.11.23", + "i18next": "^26.4.0", + "react": "^19.2.8", + "react-dom": "^19.2.8", + "react-hook-form": "^7.86.0", + "react-i18next": "^17.0.12", + "react-router": "^8.3.0", + "uplot": "^1.6.32", + "zod": "^4.4.3" + }, + "devDependencies": { + "@storybook/addon-a11y": "^10.5.10", + "@storybook/addon-docs": "^10.5.10", + "@storybook/addon-vitest": "^10.5.10", + "@storybook/react-vite": "^10.5.10", + "@testing-library/dom": "^10.4.1", + "@testing-library/react": "^16.3.2", + "@types/react": "^19.2.18", + "@types/react-dom": "^19.2.5", + "@vitejs/plugin-react": "^6.1.0", + "@vitest/browser-playwright": "4.1.11", + "@vitest/coverage-v8": "^4.1.11", + "jsdom": "^30.0.1", + "lint-staged": "^17.3.0", + "msw": "^2.15.0", + "oxfmt": "0.64.0", + "oxlint": "1.79.0", + "oxlint-tsgolint": "^7.0.2001", + "playwright": "^1.62.1", + "storybook": "^10.5.10", + "typescript": "7.0.2", + "vite": "8.2.2", + "vitest": "^4.1.11" + }, + "lint-staged": { + "src/**/*.{ts,tsx}": [ + "oxfmt", + "oxlint --fix" + ] + }, + "engines": { + "node": ">=24.0.0", + "npm": ">=10.0.0" + }, + "msw": { + "workerDirectory": [ + "public" + ] + } +} diff --git a/public/mockServiceWorker.js b/public/mockServiceWorker.js new file mode 100644 index 0000000..0c970ef --- /dev/null +++ b/public/mockServiceWorker.js @@ -0,0 +1,361 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.15.0' +const INTEGRITY_CHECKSUM = '03cb67ac84128e63d7cd722a6e5b7f1e' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Omit the body of server-sent event stream responses. + // Cloning such responses would prevent client-side stream cancelations + // from reaching the original stream (a teed stream only cancels its + // source once both of its branches cancel) and would buffer the + // entire stream into the unconsumed clone indefinitely. + const isEventStreamResponse = response.headers + .get('content-type') + ?.toLowerCase() + .startsWith('text/event-stream') + + // Clone the response so both the client and the library could consume it. + const responseClone = isEventStreamResponse ? null : response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: response.type, + status: response.status, + statusText: response.statusText, + headers: Object.fromEntries(response.headers.entries()), + body: responseClone ? responseClone.body : null, + }, + }, + }, + responseClone && responseClone.body + ? [serializedRequest.body, responseClone.body] + : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/src/api/QueryProvider.tsx b/src/api/QueryProvider.tsx new file mode 100644 index 0000000..a288ffe --- /dev/null +++ b/src/api/QueryProvider.tsx @@ -0,0 +1,8 @@ +import type { ReactNode } from 'react'; +import { QueryClientProvider } from '@tanstack/react-query'; + +import { queryClient } from '@/queryClient'; + +export function QueryProvider({ children }: { children: ReactNode }) { + return {children}; +} diff --git a/src/api/http-init.ts b/src/api/http-init.ts new file mode 100644 index 0000000..acabd73 --- /dev/null +++ b/src/api/http-init.ts @@ -0,0 +1,211 @@ +const SAFE_METHODS = new Set(['GET', 'HEAD', 'OPTIONS', 'TRACE']); +const CSRF_TOKEN_PATH = '/csrf-token'; + +let csrfToken: string | null = null; +let csrfFetchPromise: Promise | null = null; +let sessionExpired = false; +let basePathPrefix = ''; + +export interface HttpResponse { + ok: boolean; + status: number; + statusText: string; + data: unknown; +} + +export class HttpError extends Error { + status: number; + response: { status: number; statusText: string; data: unknown }; + + constructor(status: number, statusText: string, data: unknown) { + super(`Request failed with status ${status}`); + this.name = 'HttpError'; + this.status = status; + this.response = { status, statusText, data }; + } +} + +export interface HttpRequestOptions { + headers?: Record | Headers; + params?: unknown; + timeout?: number; + signal?: AbortSignal; +} + +function readMetaToken(): string | null { + return document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || null; +} + +async function fetchCsrfToken(): Promise { + try { + const res = await fetch(basePathPrefix + CSRF_TOKEN_PATH, { + method: 'GET', + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }); + if (!res.ok) return null; + const json = (await res.json()) as { success?: boolean; obj?: unknown } | null; + return json?.success && typeof json.obj === 'string' ? json.obj : null; + } catch { + return null; + } +} + +async function ensureCsrfToken(): Promise { + if (csrfToken) return csrfToken; + const meta = readMetaToken(); + if (meta) { + csrfToken = meta; + return csrfToken; + } + if (!csrfFetchPromise) csrfFetchPromise = fetchCsrfToken(); + const fetched = await csrfFetchPromise; + csrfFetchPromise = null; + if (fetched) csrfToken = fetched; + return csrfToken; +} + +function encodeForm(data: unknown): string { + if (data == null || typeof data !== 'object') return ''; + const parts: string[] = []; + const append = (key: string, value: unknown): void => { + if (value === undefined) return; + if (value === null) { + parts.push(`${encodeURIComponent(key)}=`); + return; + } + if (Array.isArray(value)) { + value.forEach((item) => append(key, item)); + return; + } + if (typeof value === 'object') { + Object.entries(value as Record).forEach(([k, v]) => + append(`${key}[${k}]`, v), + ); + return; + } + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + Object.entries(data as Record).forEach(([k, v]) => append(k, v)); + return parts.join('&'); +} + +function appendQuery(url: string, query: string): string { + if (query === '') return url; + const hashIndex = url.indexOf('#'); + const path = hashIndex === -1 ? url : url.slice(0, hashIndex); + const hash = hashIndex === -1 ? '' : url.slice(hashIndex); + const hasQuery = path.includes('?'); + const separator = !hasQuery ? '?' : path.endsWith('?') || path.endsWith('&') ? '' : '&'; + return `${path}${separator}${query}${hash}`; +} + +function requestSignal(options: HttpRequestOptions): AbortSignal | undefined { + if (!options.timeout) return options.signal; + const timeout = AbortSignal.timeout(options.timeout); + if (!options.signal) return timeout; + if (typeof AbortSignal.any === 'function') return AbortSignal.any([options.signal, timeout]); + const controller = new AbortController(); + const abort = () => controller.abort(); + options.signal.addEventListener('abort', abort, { once: true }); + timeout.addEventListener('abort', abort, { once: true }); + return controller.signal; +} + +async function performFetch( + method: string, + url: string, + data: unknown, + options: HttpRequestOptions, + csrfOverride?: string, +): Promise { + const upper = method.toUpperCase(); + const headers = new Headers(options.headers); + headers.set('X-Requested-With', 'XMLHttpRequest'); + + let body: BodyInit | undefined; + if (data instanceof FormData) { + body = data; + headers.delete('Content-Type'); + } else if (!SAFE_METHODS.has(upper)) { + const declaredType = (headers.get('Content-Type') || '').toLowerCase(); + if (declaredType.startsWith('application/json')) { + if (data !== undefined) { + body = typeof data === 'string' ? data : JSON.stringify(data); + } + } else { + headers.set('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); + body = encodeForm(data); + } + } + + if (!SAFE_METHODS.has(upper)) { + const token = csrfOverride ?? (await ensureCsrfToken()); + if (token) headers.set('X-CSRF-Token', token); + } + + const query = encodeForm(options.params); + const fullUrl = basePathPrefix + appendQuery(url, query); + const signal = requestSignal(options); + + return fetch(fullUrl, { method: upper, headers, body, credentials: 'same-origin', signal }); +} + +async function parseBody(res: Response): Promise { + if (res.status === 204 || res.status === 205) return ''; + const text = await res.text(); + if (text === '') return ''; + const contentType = (res.headers.get('content-type') || '').toLowerCase(); + if (contentType.includes('application/json') || text[0] === '{' || text[0] === '[') { + try { + return JSON.parse(text); + } catch { + return text; + } + } + return text; +} + +export async function httpRequest( + method: string, + url: string, + data?: unknown, + options: HttpRequestOptions = {}, +): Promise { + let res = await performFetch(method, url, data, options); + + if (res.status === 403 && !SAFE_METHODS.has(method.toUpperCase())) { + csrfToken = null; + const fresh = await fetchCsrfToken(); + if (fresh) { + csrfToken = fresh; + res = await performFetch(method, url, data, options, fresh); + } + } + + if (res.status === 401) { + if (!sessionExpired) { + sessionExpired = true; + window.location.replace(window.ADMIN_UI_BASE_PATH || basePathPrefix || '/'); + } + return new Promise(() => {}); + } + + const parsed = await parseBody(res); + if (!res.ok) throw new HttpError(res.status, res.statusText, parsed); + return { ok: true, status: res.status, statusText: res.statusText, data: parsed }; +} + +export function setupHttp(): void { + let basePath: string | null | undefined = window.ADMIN_UI_BASE_PATH; + if (!basePath) { + const metaTag = document.querySelector('meta[name="base-path"]'); + basePath = metaTag ? metaTag.getAttribute('content') : null; + } + basePathPrefix = + typeof basePath === 'string' && basePath !== '' && basePath !== '/' + ? basePath.replace(/\/$/, '') + : ''; + + csrfToken = readMetaToken(); +} diff --git a/src/api/queries/useGameAccountsQuery.ts b/src/api/queries/useGameAccountsQuery.ts new file mode 100644 index 0000000..352215a --- /dev/null +++ b/src/api/queries/useGameAccountsQuery.ts @@ -0,0 +1,80 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { GameAccountListSchema, GameAccountSchema } from '@/schemas/gameAccount'; +import type { + GameAccountList, + GameAccountPlatform, + GameAccountStatus, +} from '@/schemas/gameAccount'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GameAccountFilters { + page: number; + pageSize: number; + keyword: string; + status: GameAccountStatus | 'all'; + platform: GameAccountPlatform | 'all'; +} + +export interface UpdateGameAccountStatusInput { + accountId: string; + status: 'active' | 'banned'; + reason?: string; + durationDays?: number; +} + +async function fetchGameAccounts(filters: GameAccountFilters): Promise { + const msg = await HttpUtil.get('/panel/api/game/accounts', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game accounts'); + const validated = parseMsg(msg, GameAccountListSchema, 'game/accounts', { strict: true }); + if (!validated.obj) throw new Error('Game account list response is empty'); + return validated.obj; +} + +export async function updateGameAccountStatus(input: UpdateGameAccountStatusInput) { + const { accountId, ...payload } = input; + const msg = await HttpUtil.post(`/panel/api/game/accounts/${accountId}/status`, payload, { + headers: { 'Content-Type': 'application/json' }, + silentSuccess: true, + }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update account status'); + const validated = parseMsg(msg, GameAccountSchema, 'game/accounts/status', { strict: true }); + if (!validated.obj) throw new Error('Updated account response is empty'); + return validated.obj; +} + +export function useGameAccountsQuery(filters: GameAccountFilters) { + const query = useQuery({ + queryKey: keys.game.accounts.list(filters), + queryFn: () => fetchGameAccounts(filters), + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + accounts: query.data?.items ?? [], + total: query.data?.total ?? 0, + summary: query.data?.summary ?? { + total: 0, + newToday: 0, + active: 0, + banned: 0, + locked: 0, + }, + platformOptions: query.data?.platformOptions ?? [], + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} + +export function useGameAccountStatusMutation() { + const queryClient = useQueryClient(); + return useMutation({ + mutationFn: updateGameAccountStatus, + onSuccess: () => queryClient.invalidateQueries({ queryKey: keys.game.accounts.root() }), + }); +} diff --git a/src/api/queries/useGameAnnouncementsQuery.ts b/src/api/queries/useGameAnnouncementsQuery.ts new file mode 100644 index 0000000..265e6fb --- /dev/null +++ b/src/api/queries/useGameAnnouncementsQuery.ts @@ -0,0 +1,123 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { + GameAnnouncementListSchema, + GameAnnouncementSchema, + GameAnnouncementStatusInputSchema, + GameAnnouncementUpsertInputSchema, +} from '@/schemas/gameAnnouncement'; +import type { + GameAnnouncementChannel, + GameAnnouncementList, + GameAnnouncementStatus, + GameAnnouncementStatusInput, + GameAnnouncementUpsertInput, +} from '@/schemas/gameAnnouncement'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GameAnnouncementFilters { + page: number; + pageSize: number; + keyword: string; + status: GameAnnouncementStatus | 'all'; + channel: GameAnnouncementChannel | 'all'; +} + +export interface SaveGameAnnouncementInput { + announcementId?: string; + data: GameAnnouncementUpsertInput; +} + +export interface UpdateGameAnnouncementStatusInput extends GameAnnouncementStatusInput { + announcementId: string; +} + +const JSON_OPTIONS = { + headers: { 'Content-Type': 'application/json' }, + silentSuccess: true, +} as const; + +export async function fetchGameAnnouncements( + filters: GameAnnouncementFilters, +): Promise { + const msg = await HttpUtil.get('/panel/api/game/announcements', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game announcements'); + const validated = parseMsg(msg, GameAnnouncementListSchema, 'game/announcements', { + strict: true, + }); + if (!validated.obj) throw new Error('Game announcement list response is empty'); + return validated.obj; +} + +export async function saveGameAnnouncement(input: SaveGameAnnouncementInput) { + const payload = GameAnnouncementUpsertInputSchema.parse(input.data); + const endpoint = input.announcementId + ? `/panel/api/game/announcements/${input.announcementId}` + : '/panel/api/game/announcements'; + const msg = await HttpUtil.post(endpoint, payload, JSON_OPTIONS); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to save game announcement'); + const validated = parseMsg(msg, GameAnnouncementSchema, 'game/announcements/save', { + strict: true, + }); + if (!validated.obj) throw new Error('Saved announcement response is empty'); + return validated.obj; +} + +export async function updateGameAnnouncementStatus(input: UpdateGameAnnouncementStatusInput) { + const { announcementId, ...rawPayload } = input; + const payload = GameAnnouncementStatusInputSchema.parse(rawPayload); + const msg = await HttpUtil.post( + `/panel/api/game/announcements/${announcementId}/status`, + payload, + JSON_OPTIONS, + ); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update announcement status'); + const validated = parseMsg(msg, GameAnnouncementSchema, 'game/announcements/status', { + strict: true, + }); + if (!validated.obj) throw new Error('Updated announcement response is empty'); + return validated.obj; +} + +export function useGameAnnouncementsQuery(filters: GameAnnouncementFilters) { + const query = useQuery({ + queryKey: keys.game.announcements.list(filters), + queryFn: () => fetchGameAnnouncements(filters), + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + announcements: query.data?.items ?? [], + total: query.data?.total ?? 0, + summary: query.data?.summary ?? { + total: 0, + active: 0, + scheduled: 0, + draft: 0, + inactive: 0, + }, + serverOptions: query.data?.serverOptions ?? [], + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} + +function useInvalidateGameAnnouncements() { + const queryClient = useQueryClient(); + return () => queryClient.invalidateQueries({ queryKey: keys.game.announcements.root() }); +} + +export function useSaveGameAnnouncementMutation() { + const invalidate = useInvalidateGameAnnouncements(); + return useMutation({ mutationFn: saveGameAnnouncement, onSuccess: invalidate }); +} + +export function useGameAnnouncementStatusMutation() { + const invalidate = useInvalidateGameAnnouncements(); + return useMutation({ mutationFn: updateGameAnnouncementStatus, onSuccess: invalidate }); +} diff --git a/src/api/queries/useGameCdksQuery.ts b/src/api/queries/useGameCdksQuery.ts new file mode 100644 index 0000000..5fe7eff --- /dev/null +++ b/src/api/queries/useGameCdksQuery.ts @@ -0,0 +1,109 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { + GameCdkBatchCreateInputSchema, + GameCdkBatchCreateResultSchema, + GameCdkListSchema, + GameCdkSchema, + GameCdkStatusInputSchema, +} from '@/schemas/gameCdk'; +import type { + GameCdkBatchCreateInput, + GameCdkBatchCreateResult, + GameCdkList, + GameCdkStatus, + GameCdkStatusInput, +} from '@/schemas/gameCdk'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GameCdkFilters { + page: number; + pageSize: number; + keyword: string; + status: GameCdkStatus | 'all'; + batchId: string | 'all'; +} + +export interface UpdateGameCdkStatusInput extends GameCdkStatusInput { + cdkId: string; +} + +const JSON_OPTIONS = { + headers: { 'Content-Type': 'application/json' }, + silentSuccess: true, +} as const; + +export async function fetchGameCdks(filters: GameCdkFilters): Promise { + const msg = await HttpUtil.get('/panel/api/game/cdks', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game CDKs'); + const validated = parseMsg(msg, GameCdkListSchema, 'game/cdks', { strict: true }); + if (!validated.obj) throw new Error('Game CDK list response is empty'); + return validated.obj; +} + +export async function createGameCdkBatch( + input: GameCdkBatchCreateInput, +): Promise { + const payload = GameCdkBatchCreateInputSchema.parse(input); + const msg = await HttpUtil.post('/panel/api/game/cdks/batches', payload, JSON_OPTIONS); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to create game CDK batch'); + const validated = parseMsg(msg, GameCdkBatchCreateResultSchema, 'game/cdks/batches', { + strict: true, + }); + if (!validated.obj) throw new Error('Created CDK batch response is empty'); + return validated.obj; +} + +export async function updateGameCdkStatus(input: UpdateGameCdkStatusInput) { + const { cdkId, ...rawPayload } = input; + const payload = GameCdkStatusInputSchema.parse(rawPayload); + const msg = await HttpUtil.post(`/panel/api/game/cdks/${cdkId}/status`, payload, JSON_OPTIONS); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update game CDK status'); + const validated = parseMsg(msg, GameCdkSchema, 'game/cdks/status', { strict: true }); + if (!validated.obj) throw new Error('Updated CDK response is empty'); + return validated.obj; +} + +export function useGameCdksQuery(filters: GameCdkFilters) { + const query = useQuery({ + queryKey: keys.game.cdks.list(filters), + queryFn: () => fetchGameCdks(filters), + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + cdks: query.data?.items ?? [], + total: query.data?.total ?? 0, + summary: query.data?.summary ?? { + total: 0, + unused: 0, + used: 0, + expired: 0, + disabled: 0, + }, + batchOptions: query.data?.batchOptions ?? [], + serverOptions: query.data?.serverOptions ?? [], + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} + +function useInvalidateGameCdks() { + const queryClient = useQueryClient(); + return () => queryClient.invalidateQueries({ queryKey: keys.game.cdks.root() }); +} + +export function useCreateGameCdkBatchMutation() { + const invalidate = useInvalidateGameCdks(); + return useMutation({ mutationFn: createGameCdkBatch, onSuccess: invalidate }); +} + +export function useGameCdkStatusMutation() { + const invalidate = useInvalidateGameCdks(); + return useMutation({ mutationFn: updateGameCdkStatus, onSuccess: invalidate }); +} diff --git a/src/api/queries/useGameOrdersQuery.ts b/src/api/queries/useGameOrdersQuery.ts new file mode 100644 index 0000000..ac3be9b --- /dev/null +++ b/src/api/queries/useGameOrdersQuery.ts @@ -0,0 +1,117 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { + GameOrderDeliveryInputSchema, + GameOrderListSchema, + GameOrderRefundInputSchema, + GameOrderSchema, +} from '@/schemas/gameOrder'; +import type { + GameOrderChannel, + GameOrderDeliveryInput, + GameOrderList, + GameOrderRefundInput, + GameOrderStatus, +} from '@/schemas/gameOrder'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GameOrderFilters { + page: number; + pageSize: number; + keyword: string; + status: GameOrderStatus | 'all'; + channel: GameOrderChannel | 'all'; + serverId: string | 'all'; +} + +export interface DeliverGameOrderInput extends GameOrderDeliveryInput { + orderId: string; +} + +export interface RefundGameOrderInput extends GameOrderRefundInput { + orderId: string; +} + +const JSON_OPTIONS = { + headers: { 'Content-Type': 'application/json' }, + silentSuccess: true, +} as const; + +export async function fetchGameOrders(filters: GameOrderFilters): Promise { + const msg = await HttpUtil.get('/panel/api/game/orders', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game orders'); + const validated = parseMsg(msg, GameOrderListSchema, 'game/orders', { strict: true }); + if (!validated.obj) throw new Error('Game order list response is empty'); + return validated.obj; +} + +export async function deliverGameOrder(input: DeliverGameOrderInput) { + const { orderId, ...rawPayload } = input; + const payload = GameOrderDeliveryInputSchema.parse(rawPayload); + const msg = await HttpUtil.post( + `/panel/api/game/orders/${orderId}/deliver`, + payload, + JSON_OPTIONS, + ); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to deliver game order'); + const validated = parseMsg(msg, GameOrderSchema, 'game/orders/deliver', { strict: true }); + if (!validated.obj) throw new Error('Delivered order response is empty'); + return validated.obj; +} + +export async function refundGameOrder(input: RefundGameOrderInput) { + const { orderId, ...rawPayload } = input; + const payload = GameOrderRefundInputSchema.parse(rawPayload); + const msg = await HttpUtil.post( + `/panel/api/game/orders/${orderId}/refund`, + payload, + JSON_OPTIONS, + ); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to refund game order'); + const validated = parseMsg(msg, GameOrderSchema, 'game/orders/refund', { strict: true }); + if (!validated.obj) throw new Error('Refunded order response is empty'); + return validated.obj; +} + +export function useGameOrdersQuery(filters: GameOrderFilters) { + const query = useQuery({ + queryKey: keys.game.orders.list(filters), + queryFn: () => fetchGameOrders(filters), + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + orders: query.data?.items ?? [], + total: query.data?.total ?? 0, + summary: query.data?.summary ?? { + total: 0, + paidAmount: 0, + pending: 0, + awaitingDelivery: 0, + refunding: 0, + }, + serverOptions: query.data?.serverOptions ?? [], + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} + +function useInvalidateGameOrders() { + const queryClient = useQueryClient(); + return () => queryClient.invalidateQueries({ queryKey: keys.game.orders.root() }); +} + +export function useDeliverGameOrderMutation() { + const invalidate = useInvalidateGameOrders(); + return useMutation({ mutationFn: deliverGameOrder, onSuccess: invalidate }); +} + +export function useRefundGameOrderMutation() { + const invalidate = useInvalidateGameOrders(); + return useMutation({ mutationFn: refundGameOrder, onSuccess: invalidate }); +} diff --git a/src/api/queries/useGameOverviewQuery.ts b/src/api/queries/useGameOverviewQuery.ts new file mode 100644 index 0000000..8430f12 --- /dev/null +++ b/src/api/queries/useGameOverviewQuery.ts @@ -0,0 +1,39 @@ +import { useQuery } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { GameOverviewSchema } from '@/schemas/gameOverview'; +import type { GameOverview, OverviewRange } from '@/schemas/gameOverview'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GameOverviewFilters { + range: OverviewRange; + serverId: string; +} + +async function fetchGameOverview(filters: GameOverviewFilters): Promise { + const msg = await HttpUtil.get('/panel/api/game/overview', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game overview'); + const validated = parseMsg(msg, GameOverviewSchema, 'game/overview', { strict: true }); + if (!validated.obj) throw new Error('Game overview response is empty'); + return validated.obj; +} + +export function useGameOverviewQuery(filters: GameOverviewFilters) { + const query = useQuery({ + queryKey: keys.game.overview(filters.range, filters.serverId), + queryFn: () => fetchGameOverview(filters), + refetchInterval: 30_000, + refetchIntervalInBackground: false, + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + overview: query.data, + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} diff --git a/src/api/queries/useGamePlayersQuery.ts b/src/api/queries/useGamePlayersQuery.ts new file mode 100644 index 0000000..55f0b42 --- /dev/null +++ b/src/api/queries/useGamePlayersQuery.ts @@ -0,0 +1,115 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { + GamePlayerListSchema, + GamePlayerResourceInputSchema, + GamePlayerSchema, + GamePlayerStatusInputSchema, +} from '@/schemas/gamePlayer'; +import type { + GamePlayerList, + GamePlayerResourceInput, + GamePlayerStatus, + GamePlayerStatusInput, +} from '@/schemas/gamePlayer'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GamePlayerFilters { + page: number; + pageSize: number; + keyword: string; + status: GamePlayerStatus | 'all'; + serverId: string | 'all'; +} + +export interface UpdateGamePlayerResourcesInput extends GamePlayerResourceInput { + playerId: string; +} + +export interface UpdateGamePlayerStatusInput extends GamePlayerStatusInput { + playerId: string; +} + +const JSON_OPTIONS = { + headers: { 'Content-Type': 'application/json' }, + silentSuccess: true, +} as const; + +export async function fetchGamePlayers(filters: GamePlayerFilters): Promise { + const msg = await HttpUtil.get('/panel/api/game/players', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game players'); + const validated = parseMsg(msg, GamePlayerListSchema, 'game/players', { strict: true }); + if (!validated.obj) throw new Error('Game player list response is empty'); + return validated.obj; +} + +export async function updateGamePlayerResources(input: UpdateGamePlayerResourcesInput) { + const { playerId, ...rawPayload } = input; + const payload = GamePlayerResourceInputSchema.parse(rawPayload); + const msg = await HttpUtil.post( + `/panel/api/game/players/${playerId}/resources`, + payload, + JSON_OPTIONS, + ); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update player resources'); + const validated = parseMsg(msg, GamePlayerSchema, 'game/players/resources', { strict: true }); + if (!validated.obj) throw new Error('Updated player response is empty'); + return validated.obj; +} + +export async function updateGamePlayerStatus(input: UpdateGamePlayerStatusInput) { + const { playerId, ...rawPayload } = input; + const payload = GamePlayerStatusInputSchema.parse(rawPayload); + const msg = await HttpUtil.post( + `/panel/api/game/players/${playerId}/status`, + payload, + JSON_OPTIONS, + ); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update player status'); + const validated = parseMsg(msg, GamePlayerSchema, 'game/players/status', { strict: true }); + if (!validated.obj) throw new Error('Updated player response is empty'); + return validated.obj; +} + +export function useGamePlayersQuery(filters: GamePlayerFilters) { + const query = useQuery({ + queryKey: keys.game.players.list(filters), + queryFn: () => fetchGamePlayers(filters), + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + players: query.data?.items ?? [], + total: query.data?.total ?? 0, + summary: query.data?.summary ?? { + total: 0, + online: 0, + activeToday: 0, + newToday: 0, + banned: 0, + }, + serverOptions: query.data?.serverOptions ?? [], + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} + +function useInvalidateGamePlayers() { + const queryClient = useQueryClient(); + return () => queryClient.invalidateQueries({ queryKey: keys.game.players.root() }); +} + +export function useGamePlayerResourcesMutation() { + const invalidate = useInvalidateGamePlayers(); + return useMutation({ mutationFn: updateGamePlayerResources, onSuccess: invalidate }); +} + +export function useGamePlayerStatusMutation() { + const invalidate = useInvalidateGamePlayers(); + return useMutation({ mutationFn: updateGamePlayerStatus, onSuccess: invalidate }); +} diff --git a/src/api/queries/useGameServersQuery.ts b/src/api/queries/useGameServersQuery.ts new file mode 100644 index 0000000..24e4042 --- /dev/null +++ b/src/api/queries/useGameServersQuery.ts @@ -0,0 +1,125 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'; + +import { keys } from '@/api/queryKeys'; +import { + GameServerListSchema, + GameServerSchema, + GameServerStatusInputSchema, + GameServerUpsertInputSchema, +} from '@/schemas/gameServer'; +import type { + GameServerList, + GameServerStatus, + GameServerStatusInput, + GameServerUpsertInput, +} from '@/schemas/gameServer'; +import { HttpUtil } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +export interface GameServerFilters { + page: number; + pageSize: number; + keyword: string; + status: GameServerStatus | 'all'; + region: string | 'all'; +} + +export interface UpdateGameServerInput extends GameServerUpsertInput { + serverId: string; +} + +export interface UpdateGameServerStatusInput extends GameServerStatusInput { + serverId: string; +} + +const JSON_OPTIONS = { + headers: { 'Content-Type': 'application/json' }, + silentSuccess: true, +} as const; + +export async function fetchGameServers(filters: GameServerFilters): Promise { + const msg = await HttpUtil.get('/panel/api/game/servers', filters, { silent: true }); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to fetch game servers'); + const validated = parseMsg(msg, GameServerListSchema, 'game/servers', { strict: true }); + if (!validated.obj) throw new Error('Game server list response is empty'); + return validated.obj; +} + +export async function createGameServer(input: GameServerUpsertInput) { + const payload = GameServerUpsertInputSchema.parse(input); + const msg = await HttpUtil.post('/panel/api/game/servers', payload, JSON_OPTIONS); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to create game server'); + const validated = parseMsg(msg, GameServerSchema, 'game/servers/create', { strict: true }); + if (!validated.obj) throw new Error('Created game server response is empty'); + return validated.obj; +} + +export async function updateGameServer(input: UpdateGameServerInput) { + const { serverId, ...rawPayload } = input; + const payload = GameServerUpsertInputSchema.parse(rawPayload); + const msg = await HttpUtil.post(`/panel/api/game/servers/${serverId}`, payload, JSON_OPTIONS); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update game server'); + const validated = parseMsg(msg, GameServerSchema, 'game/servers/update', { strict: true }); + if (!validated.obj) throw new Error('Updated game server response is empty'); + return validated.obj; +} + +export async function updateGameServerStatus(input: UpdateGameServerStatusInput) { + const { serverId, ...rawPayload } = input; + const payload = GameServerStatusInputSchema.parse(rawPayload); + const msg = await HttpUtil.post( + `/panel/api/game/servers/${serverId}/status`, + payload, + JSON_OPTIONS, + ); + if (!msg?.success) throw new Error(msg?.msg || 'Failed to update game server status'); + const validated = parseMsg(msg, GameServerSchema, 'game/servers/status', { strict: true }); + if (!validated.obj) throw new Error('Updated game server response is empty'); + return validated.obj; +} + +export function useGameServersQuery(filters: GameServerFilters) { + const query = useQuery({ + queryKey: keys.game.servers.list(filters), + queryFn: () => fetchGameServers(filters), + staleTime: 10_000, + placeholderData: (previous) => previous, + }); + + return { + servers: query.data?.items ?? [], + total: query.data?.total ?? 0, + summary: query.data?.summary ?? { + total: 0, + online: 0, + maintenance: 0, + offline: 0, + onlinePlayers: 0, + }, + regionOptions: query.data?.regionOptions ?? [], + loading: query.isFetching, + fetched: query.data !== undefined || query.isError, + fetchError: query.error ? (query.error as Error).message : '', + refresh: query.refetch, + }; +} + +function useInvalidateGameServers() { + const queryClient = useQueryClient(); + return () => queryClient.invalidateQueries({ queryKey: keys.game.servers.root() }); +} + +export function useCreateGameServerMutation() { + const invalidate = useInvalidateGameServers(); + return useMutation({ mutationFn: createGameServer, onSuccess: invalidate }); +} + +export function useUpdateGameServerMutation() { + const invalidate = useInvalidateGameServers(); + return useMutation({ mutationFn: updateGameServer, onSuccess: invalidate }); +} + +export function useGameServerStatusMutation() { + const invalidate = useInvalidateGameServers(); + return useMutation({ mutationFn: updateGameServerStatus, onSuccess: invalidate }); +} diff --git a/src/api/queryKeys.ts b/src/api/queryKeys.ts new file mode 100644 index 0000000..043674e --- /dev/null +++ b/src/api/queryKeys.ts @@ -0,0 +1,29 @@ +export const keys = { + game: { + overview: (range: string, serverId: string) => ['game', 'overview', range, serverId] as const, + accounts: { + root: () => ['game', 'accounts'] as const, + list: (filters: unknown) => ['game', 'accounts', 'list', filters] as const, + }, + servers: { + root: () => ['game', 'servers'] as const, + list: (filters: unknown) => ['game', 'servers', 'list', filters] as const, + }, + players: { + root: () => ['game', 'players'] as const, + list: (filters: unknown) => ['game', 'players', 'list', filters] as const, + }, + cdks: { + root: () => ['game', 'cdks'] as const, + list: (filters: unknown) => ['game', 'cdks', 'list', filters] as const, + }, + announcements: { + root: () => ['game', 'announcements'] as const, + list: (filters: unknown) => ['game', 'announcements', 'list', filters] as const, + }, + orders: { + root: () => ['game', 'orders'] as const, + list: (filters: unknown) => ['game', 'orders', 'list', filters] as const, + }, + }, +} as const; diff --git a/src/components/form/rhf/FormField.stories.tsx b/src/components/form/rhf/FormField.stories.tsx new file mode 100644 index 0000000..91d1afd --- /dev/null +++ b/src/components/form/rhf/FormField.stories.tsx @@ -0,0 +1,171 @@ +import { useEffect } from 'react'; +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { expect, waitFor } from 'storybook/test'; +import { Button, Form, Input, InputNumber, Select, Switch, Typography } from 'antd'; +import { FormProvider } from 'react-hook-form'; +import { z } from 'zod'; + +import { FormField } from './FormField'; +import { useZodForm } from './useZodForm'; + +const meta = { + title: 'Form/RHF/FormField', + component: FormField, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Connects an Ant Design control to react-hook-form, normalizes its change payload, and renders translated validation feedback.', + }, + }, + }, + argTypes: { + name: { description: 'Field path as a dotted string or an array of path segments.' }, + control: { + description: 'Optional react-hook-form control; defaults to the surrounding FormProvider.', + }, + label: { description: 'Form.Item label.' }, + tooltip: { description: 'Form.Item tooltip shown beside the label.' }, + extra: { description: 'Helper text rendered below the input.' }, + valueProp: { + description: 'Value prop passed to the child, such as value or checked.', + }, + transform: { + description: 'Optional input and output mappers between form state and the displayed value.', + }, + onAfterChange: { description: 'Called with the stored value after every change.' }, + rules: { description: 'Controller validation rules.' }, + required: { description: 'Marks the field as required.' }, + noStyle: { description: 'Renders without Form.Item layout chrome.' }, + children: { description: 'The Ant Design control to connect.' }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +const AccountSchema = z.object({ + username: z.string(), + platform: z.string(), + enabled: z.boolean(), +}); + +function AccountDemo() { + const methods = useZodForm(AccountSchema, { + defaultValues: { username: 'player_1001', platform: 'official', enabled: true }, + }); + return ( + +
+ + + + + + + + + + +
+
+ ); +} + +const placeholderArgs = { + name: 'username', + children: , +}; + +export const AccountFields: Story = { + args: placeholderArgs, + render: () => , +}; + +export const NumericTransform: Story = { + args: placeholderArgs, + render: () => , + play: async ({ canvas, userEvent }) => { + const input = canvas.getByRole('spinbutton'); + await userEvent.clear(input); + await userEvent.type(input, '125'); + await expect(await canvas.findByText('Form state: 12500 points')).toBeVisible(); + }, +}; + +export const ValidationErrors: Story = { + args: placeholderArgs, + render: () => , + play: async ({ canvas, userEvent }) => { + const nameError = await canvas.findByText('Server name is required'); + await waitFor(() => expect(nameError).toBeVisible()); + await waitFor(() => expect(canvas.getByText('Port must be between 1 and 65535')).toBeVisible()); + await userEvent.type(canvas.getByPlaceholderText('Aurora One'), 'Aurora One'); + await userEvent.click(canvas.getByRole('button', { name: 'Validate' })); + await waitFor(() => + expect(canvas.queryByText('Server name is required')).not.toBeInTheDocument(), + ); + }, +}; diff --git a/src/components/form/rhf/FormField.tsx b/src/components/form/rhf/FormField.tsx new file mode 100644 index 0000000..89c1245 --- /dev/null +++ b/src/components/form/rhf/FormField.tsx @@ -0,0 +1,98 @@ +import { cloneElement, useId } from 'react'; +import type { CSSProperties, ReactElement, ReactNode } from 'react'; +import { Controller } from 'react-hook-form'; +import type { Control, ControllerProps, FieldValues, Path } from 'react-hook-form'; +import { Form } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import { normalizeAntdOnChange, type ValueProp } from './normalizeAntdOnChange'; +import { toDotted, type FieldName } from './toDotted'; + +interface FormFieldTransform { + input?: (value: unknown) => unknown; + output?: (eventValue: unknown) => unknown; +} + +export interface FormFieldProps { + name: FieldName; + control?: Control; + label?: ReactNode; + tooltip?: ReactNode; + extra?: ReactNode; + valueProp?: ValueProp; + transform?: FormFieldTransform; + onAfterChange?: (value: unknown) => void; + rules?: ControllerProps['rules']; + required?: boolean; + noStyle?: boolean; + className?: string; + style?: CSSProperties; + children: ReactElement; +} + +export function FormField({ + name, + control, + label, + tooltip, + extra, + valueProp = 'value', + transform, + onAfterChange, + rules, + required, + noStyle, + className, + style, + children, +}: FormFieldProps) { + const { t } = useTranslation(); + const dottedName = toDotted(name) as Path; + const generatedId = useId(); + const explicitId = (children as ReactElement<{ id?: string }>).props.id; + const fieldId = explicitId ?? generatedId; + + return ( + { + const displayValue = transform?.input ? transform.input(field.value) : field.value; + const help = fieldState.error?.message + ? t(fieldState.error.message, { defaultValue: fieldState.error.message }) + : undefined; + const childProps: Record = { + id: fieldId, + [valueProp]: displayValue, + onChange: (...args: unknown[]) => { + const raw = normalizeAntdOnChange(args, valueProp); + const next = transform?.output ? transform.output(raw) : raw; + field.onChange(next); + onAfterChange?.(next); + }, + onBlur: field.onBlur, + ref: field.ref, + }; + return ( + + {cloneElement(children as ReactElement>, childProps)} + + ); + }} + /> + ); +} + +export default FormField; diff --git a/src/components/form/rhf/index.ts b/src/components/form/rhf/index.ts new file mode 100644 index 0000000..75151bc --- /dev/null +++ b/src/components/form/rhf/index.ts @@ -0,0 +1,5 @@ +export { FormField, type FormFieldProps } from './FormField'; +export { useZodForm } from './useZodForm'; +export { rhfZodValidate } from './rhfZodValidate'; +export { normalizeAntdOnChange, type ValueProp } from './normalizeAntdOnChange'; +export { toDotted, type FieldName } from './toDotted'; diff --git a/src/components/form/rhf/normalizeAntdOnChange.ts b/src/components/form/rhf/normalizeAntdOnChange.ts new file mode 100644 index 0000000..617fcb1 --- /dev/null +++ b/src/components/form/rhf/normalizeAntdOnChange.ts @@ -0,0 +1,10 @@ +export type ValueProp = 'value' | 'checked'; + +export function normalizeAntdOnChange(args: unknown[], valueProp: ValueProp): unknown { + const first = args[0]; + if (first !== null && typeof first === 'object' && 'target' in first) { + const target = (first as { target: { value?: unknown; checked?: unknown } }).target; + return valueProp === 'checked' ? target.checked : target.value; + } + return first; +} diff --git a/src/components/form/rhf/rhfZodValidate.ts b/src/components/form/rhf/rhfZodValidate.ts new file mode 100644 index 0000000..add87f9 --- /dev/null +++ b/src/components/form/rhf/rhfZodValidate.ts @@ -0,0 +1,9 @@ +import type { z } from 'zod'; + +export function rhfZodValidate(schema: S) { + return (value: unknown): string | true => { + const result = schema.safeParse(value); + if (result.success) return true; + return result.error.issues[0]?.message ?? 'validation.invalid'; + }; +} diff --git a/src/components/form/rhf/toDotted.ts b/src/components/form/rhf/toDotted.ts new file mode 100644 index 0000000..40e7e83 --- /dev/null +++ b/src/components/form/rhf/toDotted.ts @@ -0,0 +1,5 @@ +export type FieldName = string | ReadonlyArray; + +export function toDotted(name: FieldName): string { + return typeof name === 'string' ? name : name.join('.'); +} diff --git a/src/components/form/rhf/useZodForm.ts b/src/components/form/rhf/useZodForm.ts new file mode 100644 index 0000000..62546c7 --- /dev/null +++ b/src/components/form/rhf/useZodForm.ts @@ -0,0 +1,20 @@ +import { useForm } from 'react-hook-form'; +import type { FieldValues, Resolver, UseFormProps, UseFormReturn } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import type { z } from 'zod'; + +export function useZodForm( + schema: z.ZodType, + options?: Omit, 'resolver'>, +): UseFormReturn { + const resolver = zodResolver( + schema as z.ZodType, + ) as Resolver; + return useForm({ + mode: 'onSubmit', + reValidateMode: 'onChange', + shouldUnregister: false, + ...options, + resolver, + }); +} diff --git a/src/components/viz/Sparkline.css b/src/components/viz/Sparkline.css new file mode 100644 index 0000000..073c52e --- /dev/null +++ b/src/components/viz/Sparkline.css @@ -0,0 +1,113 @@ +.sparkline-plot { + display: block; + width: 100%; +} + +.sparkline-plot .uplot, +.sparkline-plot .u-wrap { + width: 100%; +} + +.sparkline-plot .u-cursor-x { + border-right: 1px dashed var(--ant-color-border); +} + +.sparkline-container { + position: relative; + width: 100%; +} + +.sparkline-tooltip { + position: absolute; + top: 0; + left: 0; + pointer-events: none; + white-space: nowrap; + z-index: 2; + padding: 6px 10px; + border: 1px solid var(--ant-color-border-secondary); + border-radius: 6px; + background: var(--ant-color-bg-elevated); + box-shadow: 0 4px 14px rgba(0, 0, 0, 0.12); + color: var(--ant-color-text); + font-size: 12px; +} + +.sparkline-tooltip .spk-tt-label { + margin-bottom: 4px; + color: var(--ant-color-text-tertiary); + font-size: 11px; +} + +.sparkline-tooltip .spk-tt-row { + display: flex; + align-items: center; + gap: 6px; + font-weight: 500; + line-height: 16px; +} + +.sparkline-tooltip .spk-tt-dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex: none; +} + +.sparkline-tooltip .spk-tt-name { + color: var(--ant-color-text-tertiary); +} + +.sparkline-tooltip .spk-tt-val { + margin-left: auto; +} + +.sparkline-extrema { + position: absolute; + top: 2px; + right: 8px; + display: inline-flex; + align-items: center; + gap: 12px; + padding: 2px 8px; + background: color-mix(in srgb, var(--ant-color-bg-elevated) 88%, transparent); + border: 1px solid var(--ant-color-border-secondary); + border-radius: 999px; + font-size: 11px; + font-weight: 600; + line-height: 16px; + pointer-events: none; + z-index: 1; +} + +.sparkline-extrema .extrema-item { + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; +} + +.sparkline-legend { + position: absolute; + top: 2px; + right: 8px; + display: inline-flex; + align-items: center; + gap: 12px; + padding: 2px 8px; + background: color-mix(in srgb, var(--ant-color-bg-elevated) 88%, transparent); + border: 1px solid var(--ant-color-border-secondary); + border-radius: 999px; + font-size: 11px; + font-weight: 600; + line-height: 16px; + pointer-events: none; + z-index: 1; +} + +.sparkline-legend .extrema-item { + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; +} diff --git a/src/components/viz/Sparkline.stories.tsx b/src/components/viz/Sparkline.stories.tsx new file mode 100644 index 0000000..59e2309 --- /dev/null +++ b/src/components/viz/Sparkline.stories.tsx @@ -0,0 +1,64 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import Sparkline from './Sparkline'; + +const wave = Array.from( + { length: 48 }, + (_, i) => 45 + Math.round(28 * Math.sin(i / 4) + (i % 5) * 3), +); +const inverse = wave.map((v) => Math.max(0, 100 - v)); + +const meta = { + title: 'Viz/Sparkline', + component: Sparkline, + tags: ['autodocs'], + parameters: { + layout: 'padded', + docs: { + description: { + component: + 'Compact canvas line chart (uPlot) for CPU, memory, and traffic trends. Supports up to three series, optional axes/grid, a hover tooltip, min/max markers, reference lines, and light/dark theming.', + }, + }, + }, + argTypes: { + data: { description: 'Primary series values, oldest to newest.' }, + data2: { description: 'Optional second series (e.g. download vs upload).' }, + data3: { description: 'Optional third series.' }, + height: { description: 'Chart height in pixels.' }, + name1: { description: 'Legend/tooltip label for the primary series.' }, + name2: { description: 'Legend/tooltip label for the second series.' }, + showAxes: { description: 'Render x/y axes and tick labels.' }, + showGrid: { description: 'Draw horizontal grid lines.' }, + showTooltip: { description: 'Show a value tooltip on hover.' }, + extrema: { description: 'Highlight the min and max points (single-series only).' }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { data: wave, height: 80 }, +}; + +export const AxesAndGrid: Story = { + args: { data: wave, height: 140, showAxes: true, showGrid: true, name1: 'CPU' }, +}; + +export const Extrema: Story = { + args: { data: wave, height: 140, name1: 'CPU', extrema: { show: true } }, +}; + +export const MultiSeriesTooltip: Story = { + args: { + data: wave, + data2: inverse, + name1: 'Upload', + name2: 'Download', + height: 140, + showTooltip: true, + showAxes: true, + }, +}; diff --git a/src/components/viz/Sparkline.tsx b/src/components/viz/Sparkline.tsx new file mode 100644 index 0000000..5f59414 --- /dev/null +++ b/src/components/viz/Sparkline.tsx @@ -0,0 +1,611 @@ +import { useEffect, useMemo, useRef } from 'react'; +import uPlot from 'uplot'; +import 'uplot/dist/uPlot.min.css'; +import './Sparkline.css'; + +export interface SparklineReferenceLine { + y: number; + label?: string; + color?: string; + dash?: string; +} + +export interface SparklineExtrema { + show?: boolean; + formatter?: (v: number) => string; + minColor?: string; + maxColor?: string; +} + +const DEFAULT_STROKE = '#008771'; +const DEFAULT_STROKE2 = '#722ed1'; +const DEFAULT_STROKE3 = '#a0d911'; +const DEFAULT_MIN_COLOR = '#52c41a'; +const DEFAULT_MAX_COLOR = '#fa541c'; +const GRID_COLOR = 'rgba(128, 128, 140, 0.35)'; +const AXIS_FONT = '10px system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'; +const LABEL_FONT = 'system-ui, -apple-system, "Segoe UI", Roboto, sans-serif'; + +interface SparklineProps { + data: number[]; + data2?: number[]; + data3?: number[]; + stroke2?: string; + stroke3?: string; + name1?: string; + name2?: string; + name3?: string; + labels?: (string | number)[]; + height?: number; + stroke?: string; + strokeWidth?: number; + maxPoints?: number; + showGrid?: boolean; + fillOpacity?: number; + showMarker?: boolean; + markerRadius?: number; + showAxes?: boolean; + yTickStep?: number; + tickCountX?: number; + showTooltip?: boolean; + showLegend?: boolean; + valueMin?: number; + valueMax?: number | null; + yFormatter?: (v: number) => string; + tooltipFormatter?: ((v: number) => string) | null; + tooltipLabelFormatter?: ((label: string) => string) | null; + referenceLines?: SparklineReferenceLine[]; + extrema?: SparklineExtrema; +} + +interface ChartPoint { + index: number; + value: number; + value2: number; + value3: number; + label: string; +} + +interface ExtremaResult { + min: ChartPoint; + max: ChartPoint; + minIdx: number; + maxIdx: number; +} + +interface SparklineView { + points: ChartPoint[]; + yDomain: [number, number]; + yTicks: number[] | undefined; + xTickIndexes: number[] | undefined; + extremaPoints: ExtremaResult | null; +} + +function hexToRgba(color: string, alpha: number): string { + const trimmed = color.trim(); + const fn = trimmed.match(/^rgba?\(([^)]+)\)$/i); + if (fn) { + const parts = fn[1] + .split(/[,/]\s*|\s+/) + .filter(Boolean) + .map(Number); + if (parts.length >= 3 && parts.slice(0, 3).every((n) => Number.isFinite(n))) { + const baseAlpha = parts.length > 3 && Number.isFinite(parts[3]) ? parts[3] : 1; + return `rgba(${parts[0]}, ${parts[1]}, ${parts[2]}, ${baseAlpha * alpha})`; + } + return trimmed; + } + let h = trimmed; + if (h.startsWith('#')) h = h.slice(1); + if (h.length === 3) + h = h + .split('') + .map((c) => c + c) + .join(''); + if (h.length !== 6) return trimmed; + const int = Number.parseInt(h, 16); + if (Number.isNaN(int)) return trimmed; + const r = (int >> 16) & 255; + const g = (int >> 8) & 255; + const b = int & 255; + return `rgba(${r}, ${g}, ${b}, ${alpha})`; +} + +function cssVar(el: HTMLElement, name: string, fallback: string): string { + const v = getComputedStyle(el).getPropertyValue(name).trim(); + return v || fallback; +} + +function parseDash(dash: string, dpr: number): number[] { + return dash + .trim() + .split(/\s+/) + .map((n) => (Number(n) || 0) * dpr); +} + +function dprOf(u: uPlot): number { + return u.width > 0 ? u.ctx.canvas.width / u.width : uPlot.pxRatio || 1; +} + +export default function Sparkline(props: SparklineProps) { + const { + data, + data2 = [], + data3 = [], + stroke = DEFAULT_STROKE, + stroke2 = DEFAULT_STROKE2, + stroke3 = DEFAULT_STROKE3, + name1, + name2, + name3, + labels = [], + height = 80, + strokeWidth = 2, + maxPoints = 120, + showGrid = true, + fillOpacity = 0.22, + showMarker = true, + markerRadius = 3, + showAxes = false, + yTickStep = 25, + tickCountX = 4, + showTooltip = false, + showLegend = true, + valueMin = 0, + valueMax = 100, + yFormatter = (v: number) => `${Math.round(v)}%`, + tooltipFormatter = null, + tooltipLabelFormatter = null, + referenceLines, + extrema, + } = props; + + const hasSeries2 = data2.length > 0; + const hasSeries3 = data3.length > 0; + const multiSeries = hasSeries2 || hasSeries3; + + const points = useMemo(() => { + const n = Math.min(data.length, maxPoints); + if (n === 0) return []; + const sliceStart = data.length - n; + const labelStart = Math.max(0, labels.length - n); + const slice2Start = data2.length - n; + const slice3Start = data3.length - n; + return data.slice(sliceStart).map((value, i) => ({ + index: i, + value: Number(value) || 0, + value2: data2.length ? Number(data2[slice2Start + i]) || 0 : 0, + value3: data3.length ? Number(data3[slice3Start + i]) || 0 : 0, + label: String(labels[labelStart + i] ?? i + 1), + })); + }, [data, data2, data3, labels, maxPoints]); + + const yDomain = useMemo<[number, number]>(() => { + if (valueMax != null) return [valueMin, valueMax]; + let max = valueMin; + for (const p of points) { + if (Number.isFinite(p.value) && p.value > max) max = p.value; + if (hasSeries2 && Number.isFinite(p.value2) && p.value2 > max) max = p.value2; + if (hasSeries3 && Number.isFinite(p.value3) && p.value3 > max) max = p.value3; + } + if (max <= valueMin) max = valueMin + 1; + return [valueMin, max * 1.1]; + }, [points, valueMin, valueMax, hasSeries2, hasSeries3]); + + const yTicks = useMemo(() => { + if (!showAxes) return undefined; + const [min, max] = yDomain; + if (valueMax === 100 && valueMin === 0 && yTickStep > 0) { + const out: number[] = []; + for (let v = min; v <= max; v += yTickStep) out.push(v); + return out; + } + const n = 5; + return Array.from({ length: n }, (_, i) => min + ((max - min) * i) / (n - 1)); + }, [showAxes, yDomain, valueMin, valueMax, yTickStep]); + + const xTickIndexes = useMemo(() => { + if (!showAxes || points.length === 0) return undefined; + const m = Math.max(2, tickCountX); + return Array.from({ length: m }, (_, i) => Math.round((i * (points.length - 1)) / (m - 1))); + }, [showAxes, tickCountX, points.length]); + + const extremaPoints = useMemo(() => { + if (!extrema?.show || multiSeries || points.length < 2) return null; + let minIdx = 0; + let maxIdx = 0; + for (let i = 1; i < points.length; i++) { + if (points[i].value < points[minIdx].value) minIdx = i; + if (points[i].value > points[maxIdx].value) maxIdx = i; + } + if (minIdx === maxIdx) return null; + return { min: points[minIdx], max: points[maxIdx], minIdx, maxIdx }; + }, [points, extrema?.show, multiSeries]); + + const legendItems = useMemo( + () => + [ + { name: name1, color: stroke }, + { name: name2, color: stroke2 }, + { name: name3, color: stroke3 }, + ].filter((s, i) => s.name && (i === 0 ? multiSeries : i === 1 ? hasSeries2 : hasSeries3)), + [name1, name2, name3, stroke, stroke2, stroke3, multiSeries, hasSeries2, hasSeries3], + ); + + const fmtExtrema = extrema?.formatter ?? yFormatter; + const minColor = extrema?.minColor ?? DEFAULT_MIN_COLOR; + const maxColor = extrema?.maxColor ?? DEFAULT_MAX_COLOR; + + const ariaSummary = useMemo(() => { + if (points.length === 0) return name1 ?? ''; + const last = points[points.length - 1]; + const parts: string[] = []; + parts.push(name1 ? `${name1}: ${yFormatter(last.value)}` : yFormatter(last.value)); + if (hasSeries2 && name2) parts.push(`${name2}: ${yFormatter(last.value2)}`); + if (hasSeries3 && name3) parts.push(`${name3}: ${yFormatter(last.value3)}`); + return parts.join(', '); + }, [points, name1, name2, name3, hasSeries2, hasSeries3, yFormatter]); + + const cfg = { + stroke, + stroke2, + stroke3, + strokeWidth, + fillOpacity, + markerRadius, + showGrid, + showMarker, + showAxes, + showTooltip, + height, + name1, + name2, + name3, + yFormatter, + tooltipFormatter, + tooltipLabelFormatter, + referenceLines, + extrema, + }; + const cfgRef = useRef(cfg); + const viewRef = useRef({ points, yDomain, yTicks, xTickIndexes, extremaPoints }); + useEffect(() => { + cfgRef.current = cfg; + viewRef.current = { points, yDomain, yTicks, xTickIndexes, extremaPoints }; + }); + + const containerRef = useRef(null); + const plotRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (!container) return; + + let tooltipEl: HTMLDivElement | null = null; + + const seriesColor = (idx: number): string => { + const p = cfgRef.current; + if (idx <= 1) return p.stroke ?? DEFAULT_STROKE; + if (idx === 2) return p.stroke2 ?? DEFAULT_STROKE2; + return p.stroke3 ?? DEFAULT_STROKE3; + }; + + const gridTicks = (): number[] => { + const yt = viewRef.current.yTicks; + if (yt && yt.length) return yt; + const [mn, mx] = viewRef.current.yDomain; + const n = 4; + return Array.from({ length: n + 1 }, (_, i) => mn + ((mx - mn) * i) / n); + }; + + const buildData = (): uPlot.AlignedData => { + const v = viewRef.current; + const xs = v.points.map((_, i) => i); + const series: number[][] = [v.points.map((p) => p.value)]; + if (hasSeries2) series.push(v.points.map((p) => p.value2)); + if (hasSeries3) series.push(v.points.map((p) => p.value3)); + return [xs, ...series]; + }; + + const makeSeries = (): uPlot.Series => ({ + stroke: (_u, sidx) => seriesColor(sidx), + width: cfgRef.current.strokeWidth ?? 2, + fill: (u, sidx) => { + const { ctx, bbox } = u; + const color = seriesColor(sidx); + const grad = ctx.createLinearGradient(0, bbox.top, 0, bbox.top + bbox.height); + grad.addColorStop(0, hexToRgba(color, cfgRef.current.fillOpacity ?? 0.22)); + grad.addColorStop(1, hexToRgba(color, 0)); + return grad; + }, + paths: uPlot.paths.spline?.(), + points: { show: false }, + spanGaps: true, + }); + + const series: uPlot.Series[] = [{}, makeSeries()]; + if (hasSeries2) series.push(makeSeries()); + if (hasSeries3) series.push(makeSeries()); + + const axisStroke = (u: uPlot) => cssVar(u.root, '--ant-color-text-tertiary', '#8c8c8c'); + + const axes: uPlot.Axis[] = [ + { + show: showAxes, + stroke: axisStroke, + grid: { show: false }, + ticks: { show: false }, + font: AXIS_FONT, + gap: 6, + size: 28, + splits: () => viewRef.current.xTickIndexes ?? [], + values: (_u, splits) => splits.map((i) => viewRef.current.points[i]?.label ?? ''), + }, + { + show: showAxes, + scale: 'y', + side: 3, + stroke: axisStroke, + grid: { show: false }, + ticks: { show: false }, + font: AXIS_FONT, + gap: 4, + size: 56, + splits: () => viewRef.current.yTicks ?? [], + values: (_u, splits) => + splits.map((v) => (cfgRef.current.yFormatter ? cfgRef.current.yFormatter(v) : String(v))), + }, + ]; + + const drawGrid = (u: uPlot) => { + if (cfgRef.current.showGrid === false) return; + const { ctx, bbox } = u; + const dpr = dprOf(u); + ctx.save(); + ctx.strokeStyle = GRID_COLOR; + ctx.lineWidth = dpr; + ctx.setLineDash([3 * dpr, 4 * dpr]); + ctx.beginPath(); + for (const ty of gridTicks()) { + const py = Math.round(u.valToPos(ty, 'y', true)) + 0.5; + ctx.moveTo(bbox.left, py); + ctx.lineTo(bbox.left + bbox.width, py); + } + ctx.stroke(); + ctx.restore(); + }; + + const drawOverlay = (u: uPlot) => { + const p = cfgRef.current; + const v = viewRef.current; + const { ctx, bbox } = u; + const dpr = dprOf(u); + const right = bbox.left + bbox.width; + + if (p.referenceLines?.length) { + for (const rl of p.referenceLines) { + const color = rl.color || p.stroke || DEFAULT_STROKE; + const py = Math.round(u.valToPos(rl.y, 'y', true)) + 0.5; + ctx.save(); + ctx.strokeStyle = color; + ctx.lineWidth = 1.4 * dpr; + ctx.setLineDash(parseDash(rl.dash ?? '5 4', dpr)); + ctx.beginPath(); + ctx.moveTo(bbox.left, py); + ctx.lineTo(right, py); + ctx.stroke(); + ctx.restore(); + if (rl.label) { + ctx.save(); + ctx.fillStyle = color; + ctx.font = `600 ${10 * dpr}px ${LABEL_FONT}`; + ctx.textAlign = 'right'; + ctx.textBaseline = 'bottom'; + ctx.fillText(rl.label, right - 4 * dpr, py - 3 * dpr); + ctx.restore(); + } + } + } + + const ex = v.extremaPoints; + if (p.extrema?.show && ex) { + const ringColor = cssVar(u.root, '--ant-color-bg-elevated', '#ffffff'); + const dot = (value: number, idx: number, color: string) => { + const px = u.valToPos(idx, 'x', true); + const py = u.valToPos(value, 'y', true); + ctx.save(); + ctx.beginPath(); + ctx.arc(px, py, 4.5 * dpr, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.fill(); + ctx.lineWidth = 2 * dpr; + ctx.strokeStyle = ringColor; + ctx.stroke(); + ctx.restore(); + }; + dot(ex.max.value, ex.maxIdx, p.extrema.maxColor ?? DEFAULT_MAX_COLOR); + dot(ex.min.value, ex.minIdx, p.extrema.minColor ?? DEFAULT_MIN_COLOR); + } + }; + + const updateTooltip = (u: uPlot) => { + if (!tooltipEl) return; + const idx = u.cursor.idx; + const v = viewRef.current; + const p = cfgRef.current; + if (idx == null || idx < 0 || idx >= v.points.length) { + tooltipEl.style.display = 'none'; + return; + } + const pt = v.points[idx]; + const fmt = p.tooltipFormatter ?? p.yFormatter ?? ((x: number) => String(x)); + const label = p.tooltipLabelFormatter + ? p.tooltipLabelFormatter(String(pt.label)) + : String(pt.label); + const multi = hasSeries2 || hasSeries3; + + tooltipEl.textContent = ''; + const labelDiv = document.createElement('div'); + labelDiv.className = 'spk-tt-label'; + labelDiv.textContent = label; + tooltipEl.appendChild(labelDiv); + + const rows = [ + { name: p.name1, color: p.stroke ?? DEFAULT_STROKE, val: pt.value, on: true }, + { name: p.name2, color: p.stroke2 ?? DEFAULT_STROKE2, val: pt.value2, on: hasSeries2 }, + { name: p.name3, color: p.stroke3 ?? DEFAULT_STROKE3, val: pt.value3, on: hasSeries3 }, + ]; + for (const r of rows) { + if (!r.on) continue; + const row = document.createElement('div'); + row.className = 'spk-tt-row'; + if (multi) { + const marker = document.createElement('span'); + marker.className = 'spk-tt-dot'; + marker.style.background = r.color; + row.appendChild(marker); + const nm = document.createElement('span'); + nm.className = 'spk-tt-name'; + nm.textContent = r.name ?? ''; + row.appendChild(nm); + } + const val = document.createElement('span'); + val.className = 'spk-tt-val'; + val.textContent = fmt(r.val); + row.appendChild(val); + tooltipEl.appendChild(row); + } + + tooltipEl.style.display = ''; + const overW = u.over.clientWidth; + const overH = u.over.clientHeight; + const cx = u.cursor.left ?? 0; + const cy = u.cursor.top ?? 0; + const w = tooltipEl.offsetWidth; + const h = tooltipEl.offsetHeight; + let x = cx + 12; + if (x + w + 8 > overW) x = cx - w - 12; + if (x < 0) x = 4; + let y = cy - h - 12; + if (y < 0) y = Math.min(cy + 12, overH - h - 4); + tooltipEl.style.transform = `translate(${Math.round(x)}px, ${Math.round(y)}px)`; + }; + + const opts: uPlot.Options = { + width: container.clientWidth || 600, + height, + padding: [8, 8, showAxes ? 0 : 2, showAxes ? 0 : 2], + legend: { show: false }, + cursor: { + show: showTooltip, + x: showTooltip, + y: false, + drag: { x: false, y: false, setScale: false }, + points: { + show: showMarker, + size: () => (cfgRef.current.markerRadius ?? 3) * 2, + width: 0, + stroke: (_u, sidx) => seriesColor(sidx), + fill: (_u, sidx) => seriesColor(sidx), + }, + }, + scales: { + x: { + time: false, + range: (_u, dmin, dmax) => (dmin === dmax ? [dmin - 0.5, dmax + 0.5] : [dmin, dmax]), + }, + y: { + range: () => { + const [mn, mx] = viewRef.current.yDomain; + return [mn, mx]; + }, + }, + }, + series, + axes, + hooks: { + init: [ + (u) => { + if (!cfgRef.current.showTooltip) return; + tooltipEl = document.createElement('div'); + tooltipEl.className = 'sparkline-tooltip'; + tooltipEl.style.display = 'none'; + u.over.appendChild(tooltipEl); + }, + ], + drawClear: [drawGrid], + draw: [drawOverlay], + setCursor: [updateTooltip], + }, + }; + + const u = new uPlot(opts, buildData(), container); + plotRef.current = u; + + const ro = new ResizeObserver(() => { + const w = container.clientWidth; + if (w > 0) u.setSize({ width: w, height }); + }); + ro.observe(container); + + return () => { + ro.disconnect(); + u.destroy(); + plotRef.current = null; + tooltipEl = null; + }; + }, [hasSeries2, hasSeries3, showAxes, showTooltip, showMarker, height]); + + useEffect(() => { + plotRef.current?.setData( + (() => { + const xs = points.map((_, i) => i); + const s: number[][] = [points.map((p) => p.value)]; + if (hasSeries2) s.push(points.map((p) => p.value2)); + if (hasSeries3) s.push(points.map((p) => p.value3)); + return [xs, ...s] as uPlot.AlignedData; + })(), + ); + }, [points, hasSeries2, hasSeries3, valueMin, valueMax]); + + useEffect(() => { + const redraw = () => plotRef.current?.redraw(false); + const moBody = new MutationObserver(redraw); + moBody.observe(document.body, { attributes: true, attributeFilter: ['class'] }); + const moRoot = new MutationObserver(redraw); + moRoot.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] }); + return () => { + moBody.disconnect(); + moRoot.disconnect(); + }; + }, []); + + return ( +
+ {extremaPoints && ( + + )} + {showLegend && legendItems.length > 0 && ( + + )} +
+
+ ); +} diff --git a/src/components/viz/index.ts b/src/components/viz/index.ts new file mode 100644 index 0000000..1b40293 --- /dev/null +++ b/src/components/viz/index.ts @@ -0,0 +1 @@ +export { default as Sparkline } from './Sparkline'; diff --git a/src/entries/login.tsx b/src/entries/login.tsx new file mode 100644 index 0000000..6ba7a9c --- /dev/null +++ b/src/entries/login.tsx @@ -0,0 +1,37 @@ +import { createRoot } from 'react-dom/client'; +import { message } from 'antd'; +import 'antd/dist/reset.css'; +import '@/styles/global.css'; + +import { setupHttp } from '@/api/http-init'; +import { applyDocumentTitle } from '@/utils'; +import { readyI18n } from '@/i18n/react'; +import { ThemeProvider } from '@/hooks/useTheme'; +import { QueryProvider } from '@/api/QueryProvider'; +import LoginPage from '@/pages/login/LoginPage'; +import { enableMockApi } from '@/mocks'; + +async function bootstrap() { + await enableMockApi(); + setupHttp(); + applyDocumentTitle(); + + const messageContainer = document.getElementById('message'); + if (messageContainer) { + message.config({ getContainer: () => messageContainer }); + } + + await readyI18n(); + const root = document.getElementById('app'); + if (root) { + createRoot(root).render( + + + + + , + ); + } +} + +void bootstrap(); diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..199239a --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1,5 @@ +/// + +interface Window { + ADMIN_UI_BASE_PATH?: string; +} diff --git a/src/hooks/usePageTitle.ts b/src/hooks/usePageTitle.ts new file mode 100644 index 0000000..5d79497 --- /dev/null +++ b/src/hooks/usePageTitle.ts @@ -0,0 +1,22 @@ +import { useEffect } from 'react'; +import { useLocation } from 'react-router'; + +const STATIC_TITLES: Record = { + '/': '总览', + '/accounts': '账号管理', + '/servers': '区服管理', + '/players': '玩家管理', + '/cdks': 'CDK管理', + '/announcements': '公告管理', + '/orders': '订单管理', +}; + +export function usePageTitle() { + const { pathname } = useLocation(); + + useEffect(() => { + const title = STATIC_TITLES[pathname] || '游戏运营后台'; + const host = window.location.hostname; + document.title = host ? `${host} - ${title}` : title; + }, [pathname]); +} diff --git a/src/hooks/useTheme.tsx b/src/hooks/useTheme.tsx new file mode 100644 index 0000000..5491386 --- /dev/null +++ b/src/hooks/useTheme.tsx @@ -0,0 +1,188 @@ +import { createContext, useCallback, useContext, useLayoutEffect, useMemo, useState } from 'react'; +import type { ReactNode } from 'react'; +import { theme as antdTheme } from 'antd'; +import type { ThemeConfig } from 'antd'; + +const STORAGE_DARK = 'dark-mode'; +const STORAGE_ULTRA = 'isUltraDarkThemeEnabled'; + +function readBool(key: string, fallback: boolean): boolean { + const raw = localStorage.getItem(key); + if (raw === null) return fallback; + return raw === 'true'; +} + +function applyDom(isDark: boolean, isUltra: boolean) { + document.body.classList.remove('dark', 'light'); + document.body.classList.add(isDark ? 'dark' : 'light'); + if (isUltra) { + document.documentElement.setAttribute('data-theme', 'ultra-dark'); + } else { + document.documentElement.removeAttribute('data-theme'); + } + const msg = document.getElementById('message'); + if (msg) { + msg.classList.remove('dark', 'light'); + msg.classList.add(isDark ? 'dark' : 'light'); + } +} + +// module load so the document is in the right theme before React mounts. +const initialDark = readBool(STORAGE_DARK, true); +const initialUltra = readBool(STORAGE_ULTRA, false); +applyDom(initialDark, initialUltra); + +const DARK_TOKENS = { + colorBgBase: '#1a1b1f', + colorBgLayout: '#1a1b1f', + colorBgContainer: '#23252b', + colorBgElevated: '#2d2f37', +}; +const ULTRA_DARK_TOKENS = { + colorBgBase: '#000', + colorBgLayout: '#000', + colorBgContainer: '#101013', + colorBgElevated: '#1a1a1e', +}; +const DARK_LAYOUT_TOKENS = { + bodyBg: '#1a1b1f', + headerBg: '#15161a', + headerColor: '#ffffff', + footerBg: '#1a1b1f', + siderBg: '#15161a', + triggerBg: '#23252b', + triggerColor: '#ffffff', +}; +const ULTRA_DARK_LAYOUT_TOKENS = { + bodyBg: '#000', + headerBg: '#050507', + headerColor: '#ffffff', + footerBg: '#000', + siderBg: '#050507', + triggerBg: '#1a1a1e', + triggerColor: '#ffffff', +}; +const DARK_MENU_TOKENS = { + darkItemBg: '#15161a', + darkSubMenuItemBg: '#1a1b1f', + darkPopupBg: '#23252b', +}; +const ULTRA_DARK_MENU_TOKENS = { + darkItemBg: '#050507', + darkSubMenuItemBg: '#000', + darkPopupBg: '#101013', +}; +const DARK_CARD_TOKENS = { + colorBorderSecondary: 'rgba(255, 255, 255, 0.06)', +}; +const ULTRA_DARK_CARD_TOKENS = { + colorBorderSecondary: 'rgba(255, 255, 255, 0.04)', +}; +const STATISTIC_TOKENS = { + contentFontSize: 17, + titleFontSize: 11, +}; +const LIGHT_CONTRAST_TOKENS = { + colorTextDescription: 'rgba(0, 0, 0, 0.58)', + colorTextTertiary: 'rgba(0, 0, 0, 0.58)', + colorTextPlaceholder: '#767676', + colorError: '#cf1322', + colorErrorText: '#cf1322', + colorSuccessText: '#237804', +}; +const LIGHT_BUTTON_TOKENS = { + colorPrimary: '#0958d9', + colorPrimaryHover: '#2468e5', + colorPrimaryActive: '#073ea8', +}; + +// hashed:false drops the `:where(.css-)` wrapper antd puts around every +// rule. It costs nothing in specificity — `:where()` contributes zero, so the +// panel's own `.ant-*` overrides still win — and it removes roughly 5,700 +// wrappers, 16% of the generated stylesheet, from what the browser has to parse. +// +// cssVar.key pins the CSS-variable scope. Every panel page mounts its own +// ConfigProvider (there is no root one), and without a fixed key each mints a +// fresh useId-derived scope, so navigating re-serialises and re-injects the whole +// token block under a new class instead of reusing the one already in the head. +const SHARED_STYLE_CONFIG = { + hashed: false, + cssVar: { key: 'game-admin' }, +} as const; + +export function buildAntdThemeConfig(isDark: boolean, isUltra: boolean): ThemeConfig { + if (!isDark) { + return { + ...SHARED_STYLE_CONFIG, + algorithm: antdTheme.defaultAlgorithm, + token: LIGHT_CONTRAST_TOKENS, + components: { + Statistic: STATISTIC_TOKENS, + Button: LIGHT_BUTTON_TOKENS, + }, + }; + } + return { + ...SHARED_STYLE_CONFIG, + algorithm: antdTheme.darkAlgorithm, + token: isUltra ? ULTRA_DARK_TOKENS : DARK_TOKENS, + components: { + Layout: isUltra ? ULTRA_DARK_LAYOUT_TOKENS : DARK_LAYOUT_TOKENS, + Menu: isUltra ? ULTRA_DARK_MENU_TOKENS : DARK_MENU_TOKENS, + Card: isUltra ? ULTRA_DARK_CARD_TOKENS : DARK_CARD_TOKENS, + Statistic: STATISTIC_TOKENS, + }, + }; +} + +export function pauseAnimationsUntilLeave(elementId: string): void { + document.documentElement.setAttribute('data-theme-animations', 'off'); + const el = document.getElementById(elementId); + if (!el) return; + const restore = () => { + document.documentElement.removeAttribute('data-theme-animations'); + el.removeEventListener('mouseleave', restore); + el.removeEventListener('touchend', restore); + }; + el.addEventListener('mouseleave', restore); + el.addEventListener('touchend', restore); +} + +interface ThemeContextValue { + isDark: boolean; + isUltra: boolean; + toggleTheme: () => void; + toggleUltra: () => void; + antdThemeConfig: ThemeConfig; +} + +const ThemeContext = createContext(null); + +export function ThemeProvider({ children }: { children: ReactNode }) { + const [isDark, setIsDark] = useState(initialDark); + const [isUltra, setIsUltra] = useState(initialUltra); + + useLayoutEffect(() => { + applyDom(isDark, isUltra); + localStorage.setItem(STORAGE_DARK, String(isDark)); + localStorage.setItem(STORAGE_ULTRA, String(isUltra)); + }, [isDark, isUltra]); + + const toggleTheme = useCallback(() => setIsDark((v) => !v), []); + const toggleUltra = useCallback(() => setIsUltra((v) => !v), []); + + const antdThemeConfig = useMemo(() => buildAntdThemeConfig(isDark, isUltra), [isDark, isUltra]); + + const value = useMemo( + () => ({ isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig }), + [isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig], + ); + + return {children}; +} + +export function useTheme(): ThemeContextValue { + const ctx = useContext(ThemeContext); + if (!ctx) throw new Error('useTheme must be used inside '); + return ctx; +} diff --git a/src/i18n/locales/ar-EG.json b/src/i18n/locales/ar-EG.json new file mode 100644 index 0000000..22823ed --- /dev/null +++ b/src/i18n/locales/ar-EG.json @@ -0,0 +1,18 @@ +{ + "username": "اسم المستخدم", + "password": "الباسورد", + "twoFactorCode": "الكود", + "login": "تسجيل الدخول", + "menu": { + "theme": "الثيم" + }, + "pages": { + "login": { + "hello": "أهلا", + "title": "أهلاً وسهلاً" + }, + "settings": { + "language": "اللغة" + } + } +} diff --git a/src/i18n/locales/en-US.json b/src/i18n/locales/en-US.json new file mode 100644 index 0000000..9becdbe --- /dev/null +++ b/src/i18n/locales/en-US.json @@ -0,0 +1,18 @@ +{ + "username": "Username", + "password": "Password", + "twoFactorCode": "Code", + "login": "Log In", + "menu": { + "theme": "Theme" + }, + "pages": { + "login": { + "hello": "Hello", + "title": "Welcome" + }, + "settings": { + "language": "Language" + } + } +} diff --git a/src/i18n/locales/es-ES.json b/src/i18n/locales/es-ES.json new file mode 100644 index 0000000..53eb0e2 --- /dev/null +++ b/src/i18n/locales/es-ES.json @@ -0,0 +1,18 @@ +{ + "username": "Nombre de Usuario", + "password": "Contraseña", + "twoFactorCode": "Código", + "login": "Acceder", + "menu": { + "theme": "Tema" + }, + "pages": { + "login": { + "hello": "Hola", + "title": "Bienvenido" + }, + "settings": { + "language": "Idioma" + } + } +} diff --git a/src/i18n/locales/fa-IR.json b/src/i18n/locales/fa-IR.json new file mode 100644 index 0000000..93125ce --- /dev/null +++ b/src/i18n/locales/fa-IR.json @@ -0,0 +1,18 @@ +{ + "username": "نام‌کاربری", + "password": "رمزعبور", + "twoFactorCode": "کد", + "login": "ورود", + "menu": { + "theme": "تم" + }, + "pages": { + "login": { + "hello": "سلام", + "title": "خوش‌آمدید" + }, + "settings": { + "language": "زبان" + } + } +} diff --git a/src/i18n/locales/id-ID.json b/src/i18n/locales/id-ID.json new file mode 100644 index 0000000..b26758d --- /dev/null +++ b/src/i18n/locales/id-ID.json @@ -0,0 +1,18 @@ +{ + "username": "Nama Pengguna", + "password": "Kata Sandi", + "twoFactorCode": "Kode", + "login": "Masuk", + "menu": { + "theme": "Tema" + }, + "pages": { + "login": { + "hello": "Halo", + "title": "Selamat Datang" + }, + "settings": { + "language": "Bahasa" + } + } +} diff --git a/src/i18n/locales/ja-JP.json b/src/i18n/locales/ja-JP.json new file mode 100644 index 0000000..48c218c --- /dev/null +++ b/src/i18n/locales/ja-JP.json @@ -0,0 +1,18 @@ +{ + "username": "ユーザー名", + "password": "パスワード", + "twoFactorCode": "コード", + "login": "ログイン", + "menu": { + "theme": "テーマ" + }, + "pages": { + "login": { + "hello": "こんにちは", + "title": "ようこそ" + }, + "settings": { + "language": "言語" + } + } +} diff --git a/src/i18n/locales/pt-BR.json b/src/i18n/locales/pt-BR.json new file mode 100644 index 0000000..6061df1 --- /dev/null +++ b/src/i18n/locales/pt-BR.json @@ -0,0 +1,18 @@ +{ + "username": "Nome de Usuário", + "password": "Senha", + "twoFactorCode": "Código", + "login": "Entrar", + "menu": { + "theme": "Tema" + }, + "pages": { + "login": { + "hello": "Olá", + "title": "Bem-vindo" + }, + "settings": { + "language": "Idioma" + } + } +} diff --git a/src/i18n/locales/ru-RU.json b/src/i18n/locales/ru-RU.json new file mode 100644 index 0000000..ec95a09 --- /dev/null +++ b/src/i18n/locales/ru-RU.json @@ -0,0 +1,18 @@ +{ + "username": "Имя пользователя", + "password": "Пароль", + "twoFactorCode": "Код 2FA", + "login": "Войти", + "menu": { + "theme": "Тема" + }, + "pages": { + "login": { + "hello": "Привет!", + "title": "Добро пожаловать!" + }, + "settings": { + "language": "Язык интерфейса" + } + } +} diff --git a/src/i18n/locales/tr-TR.json b/src/i18n/locales/tr-TR.json new file mode 100644 index 0000000..31d5f3a --- /dev/null +++ b/src/i18n/locales/tr-TR.json @@ -0,0 +1,18 @@ +{ + "username": "Kullanıcı Adı", + "password": "Şifre", + "twoFactorCode": "Kod", + "login": "Giriş Yap", + "menu": { + "theme": "Tema" + }, + "pages": { + "login": { + "hello": "Merhaba", + "title": "Hoş Geldiniz" + }, + "settings": { + "language": "Dil" + } + } +} diff --git a/src/i18n/locales/uk-UA.json b/src/i18n/locales/uk-UA.json new file mode 100644 index 0000000..1dfd5e5 --- /dev/null +++ b/src/i18n/locales/uk-UA.json @@ -0,0 +1,18 @@ +{ + "username": "Ім'я користувача", + "password": "Пароль", + "twoFactorCode": "Код", + "login": "Увійти", + "menu": { + "theme": "Тема" + }, + "pages": { + "login": { + "hello": "Привіт", + "title": "Привітання!" + }, + "settings": { + "language": "Мова" + } + } +} diff --git a/src/i18n/locales/vi-VN.json b/src/i18n/locales/vi-VN.json new file mode 100644 index 0000000..6fde8c6 --- /dev/null +++ b/src/i18n/locales/vi-VN.json @@ -0,0 +1,18 @@ +{ + "username": "Tên người dùng", + "password": "Mật khẩu", + "twoFactorCode": "Mã", + "login": "Đăng nhập", + "menu": { + "theme": "Chủ đề" + }, + "pages": { + "login": { + "hello": "Xin chào", + "title": "Chào mừng" + }, + "settings": { + "language": "Ngôn ngữ" + } + } +} diff --git a/src/i18n/locales/zh-CN.json b/src/i18n/locales/zh-CN.json new file mode 100644 index 0000000..29142d9 --- /dev/null +++ b/src/i18n/locales/zh-CN.json @@ -0,0 +1,18 @@ +{ + "username": "用户名", + "password": "密码", + "twoFactorCode": "代码", + "login": "登录", + "menu": { + "theme": "主题" + }, + "pages": { + "login": { + "hello": "你好", + "title": "欢迎" + }, + "settings": { + "language": "语言" + } + } +} diff --git a/src/i18n/locales/zh-TW.json b/src/i18n/locales/zh-TW.json new file mode 100644 index 0000000..00b0a56 --- /dev/null +++ b/src/i18n/locales/zh-TW.json @@ -0,0 +1,18 @@ +{ + "username": "使用者名稱", + "password": "密碼", + "twoFactorCode": "代碼", + "login": "登入", + "menu": { + "theme": "主題" + }, + "pages": { + "login": { + "hello": "你好", + "title": "歡迎" + }, + "settings": { + "language": "語言" + } + } +} diff --git a/src/i18n/react.ts b/src/i18n/react.ts new file mode 100644 index 0000000..104f399 --- /dev/null +++ b/src/i18n/react.ts @@ -0,0 +1,45 @@ +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import { LanguageManager } from '@/utils'; +import enUS from './locales/en-US.json'; + +const FALLBACK = 'en-US'; + +const lazyModules = import.meta.glob(['./locales/*.json', '!./locales/en-US.json']); + +function moduleKeyFor(code: string): string { + return `./locales/${code}.json`; +} + +let active: string = LanguageManager.getLanguage(); +if ( + active !== FALLBACK && + !Object.prototype.hasOwnProperty.call(lazyModules, moduleKeyFor(active)) +) { + active = FALLBACK; +} + +export async function readyI18n() { + await i18next.use(initReactI18next).init({ + lng: active, + fallbackLng: FALLBACK, + resources: { [FALLBACK]: { translation: enUS } }, + interpolation: { escapeValue: false, prefix: '{', suffix: '}' }, + returnNull: false, + }); + if (active !== FALLBACK) { + const loader = lazyModules[moduleKeyFor(active)] as + | (() => Promise<{ default: Record }>) + | undefined; + if (loader) { + const mod = await loader(); + const messages = (mod.default ?? mod) as Record; + i18next.addResourceBundle(active, 'translation', messages, true, true); + await i18next.changeLanguage(active); + } + } + return i18next; +} + +export { i18next as i18n }; diff --git a/src/layouts/AppSidebar.css b/src/layouts/AppSidebar.css new file mode 100644 index 0000000..ac0b481 --- /dev/null +++ b/src/layouts/AppSidebar.css @@ -0,0 +1,170 @@ +.ant-sidebar { + flex: 0 0 220px; + width: 220px; + position: relative; + z-index: 210; +} + +.ant-sidebar > .ant-layout-sider { + position: sticky; + top: 0; + height: 100vh; + align-self: flex-start; +} + +.sider-nav .ant-menu-item .anticon, +.sider-nav .ant-menu-submenu-title .anticon { + font-size: 16px; +} + +.sider-brand, +.drawer-brand { + font-weight: 600; + font-size: 18px; + letter-spacing: 0.5px; + color: var(--ant-color-text); +} + +.sider-brand { + display: flex; + align-items: center; + justify-content: flex-start; + gap: 8px; + height: 42px; + padding: 0 16px 0 24px; + border-bottom: 1px solid var(--ant-color-border-secondary); + user-select: none; + white-space: nowrap; + overflow: hidden; +} + +.brand-block { + display: inline-flex; + align-items: center; + min-width: 0; + line-height: 1.1; +} + +.drawer-handle { + position: fixed; + top: 12px; + left: 12px; + z-index: 1100; + background: rgba(0, 0, 0, 0.55); + color: #fff; + border: none; + width: 40px; + height: 40px; + border-radius: 50%; + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + font-size: 18px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25); +} + +.drawer-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-bottom: 1px solid var(--ant-color-border-secondary); +} + +.drawer-close { + background: transparent; + border: none; + width: 32px; + height: 32px; + border-radius: 50%; + display: inline-flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 16px; + color: var(--ant-color-text-secondary); +} + +.drawer-close:hover, +.drawer-close:focus-visible { + background: var(--ant-color-fill-tertiary); +} + +.drawer-menu .ant-menu-item { + height: 48px; + line-height: 48px; + margin: 0; + border-radius: 0; +} + +.drawer-menu .ant-menu-item .anticon { + font-size: 16px; +} + +.ant-sidebar > .ant-layout-sider .ant-layout-sider-children { + display: flex; + flex-direction: column; + height: 100%; +} + +.sider-nav { + flex: 1 1 auto; + overflow-y: auto; + overflow-x: hidden; + min-height: 0; +} + +@media (max-width: 768px) { + .drawer-handle { + display: inline-flex; + } + + .ant-sidebar > .ant-layout-sider .ant-layout-sider-children { + display: none; + } + + .ant-sidebar > .ant-layout-sider { + flex: 0 0 0 !important; + max-width: 0 !important; + min-width: 0 !important; + width: 0 !important; + } + + .ant-sidebar { + flex: 0 0 0; + width: 0; + } +} + +body.dark .ant-drawer-content, +body.dark .ant-drawer-body { + background-color: #15161a; +} + +html[data-theme='ultra-dark'] body.dark .ant-drawer-content, +html[data-theme='ultra-dark'] body.dark .ant-drawer-body { + background-color: #050507; +} + +body.dark .ant-drawer-body .drawer-menu, +body.dark .ant-drawer-body .drawer-menu.ant-menu-dark, +body.dark .ant-drawer-body .drawer-menu .ant-menu-item, +body.dark .ant-drawer-body .drawer-menu .ant-menu-sub, +body.dark .ant-drawer-body .drawer-menu .ant-menu-item-group-list { + background-color: transparent; +} + +.sider-nav .ant-menu-item-selected, +.drawer-menu .ant-menu-item-selected { + background-color: color-mix(in srgb, var(--ant-color-primary) 20%, transparent) !important; + color: var(--ant-color-primary) !important; +} + +.sider-nav .ant-menu-item-active:not(.ant-menu-item-selected), +.drawer-menu .ant-menu-item-active:not(.ant-menu-item-selected), +.sider-nav .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover, +.drawer-menu .ant-menu-item:not(.ant-menu-item-selected):not(.ant-menu-item-disabled):hover { + background-color: color-mix(in srgb, var(--ant-color-primary) 10%, transparent) !important; + color: var(--ant-color-primary) !important; +} diff --git a/src/layouts/AppSidebar.tsx b/src/layouts/AppSidebar.tsx new file mode 100644 index 0000000..264a1e9 --- /dev/null +++ b/src/layouts/AppSidebar.tsx @@ -0,0 +1,148 @@ +import { useCallback, useState } from 'react'; +import type { ComponentType } from 'react'; +import { useLocation, useNavigate } from 'react-router'; +import { Drawer, Layout, Menu } from 'antd'; +import type { MenuProps } from 'antd'; +import { + BellOutlined, + CloseOutlined, + CloudServerOutlined, + DashboardOutlined, + IdcardOutlined, + KeyOutlined, + MenuOutlined, + ShoppingCartOutlined, + UserOutlined, +} from '@ant-design/icons'; + +import { useTheme } from '@/hooks/useTheme'; +import './AppSidebar.css'; + +const SIDER_WIDTH = 220; +const SIDEBAR_BRAND = '游戏运营后台'; + +type IconName = + | 'dashboard' + | 'accounts' + | 'servers' + | 'players' + | 'cdks' + | 'announcements' + | 'orders'; + +interface SidebarTab { + key: string; + icon: IconName; + title: string; +} + +const iconByName: Record = { + dashboard: DashboardOutlined, + accounts: IdcardOutlined, + servers: CloudServerOutlined, + players: UserOutlined, + cdks: KeyOutlined, + announcements: BellOutlined, + orders: ShoppingCartOutlined, +}; + +const tabs: SidebarTab[] = [ + { key: '/', icon: 'dashboard', title: '总览' }, + { key: '/accounts', icon: 'accounts', title: '账号管理' }, + { key: '/servers', icon: 'servers', title: '区服管理' }, + { key: '/players', icon: 'players', title: '玩家管理' }, + { key: '/cdks', icon: 'cdks', title: 'CDK管理' }, + { key: '/announcements', icon: 'announcements', title: '公告管理' }, + { key: '/orders', icon: 'orders', title: '订单管理' }, +]; + +const menuItems: MenuProps['items'] = tabs.map((tab) => { + const Icon = iconByName[tab.icon]; + return { key: tab.key, icon: , label: tab.title, title: '' }; +}); + +export default function AppSidebar() { + const { isDark } = useTheme(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const [drawerOpen, setDrawerOpen] = useState(false); + + const currentTheme: 'light' | 'dark' = isDark ? 'dark' : 'light'; + const selectedKey = pathname || '/'; + const openLink = useCallback((key: string) => navigate(key), [navigate]); + + const onMenuClick = useCallback>( + ({ key }) => openLink(String(key)), + [openLink], + ); + + return ( +
+ +
+
+ {SIDEBAR_BRAND} +
+
+ + + + setDrawerOpen(false)} + > +
+
+ {SIDEBAR_BRAND} +
+ +
+ { + onMenuClick(info); + setDrawerOpen(false); + }} + /> + + + {!drawerOpen && ( + + )} +
+ ); +} diff --git a/src/layouts/PanelLayout.tsx b/src/layouts/PanelLayout.tsx new file mode 100644 index 0000000..4e53fdd --- /dev/null +++ b/src/layouts/PanelLayout.tsx @@ -0,0 +1,8 @@ +import { Outlet } from 'react-router'; + +import { usePageTitle } from '@/hooks/usePageTitle'; + +export default function PanelLayout() { + usePageTitle(); + return ; +} diff --git a/src/layouts/WorkspaceTabs.css b/src/layouts/WorkspaceTabs.css new file mode 100644 index 0000000..f413238 --- /dev/null +++ b/src/layouts/WorkspaceTabs.css @@ -0,0 +1,215 @@ +.workspace-tabs { + position: sticky; + top: 0; + z-index: 30; + display: flex; + flex: 0 0 42px; + align-items: flex-end; + min-width: 0; + height: 42px; + padding: 0 12px; + border-bottom: 1px solid var(--ant-color-border-secondary); + background: var(--ant-color-bg-container); + box-shadow: 0 1px 3px rgb(0 0 0 / 5%); +} + +.workspace-tabs__page-title { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +.workspace-tabs__desktop { + display: flex; + align-items: flex-end; + width: 100%; + min-width: 0; + height: 100%; +} + +.workspace-tabs__track { + display: flex; + flex: 1 1 auto; + align-items: flex-end; + min-width: 0; + height: 100%; + overflow-x: auto; + overflow-y: hidden; + scrollbar-width: none; +} + +.workspace-tabs__track::-webkit-scrollbar { + display: none; +} + +.workspace-tab { + position: relative; + display: flex; + flex: 0 0 auto; + align-items: center; + height: 36px; + margin-top: 6px; + border-radius: 4px 4px 0 0; + color: var(--ant-color-text-secondary); + transition: + color 160ms ease, + background-color 160ms ease; +} + +.workspace-tab:hover { + color: var(--ant-color-text); + background: var(--ant-color-fill-tertiary); +} + +.workspace-tab.is-active { + color: var(--ant-color-primary); + background: color-mix(in srgb, var(--ant-color-primary) 10%, transparent); +} + +.workspace-tab.is-active::after { + position: absolute; + right: 8px; + bottom: 0; + left: 8px; + height: 2px; + border-radius: 2px 2px 0 0; + background: var(--ant-color-primary); + content: ''; +} + +.workspace-tab__main, +.workspace-tab__close, +.workspace-tabs__more, +.workspace-tabs__theme, +.workspace-tabs__logout, +.workspace-tabs__mobile-trigger { + border: 0; + color: inherit; + background: transparent; + cursor: pointer; +} + +.workspace-tab__main { + display: inline-flex; + align-items: center; + gap: 7px; + height: 100%; + padding: 0 10px 0 12px; + font-size: 13px; + white-space: nowrap; +} + +.workspace-tab__label { + max-width: 132px; + overflow: hidden; + text-overflow: ellipsis; +} + +.workspace-tab__close { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + margin-right: 5px; + border-radius: 4px; + font-size: 11px; + opacity: 0.62; +} + +.workspace-tab__close:hover { + color: var(--ant-color-error); + background: var(--ant-color-fill-secondary); + opacity: 1; +} + +.workspace-tab__main:focus-visible, +.workspace-tab__close:focus-visible, +.workspace-tabs__more:focus-visible, +.workspace-tabs__theme:focus-visible, +.workspace-tabs__logout:focus-visible, +.workspace-tabs__mobile-trigger:focus-visible { + outline: 2px solid var(--ant-color-primary-border); + outline-offset: -2px; +} + +.workspace-tabs__more, +.workspace-tabs__theme, +.workspace-tabs__logout { + display: inline-flex; + flex: 0 0 32px; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + margin: 0 0 4px 8px; + border-radius: 4px; + color: var(--ant-color-text-secondary); + font-size: 16px; +} + +.workspace-tabs__more:hover, +.workspace-tabs__theme:hover, +.workspace-tabs__logout:hover { + color: var(--ant-color-text); + background: var(--ant-color-fill-tertiary); +} + +.workspace-tabs__theme, +.workspace-tabs__logout { + margin-left: 4px; +} + +.workspace-tabs__logout:hover { + color: var(--ant-color-error); +} + +.workspace-tabs__mobile { + display: none; + width: 100%; +} + +@media (max-width: 768px) { + .workspace-tabs { + flex-basis: 64px; + align-items: center; + height: 64px; + padding: 8px 8px 8px 60px; + } + + .workspace-tabs__desktop { + display: none; + } + + .workspace-tabs__mobile { + display: block; + } + + .workspace-tabs__mobile-trigger { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + height: 40px; + padding: 0 12px; + border: 1px solid var(--ant-color-border-secondary); + border-radius: 6px; + background: var(--ant-color-bg-container); + color: var(--ant-color-text); + box-shadow: 0 1px 2px rgb(0 0 0 / 4%); + } + + .workspace-tabs__mobile-current { + display: inline-flex; + align-items: center; + min-width: 0; + gap: 8px; + font-weight: 600; + } +} diff --git a/src/layouts/WorkspaceTabs.tsx b/src/layouts/WorkspaceTabs.tsx new file mode 100644 index 0000000..a3b448d --- /dev/null +++ b/src/layouts/WorkspaceTabs.tsx @@ -0,0 +1,329 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import type { ComponentType } from 'react'; +import { Dropdown, Tooltip } from 'antd'; +import type { MenuProps } from 'antd'; +import { + BellOutlined, + ClearOutlined, + CloseOutlined, + CloudServerOutlined, + DashboardOutlined, + DownOutlined, + IdcardOutlined, + KeyOutlined, + LogoutOutlined, + MoreOutlined, + MoonFilled, + MoonOutlined, + ShoppingCartOutlined, + SunOutlined, + UserOutlined, +} from '@ant-design/icons'; +import { useLocation, useNavigate } from 'react-router'; + +import { HttpUtil } from '@/utils'; +import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; +import './WorkspaceTabs.css'; + +const OPEN_TABS_KEY = 'game-admin-open-tabs'; +const HOME_PATH = '/'; + +interface WorkspaceTabDefinition { + path: string; + title: string; + icon: ComponentType; + pinned?: boolean; +} + +function ThemeModeIcon({ isDark, isUltra }: { isDark: boolean; isUltra: boolean }) { + if (!isDark) return ; + return isUltra ? : ; +} + +const workspaceTabDefinitions: WorkspaceTabDefinition[] = [ + { path: HOME_PATH, title: '总览', icon: DashboardOutlined, pinned: true }, + { path: '/accounts', title: '账号管理', icon: IdcardOutlined }, + { path: '/servers', title: '区服管理', icon: CloudServerOutlined }, + { path: '/players', title: '玩家管理', icon: UserOutlined }, + { path: '/cdks', title: 'CDK管理', icon: KeyOutlined }, + { path: '/announcements', title: '公告管理', icon: BellOutlined }, + { path: '/orders', title: '订单管理', icon: ShoppingCartOutlined }, +]; + +const definitionByPath = new Map(workspaceTabDefinitions.map((tab) => [tab.path, tab])); + +function normalizePath(pathname: string) { + const trimmed = pathname.replace(/\/+$/, ''); + return trimmed || HOME_PATH; +} + +function saveOpenedPaths(paths: string[]) { + try { + sessionStorage.setItem(OPEN_TABS_KEY, JSON.stringify(paths)); + } catch {} +} + +function readOpenedPaths(activePath: string) { + let storedPaths: string[] = []; + + try { + const stored = sessionStorage.getItem(OPEN_TABS_KEY); + const parsed: unknown = stored ? JSON.parse(stored) : []; + if (Array.isArray(parsed)) { + storedPaths = parsed.filter( + (path): path is string => typeof path === 'string' && definitionByPath.has(path), + ); + } + } catch {} + + const openedPaths = Array.from(new Set(storedPaths)); + if (!openedPaths.includes(HOME_PATH)) openedPaths.unshift(HOME_PATH); + if (definitionByPath.has(activePath) && !openedPaths.includes(activePath)) { + openedPaths.push(activePath); + } + return openedPaths; +} + +export default function WorkspaceTabs() { + const { isDark, isUltra, toggleTheme, toggleUltra } = useTheme(); + const navigate = useNavigate(); + const { pathname } = useLocation(); + const activePath = normalizePath(pathname); + const [openedPaths, setOpenedPaths] = useState(() => readOpenedPaths(activePath)); + + const openedTabs = useMemo( + () => + openedPaths + .map((path) => definitionByPath.get(path)) + .filter((tab): tab is WorkspaceTabDefinition => tab !== undefined), + [openedPaths], + ); + const activeTab = definitionByPath.get(activePath) ?? definitionByPath.get(HOME_PATH)!; + + const commitOpenedPaths = useCallback((paths: string[]) => { + saveOpenedPaths(paths); + setOpenedPaths(paths); + }, []); + + useEffect(() => { + saveOpenedPaths(openedPaths); + }, [openedPaths]); + + const closeTab = useCallback( + (path: string) => { + const target = definitionByPath.get(path); + if (!target || target.pinned) return; + + const targetIndex = openedPaths.indexOf(path); + const next = openedPaths.filter((openedPath) => openedPath !== path); + commitOpenedPaths(next); + + if (path === activePath) { + const fallbackIndex = Math.max(0, Math.min(targetIndex - 1, next.length - 1)); + navigate(next[fallbackIndex] ?? HOME_PATH); + } + }, + [activePath, commitOpenedPaths, navigate, openedPaths], + ); + + const closeOtherTabs = useCallback(() => { + const next = activePath === HOME_PATH ? [HOME_PATH] : [HOME_PATH, activePath]; + commitOpenedPaths(next); + }, [activePath, commitOpenedPaths]); + + const closeAllTabs = useCallback(() => { + commitOpenedPaths([HOME_PATH]); + if (activePath !== HOME_PATH) navigate(HOME_PATH); + }, [activePath, commitOpenedPaths, navigate]); + + const cycleTheme = useCallback( + (id?: string) => { + if (id) pauseAnimationsUntilLeave(id); + if (!isDark) { + toggleTheme(); + if (isUltra) toggleUltra(); + } else if (!isUltra) { + toggleUltra(); + } else { + toggleUltra(); + toggleTheme(); + } + }, + [isDark, isUltra, toggleTheme, toggleUltra], + ); + + const logout = useCallback(async () => { + try { + sessionStorage.removeItem(OPEN_TABS_KEY); + } catch {} + await HttpUtil.post('/logout'); + window.location.href = window.ADMIN_UI_BASE_PATH || '/'; + }, []); + + const actionItems = useMemo>( + () => [ + { + key: 'close-current', + icon: , + label: '关闭当前', + disabled: activeTab.pinned, + }, + { + key: 'close-others', + icon: , + label: '关闭其他', + disabled: openedPaths.every((path) => path === HOME_PATH || path === activePath), + }, + { + key: 'close-all', + icon: , + label: '关闭全部', + disabled: openedPaths.length === 1, + }, + ], + [activePath, activeTab.pinned, openedPaths], + ); + + const handleAction = useCallback>( + ({ key }) => { + if (key === 'close-current') closeTab(activePath); + if (key === 'close-others') closeOtherTabs(); + if (key === 'close-all') closeAllTabs(); + }, + [activePath, closeAllTabs, closeOtherTabs, closeTab], + ); + + const mobileItems = useMemo>( + () => [ + ...openedTabs.map((tab) => { + const Icon = tab.icon; + return { + key: `open:${tab.path}`, + icon: , + label: tab.title, + }; + }), + { type: 'divider' as const }, + ...actionItems, + { type: 'divider' as const }, + { + key: 'theme', + icon: , + label: '切换主题', + }, + { + key: 'logout', + icon: , + label: '退出登录', + danger: true, + }, + ], + [actionItems, isDark, isUltra, openedTabs], + ); + + const handleMobileMenu = useCallback>( + (info) => { + if (info.key === 'logout') { + void logout(); + return; + } + if (info.key === 'theme') { + cycleTheme(); + return; + } + if (info.key.startsWith('open:')) { + navigate(info.key.slice(5)); + return; + } + handleAction(info); + }, + [cycleTheme, handleAction, logout, navigate], + ); + + const ActiveIcon = activeTab.icon; + + return ( +
+

{activeTab.title}

+ +
+
+ {openedTabs.map((tab) => { + const Icon = tab.icon; + const active = tab.path === activePath; + return ( +
+ + {!tab.pinned && ( + + )} +
+ ); + })} +
+ + + + + + + + + + + + + + +
+ +
+ + + +
+
+ ); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..4b49323 --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,36 @@ +import { createRoot } from 'react-dom/client'; +import { RouterProvider } from 'react-router/dom'; +import { message } from 'antd'; +import 'antd/dist/reset.css'; +import '@/styles/global.css'; + +import { setupHttp } from '@/api/http-init'; +import { readyI18n } from '@/i18n/react'; +import { ThemeProvider } from '@/hooks/useTheme'; +import { QueryProvider } from '@/api/QueryProvider'; +import { router } from '@/routes'; +import { enableMockApi } from '@/mocks'; + +async function bootstrap() { + await enableMockApi(); + setupHttp(); + + const messageContainer = document.getElementById('message'); + if (messageContainer) { + message.config({ getContainer: () => messageContainer }); + } + + await readyI18n(); + const root = document.getElementById('app'); + if (root) { + createRoot(root).render( + + + + + , + ); + } +} + +void bootstrap(); diff --git a/src/mocks/browser.ts b/src/mocks/browser.ts new file mode 100644 index 0000000..c95883b --- /dev/null +++ b/src/mocks/browser.ts @@ -0,0 +1,5 @@ +import { setupWorker } from 'msw/browser'; + +import { handlers } from './handlers'; + +export const worker = setupWorker(...handlers); diff --git a/src/mocks/handlers.ts b/src/mocks/handlers.ts new file mode 100644 index 0000000..e6359b9 --- /dev/null +++ b/src/mocks/handlers.ts @@ -0,0 +1,1575 @@ +import { delay, http, HttpResponse } from 'msw'; + +import type { GameOverview, GameServerHealth, OverviewRange } from '@/schemas/gameOverview'; +import type { GameAccount, GameAccountPlatform, GameAccountStatus } from '@/schemas/gameAccount'; +import { GameAnnouncementUpsertInputSchema } from '@/schemas/gameAnnouncement'; +import type { + GameAnnouncement, + GameAnnouncementChannel, + GameAnnouncementStatus, +} from '@/schemas/gameAnnouncement'; +import { GameCdkBatchCreateInputSchema } from '@/schemas/gameCdk'; +import type { GameCdk, GameCdkReward, GameCdkStatus } from '@/schemas/gameCdk'; +import { GameOrderDeliveryInputSchema, GameOrderRefundInputSchema } from '@/schemas/gameOrder'; +import type { GameOrder, GameOrderChannel, GameOrderStatus } from '@/schemas/gameOrder'; +import { GamePlayerResourceInputSchema } from '@/schemas/gamePlayer'; +import type { GamePlayer, GamePlayerStatus } from '@/schemas/gamePlayer'; +import type { GameServer, GameServerUpsertInput } from '@/schemas/gameServer'; + +const SERVER_REGIONS = ['华东', '华南', '华北', '西南', '东北', '海外', '测试']; +const SERVER_NAMES = [ + '曙光一区', + '星火二区', + '远征三区', + '荣耀四区', + '竞技测试服', + '云海五区', + '龙城六区', + '天穹七区', + '烽火八区', + '神州九区', + '破晓十区', + '银月十一区', + '苍穹十二区', + '赤焰十三区', + '山河十四区', + '瀚海十五区', + '霜华十六区', + '雷霆十七区', + '长风十八区', + '星河十九区', + '天启二十区', + '先锋二十一区', + '凌云二十二区', + '逐鹿二十三区', + '归墟二十四区', + '昆仑二十五区', + '九霄二十六区', + '无界二十七区', + '征途二十八区', + '王城二十九区', + '跨服匹配服', + '版本验证服', +]; +const PRIMARY_SERVER_STATE: Array<{ + id: string; + status: GameServer['status']; + onlinePlayers: number; + capacity: number; + loadPercent: number; + version: string; + heartbeatOffset: number; +}> = [ + { + id: 'aurora-1', + status: 'online', + onlinePlayers: 4216, + capacity: 8000, + loadPercent: 53, + version: '1.8.12', + heartbeatOffset: 4, + }, + { + id: 'spark-2', + status: 'online', + onlinePlayers: 3380, + capacity: 7000, + loadPercent: 48, + version: '1.8.12', + heartbeatOffset: 7, + }, + { + id: 'expedition-3', + status: 'online', + onlinePlayers: 2617, + capacity: 6000, + loadPercent: 44, + version: '1.8.11', + heartbeatOffset: 11, + }, + { + id: 'glory-4', + status: 'maintenance', + onlinePlayers: 0, + capacity: 6000, + loadPercent: 0, + version: '1.8.12', + heartbeatOffset: 540, + }, + { + id: 'arena-test', + status: 'offline', + onlinePlayers: 0, + capacity: 2000, + loadPercent: 0, + version: '1.9.0-rc2', + heartbeatOffset: 1860, + }, +]; +const gameServerMockStartedAt = new Date('2026-08-27T12:00:00.000Z').getTime(); + +let mockGameServers: GameServer[] = SERVER_NAMES.map((name, index) => { + const primary = PRIMARY_SERVER_STATE[index]; + const status = primary?.status ?? (index % 14 === 0 ? 'offline' : 'online'); + const capacity = primary?.capacity ?? 5000 + (index % 4) * 1000; + const onlinePlayers = + primary?.onlinePlayers ?? (status === 'online' ? 420 + ((index * 619) % 4300) : 0); + const loadPercent = primary?.loadPercent ?? Math.round((onlinePlayers / capacity) * 100); + const heartbeatOffset = + primary?.heartbeatOffset ?? + (status === 'online' ? 3 + (index % 12) : status === 'maintenance' ? 540 : 1860); + + return { + id: primary?.id ?? `server-${index + 1}`, + name, + code: index === 4 ? 'ARENA-TEST' : `S${String(index + 1).padStart(3, '0')}`, + region: index === 4 ? '测试' : SERVER_REGIONS[index % (SERVER_REGIONS.length - 1)], + status, + host: `10.${20 + Math.floor(index / 200)}.${1 + Math.floor(index / 50)}.${10 + index}`, + port: 7001 + index, + onlinePlayers, + capacity, + loadPercent, + version: primary?.version ?? (index % 6 === 0 ? '1.8.11' : '1.8.12'), + openedAt: new Date(gameServerMockStartedAt - (index + 14) * 24 * 60 * 60 * 1000).toISOString(), + lastHeartbeat: new Date(gameServerMockStartedAt - heartbeatOffset * 1000).toISOString(), + maintenanceNote: status === 'maintenance' ? '例行更新与数据校验' : null, + }; +}); + +const ACCOUNT_PLATFORMS: Array<{ value: GameAccountPlatform; label: string }> = [ + { value: 'android', label: 'Android' }, + { value: 'ios', label: 'iOS' }, + { value: 'pc', label: 'PC' }, +]; +const ACCOUNT_CHANNELS = ['官网', 'TapTap', '应用商店', 'Steam']; +const accountMockStartedAt = Date.now(); + +let mockGameAccounts: GameAccount[] = Array.from({ length: 72 }, (_, index) => { + const status: GameAccountStatus = + index % 17 === 0 ? 'locked' : index % 11 === 0 ? 'banned' : 'active'; + const platform = ACCOUNT_PLATFORMS[index % ACCOUNT_PLATFORMS.length].value; + const registeredAt = + index < 6 + ? accountMockStartedAt - (index + 1) * 42 * 60 * 1000 + : accountMockStartedAt - (index + 2) * 19 * 60 * 60 * 1000; + const lastLoginAt = accountMockStartedAt - (index % 18) * 53 * 60 * 1000; + + return { + id: String(10000001 + index), + username: `account_${String(index + 1).padStart(4, '0')}`, + platform, + channel: ACCOUNT_CHANNELS[index % ACCOUNT_CHANNELS.length], + characterCount: 1 + (index % 4), + deviceCount: 1 + (index % 3), + totalRecharge: index % 7 === 0 ? 0 : 68 + ((index * 137) % 8800), + status, + registerTime: new Date(registeredAt).toISOString(), + lastLoginTime: new Date(lastLoginAt).toISOString(), + lastLoginIp: `203.0.113.${20 + (index % 180)}`, + banReason: status === 'banned' ? '异常支付行为复核中' : null, + banUntil: + status === 'banned' + ? new Date(accountMockStartedAt + (7 + (index % 24)) * 24 * 60 * 60 * 1000).toISOString() + : null, + }; +}); + +const PLAYER_NAMES = [ + '长风', + '流光', + '星河', + '凌霄', + '青岚', + '逐月', + '惊鸿', + '云舟', + '玄羽', + '霜刃', + '赤焰', + '听雪', + '无涯', + '沉星', + '归尘', + '朝歌', +]; +const playerMockStartedAt = Date.now(); + +let mockGamePlayers: GamePlayer[] = Array.from({ length: 128 }, (_, index) => { + const account = mockGameAccounts[index % mockGameAccounts.length]; + const server = mockGameServers[index % mockGameServers.length]; + const status: GamePlayerStatus = + index % 19 === 0 + ? 'banned' + : index % 4 === 0 && server.status === 'online' + ? 'online' + : 'offline'; + const createdAt = + index < 8 + ? playerMockStartedAt - (index + 1) * 38 * 60 * 1000 + : playerMockStartedAt - (index + 3) * 17 * 60 * 60 * 1000; + const lastLoginAt = playerMockStartedAt - (index % 48) * 47 * 60 * 1000; + + return { + id: String(50000001 + index), + name: `${PLAYER_NAMES[index % PLAYER_NAMES.length]}${String(Math.floor(index / PLAYER_NAMES.length) + 1).padStart(2, '0')}`, + accountId: account.id, + accountName: account.username, + serverId: server.id, + serverName: server.name, + level: 18 + ((index * 7) % 83), + vipLevel: index % 16, + combatPower: 12000 + ((index * 97331) % 9800000), + gold: + index % 9 === 0 ? 3200000000000 + index * 1700000000 : 180000 + ((index * 791923) % 86000000), + diamonds: index % 13 === 0 ? 98000000 + index * 23000 : 120 + ((index * 337) % 68000), + totalRecharge: index % 7 === 0 ? 0 : 68 + ((index * 527) % 168000), + status, + createdAt: new Date(createdAt).toISOString(), + lastLoginTime: new Date(lastLoginAt).toISOString(), + lastLoginIp: `198.51.100.${30 + (index % 190)}`, + banReason: status === 'banned' ? '异常资源流转复核中' : null, + banUntil: + status === 'banned' + ? new Date(playerMockStartedAt + (7 + (index % 20)) * 24 * 60 * 60 * 1000).toISOString() + : null, + }; +}); + +const CDK_REWARD_TEMPLATES: Array<{ + batchId: string; + batchName: string; + prefix: string; + rewardName: string; + rewards: GameCdkReward[]; + serverIndex: number | null; +}> = [ + { + batchId: 'launch-gift', + batchName: '开服庆典礼包', + prefix: 'LAUNCH', + rewardName: '开服庆典礼包', + rewards: [ + { itemId: 'diamond', itemName: '钻石', quantity: 500 }, + { itemId: 'gold', itemName: '金币', quantity: 200000 }, + ], + serverIndex: null, + }, + { + batchId: 'return-gift', + batchName: '老玩家回归礼包', + prefix: 'RETURN', + rewardName: '回归助力礼包', + rewards: [ + { itemId: 'diamond', itemName: '钻石', quantity: 300 }, + { itemId: 'stamina', itemName: '体力药剂', quantity: 10 }, + ], + serverIndex: null, + }, + { + batchId: 'stream-gift', + batchName: '直播合作礼包', + prefix: 'LIVE', + rewardName: '直播专属礼包', + rewards: [ + { itemId: 'gold', itemName: '金币', quantity: 88000 }, + { itemId: 'ticket', itemName: '招募券', quantity: 5 }, + ], + serverIndex: 0, + }, + { + batchId: 'community-gift', + batchName: '社区活动礼包', + prefix: 'COMM', + rewardName: '社区福利礼包', + rewards: [ + { itemId: 'diamond', itemName: '钻石', quantity: 188 }, + { itemId: 'chest', itemName: '稀有宝箱', quantity: 2 }, + ], + serverIndex: null, + }, + { + batchId: 'test-gift', + batchName: '测试服补偿礼包', + prefix: 'TEST', + rewardName: '测试服补偿', + rewards: [ + { itemId: 'diamond', itemName: '钻石', quantity: 2000 }, + { itemId: 'gold', itemName: '金币', quantity: 5000000 }, + ], + serverIndex: 4, + }, + { + batchId: 'season-gift', + batchName: '赛季冲刺礼包', + prefix: 'SEASON', + rewardName: '赛季冲刺礼包', + rewards: [ + { itemId: 'season_coin', itemName: '赛季币', quantity: 1200 }, + { itemId: 'ticket', itemName: '招募券', quantity: 3 }, + ], + serverIndex: null, + }, +]; +const cdkMockStartedAt = new Date('2026-08-27T12:00:00.000Z').getTime(); + +let mockGameCdks: GameCdk[] = Array.from({ length: 96 }, (_, index) => { + const template = CDK_REWARD_TEMPLATES[index % CDK_REWARD_TEMPLATES.length]; + const server = template.serverIndex === null ? null : mockGameServers[template.serverIndex]; + const status: GameCdkStatus = + index % 13 === 0 + ? 'expired' + : index % 7 === 0 + ? 'used' + : index % 17 === 0 + ? 'disabled' + : 'unused'; + const player = status === 'used' ? mockGamePlayers[index % mockGamePlayers.length] : null; + const createdAt = cdkMockStartedAt - (index + 4) * 8 * 60 * 60 * 1000; + const expiresAt = + status === 'expired' + ? cdkMockStartedAt - (index + 1) * 2 * 60 * 60 * 1000 + : index % 9 === 0 + ? null + : cdkMockStartedAt + (30 + (index % 120)) * 24 * 60 * 60 * 1000; + + return { + id: `cdk-${index + 1}`, + code: `${template.prefix}-${String(index + 1).padStart(6, '0')}`, + batchId: template.batchId, + batchName: template.batchName, + status, + rewardName: template.rewardName, + rewards: template.rewards.map((reward) => ({ ...reward })), + scope: server ? 'server' : 'all', + serverId: server?.id ?? null, + serverName: server?.name ?? null, + createdAt: new Date(createdAt).toISOString(), + expiresAt: expiresAt === null ? null : new Date(expiresAt).toISOString(), + usedByPlayerId: player?.id ?? null, + usedByPlayerName: player?.name ?? null, + usedAt: + status === 'used' + ? new Date(cdkMockStartedAt - (index + 1) * 37 * 60 * 1000).toISOString() + : null, + createdBy: index % 4 === 0 ? 'operator_01' : 'admin', + }; +}); + +const ANNOUNCEMENT_TEMPLATES: Array<{ + title: string; + content: string; + channel: GameAnnouncementChannel; + priority: number; + serverIndexes: number[]; +}> = [ + { + title: '版本更新公告', + content: '游戏将更新至最新版本,请各位玩家提前结束副本并保存进度。', + channel: 'login', + priority: 80, + serverIndexes: [], + }, + { + title: '限时活动开启', + content: '限时挑战活动已经开启,完成指定任务即可领取活动奖励。', + channel: 'banner', + priority: 70, + serverIndexes: [], + }, + { + title: '临时维护通知', + content: '部分区服将进行临时维护,维护期间无法登录对应区服。', + channel: 'login', + priority: 100, + serverIndexes: [0, 1], + }, + { + title: '赛季结算提醒', + content: '本赛季即将结算,请尚未完成目标的玩家及时参与。', + channel: 'marquee', + priority: 90, + serverIndexes: [], + }, + { + title: '新区开放公告', + content: '新区已经开放,创建角色即可领取新区专属成长礼包。', + channel: 'banner', + priority: 75, + serverIndexes: [5], + }, + { + title: '充值返利活动', + content: '活动期间累计充值达到指定档位,可在活动页面领取额外奖励。', + channel: 'login', + priority: 60, + serverIndexes: [], + }, + { + title: '公会战报名提醒', + content: '公会战报名即将截止,请公会管理及时完成参赛阵容确认。', + channel: 'marquee', + priority: 50, + serverIndexes: [], + }, + { + title: '违规账号处置通告', + content: '运营团队已完成本周违规账号处置,请共同维护公平游戏环境。', + channel: 'banner', + priority: 40, + serverIndexes: [], + }, + { + title: '维护补偿领取提醒', + content: '维护补偿已通过邮件发放,请在有效期内登录游戏领取。', + channel: 'login', + priority: 95, + serverIndexes: [3], + }, + { + title: '周末双倍掉落', + content: '周末指定副本开启双倍掉落,活动时间内参与即可生效。', + channel: 'marquee', + priority: 65, + serverIndexes: [], + }, +]; +const announcementMockStartedAt = new Date('2026-08-27T12:00:00.000Z').getTime(); + +let mockGameAnnouncements: GameAnnouncement[] = Array.from({ length: 72 }, (_, index) => { + const template = ANNOUNCEMENT_TEMPLATES[index % ANNOUNCEMENT_TEMPLATES.length]; + const status: GameAnnouncementStatus = + index % 13 === 0 + ? 'draft' + : index % 11 === 0 + ? 'scheduled' + : index % 7 === 0 + ? 'offline' + : index % 5 === 0 + ? 'ended' + : 'active'; + const createdAt = announcementMockStartedAt - (index + 2) * 9 * 60 * 60 * 1000; + let startAt: number | null = null; + let endAt: number | null = null; + + if (status === 'scheduled') { + startAt = announcementMockStartedAt + (1 + (index % 6)) * 24 * 60 * 60 * 1000; + endAt = startAt + (5 + (index % 7)) * 24 * 60 * 60 * 1000; + } else if (status === 'active') { + startAt = announcementMockStartedAt - (1 + (index % 6)) * 24 * 60 * 60 * 1000; + endAt = announcementMockStartedAt + (2 + (index % 10)) * 24 * 60 * 60 * 1000; + } else if (status === 'ended') { + startAt = announcementMockStartedAt - (18 + index) * 24 * 60 * 60 * 1000; + endAt = announcementMockStartedAt - (1 + (index % 8)) * 24 * 60 * 60 * 1000; + } else if (status === 'offline') { + startAt = announcementMockStartedAt - (4 + (index % 8)) * 24 * 60 * 60 * 1000; + endAt = announcementMockStartedAt + (3 + (index % 8)) * 24 * 60 * 60 * 1000; + } + + const servers = template.serverIndexes.map((serverIndex) => mockGameServers[serverIndex]); + const visible = status === 'active' || status === 'ended' || status === 'offline'; + + return { + id: `announcement-${index + 1}`, + title: `${template.title}${index >= ANNOUNCEMENT_TEMPLATES.length ? ` ${index + 1}` : ''}`, + content: template.content, + channel: template.channel, + status, + scope: servers.length === 0 ? 'all' : 'servers', + serverIds: servers.map((server) => server.id), + serverNames: servers.map((server) => server.name), + priority: Math.max(1, template.priority - (index % 4) * 5), + startAt: startAt === null ? null : new Date(startAt).toISOString(), + endAt: endAt === null ? null : new Date(endAt).toISOString(), + createdAt: new Date(createdAt).toISOString(), + updatedAt: new Date(createdAt + (index % 5) * 90 * 60 * 1000).toISOString(), + createdBy: index % 4 === 0 ? 'operator_01' : 'admin', + publishedAt: + status === 'draft' + ? null + : new Date( + Math.max(createdAt, (startAt ?? announcementMockStartedAt) - 60 * 60 * 1000), + ).toISOString(), + viewCount: visible ? 1200 + index * 2783 : 0, + clickCount: visible ? 80 + index * 137 : 0, + }; +}); + +const ORDER_PRODUCTS: Array<{ + id: string; + name: string; + amount: number; + currency: GameOrder['currency']; + items: GameOrder['items']; +}> = [ + { + id: 'diamond-60', + name: '60 钻石', + amount: 6, + currency: 'CNY', + items: [{ itemId: 'diamond', itemName: '钻石', quantity: 60 }], + }, + { + id: 'diamond-680', + name: '680 钻石', + amount: 68, + currency: 'CNY', + items: [ + { itemId: 'diamond', itemName: '钻石', quantity: 680 }, + { itemId: 'vip_point', itemName: 'VIP 经验', quantity: 68 }, + ], + }, + { + id: 'monthly-card', + name: '月卡', + amount: 30, + currency: 'CNY', + items: [ + { itemId: 'monthly_card', itemName: '月卡权益', quantity: 1 }, + { itemId: 'diamond', itemName: '钻石', quantity: 300 }, + ], + }, + { + id: 'battle-pass', + name: '赛季通行证', + amount: 68, + currency: 'CNY', + items: [{ itemId: 'battle_pass', itemName: '高级通行证', quantity: 1 }], + }, + { + id: 'growth-pack', + name: '成长基金', + amount: 98, + currency: 'CNY', + items: [ + { itemId: 'growth_fund', itemName: '成长基金', quantity: 1 }, + { itemId: 'diamond', itemName: '钻石', quantity: 980 }, + ], + }, + { + id: 'weekly-pack', + name: '每周特惠礼包', + amount: 12, + currency: 'CNY', + items: [ + { itemId: 'diamond', itemName: '钻石', quantity: 120 }, + { itemId: 'stamina', itemName: '体力药剂', quantity: 5 }, + ], + }, + { + id: 'steam-founder', + name: '创始人礼包', + amount: 19.99, + currency: 'USD', + items: [ + { itemId: 'founder_badge', itemName: '创始人徽章', quantity: 1 }, + { itemId: 'diamond', itemName: '钻石', quantity: 1980 }, + ], + }, + { + id: 'anniversary-pack', + name: '周年庆典礼包', + amount: 198, + currency: 'CNY', + items: [ + { itemId: 'diamond', itemName: '钻石', quantity: 3280 }, + { itemId: 'limited_skin', itemName: '限定外观', quantity: 1 }, + ], + }, +]; +const ORDER_CHANNELS: GameOrderChannel[] = [ + 'official', + 'app_store', + 'google_play', + 'steam', + 'third_party', +]; +const ORDER_USD_TO_CNY_RATE = 7.2; +const orderMockStartedAt = new Date('2026-08-27T12:00:00.000Z').getTime(); + +let mockGameOrders: GameOrder[] = Array.from({ length: 128 }, (_, index) => { + const product = ORDER_PRODUCTS[index % ORDER_PRODUCTS.length]; + const account = mockGameAccounts[index % mockGameAccounts.length]; + const player = mockGamePlayers[index % mockGamePlayers.length]; + const server = mockGameServers[index % mockGameServers.length]; + const channel = ORDER_CHANNELS[index % ORDER_CHANNELS.length]; + const status: GameOrderStatus = + index % 19 === 0 + ? 'refunded' + : index % 17 === 0 + ? 'refunding' + : index % 13 === 0 + ? 'closed' + : index % 7 === 0 + ? 'paid' + : index % 5 === 0 + ? 'pending' + : 'delivered'; + const quantity = index % 23 === 0 ? 2 : 1; + const createdAt = orderMockStartedAt - (index + 1) * 47 * 60 * 1000; + const paidAt = + status === 'pending' || status === 'closed' ? null : createdAt + (2 + (index % 8)) * 60 * 1000; + const deliveredAt = + status === 'delivered' || status === 'refunding' || status === 'refunded' + ? (paidAt ?? createdAt) + (1 + (index % 6)) * 60 * 1000 + : null; + const refundedAt = + status === 'refunded' ? (deliveredAt ?? createdAt) + (4 + (index % 12)) * 60 * 60 * 1000 : null; + const channelPrefix: Record = { + official: 'WEB', + app_store: 'IOS', + google_play: 'GP', + steam: 'STM', + third_party: 'PAY', + }; + + return { + id: `GO20260827${String(index + 1).padStart(6, '0')}`, + providerOrderId: `${channelPrefix[channel]}-${String(860000000 + index * 7919)}`, + accountId: account.id, + accountName: account.username, + playerId: player.id, + playerName: player.name, + serverId: server.id, + serverName: server.name, + productId: product.id, + productName: product.name, + quantity, + items: product.items.map((item) => ({ ...item, quantity: item.quantity * quantity })), + amount: Number((product.amount * quantity).toFixed(2)), + currency: product.currency, + channel, + status, + createdAt: new Date(createdAt).toISOString(), + paidAt: paidAt === null ? null : new Date(paidAt).toISOString(), + deliveredAt: deliveredAt === null ? null : new Date(deliveredAt).toISOString(), + refundedAt: refundedAt === null ? null : new Date(refundedAt).toISOString(), + updatedAt: new Date(refundedAt ?? deliveredAt ?? paidAt ?? createdAt).toISOString(), + clientIp: `198.51.100.${40 + (index % 180)}`, + refundReason: + status === 'refunding' || status === 'refunded' ? '玩家重复购买,客服审核通过' : null, + operatorNote: status === 'paid' && index % 2 === 0 ? '渠道回调成功,等待补发' : null, + }; +}); + +function envelope(obj: unknown = null, msg = '') { + return HttpResponse.json({ success: true, msg, obj }); +} + +function readGameServerUpsert(body: unknown): GameServerUpsertInput | null { + if (!body || typeof body !== 'object') return null; + const value = body as Record; + const port = Number(value.port); + const capacity = Number(value.capacity); + const textFields = ['name', 'code', 'region', 'host', 'version', 'openedAt'] as const; + + if (textFields.some((key) => typeof value[key] !== 'string' || !value[key].trim())) return null; + if (!Number.isInteger(port) || port < 1 || port > 65535) return null; + if (!Number.isInteger(capacity) || capacity < 1) return null; + + return { + name: String(value.name).trim(), + code: String(value.code).trim(), + region: String(value.region).trim(), + host: String(value.host).trim(), + port, + capacity, + version: String(value.version).trim(), + openedAt: String(value.openedAt), + }; +} + +function overviewLabels(range: OverviewRange) { + const now = new Date(); + if (range === 'today') { + return Array.from({ length: 24 }, (_, index) => `${String(index).padStart(2, '0')}:00`); + } + + const days = range === '7d' ? 7 : 30; + return Array.from({ length: days }, (_, index) => { + const date = new Date(now); + date.setDate(now.getDate() - (days - index - 1)); + return `${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; + }); +} + +function mockGameOverview(range: OverviewRange, serverId: string): GameOverview { + const now = Date.now(); + const allServers: GameServerHealth[] = mockGameServers.slice(0, 5).map((server) => ({ + id: server.id, + name: server.name, + region: server.region, + status: server.status, + onlinePlayers: server.onlinePlayers, + capacity: server.capacity, + loadPercent: server.loadPercent, + version: server.version, + lastHeartbeat: server.lastHeartbeat, + })); + const selectedServers = + serverId === 'all' ? allServers : allServers.filter((server) => server.id === serverId); + const visibleServers = selectedServers.length > 0 ? selectedServers : allServers; + const totalCapacity = allServers.reduce((sum, server) => sum + server.capacity, 0); + const selectedCapacity = visibleServers.reduce((sum, server) => sum + server.capacity, 0); + const scale = serverId === 'all' ? 1 : Math.max(0.08, selectedCapacity / totalCapacity); + const rangeScale = range === 'today' ? 1 : range === '7d' ? 6.8 : 28.4; + const labels = overviewLabels(range); + const currentOnline = visibleServers.reduce((sum, server) => sum + server.onlinePlayers, 0); + const trendBase = currentOnline || Math.round(2800 * scale); + const onlinePlayers = labels.map((_, index) => + Math.max( + 0, + Math.round(trendBase * (0.76 + Math.sin(index / 2.8) * 0.16 + Math.cos(index / 5.2) * 0.08)), + ), + ); + const activePlayers = onlinePlayers.map((value, index) => + Math.round(value * (2.45 + Math.sin(index / 4.1) * 0.18)), + ); + const revenue = labels.map((_, index) => + Math.max( + 0, + Math.round( + (range === 'today' ? 18_500 : 482_000) * + scale * + (0.82 + Math.sin(index / 2.3) * 0.19 + Math.cos(index / 5.4) * 0.11), + ), + ), + ); + + return { + generatedAt: new Date(now).toISOString(), + range, + serverOptions: [ + { id: 'all', name: '全部区服' }, + ...allServers.map(({ id, name }) => ({ id, name })), + ], + metrics: { + totalPlayers: { value: Math.round(1_286_420 * scale), change: 3.6 }, + newPlayers: { value: Math.round(8_642 * scale * rangeScale), change: 11.8 }, + activePlayers: { value: Math.round(68_421 * scale * Math.sqrt(rangeScale)), change: 7.2 }, + onlinePlayers: { value: currentOnline, change: 5.4 }, + revenue: { value: Math.round(486_320 * scale * rangeScale), change: 9.6 }, + orders: { value: Math.round(12_684 * scale * rangeScale), change: 8.1 }, + }, + trends: { labels, onlinePlayers, activePlayers, revenue }, + servers: visibleServers, + alerts: [ + { + id: 'failed-orders', + level: 'error', + title: '异常订单待处理', + description: '支付成功但尚未完成发货', + count: 3, + }, + { + id: 'scheduled-maintenance', + level: 'warning', + title: '区服维护中', + description: '荣耀四区计划维护至 22:30', + count: 1, + }, + { + id: 'mail-review', + level: 'info', + title: '邮件等待审核', + description: '全服补偿邮件尚未发布', + count: 2, + }, + { + id: 'activity-ending', + level: 'warning', + title: '活动即将结束', + description: '限时累充活动将在 2 小时后结束', + count: 1, + }, + ], + }; +} + +export const handlers = [ + http.get('/csrf-token', () => envelope('admin-ui-dev-csrf')), + http.post('/getTwoFactorEnable', async () => { + await delay(180); + return envelope(false); + }), + http.post('/login', async () => { + await delay(250); + return envelope(null, 'Signed in'); + }), + http.post('/logout', () => envelope(null, 'Signed out')), + http.get('/panel/api/game/overview', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const rawRange = url.searchParams.get('range'); + const range: OverviewRange = rawRange === '7d' || rawRange === '30d' ? rawRange : 'today'; + const serverId = url.searchParams.get('serverId') || 'all'; + return envelope(mockGameOverview(range, serverId)); + }), + http.get('*/panel/api/game/servers', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase(); + const rawStatus = url.searchParams.get('status'); + const region = url.searchParams.get('region') || 'all'; + const page = Math.max(1, Number(url.searchParams.get('page')) || 1); + const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get('pageSize')) || 20)); + const status = + rawStatus === 'online' || rawStatus === 'maintenance' || rawStatus === 'offline' + ? rawStatus + : 'all'; + const filtered = mockGameServers.filter((server) => { + const matchesKeyword = + !keyword || + server.name.toLowerCase().includes(keyword) || + server.code.toLowerCase().includes(keyword) || + server.host.toLowerCase().includes(keyword); + const matchesStatus = status === 'all' || server.status === status; + const matchesRegion = region === 'all' || server.region === region; + return matchesKeyword && matchesStatus && matchesRegion; + }); + const start = (page - 1) * pageSize; + const summary = { + total: mockGameServers.length, + online: mockGameServers.filter((server) => server.status === 'online').length, + maintenance: mockGameServers.filter((server) => server.status === 'maintenance').length, + offline: mockGameServers.filter((server) => server.status === 'offline').length, + onlinePlayers: mockGameServers.reduce((sum, server) => sum + server.onlinePlayers, 0), + }; + const regionOptions = Array.from(new Set(mockGameServers.map((server) => server.region))) + .sort((left, right) => left.localeCompare(right, 'zh-CN')) + .map((value) => ({ value, label: value })); + + return envelope({ + items: filtered.slice(start, start + pageSize), + total: filtered.length, + page, + pageSize, + summary, + regionOptions, + }); + }), + http.post('*/panel/api/game/servers', async ({ request }) => { + await delay(160); + const payload = readGameServerUpsert(await request.json()); + if (!payload) { + return HttpResponse.json({ success: false, msg: '区服参数无效', obj: null }, { status: 400 }); + } + if ( + mockGameServers.some((server) => server.code.toLowerCase() === payload.code.toLowerCase()) + ) { + return HttpResponse.json( + { success: false, msg: '区服代号已存在', obj: null }, + { status: 409 }, + ); + } + + const created: GameServer = { + id: `server-${Date.now()}`, + ...payload, + status: 'offline', + onlinePlayers: 0, + loadPercent: 0, + lastHeartbeat: new Date(gameServerMockStartedAt).toISOString(), + maintenanceNote: null, + }; + mockGameServers = [created, ...mockGameServers]; + return envelope(created, '区服已新增'); + }), + http.post('*/panel/api/game/servers/:serverId', async ({ params, request }) => { + await delay(160); + const serverId = String(params.serverId); + const payload = readGameServerUpsert(await request.json()); + const current = mockGameServers.find((server) => server.id === serverId); + if (!current) { + return HttpResponse.json({ success: false, msg: '区服不存在', obj: null }, { status: 404 }); + } + if (!payload) { + return HttpResponse.json({ success: false, msg: '区服参数无效', obj: null }, { status: 400 }); + } + if ( + mockGameServers.some( + (server) => + server.id !== serverId && server.code.toLowerCase() === payload.code.toLowerCase(), + ) + ) { + return HttpResponse.json( + { success: false, msg: '区服代号已存在', obj: null }, + { status: 409 }, + ); + } + + const updated: GameServer = { ...current, ...payload }; + mockGameServers = mockGameServers.map((server) => (server.id === serverId ? updated : server)); + return envelope(updated, '区服已更新'); + }), + http.post('*/panel/api/game/servers/:serverId/status', async ({ params, request }) => { + await delay(160); + const serverId = String(params.serverId); + const body = (await request.json()) as Record; + const current = mockGameServers.find((server) => server.id === serverId); + if (!current) { + return HttpResponse.json({ success: false, msg: '区服不存在', obj: null }, { status: 404 }); + } + if (body.status !== 'online' && body.status !== 'maintenance') { + return HttpResponse.json({ success: false, msg: '区服状态无效', obj: null }, { status: 400 }); + } + const maintenanceNote = + body.status === 'maintenance' && typeof body.maintenanceNote === 'string' + ? body.maintenanceNote.trim() || '运营维护' + : null; + const updated: GameServer = { + ...current, + status: body.status, + onlinePlayers: body.status === 'maintenance' ? 0 : Math.max(current.onlinePlayers, 120), + loadPercent: body.status === 'maintenance' ? 0 : Math.max(current.loadPercent, 3), + lastHeartbeat: new Date(gameServerMockStartedAt).toISOString(), + maintenanceNote, + }; + mockGameServers = mockGameServers.map((server) => (server.id === serverId ? updated : server)); + return envelope(updated, body.status === 'maintenance' ? '区服已进入维护' : '区服已恢复运行'); + }), + http.get('*/panel/api/game/accounts', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase(); + const rawStatus = url.searchParams.get('status'); + const rawPlatform = url.searchParams.get('platform'); + const page = Math.max(1, Number(url.searchParams.get('page')) || 1); + const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get('pageSize')) || 20)); + const status = + rawStatus === 'active' || rawStatus === 'banned' || rawStatus === 'locked' + ? rawStatus + : 'all'; + const platform = + rawPlatform === 'android' || rawPlatform === 'ios' || rawPlatform === 'pc' + ? rawPlatform + : 'all'; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const filtered = mockGameAccounts.filter((account) => { + const matchesKeyword = + !keyword || + account.id.toLowerCase().includes(keyword) || + account.username.toLowerCase().includes(keyword) || + account.lastLoginIp.toLowerCase().includes(keyword); + const matchesStatus = status === 'all' || account.status === status; + const matchesPlatform = platform === 'all' || account.platform === platform; + return matchesKeyword && matchesStatus && matchesPlatform; + }); + const start = (page - 1) * pageSize; + const summary = { + total: mockGameAccounts.length, + newToday: mockGameAccounts.filter( + (account) => new Date(account.registerTime).getTime() >= today.getTime(), + ).length, + active: mockGameAccounts.filter((account) => account.status === 'active').length, + banned: mockGameAccounts.filter((account) => account.status === 'banned').length, + locked: mockGameAccounts.filter((account) => account.status === 'locked').length, + }; + + return envelope({ + items: filtered.slice(start, start + pageSize), + total: filtered.length, + page, + pageSize, + summary, + platformOptions: ACCOUNT_PLATFORMS, + }); + }), + http.post('*/panel/api/game/accounts/:accountId/status', async ({ params, request }) => { + await delay(160); + const accountId = String(params.accountId); + const body = (await request.json()) as { + status?: unknown; + reason?: unknown; + durationDays?: unknown; + }; + const index = mockGameAccounts.findIndex((account) => account.id === accountId); + if (index < 0) { + return HttpResponse.json({ success: false, msg: '账号不存在', obj: null }, { status: 404 }); + } + if (body.status !== 'active' && body.status !== 'banned') { + return HttpResponse.json({ success: false, msg: '账号状态无效', obj: null }, { status: 400 }); + } + + const current = mockGameAccounts[index]; + const durationDays = Number(body.durationDays) || 0; + const next: GameAccount = { + ...current, + status: body.status, + banReason: + body.status === 'banned' && typeof body.reason === 'string' + ? body.reason.trim() || '运营封禁' + : null, + banUntil: + body.status === 'banned' && durationDays > 0 + ? new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000).toISOString() + : null, + }; + mockGameAccounts = mockGameAccounts.map((account) => + account.id === accountId ? next : account, + ); + return envelope(next, body.status === 'active' ? '账号已恢复' : '账号已封禁'); + }), + http.get('*/panel/api/game/players', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase(); + const rawStatus = url.searchParams.get('status'); + const serverId = url.searchParams.get('serverId') || 'all'; + const page = Math.max(1, Number(url.searchParams.get('page')) || 1); + const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get('pageSize')) || 20)); + const status = + rawStatus === 'online' || rawStatus === 'offline' || rawStatus === 'banned' + ? rawStatus + : 'all'; + const today = new Date(); + today.setHours(0, 0, 0, 0); + + const filtered = mockGamePlayers.filter((player) => { + const matchesKeyword = + !keyword || + player.id.toLowerCase().includes(keyword) || + player.name.toLowerCase().includes(keyword) || + player.accountId.toLowerCase().includes(keyword) || + player.accountName.toLowerCase().includes(keyword); + const matchesStatus = status === 'all' || player.status === status; + const matchesServer = serverId === 'all' || player.serverId === serverId; + return matchesKeyword && matchesStatus && matchesServer; + }); + const start = (page - 1) * pageSize; + const summary = { + total: mockGamePlayers.length, + online: mockGamePlayers.filter((player) => player.status === 'online').length, + activeToday: mockGamePlayers.filter( + (player) => new Date(player.lastLoginTime).getTime() >= today.getTime(), + ).length, + newToday: mockGamePlayers.filter( + (player) => new Date(player.createdAt).getTime() >= today.getTime(), + ).length, + banned: mockGamePlayers.filter((player) => player.status === 'banned').length, + }; + const serverOptions = mockGameServers.map((server) => ({ + value: server.id, + label: server.name, + })); + + return envelope({ + items: filtered.slice(start, start + pageSize), + total: filtered.length, + page, + pageSize, + summary, + serverOptions, + }); + }), + http.post('*/panel/api/game/players/:playerId/resources', async ({ params, request }) => { + await delay(160); + const playerId = String(params.playerId); + const parsed = GamePlayerResourceInputSchema.safeParse(await request.json()); + const current = mockGamePlayers.find((player) => player.id === playerId); + if (!current) { + return HttpResponse.json({ success: false, msg: '玩家不存在', obj: null }, { status: 404 }); + } + if (!parsed.success) { + return HttpResponse.json({ success: false, msg: '玩家数值无效', obj: null }, { status: 400 }); + } + + const updated: GamePlayer = { ...current, ...parsed.data }; + mockGamePlayers = mockGamePlayers.map((player) => (player.id === playerId ? updated : player)); + return envelope(updated, '玩家数值已更新'); + }), + http.post('*/panel/api/game/players/:playerId/status', async ({ params, request }) => { + await delay(160); + const playerId = String(params.playerId); + const body = (await request.json()) as { + status?: unknown; + reason?: unknown; + durationDays?: unknown; + }; + const current = mockGamePlayers.find((player) => player.id === playerId); + if (!current) { + return HttpResponse.json({ success: false, msg: '玩家不存在', obj: null }, { status: 404 }); + } + if (body.status !== 'offline' && body.status !== 'banned') { + return HttpResponse.json({ success: false, msg: '玩家状态无效', obj: null }, { status: 400 }); + } + + const durationDays = Number(body.durationDays) || 0; + const updated: GamePlayer = { + ...current, + status: body.status, + banReason: + body.status === 'banned' && typeof body.reason === 'string' + ? body.reason.trim() || '运营封禁' + : null, + banUntil: + body.status === 'banned' && durationDays > 0 + ? new Date(Date.now() + durationDays * 24 * 60 * 60 * 1000).toISOString() + : null, + }; + mockGamePlayers = mockGamePlayers.map((player) => (player.id === playerId ? updated : player)); + return envelope(updated, body.status === 'offline' ? '玩家已解封' : '玩家已封禁'); + }), + http.get('*/panel/api/game/cdks', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase(); + const rawStatus = url.searchParams.get('status'); + const batchId = url.searchParams.get('batchId') || 'all'; + const page = Math.max(1, Number(url.searchParams.get('page')) || 1); + const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get('pageSize')) || 20)); + const status = + rawStatus === 'unused' || + rawStatus === 'used' || + rawStatus === 'expired' || + rawStatus === 'disabled' + ? rawStatus + : 'all'; + + const filtered = mockGameCdks.filter((cdk) => { + const matchesKeyword = + !keyword || + cdk.code.toLowerCase().includes(keyword) || + cdk.batchName.toLowerCase().includes(keyword) || + cdk.usedByPlayerId?.toLowerCase().includes(keyword) || + cdk.usedByPlayerName?.toLowerCase().includes(keyword); + const matchesStatus = status === 'all' || cdk.status === status; + const matchesBatch = batchId === 'all' || cdk.batchId === batchId; + return matchesKeyword && matchesStatus && matchesBatch; + }); + const start = (page - 1) * pageSize; + const batchOptions = Array.from( + new Map(mockGameCdks.map((cdk) => [cdk.batchId, cdk.batchName])).entries(), + ).map(([value, label]) => ({ value, label })); + const serverOptions = mockGameServers.map((server) => ({ + value: server.id, + label: server.name, + })); + const summary = { + total: mockGameCdks.length, + unused: mockGameCdks.filter((cdk) => cdk.status === 'unused').length, + used: mockGameCdks.filter((cdk) => cdk.status === 'used').length, + expired: mockGameCdks.filter((cdk) => cdk.status === 'expired').length, + disabled: mockGameCdks.filter((cdk) => cdk.status === 'disabled').length, + }; + + return envelope({ + items: filtered.slice(start, start + pageSize), + total: filtered.length, + page, + pageSize, + summary, + batchOptions, + serverOptions, + }); + }), + http.post('*/panel/api/game/cdks/batches', async ({ request }) => { + await delay(180); + const parsed = GameCdkBatchCreateInputSchema.safeParse(await request.json()); + if (!parsed.success) { + return HttpResponse.json( + { success: false, msg: 'CDK 批次参数无效', obj: null }, + { status: 400 }, + ); + } + + const payload = parsed.data; + const server = payload.serverId + ? mockGameServers.find((item) => item.id === payload.serverId) + : null; + if (payload.serverId && !server) { + return HttpResponse.json({ success: false, msg: '区服不存在', obj: null }, { status: 400 }); + } + + const createdAt = new Date().toISOString(); + const batchToken = String(Date.now()); + const batchId = `batch-${batchToken}`; + const normalizedPrefix = payload.prefix.toUpperCase(); + const created = Array.from({ length: payload.quantity }, (_, index): GameCdk => ({ + id: `cdk-${batchToken}-${index + 1}`, + code: `${normalizedPrefix}-${batchToken.slice(-6)}-${String(index + 1).padStart(4, '0')}`, + batchId, + batchName: payload.batchName, + status: 'unused', + rewardName: payload.rewardName, + rewards: payload.rewards.map((reward) => ({ ...reward })), + scope: server ? 'server' : 'all', + serverId: server?.id ?? null, + serverName: server?.name ?? null, + createdAt, + expiresAt: payload.expiresAt, + usedByPlayerId: null, + usedByPlayerName: null, + usedAt: null, + createdBy: 'admin', + })); + mockGameCdks = [...created, ...mockGameCdks]; + + return envelope( + { batchId, batchName: payload.batchName, createdCount: created.length }, + 'CDK 批次已生成', + ); + }), + http.post('*/panel/api/game/cdks/:cdkId/status', async ({ params, request }) => { + await delay(160); + const cdkId = String(params.cdkId); + const body = (await request.json()) as { status?: unknown }; + const current = mockGameCdks.find((cdk) => cdk.id === cdkId); + if (!current) { + return HttpResponse.json({ success: false, msg: 'CDK 不存在', obj: null }, { status: 404 }); + } + if (body.status !== 'unused' && body.status !== 'disabled') { + return HttpResponse.json({ success: false, msg: 'CDK 状态无效', obj: null }, { status: 400 }); + } + if (current.status === 'used' || current.status === 'expired') { + return HttpResponse.json( + { success: false, msg: '已使用或已过期的 CDK 不可修改', obj: null }, + { status: 409 }, + ); + } + + const updated: GameCdk = { ...current, status: body.status }; + mockGameCdks = mockGameCdks.map((cdk) => (cdk.id === cdkId ? updated : cdk)); + return envelope(updated, body.status === 'disabled' ? 'CDK 已停用' : 'CDK 已启用'); + }), + http.get('*/panel/api/game/announcements', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase(); + const rawStatus = url.searchParams.get('status'); + const rawChannel = url.searchParams.get('channel'); + const page = Math.max(1, Number(url.searchParams.get('page')) || 1); + const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get('pageSize')) || 20)); + const status = + rawStatus === 'draft' || + rawStatus === 'scheduled' || + rawStatus === 'active' || + rawStatus === 'ended' || + rawStatus === 'offline' + ? rawStatus + : 'all'; + const channel = + rawChannel === 'login' || rawChannel === 'banner' || rawChannel === 'marquee' + ? rawChannel + : 'all'; + + const filtered = mockGameAnnouncements.filter((announcement) => { + const matchesKeyword = + !keyword || + announcement.title.toLowerCase().includes(keyword) || + announcement.content.toLowerCase().includes(keyword) || + announcement.id.toLowerCase().includes(keyword); + const matchesStatus = status === 'all' || announcement.status === status; + const matchesChannel = channel === 'all' || announcement.channel === channel; + return matchesKeyword && matchesStatus && matchesChannel; + }); + const start = (page - 1) * pageSize; + const summary = { + total: mockGameAnnouncements.length, + active: mockGameAnnouncements.filter((item) => item.status === 'active').length, + scheduled: mockGameAnnouncements.filter((item) => item.status === 'scheduled').length, + draft: mockGameAnnouncements.filter((item) => item.status === 'draft').length, + inactive: mockGameAnnouncements.filter( + (item) => item.status === 'ended' || item.status === 'offline', + ).length, + }; + const serverOptions = mockGameServers.map((server) => ({ + value: server.id, + label: server.name, + })); + + return envelope({ + items: filtered.slice(start, start + pageSize), + total: filtered.length, + page, + pageSize, + summary, + serverOptions, + }); + }), + http.post('*/panel/api/game/announcements', async ({ request }) => { + await delay(180); + const parsed = GameAnnouncementUpsertInputSchema.safeParse(await request.json()); + if (!parsed.success) { + return HttpResponse.json({ success: false, msg: '公告参数无效', obj: null }, { status: 400 }); + } + + const payload = parsed.data; + const servers = payload.serverIds.map((serverId) => + mockGameServers.find((server) => server.id === serverId), + ); + if (servers.some((server) => !server)) { + return HttpResponse.json({ success: false, msg: '区服不存在', obj: null }, { status: 400 }); + } + + const now = new Date(); + const startAt = + payload.mode === 'publish' ? (payload.startAt ?? now.toISOString()) : payload.startAt; + const status: GameAnnouncementStatus = + payload.mode === 'draft' + ? 'draft' + : startAt && new Date(startAt).getTime() > now.getTime() + ? 'scheduled' + : payload.endAt && new Date(payload.endAt).getTime() <= now.getTime() + ? 'ended' + : 'active'; + const created: GameAnnouncement = { + id: `announcement-${Date.now()}`, + title: payload.title, + content: payload.content, + channel: payload.channel, + status, + scope: servers.length === 0 ? 'all' : 'servers', + serverIds: payload.serverIds, + serverNames: servers.map((server) => server!.name), + priority: payload.priority, + startAt, + endAt: payload.endAt, + createdAt: now.toISOString(), + updatedAt: now.toISOString(), + createdBy: 'admin', + publishedAt: payload.mode === 'publish' ? now.toISOString() : null, + viewCount: 0, + clickCount: 0, + }; + mockGameAnnouncements = [created, ...mockGameAnnouncements]; + return envelope(created, payload.mode === 'publish' ? '公告已发布' : '公告草稿已保存'); + }), + http.post('*/panel/api/game/announcements/:announcementId', async ({ params, request }) => { + await delay(180); + const announcementId = String(params.announcementId); + const current = mockGameAnnouncements.find((item) => item.id === announcementId); + if (!current) { + return HttpResponse.json({ success: false, msg: '公告不存在', obj: null }, { status: 404 }); + } + + const parsed = GameAnnouncementUpsertInputSchema.safeParse(await request.json()); + if (!parsed.success) { + return HttpResponse.json({ success: false, msg: '公告参数无效', obj: null }, { status: 400 }); + } + + const payload = parsed.data; + const servers = payload.serverIds.map((serverId) => + mockGameServers.find((server) => server.id === serverId), + ); + if (servers.some((server) => !server)) { + return HttpResponse.json({ success: false, msg: '区服不存在', obj: null }, { status: 400 }); + } + + const now = new Date(); + const startAt = + payload.mode === 'publish' + ? (payload.startAt ?? current.startAt ?? now.toISOString()) + : payload.startAt; + const status: GameAnnouncementStatus = + payload.mode === 'draft' + ? 'draft' + : startAt && new Date(startAt).getTime() > now.getTime() + ? 'scheduled' + : payload.endAt && new Date(payload.endAt).getTime() <= now.getTime() + ? 'ended' + : 'active'; + const updated: GameAnnouncement = { + ...current, + title: payload.title, + content: payload.content, + channel: payload.channel, + status, + scope: servers.length === 0 ? 'all' : 'servers', + serverIds: payload.serverIds, + serverNames: servers.map((server) => server!.name), + priority: payload.priority, + startAt, + endAt: payload.endAt, + updatedAt: now.toISOString(), + publishedAt: payload.mode === 'publish' ? (current.publishedAt ?? now.toISOString()) : null, + }; + mockGameAnnouncements = mockGameAnnouncements.map((item) => + item.id === announcementId ? updated : item, + ); + return envelope(updated, payload.mode === 'publish' ? '公告已更新' : '公告草稿已保存'); + }), + http.post( + '*/panel/api/game/announcements/:announcementId/status', + async ({ params, request }) => { + await delay(160); + const announcementId = String(params.announcementId); + const body = (await request.json()) as { action?: unknown }; + const current = mockGameAnnouncements.find((item) => item.id === announcementId); + if (!current) { + return HttpResponse.json({ success: false, msg: '公告不存在', obj: null }, { status: 404 }); + } + if (body.action !== 'publish' && body.action !== 'offline') { + return HttpResponse.json( + { success: false, msg: '公告状态操作无效', obj: null }, + { status: 400 }, + ); + } + + const now = new Date(); + const startAt = + body.action === 'publish' + ? current.startAt && new Date(current.startAt).getTime() > now.getTime() + ? current.startAt + : now.toISOString() + : current.startAt; + const endAt = + body.action === 'publish' && + current.endAt && + new Date(current.endAt).getTime() <= now.getTime() + ? new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString() + : current.endAt; + const updated: GameAnnouncement = { + ...current, + status: + body.action === 'offline' + ? 'offline' + : startAt && new Date(startAt).getTime() > now.getTime() + ? 'scheduled' + : 'active', + startAt, + endAt, + updatedAt: now.toISOString(), + publishedAt: + body.action === 'publish' + ? (current.publishedAt ?? now.toISOString()) + : current.publishedAt, + }; + mockGameAnnouncements = mockGameAnnouncements.map((item) => + item.id === announcementId ? updated : item, + ); + return envelope(updated, body.action === 'offline' ? '公告已下线' : '公告已发布'); + }, + ), + http.get('*/panel/api/game/orders', async ({ request }) => { + await delay(120); + const url = new URL(request.url); + const keyword = (url.searchParams.get('keyword') || '').trim().toLowerCase(); + const rawStatus = url.searchParams.get('status'); + const rawChannel = url.searchParams.get('channel'); + const serverId = url.searchParams.get('serverId') || 'all'; + const page = Math.max(1, Number(url.searchParams.get('page')) || 1); + const pageSize = Math.max(1, Math.min(100, Number(url.searchParams.get('pageSize')) || 20)); + const status = + rawStatus === 'pending' || + rawStatus === 'paid' || + rawStatus === 'delivered' || + rawStatus === 'refunding' || + rawStatus === 'refunded' || + rawStatus === 'closed' + ? rawStatus + : 'all'; + const channel = + rawChannel === 'official' || + rawChannel === 'app_store' || + rawChannel === 'google_play' || + rawChannel === 'steam' || + rawChannel === 'third_party' + ? rawChannel + : 'all'; + + const filtered = mockGameOrders.filter((order) => { + const matchesKeyword = + !keyword || + order.id.toLowerCase().includes(keyword) || + order.providerOrderId.toLowerCase().includes(keyword) || + order.accountId.toLowerCase().includes(keyword) || + order.accountName.toLowerCase().includes(keyword) || + order.playerId.toLowerCase().includes(keyword) || + order.playerName.toLowerCase().includes(keyword) || + order.productName.toLowerCase().includes(keyword); + const matchesStatus = status === 'all' || order.status === status; + const matchesChannel = channel === 'all' || order.channel === channel; + const matchesServer = serverId === 'all' || order.serverId === serverId; + return matchesKeyword && matchesStatus && matchesChannel && matchesServer; + }); + const start = (page - 1) * pageSize; + const paidStatuses: GameOrderStatus[] = ['paid', 'delivered', 'refunding']; + const paidAmount = mockGameOrders + .filter((order) => paidStatuses.includes(order.status)) + .reduce( + (sum, order) => + sum + (order.currency === 'USD' ? order.amount * ORDER_USD_TO_CNY_RATE : order.amount), + 0, + ); + const summary = { + total: mockGameOrders.length, + paidAmount: Math.round(paidAmount * 100) / 100, + pending: mockGameOrders.filter((order) => order.status === 'pending').length, + awaitingDelivery: mockGameOrders.filter((order) => order.status === 'paid').length, + refunding: mockGameOrders.filter((order) => order.status === 'refunding').length, + }; + const serverOptions = mockGameServers.map((server) => ({ + value: server.id, + label: server.name, + })); + + return envelope({ + items: filtered.slice(start, start + pageSize), + total: filtered.length, + page, + pageSize, + summary, + serverOptions, + }); + }), + http.post('*/panel/api/game/orders/:orderId/deliver', async ({ params, request }) => { + await delay(160); + const orderId = String(params.orderId); + const parsed = GameOrderDeliveryInputSchema.safeParse(await request.json()); + const current = mockGameOrders.find((order) => order.id === orderId); + if (!current) { + return HttpResponse.json({ success: false, msg: '订单不存在', obj: null }, { status: 404 }); + } + if (!parsed.success) { + return HttpResponse.json({ success: false, msg: '补发参数无效', obj: null }, { status: 400 }); + } + if (current.status !== 'paid') { + return HttpResponse.json( + { success: false, msg: '只有待发货订单可以补发', obj: null }, + { status: 409 }, + ); + } + + const now = new Date().toISOString(); + const updated: GameOrder = { + ...current, + status: 'delivered', + deliveredAt: now, + updatedAt: now, + operatorNote: parsed.data.operatorNote, + }; + mockGameOrders = mockGameOrders.map((order) => (order.id === orderId ? updated : order)); + return envelope(updated, '订单商品已补发'); + }), + http.post('*/panel/api/game/orders/:orderId/refund', async ({ params, request }) => { + await delay(180); + const orderId = String(params.orderId); + const parsed = GameOrderRefundInputSchema.safeParse(await request.json()); + const current = mockGameOrders.find((order) => order.id === orderId); + if (!current) { + return HttpResponse.json({ success: false, msg: '订单不存在', obj: null }, { status: 404 }); + } + if (!parsed.success) { + return HttpResponse.json({ success: false, msg: '退款参数无效', obj: null }, { status: 400 }); + } + if (current.status !== 'paid' && current.status !== 'delivered') { + return HttpResponse.json( + { success: false, msg: '当前订单状态不可退款', obj: null }, + { status: 409 }, + ); + } + + const now = new Date().toISOString(); + const updated: GameOrder = { + ...current, + status: 'refunding', + updatedAt: now, + refundReason: parsed.data.reason, + }; + mockGameOrders = mockGameOrders.map((order) => (order.id === orderId ? updated : order)); + return envelope(updated, '退款申请已提交'); + }), +]; diff --git a/src/mocks/index.ts b/src/mocks/index.ts new file mode 100644 index 0000000..1579a0e --- /dev/null +++ b/src/mocks/index.ts @@ -0,0 +1,9 @@ +export async function enableMockApi(): Promise { + if (!import.meta.env.DEV || import.meta.env.VITE_MOCK_API !== 'true') return; + + const { worker } = await import('./browser'); + await worker.start({ + onUnhandledRequest: 'bypass', + quiet: true, + }); +} diff --git a/src/pages/accounts/AccountsPage.css b/src/pages/accounts/AccountsPage.css new file mode 100644 index 0000000..6b94d5b --- /dev/null +++ b/src/pages/accounts/AccountsPage.css @@ -0,0 +1,447 @@ +.game-accounts-page { + --accounts-page-bg: #e6e8ec; + --accounts-card-bg: #ffffff; + --accounts-border: var(--ant-color-border-secondary); + height: 100vh; + min-height: 0; + overflow: hidden; + background: var(--accounts-page-bg); +} + +.game-accounts-page.is-dark { + --accounts-page-bg: #1a1b1f; + --accounts-card-bg: #23252b; +} + +.game-accounts-page.is-dark.is-ultra { + --accounts-page-bg: #000000; + --accounts-card-bg: #101013; +} + +.game-accounts-page .ant-layout, +.game-accounts-page .ant-layout-content, +.accounts-content-shell { + background: transparent; +} + +.accounts-content { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden; + padding: 20px; +} + +.accounts-content-shell { + height: 100%; + min-height: 0; + overflow: hidden; +} + +.accounts-content > .ant-spin, +.accounts-content > .ant-spin > .ant-spin-container { + width: 100%; + min-height: 0; +} + +.accounts-content > .ant-spin { + display: flex; + flex: 1 1 auto; +} + +.accounts-content > .ant-spin > .ant-spin-container { + height: 100%; +} + +.accounts-loading-spacer { + height: 100%; + min-height: 0; +} + +.accounts-shell { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + height: 100%; + min-height: 0; +} + +.accounts-summary-card, +.accounts-filter-card { + flex: 0 0 auto; +} + +.accounts-summary-card, +.accounts-filter-card, +.accounts-table-card { + overflow: hidden; + border-radius: 8px; + background: var(--accounts-card-bg); +} + +.accounts-table-card { + display: flex; + flex: 1 1 0; + min-height: 0; +} + +.accounts-summary-card .ant-card-body { + padding: 0; +} + +.accounts-metric-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + background: var(--accounts-card-bg); +} + +.accounts-metric { + min-width: 0; + padding: 16px 20px; + background: var(--accounts-card-bg); +} + +.accounts-metric-heading { + overflow: hidden; + color: var(--ant-color-text-secondary); + font-size: 12px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accounts-metric-value-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + margin-top: 8px; +} + +.accounts-metric-icon { + display: inline-flex; + flex: 0 0 auto; + color: var(--metric-accent); + font-size: 16px; +} + +.accounts-metric-value { + min-width: 0; + overflow-wrap: anywhere; + color: var(--ant-color-text); + font-size: 22px; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1; +} + +.accounts-filter-card .ant-card-body, +.accounts-table-card .ant-card-body { + min-width: 0; + padding: 0; +} + +.accounts-table-card .ant-card-body { + display: flex; + flex: 1 1 auto; + min-height: 0; +} + +.accounts-table-viewport { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} + +.accounts-filter-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; +} + +.accounts-filter-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + min-width: 0; + margin-inline-start: auto; +} + +.accounts-search { + display: flex; + width: min(420px, 42vw); +} + +.accounts-search .ant-input-affix-wrapper { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.accounts-search .ant-btn { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.accounts-filter-select { + width: 132px; +} + +.accounts-filter-refresh { + flex: 0 0 auto; +} + +.accounts-table-card .ant-table-wrapper { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.accounts-table-card .ant-table-wrapper > .ant-spin, +.accounts-table-card .ant-table-wrapper > .ant-spin > .ant-spin-container, +.accounts-table-card .ant-table { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.accounts-table-card .ant-table-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.accounts-table-card .ant-table-header { + flex: 0 0 auto; +} + +.accounts-table-card .ant-table-body { + flex: 1 1 auto; + min-height: 0; + max-height: none !important; + overflow-y: auto !important; +} + +.accounts-table-card .ant-table-thead .ant-table-cell { + text-align: left !important; + vertical-align: middle; +} + +.accounts-table-card .ant-table-tbody > tr > .ant-table-cell { + text-align: left; + vertical-align: middle; +} + +.accounts-table-card .ant-table-tbody > tr > .accounts-cell-start { + text-align: left; +} + +.accounts-table-card .ant-pagination { + flex: 0 0 auto; + margin-block: 10px; + padding-inline: 16px; +} + +.accounts-identity, +.accounts-stack { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.accounts-identity .ant-typography, +.accounts-stack .ant-typography { + overflow: hidden; + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.accounts-stack .ant-typography-secondary { + font-size: 12px; +} + +.accounts-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; +} + +.accounts-numeric, +.accounts-time { + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +.accounts-row-actions { + min-width: 0; + justify-content: flex-start; +} + +.accounts-row-actions .ant-btn { + width: 32px; + height: 32px; +} + +.accounts-detail { + display: flex; + flex-direction: column; + gap: 18px; +} + +.accounts-detail-heading { + display: grid; + grid-template-columns: 42px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; +} + +.accounts-detail-heading .ant-typography { + margin: 0; +} + +.accounts-detail-heading h4.ant-typography { + overflow-wrap: anywhere; + font-size: 17px; +} + +.accounts-detail-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 6px; + background: var(--ant-color-primary-bg); + color: var(--ant-color-primary); + font-size: 20px; +} + +.accounts-detail .ant-descriptions-item-label { + width: 116px; + white-space: nowrap; +} + +.accounts-detail .ant-descriptions-item-content { + overflow-wrap: anywhere; +} + +.accounts-confirm-copy { + margin-bottom: 0; +} + +.accounts-detail-drawer .ant-drawer-content-wrapper { + height: 100dvh !important; + max-height: 100dvh; +} + +.accounts-detail-drawer .ant-drawer-content { + max-height: 100dvh; + overflow: hidden; +} + +.accounts-detail-drawer .ant-drawer-body { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +@media (max-width: 1120px) { + .accounts-filter-bar { + flex-wrap: wrap; + } + + .accounts-filter-actions { + width: 100%; + margin-inline-start: 0; + } + + .accounts-search { + flex: 1 1 320px; + width: min(100%, 460px); + } +} + +@media (max-width: 768px) { + .accounts-content { + padding: 8px; + } + + .accounts-shell { + gap: 8px; + } + + .accounts-metric { + padding: 12px 8px; + } + + .accounts-metric-heading { + font-size: 11px; + } + + .accounts-metric-value-row { + gap: 5px; + margin-top: 7px; + } + + .accounts-metric-icon { + font-size: 14px; + } + + .accounts-metric-value { + font-size: 20px; + } + + .accounts-filter-bar { + padding: 10px 12px; + } + + .accounts-filter-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px; + } + + .accounts-search { + grid-column: 1 / -1; + width: 100%; + } + + .accounts-filter-select { + width: 100%; + } + + .accounts-filter-refresh { + width: 32px; + padding-inline: 0; + } + + .accounts-table-card .ant-pagination { + padding-inline: 12px; + } + + .accounts-detail-heading { + grid-template-columns: 38px minmax(0, 1fr); + } + + .accounts-detail-heading > .ant-tag { + grid-column: 2; + justify-self: start; + } + + .accounts-detail-icon { + width: 38px; + height: 38px; + } + + .accounts-detail .ant-descriptions-item-label { + width: 96px; + } +} diff --git a/src/pages/accounts/AccountsPage.tsx b/src/pages/accounts/AccountsPage.tsx new file mode 100644 index 0000000..d1710b5 --- /dev/null +++ b/src/pages/accounts/AccountsPage.tsx @@ -0,0 +1,634 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import dayjs from 'dayjs'; +import { + Button, + Card, + ConfigProvider, + Descriptions, + Drawer, + Form, + Input, + Layout, + message, + Modal, + Result, + Select, + Space, + Spin, + Table, + Tag, + Tooltip, + Typography, +} from 'antd'; +import type { TableProps } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import { + CheckCircleOutlined, + EyeOutlined, + IdcardOutlined, + LockOutlined, + ReloadOutlined, + SafetyCertificateOutlined, + SearchOutlined, + StopOutlined, + UnlockOutlined, + UserAddOutlined, + UserOutlined, +} from '@ant-design/icons'; + +import { + useGameAccountsQuery, + useGameAccountStatusMutation, +} from '@/api/queries/useGameAccountsQuery'; +import type { GameAccountFilters } from '@/api/queries/useGameAccountsQuery'; +import { useTheme } from '@/hooks/useTheme'; +import AppSidebar from '@/layouts/AppSidebar'; +import WorkspaceTabs from '@/layouts/WorkspaceTabs'; +import type { GameAccount, GameAccountPlatform, GameAccountStatus } from '@/schemas/gameAccount'; +import { setMessageInstance } from '@/utils/messageBus'; +import './AccountsPage.css'; + +const integerFormatter = new Intl.NumberFormat('zh-CN', { + useGrouping: false, + maximumFractionDigits: 0, +}); +const currencyFormatter = new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + useGrouping: false, + maximumFractionDigits: 0, +}); +const DEFAULT_TABLE_SCROLL_HEIGHT = 240; +const MIN_TABLE_SCROLL_HEIGHT = 96; + +const statusMeta: Record = { + active: { color: 'success', label: '正常' }, + banned: { color: 'error', label: '已封禁' }, + locked: { color: 'warning', label: '已锁定' }, +}; + +const platformLabels: Record = { + android: 'Android', + ios: 'iOS', + pc: 'PC', +}; + +const statusOptions = [ + { value: 'all', label: '全部状态' }, + { value: 'active', label: '正常' }, + { value: 'banned', label: '已封禁' }, + { value: 'locked', label: '已锁定' }, +] satisfies Array<{ value: GameAccountStatus | 'all'; label: string }>; + +const banDurationOptions = [ + { value: 1, label: '1 天' }, + { value: 3, label: '3 天' }, + { value: 7, label: '7 天' }, + { value: 30, label: '30 天' }, + { value: 0, label: '永久' }, +]; + +interface BanFormValues { + reason: string; + durationDays: number; +} + +function formatDateTime(value: string | null) { + return value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'; +} + +function StatusTag({ status }: { status: GameAccountStatus }) { + const meta = statusMeta[status]; + return ( + + {meta.label} + + ); +} + +function SummaryMetric({ + label, + value, + icon, + accent, +}: { + label: string; + value: number; + icon: ReactNode; + accent: string; +}) { + return ( +
+
{label}
+
+ {icon} + {integerFormatter.format(value)} +
+
+ ); +} + +export default function AccountsPage() { + const { isDark, isUltra, antdThemeConfig } = useTheme(); + const [messageApi, messageContextHolder] = message.useMessage(); + useEffect(() => { + setMessageInstance(messageApi); + }, [messageApi]); + const [filters, setFilters] = useState({ + page: 1, + pageSize: 20, + keyword: '', + status: 'all', + platform: 'all', + }); + const [keywordInput, setKeywordInput] = useState(''); + const [selectedAccount, setSelectedAccount] = useState(null); + const [banTarget, setBanTarget] = useState(null); + const [recoverTarget, setRecoverTarget] = useState(null); + const tableViewportRef = useRef(null); + const [tableScrollHeight, setTableScrollHeight] = useState(DEFAULT_TABLE_SCROLL_HEIGHT); + const [banForm] = Form.useForm(); + const statusMutation = useGameAccountStatusMutation(); + const { accounts, total, summary, platformOptions, loading, fetched, fetchError, refresh } = + useGameAccountsQuery(filters); + + useLayoutEffect(() => { + const viewport = tableViewportRef.current; + if (!viewport || !fetched || fetchError) return; + + const measure = () => { + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + const paginationStyle = pagination ? window.getComputedStyle(pagination) : null; + const paginationOuterHeight = pagination + ? pagination.getBoundingClientRect().height + + Number.parseFloat(paginationStyle?.marginTop || '0') + + Number.parseFloat(paginationStyle?.marginBottom || '0') + : 0; + const fixedHeight = (header?.getBoundingClientRect().height ?? 0) + paginationOuterHeight; + const nextHeight = Math.max( + MIN_TABLE_SCROLL_HEIGHT, + Math.floor(viewport.clientHeight - fixedHeight), + ); + + if (viewport.clientHeight > 0) { + setTableScrollHeight((current) => (current === nextHeight ? current : nextHeight)); + } + }; + + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(viewport); + + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + if (header) resizeObserver.observe(header); + if (pagination) resizeObserver.observe(pagination); + + measure(); + return () => resizeObserver.disconnect(); + }, [fetchError, fetched, total]); + + const pageClass = useMemo(() => { + const classes = ['game-accounts-page']; + if (isDark) classes.push('is-dark'); + if (isUltra) classes.push('is-ultra'); + return classes.join(' '); + }, [isDark, isUltra]); + + const updateFilters = useCallback((patch: Partial) => { + setFilters((current) => ({ ...current, ...patch, page: patch.page ?? 1 })); + }, []); + + const openBanModal = useCallback( + (account: GameAccount) => { + setBanTarget(account); + banForm.setFieldsValue({ reason: '', durationDays: 7 }); + }, + [banForm], + ); + + const submitBan = async (values: BanFormValues) => { + if (!banTarget) return; + try { + const updated = await statusMutation.mutateAsync({ + accountId: banTarget.id, + status: 'banned', + reason: values.reason, + durationDays: values.durationDays, + }); + setSelectedAccount((current) => (current?.id === updated.id ? updated : current)); + messageApi.success('账号已封禁'); + setBanTarget(null); + banForm.resetFields(); + } catch { + // The HTTP layer already reports the server error and the modal remains open for correction. + } + }; + + const submitRecover = async () => { + if (!recoverTarget) return; + try { + const updated = await statusMutation.mutateAsync({ + accountId: recoverTarget.id, + status: 'active', + }); + setSelectedAccount((current) => (current?.id === updated.id ? updated : current)); + messageApi.success(recoverTarget.status === 'locked' ? '账号已解锁' : '账号已解封'); + setRecoverTarget(null); + } catch { + // The HTTP layer already reports the server error and keeps the confirmation open. + } + }; + + const columns = useMemo['columns']>( + () => [ + { + title: '账号', + key: 'account', + width: 210, + className: 'accounts-cell-start', + render: (_, account) => ( +
+ {account.username} + + ID {account.id} + +
+ ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 94, + className: 'accounts-cell-start', + render: (status: GameAccountStatus) => , + }, + { + title: '平台 / 渠道', + key: 'platform', + width: 130, + className: 'accounts-cell-start', + render: (_, account) => ( +
+ {platformLabels[account.platform]} + {account.channel} +
+ ), + }, + { + title: '角色数', + dataIndex: 'characterCount', + key: 'characterCount', + width: 82, + className: 'accounts-cell-start', + render: (value: number) => integerFormatter.format(value), + }, + { + title: '设备数', + dataIndex: 'deviceCount', + key: 'deviceCount', + width: 82, + className: 'accounts-cell-start', + render: (value: number) => integerFormatter.format(value), + }, + { + title: '累计充值', + dataIndex: 'totalRecharge', + key: 'totalRecharge', + width: 120, + className: 'accounts-cell-start', + render: (value: number) => ( + {currencyFormatter.format(value)} + ), + }, + { + title: '注册时间', + dataIndex: 'registerTime', + key: 'registerTime', + width: 166, + className: 'accounts-cell-start', + render: (value: string) => {formatDateTime(value)}, + }, + { + title: '最后登录', + key: 'lastLogin', + width: 190, + className: 'accounts-cell-start', + render: (_, account) => ( +
+ {formatDateTime(account.lastLoginTime)} + + {account.lastLoginIp} + +
+ ), + }, + { + title: '操作', + key: 'actions', + width: 112, + fixed: 'right', + className: 'accounts-cell-start', + render: (_, account) => ( + + + + } + /> + ) : ( +
+ +
+ } + accent="#1677ff" + /> + } + accent="#13a8a8" + /> + } + accent="#52c41a" + /> + } + accent="#ff4d4f" + /> + } + accent="#faad14" + /> +
+
+ + +
+
+
+ { + const value = event.target.value; + setKeywordInput(value); + if (value === '' && filters.keyword !== '') { + updateFilters({ keyword: '' }); + } + }} + onPressEnter={() => updateFilters({ keyword: keywordInput.trim() })} + /> + +
+ ({ + value: option.value, + label: option.label, + })), + ]} + onChange={(platform) => updateFilters({ platform })} + /> +
+
+
+ + +
+ + rowKey="id" + size="middle" + columns={columns} + dataSource={accounts} + loading={loading} + scroll={{ x: 1286, y: tableScrollHeight }} + locale={{ emptyText: '暂无符合条件的账号' }} + pagination={{ + current: filters.page, + pageSize: filters.pageSize, + total, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50], + locale: { items_per_page: '/ 页' }, + onChange: (page, pageSize) => + setFilters((current) => ({ ...current, page, pageSize })), + }} + /> +
+
+
+ )} + + + + + setSelectedAccount(null)} + > + {selectedAccount && ( +
+
+ + + +
+ {selectedAccount.username} + + ID {selectedAccount.id} + +
+ +
+ + + + {platformLabels[selectedAccount.platform]} + + {selectedAccount.channel} + + {integerFormatter.format(selectedAccount.characterCount)} + + + {integerFormatter.format(selectedAccount.deviceCount)} + + + {currencyFormatter.format(selectedAccount.totalRecharge)} + + + {formatDateTime(selectedAccount.registerTime)} + + + {formatDateTime(selectedAccount.lastLoginTime)} + + + {selectedAccount.lastLoginIp} + + {selectedAccount.status === 'banned' && ( + <> + + {selectedAccount.banReason || '—'} + + + {formatDateTime(selectedAccount.banUntil)} + + + )} + +
+ )} +
+ + banForm.submit()} + onCancel={() => { + setBanTarget(null); + banForm.resetFields(); + }} + > + + form={banForm} + layout="vertical" + requiredMark={false} + initialValues={{ durationDays: 7 }} + onFinish={submitBan} + > + + + + + { + const value = event.target.value; + setKeywordInput(value); + if (value === '' && filters.keyword !== '') { + updateFilters({ keyword: '' }); + } + }} + onPressEnter={() => updateFilters({ keyword: keywordInput.trim() })} + /> + +
+ updateFilters({ channel })} + /> + , + ...(!editingPublished + ? [ + , + ] + : []), + , + ]} + onCancel={closeEditor} + > + form={editorForm} layout="vertical" requiredMark={false}> + + + + +
+ + + + + + + + + + + setStatusTarget(null)} + > + + {statusTarget + ? `公告“${statusTarget.announcement.title}”将${ + statusTarget.action === 'offline' ? '立即停止展示' : '按设定时间发布' + }。` + : ''} + + + + + ); +} diff --git a/src/pages/cdks/CdksPage.css b/src/pages/cdks/CdksPage.css new file mode 100644 index 0000000..7c57a9a --- /dev/null +++ b/src/pages/cdks/CdksPage.css @@ -0,0 +1,572 @@ +.game-cdks-page { + --cdks-page-bg: #e6e8ec; + --cdks-card-bg: #ffffff; + height: 100vh; + min-height: 0; + overflow: hidden; + background: var(--cdks-page-bg); +} + +.game-cdks-page.is-dark { + --cdks-page-bg: #1a1b1f; + --cdks-card-bg: #23252b; +} + +.game-cdks-page.is-dark.is-ultra { + --cdks-page-bg: #000000; + --cdks-card-bg: #101013; +} + +.game-cdks-page .ant-layout, +.game-cdks-page .ant-layout-content, +.cdks-content-shell { + background: transparent; +} + +.cdks-content { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden; + padding: 20px; +} + +.cdks-content-shell { + height: 100%; + min-height: 0; + overflow: hidden; +} + +.cdks-content > .ant-spin, +.cdks-content > .ant-spin > .ant-spin-container { + width: 100%; + min-height: 0; +} + +.cdks-content > .ant-spin { + display: flex; + flex: 1 1 auto; +} + +.cdks-content > .ant-spin > .ant-spin-container { + height: 100%; +} + +.cdks-loading-spacer { + height: 100%; + min-height: 0; +} + +.cdks-shell { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + height: 100%; + min-height: 0; +} + +.cdks-summary-card, +.cdks-filter-card { + flex: 0 0 auto; +} + +.cdks-summary-card, +.cdks-filter-card, +.cdks-table-card { + overflow: hidden; + border-radius: 8px; + background: var(--cdks-card-bg); +} + +.cdks-table-card { + display: flex; + flex: 1 1 0; + min-height: 0; +} + +.cdks-summary-card .ant-card-body { + padding: 0; +} + +.cdks-metric-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + background: var(--cdks-card-bg); +} + +.cdks-metric { + min-width: 0; + padding: 16px 20px; + background: var(--cdks-card-bg); +} + +.cdks-metric-heading { + overflow: hidden; + color: var(--ant-color-text-secondary); + font-size: 12px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cdks-metric-value-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + margin-top: 8px; +} + +.cdks-metric-icon { + display: inline-flex; + flex: 0 0 auto; + color: var(--metric-accent); + font-size: 16px; +} + +.cdks-metric-value { + min-width: 0; + overflow-wrap: anywhere; + color: var(--ant-color-text); + font-size: 22px; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1; + white-space: nowrap; +} + +.cdks-filter-card .ant-card-body, +.cdks-table-card .ant-card-body { + min-width: 0; + padding: 0; +} + +.cdks-table-card .ant-card-body { + display: flex; + flex: 1 1 auto; + min-height: 0; +} + +.cdks-table-viewport { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} + +.cdks-filter-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; +} + +.cdks-filter-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + min-width: 0; + margin-inline-start: auto; +} + +.cdks-create-button, +.cdks-filter-refresh { + flex: 0 0 auto; +} + +.cdks-search { + display: flex; + width: min(370px, 33vw); +} + +.cdks-search .ant-input-affix-wrapper { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.cdks-search .ant-btn { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.cdks-filter-select { + width: 126px; +} + +.cdks-batch-select { + width: 164px; +} + +.cdks-table-card .ant-table-wrapper { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.cdks-table-card .ant-table-wrapper > .ant-spin, +.cdks-table-card .ant-table-wrapper > .ant-spin > .ant-spin-container, +.cdks-table-card .ant-table { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.cdks-table-card .ant-table-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.cdks-table-card .ant-table-header { + flex: 0 0 auto; +} + +.cdks-table-card .ant-table-body { + flex: 1 1 auto; + min-height: 0; + max-height: none !important; + overflow-y: auto !important; +} + +.cdks-table-card .ant-table-thead .ant-table-cell { + text-align: left !important; + vertical-align: middle; +} + +.cdks-table-card .ant-table-tbody > tr > .ant-table-cell { + text-align: left; + vertical-align: middle; +} + +.cdks-table-card .ant-table-tbody > tr > .cdks-cell-start { + text-align: left; +} + +.cdks-table-card .ant-pagination { + flex: 0 0 auto; + margin-block: 10px; + padding-inline: 16px; +} + +.cdks-identity, +.cdks-stack { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.cdks-identity .ant-typography, +.cdks-stack .ant-typography { + overflow: hidden; + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.cdks-stack .ant-typography-secondary { + font-size: 12px; +} + +.cdks-code, +.cdks-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; +} + +.cdks-code { + overflow-wrap: anywhere; +} + +.cdks-mono { + font-size: 12px; +} + +.cdks-numeric, +.cdks-time { + font-variant-numeric: tabular-nums; +} + +.cdks-time { + white-space: nowrap; +} + +.cdks-reward-summary { + max-width: 100%; +} + +.cdks-row-actions { + min-width: 0; + justify-content: flex-start; +} + +.cdks-row-actions .ant-btn { + width: 32px; + height: 32px; +} + +.cdks-table-card .cdks-actions-cell { + padding-inline: 8px !important; +} + +.cdks-detail { + display: flex; + flex-direction: column; + gap: 18px; +} + +.cdks-detail-heading { + display: grid; + grid-template-columns: 42px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; +} + +.cdks-detail-heading .ant-typography { + margin: 0; +} + +.cdks-detail-heading h4.ant-typography { + font-size: 17px; +} + +.cdks-detail-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 6px; + background: var(--ant-color-primary-bg); + color: var(--ant-color-primary); + font-size: 20px; +} + +.cdks-detail .ant-descriptions-item-label { + width: 116px; + white-space: nowrap; +} + +.cdks-detail .ant-descriptions-item-content { + overflow-wrap: anywhere; +} + +.cdks-detail-rewards { + display: flex; + flex-direction: column; + gap: 6px; +} + +.cdks-detail-rewards > div { + display: flex; + justify-content: space-between; + gap: 16px; +} + +.cdks-detail-drawer .ant-drawer-content-wrapper { + height: 100dvh !important; + max-height: 100dvh; +} + +.cdks-detail-drawer .ant-drawer-content { + max-height: 100dvh; + overflow: hidden; +} + +.cdks-detail-drawer .ant-drawer-body { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +.cdks-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 14px; +} + +.cdks-form-grid .ant-input-number, +.cdks-form-grid .ant-picker { + width: 100%; +} + +.cdks-batch-modal .ant-modal-body { + max-height: calc(100dvh - 210px); + overflow-y: auto; + overscroll-behavior: contain; +} + +.cdks-reward-editor { + border-top: 1px solid var(--ant-color-border-secondary); + padding-top: 12px; +} + +.cdks-reward-editor-heading { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-bottom: 8px; + font-weight: 500; +} + +.cdks-reward-row { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 150px 32px; + align-items: end; + gap: 10px; +} + +.cdks-reward-row .ant-form-item { + min-width: 0; +} + +.cdks-reward-row .ant-input-number { + width: 100%; +} + +.cdks-reward-remove { + margin-bottom: 24px; +} + +.cdks-confirm-copy { + margin-bottom: 0; +} + +@media (max-width: 1240px) { + .cdks-filter-bar { + flex-wrap: wrap; + } + + .cdks-filter-actions { + width: 100%; + margin-inline-start: 0; + } + + .cdks-search { + flex: 1 1 280px; + width: min(100%, 420px); + } +} + +@media (max-width: 900px) { + .cdks-metric { + padding-inline: 14px; + } + + .cdks-metric-value-row { + gap: 6px; + } + + .cdks-metric-value { + font-size: 20px; + } +} + +@media (max-width: 768px) { + .cdks-content { + padding: 8px; + } + + .cdks-shell { + gap: 8px; + } + + .cdks-metric { + padding: 12px 8px; + } + + .cdks-metric-heading { + font-size: 11px; + } + + .cdks-metric-value-row { + gap: 5px; + margin-top: 7px; + } + + .cdks-metric-icon { + font-size: 14px; + } + + .cdks-metric-value { + font-size: 18px; + } + + .cdks-filter-bar { + padding: 10px 12px; + } + + .cdks-filter-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px; + } + + .cdks-create-button, + .cdks-search { + grid-column: 1 / -1; + } + + .cdks-create-button { + justify-self: end; + } + + .cdks-search { + width: 100%; + } + + .cdks-filter-select, + .cdks-batch-select { + width: 100%; + } + + .cdks-filter-refresh { + width: 32px; + padding-inline: 0; + } + + .cdks-table-card .ant-pagination { + padding-inline: 12px; + } + + .cdks-table-card .ant-tag { + margin-inline-end: 0; + padding-inline: 4px; + font-size: 12px; + } + + .cdks-detail-heading { + grid-template-columns: 38px minmax(0, 1fr); + } + + .cdks-detail-heading > .ant-tag { + grid-column: 2; + justify-self: start; + } + + .cdks-detail-icon { + width: 38px; + height: 38px; + } + + .cdks-detail .ant-descriptions-item-label { + width: 96px; + } + + .cdks-form-grid, + .cdks-reward-row { + grid-template-columns: minmax(0, 1fr); + } + + .cdks-reward-remove { + justify-self: end; + margin-top: -16px; + margin-bottom: 18px; + } +} diff --git a/src/pages/cdks/CdksPage.tsx b/src/pages/cdks/CdksPage.tsx new file mode 100644 index 0000000..105fccb --- /dev/null +++ b/src/pages/cdks/CdksPage.tsx @@ -0,0 +1,814 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import dayjs from 'dayjs'; +import type { Dayjs } from 'dayjs'; +import { + Button, + Card, + ConfigProvider, + DatePicker, + Descriptions, + Drawer, + Form, + Input, + InputNumber, + Layout, + message, + Modal, + Result, + Select, + Space, + Spin, + Table, + Tag, + Tooltip, + Typography, +} from 'antd'; +import type { TableProps } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import { + CheckCircleOutlined, + ClockCircleOutlined, + CopyOutlined, + DeleteOutlined, + EyeOutlined, + GiftOutlined, + KeyOutlined, + PlusOutlined, + ReloadOutlined, + SearchOutlined, + StopOutlined, +} from '@ant-design/icons'; + +import { + useCreateGameCdkBatchMutation, + useGameCdksQuery, + useGameCdkStatusMutation, +} from '@/api/queries/useGameCdksQuery'; +import type { GameCdkFilters } from '@/api/queries/useGameCdksQuery'; +import { useTheme } from '@/hooks/useTheme'; +import AppSidebar from '@/layouts/AppSidebar'; +import WorkspaceTabs from '@/layouts/WorkspaceTabs'; +import type { GameCdk, GameCdkReward, GameCdkStatus } from '@/schemas/gameCdk'; +import { setMessageInstance } from '@/utils/messageBus'; +import './CdksPage.css'; + +const integerFormatter = new Intl.NumberFormat('zh-CN', { + useGrouping: false, + maximumFractionDigits: 0, +}); +const DEFAULT_TABLE_SCROLL_HEIGHT = 240; +const MIN_TABLE_SCROLL_HEIGHT = 96; + +const statusMeta: Record = { + unused: { color: 'success', label: '未使用' }, + used: { color: 'processing', label: '已使用' }, + expired: { color: 'default', label: '已过期' }, + disabled: { color: 'error', label: '已停用' }, +}; + +const statusOptions = [ + { value: 'all', label: '全部状态' }, + { value: 'unused', label: '未使用' }, + { value: 'used', label: '已使用' }, + { value: 'expired', label: '已过期' }, + { value: 'disabled', label: '已停用' }, +] satisfies Array<{ value: GameCdkStatus | 'all'; label: string }>; + +interface BatchFormValues { + batchName: string; + prefix: string; + quantity: number; + rewardName: string; + rewards: GameCdkReward[]; + serverId: string; + expiresAt: Dayjs | null; +} + +function formatDateTime(value: string | null) { + return value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'; +} + +function StatusTag({ status }: { status: GameCdkStatus }) { + const meta = statusMeta[status]; + return ( + + {meta.label} + + ); +} + +function SummaryMetric({ + label, + value, + icon, + accent, +}: { + label: string; + value: number; + icon: ReactNode; + accent: string; +}) { + return ( +
+
{label}
+
+ {icon} + {integerFormatter.format(value)} +
+
+ ); +} + +function rewardSummary(rewards: GameCdkReward[]) { + return rewards + .map((reward) => `${reward.itemName} ${integerFormatter.format(reward.quantity)}`) + .join(' / '); +} + +function copyTextFallback(value: string) { + const textarea = document.createElement('textarea'); + textarea.value = value; + textarea.readOnly = true; + textarea.style.position = 'fixed'; + textarea.style.insetInlineStart = '-9999px'; + textarea.style.opacity = '0'; + document.body.appendChild(textarea); + textarea.select(); + const copied = document.execCommand('copy'); + textarea.remove(); + return copied; +} + +export default function CdksPage() { + const { isDark, isUltra, antdThemeConfig } = useTheme(); + const [messageApi, messageContextHolder] = message.useMessage(); + useEffect(() => { + setMessageInstance(messageApi); + }, [messageApi]); + + const [filters, setFilters] = useState({ + page: 1, + pageSize: 20, + keyword: '', + status: 'all', + batchId: 'all', + }); + const [keywordInput, setKeywordInput] = useState(''); + const [selectedCdk, setSelectedCdk] = useState(null); + const [batchModalOpen, setBatchModalOpen] = useState(false); + const [statusTarget, setStatusTarget] = useState(null); + const tableViewportRef = useRef(null); + const [tableScrollHeight, setTableScrollHeight] = useState(DEFAULT_TABLE_SCROLL_HEIGHT); + const [batchForm] = Form.useForm(); + const createMutation = useCreateGameCdkBatchMutation(); + const statusMutation = useGameCdkStatusMutation(); + const { + cdks, + total, + summary, + batchOptions, + serverOptions, + loading, + fetched, + fetchError, + refresh, + } = useGameCdksQuery(filters); + + useLayoutEffect(() => { + const viewport = tableViewportRef.current; + if (!viewport || !fetched || fetchError) return; + + const measure = () => { + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + const paginationStyle = pagination ? window.getComputedStyle(pagination) : null; + const paginationOuterHeight = pagination + ? pagination.getBoundingClientRect().height + + Number.parseFloat(paginationStyle?.marginTop || '0') + + Number.parseFloat(paginationStyle?.marginBottom || '0') + : 0; + const fixedHeight = (header?.getBoundingClientRect().height ?? 0) + paginationOuterHeight; + const nextHeight = Math.max( + MIN_TABLE_SCROLL_HEIGHT, + Math.floor(viewport.clientHeight - fixedHeight), + ); + + if (viewport.clientHeight > 0) { + setTableScrollHeight((current) => (current === nextHeight ? current : nextHeight)); + } + }; + + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(viewport); + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + if (header) resizeObserver.observe(header); + if (pagination) resizeObserver.observe(pagination); + measure(); + return () => resizeObserver.disconnect(); + }, [fetchError, fetched, total]); + + const pageClass = useMemo(() => { + const classes = ['game-cdks-page']; + if (isDark) classes.push('is-dark'); + if (isUltra) classes.push('is-ultra'); + return classes.join(' '); + }, [isDark, isUltra]); + + const updateFilters = useCallback((patch: Partial) => { + setFilters((current) => ({ ...current, ...patch, page: patch.page ?? 1 })); + }, []); + + const openBatchModal = useCallback(() => { + batchForm.setFieldsValue({ + batchName: '', + prefix: '', + quantity: 100, + rewardName: '', + rewards: [{ itemId: 'diamond', itemName: '钻石', quantity: 500 }], + serverId: 'all', + expiresAt: dayjs().add(30, 'day'), + }); + setBatchModalOpen(true); + }, [batchForm]); + + const closeBatchModal = useCallback(() => { + setBatchModalOpen(false); + batchForm.resetFields(); + }, [batchForm]); + + const submitBatch = async (values: BatchFormValues) => { + try { + const result = await createMutation.mutateAsync({ + batchName: values.batchName.trim(), + prefix: values.prefix.trim().toUpperCase(), + quantity: values.quantity, + rewardName: values.rewardName.trim(), + rewards: values.rewards.map((reward) => ({ + itemId: reward.itemId.trim(), + itemName: reward.itemName.trim(), + quantity: reward.quantity, + })), + serverId: values.serverId === 'all' ? null : values.serverId, + expiresAt: values.expiresAt ? values.expiresAt.toISOString() : null, + }); + messageApi.success(`已生成 ${integerFormatter.format(result.createdCount)} 个 CDK`); + closeBatchModal(); + } catch { + // The HTTP layer reports the server error and keeps the editor open for correction. + } + }; + + const copyCode = useCallback( + async (code: string) => { + try { + let copied = false; + try { + await navigator.clipboard.writeText(code); + copied = true; + } catch { + // LAN development runs over HTTP, where the modern Clipboard API is commonly blocked. + copied = copyTextFallback(code); + } + if (!copied) throw new Error('Copy command failed'); + messageApi.success('CDK 已复制'); + } catch { + messageApi.error('复制失败,请手动复制'); + } + }, + [messageApi], + ); + + const submitStatus = async () => { + if (!statusTarget) return; + const nextStatus = statusTarget.status === 'disabled' ? 'unused' : 'disabled'; + try { + const updated = await statusMutation.mutateAsync({ + cdkId: statusTarget.id, + status: nextStatus, + }); + setSelectedCdk((current) => (current?.id === updated.id ? updated : current)); + messageApi.success(nextStatus === 'disabled' ? 'CDK 已停用' : 'CDK 已启用'); + setStatusTarget(null); + } catch { + // The HTTP layer reports the server error and keeps the confirmation open. + } + }; + + const columns = useMemo['columns']>( + () => [ + { + title: 'CDK', + key: 'code', + width: 196, + className: 'cdks-cell-start', + render: (_, cdk) => ( +
+ + {cdk.code} + + + ID {cdk.id} + +
+ ), + }, + { + title: '批次', + key: 'batch', + width: 172, + className: 'cdks-cell-start', + render: (_, cdk) => ( +
+ {cdk.batchName} + + {cdk.batchId} + +
+ ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 92, + className: 'cdks-cell-start', + render: (status: GameCdkStatus) => , + }, + { + title: '奖励内容', + key: 'rewards', + width: 220, + className: 'cdks-cell-start', + render: (_, cdk) => ( +
+ {cdk.rewardName} + + {rewardSummary(cdk.rewards)} + +
+ ), + }, + { + title: '适用范围', + key: 'scope', + width: 132, + className: 'cdks-cell-start', + render: (_, cdk) => (cdk.scope === 'all' ? '全区服通用' : cdk.serverName || '指定区服'), + }, + { + title: '有效期至', + dataIndex: 'expiresAt', + key: 'expiresAt', + width: 168, + className: 'cdks-cell-start', + render: (value: string | null) => ( + {value ? formatDateTime(value) : '永久有效'} + ), + }, + { + title: '兑换信息', + key: 'redemption', + width: 190, + className: 'cdks-cell-start', + render: (_, cdk) => + cdk.usedByPlayerId ? ( +
+ {cdk.usedByPlayerName} + + ID {cdk.usedByPlayerId} · {formatDateTime(cdk.usedAt)} + +
+ ) : ( + '—' + ), + }, + { + title: '创建信息', + key: 'created', + width: 172, + className: 'cdks-cell-start', + render: (_, cdk) => ( +
+ {formatDateTime(cdk.createdAt)} + {cdk.createdBy} +
+ ), + }, + { + title: '操作', + key: 'actions', + width: 112, + fixed: 'right', + className: 'cdks-cell-start cdks-actions-cell', + render: (_, cdk) => ( + + + + } + /> + ) : ( +
+ +
+ } + accent="#1677ff" + /> + } + accent="#52c41a" + /> + } + accent="#13a8a8" + /> + } + accent="#8c8c8c" + /> + } + accent="#ff4d4f" + /> +
+
+ + +
+
+ +
+ { + const value = event.target.value; + setKeywordInput(value); + if (value === '' && filters.keyword !== '') { + updateFilters({ keyword: '' }); + } + }} + onPressEnter={() => updateFilters({ keyword: keywordInput.trim() })} + /> + +
+ updateFilters({ batchId })} + /> +
+
+
+ + +
+ + rowKey="id" + size="middle" + columns={columns} + dataSource={cdks} + loading={loading} + scroll={{ x: 1454, y: tableScrollHeight }} + locale={{ emptyText: '暂无符合条件的 CDK' }} + pagination={{ + current: filters.page, + pageSize: filters.pageSize, + total, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50], + locale: { items_per_page: '/ 页' }, + onChange: (page, pageSize) => + setFilters((current) => ({ ...current, page, pageSize })), + }} + /> +
+
+
+ )} + + + + + setSelectedCdk(null)} + > + {selectedCdk && ( +
+
+ + + +
+ + {selectedCdk.code} + + {selectedCdk.batchName} +
+ +
+ + + + {selectedCdk.batchId} + + {selectedCdk.rewardName} + +
+ {selectedCdk.rewards.map((reward) => ( +
+ {reward.itemName} + + {integerFormatter.format(reward.quantity)} + +
+ ))} +
+
+ + {selectedCdk.scope === 'all' ? '全区服通用' : selectedCdk.serverName} + + {selectedCdk.createdBy} + + {formatDateTime(selectedCdk.createdAt)} + + + {selectedCdk.expiresAt ? formatDateTime(selectedCdk.expiresAt) : '永久有效'} + + {selectedCdk.usedByPlayerId && ( + <> + + {selectedCdk.usedByPlayerName} / {selectedCdk.usedByPlayerId} + + + {formatDateTime(selectedCdk.usedAt)} + + + )} +
+
+ )} +
+ + batchForm.submit()} + onCancel={closeBatchModal} + > + + form={batchForm} + layout="vertical" + requiredMark={false} + onFinish={submitBatch} + > +
+ + + + value.toUpperCase()} + rules={[ + { required: true, whitespace: true, message: '请输入 CDK 前缀' }, + { min: 2, max: 12, message: '前缀长度为 2 至 12 个字符' }, + { pattern: /^[A-Za-z0-9]+$/, message: '仅允许字母和数字' }, + ]} + > + + + + + + + + + + + + + + + + + + +
+ ))} +
+ )} + + + + + setStatusTarget(null)} + > + + {statusTarget + ? `CDK ${statusTarget.code} 将${statusTarget.status === 'disabled' ? '恢复使用' : '停止兑换'}。` + : ''} + + + + + ); +} diff --git a/src/pages/login/LoginPage.css b/src/pages/login/LoginPage.css new file mode 100644 index 0000000..2a72af2 --- /dev/null +++ b/src/pages/login/LoginPage.css @@ -0,0 +1,413 @@ +.login-app { + --bg-page: #eef2ff; + --bg-card: rgba(255, 255, 255, 0.72); + --bg-card-solid: #ffffff; + --color-text: rgba(0, 0, 0, 0.88); + --color-text-subtle: rgba(0, 0, 0, 0.55); + --color-accent: #6366f1; + --color-border: rgba(255, 255, 255, 0.6); + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.04), 0 18px 50px rgba(99, 102, 241, 0.18); + --blob-1: rgba(99, 102, 241, 0.55); + --blob-2: rgba(236, 72, 153, 0.5); + --blob-3: rgba(20, 184, 166, 0.4); + --blob-4: rgba(251, 191, 36, 0.35); + --blob-5: rgba(56, 189, 248, 0.4); + --grid-color: rgba(99, 102, 241, 0.06); + --vignette: radial-gradient(ellipse at center, transparent 30%, rgba(0, 0, 0, 0.05) 100%); + + position: relative; + min-height: 100vh; + overflow: hidden; + background: linear-gradient(135deg, #eef2ff 0%, #fdf2f8 50%, #ecfeff 100%); +} + +.login-app.is-dark { + --bg-page: #0d1117; + --bg-card: rgba(28, 30, 36, 0.55); + --bg-card-solid: #1c1e22; + --color-text: rgba(255, 255, 255, 0.92); + --color-text-subtle: rgba(255, 255, 255, 0.55); + --color-accent: #3b82f6; + --color-border: rgba(255, 255, 255, 0.1); + --shadow-card: 0 1px 3px rgba(0, 0, 0, 0.4), 0 20px 60px rgba(59, 130, 246, 0.22); + --blob-1: rgba(96, 165, 250, 0.42); + --blob-2: rgba(59, 130, 246, 0.42); + --blob-3: rgba(37, 99, 235, 0.35); + --blob-4: rgba(251, 146, 60, 0.18); + --blob-5: rgba(129, 140, 248, 0.3); + --grid-color: rgba(255, 255, 255, 0.04); + --vignette: radial-gradient(ellipse at center, transparent 25%, rgba(0, 0, 0, 0.5) 100%); + background: radial-gradient(ellipse at 25% 20%, #152038 0%, #0d1117 60%); +} + +.login-app.is-dark.is-ultra { + --bg-page: #000; + --bg-card: rgba(15, 17, 28, 0.6); + --bg-card-solid: #101013; + --color-border: rgba(255, 255, 255, 0.06); + --blob-1: rgba(99, 102, 241, 0.3); + --blob-2: rgba(236, 72, 153, 0.22); + --blob-3: rgba(34, 211, 238, 0.2); + --blob-4: rgba(251, 146, 60, 0.15); + --blob-5: rgba(168, 85, 247, 0.25); + --grid-color: rgba(255, 255, 255, 0.025); + --vignette: radial-gradient(ellipse at center, transparent 20%, rgba(0, 0, 0, 0.8) 100%); + background: radial-gradient(ellipse at 25% 20%, #0a0a1a 0%, #000 60%); +} + +.login-app::before, +.login-app::after { + content: ''; + position: absolute; + width: 70vw; + height: 70vw; + max-width: 900px; + max-height: 900px; + border-radius: 50%; + filter: blur(100px); + pointer-events: none; + z-index: 0; + will-change: transform; +} + +.login-app::before { + top: -25vw; + left: -20vw; + background: radial-gradient(circle, var(--blob-1) 0%, transparent 65%); + animation: blob-drift-a 24s ease-in-out infinite alternate; +} + +.login-app::after { + bottom: -25vw; + right: -20vw; + background: radial-gradient(circle, var(--blob-2) 0%, transparent 65%); + animation: blob-drift-b 30s ease-in-out infinite alternate; +} + +.login-content::before { + content: ''; + position: absolute; + top: 30%; + left: 50%; + width: 50vw; + height: 50vw; + max-width: 700px; + max-height: 700px; + border-radius: 50%; + background: radial-gradient(circle, var(--blob-3) 0%, transparent 65%); + filter: blur(100px); + pointer-events: none; + z-index: 0; + will-change: transform; + animation: blob-drift-c 36s ease-in-out infinite alternate; +} + +.login-content::after { + content: ''; + position: absolute; + top: 10%; + right: 15%; + width: 35vw; + height: 35vw; + max-width: 500px; + max-height: 500px; + border-radius: 50%; + background: radial-gradient(circle, var(--blob-4) 0%, transparent 65%); + filter: blur(90px); + pointer-events: none; + z-index: 0; + will-change: transform; + animation: blob-drift-d 28s ease-in-out infinite alternate; +} + +.login-wrapper::before { + content: ''; + position: absolute; + bottom: 5%; + left: 10%; + width: 35vw; + height: 35vw; + max-width: 500px; + max-height: 500px; + border-radius: 50%; + background: radial-gradient(circle, var(--blob-5) 0%, transparent 65%); + filter: blur(90px); + pointer-events: none; + z-index: 0; + will-change: transform; + animation: blob-drift-e 32s ease-in-out infinite alternate; +} + +.login-wrapper::after { + content: ''; + position: absolute; + inset: 0; + background-image: + linear-gradient(var(--grid-color) 1px, transparent 1px), + linear-gradient(90deg, var(--grid-color) 1px, transparent 1px); + background-size: 48px 48px; + background-position: center; + -webkit-mask-image: radial-gradient(ellipse at center, black 30%, transparent 75%); + mask-image: radial-gradient(ellipse at center, black 30%, transparent 75%); + pointer-events: none; + z-index: 0; +} + +@keyframes blob-drift-a { + 0% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(18vw, 10vh) scale(1.15); + } + 100% { + transform: translate(34vw, 22vh) scale(1.25); + } +} + +@keyframes blob-drift-b { + 0% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(-16vw, -10vh) scale(1.12); + } + 100% { + transform: translate(-30vw, -22vh) scale(1.2); + } +} + +@keyframes blob-drift-c { + 0% { + transform: translate(-50%, -50%) scale(1); + } + 50% { + transform: translate(-20%, -20%) scale(1.1); + } + 100% { + transform: translate(-80%, -10%) scale(1.05); + } +} + +@keyframes blob-drift-d { + 0% { + transform: translate(0, 0) scale(0.9); + } + 50% { + transform: translate(-12vw, 14vh) scale(1.05); + } + 100% { + transform: translate(8vw, -8vh) scale(1.1); + } +} + +@keyframes blob-drift-e { + 0% { + transform: translate(0, 0) scale(1); + } + 50% { + transform: translate(14vw, -8vh) scale(1.1); + } + 100% { + transform: translate(-6vw, 12vh) scale(1.15); + } +} + +@media (prefers-reduced-motion: reduce) { + .login-app::before, + .login-app::after, + .login-content::before, + .login-content::after, + .login-wrapper::before { + animation: none; + } +} + +.login-app .ant-layout-content { + background: transparent; +} + +.login-content { + position: relative; +} + +.login-content > * { + position: relative; + z-index: 1; +} + +.login-toolbar { + position: fixed; + top: 16px; + right: 16px; + z-index: 10; + display: inline-flex; + align-items: center; + gap: 8px; +} + +.toolbar-btn { + width: 40px; + height: 40px; + min-width: 40px; + border-radius: 50%; + padding: 0; +} + +.toolbar-btn .anticon { + font-size: 18px; +} + +.login-wrapper { + position: relative; + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 24px 16px; +} + +.login-loading { + text-align: center; +} + +.login-card { + position: relative; + width: 100%; + max-width: 400px; + background: var(--bg-card); + border: 1px solid var(--color-border); + border-radius: 20px; + padding: 40px 32px 28px; + box-shadow: var(--shadow-card); + -webkit-backdrop-filter: blur(24px) saturate(180%); + backdrop-filter: blur(24px) saturate(180%); + z-index: 2; +} + +.login-card::before { + content: ''; + position: absolute; + inset: 0; + border-radius: 20px; + padding: 1px; + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.5), + rgba(255, 255, 255, 0) 40%, + rgba(99, 102, 241, 0.25) 80% + ); + -webkit-mask: + linear-gradient(#000 0 0) content-box, + linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + pointer-events: none; +} + +.login-app.is-dark .login-card::before { + background: linear-gradient( + 135deg, + rgba(255, 255, 255, 0.15), + rgba(255, 255, 255, 0) 40%, + rgba(59, 130, 246, 0.4) 80% + ); +} + +@media (max-width: 480px) { + .login-card { + padding: 32px 20px 24px; + } +} + +.brand { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; + margin-bottom: 8px; +} + +.brand-name { + font-size: 28px; + font-weight: 700; + letter-spacing: 1.5px; + color: var(--color-text); + background: linear-gradient(135deg, var(--color-accent), #ec4899); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.login-app.is-dark .brand-name { + background: linear-gradient(135deg, #60a5fa, #1d4ed8); + -webkit-background-clip: text; + background-clip: text; + -webkit-text-fill-color: transparent; +} + +.brand-accent { + display: block; + width: 40px; + height: 3px; + border-radius: 2px; + background: linear-gradient(90deg, var(--color-accent), #ec4899); +} + +.login-app.is-dark .brand-accent { + background: linear-gradient(90deg, #60a5fa, #1d4ed8); +} + +.welcome { + text-align: center; + color: var(--color-text); + font-size: 32px; + font-weight: 700; + line-height: 1.2; + min-height: 42px; + margin: 12px 0 28px; + letter-spacing: 0.3px; +} + +.welcome b { + display: inline-block; + font-weight: inherit; + animation: headline-in 280ms ease both; +} + +@keyframes headline-in { + 0% { + opacity: 0; + transform: translateY(6px); + } + 100% { + opacity: 1; + transform: translateY(0); + } +} + +@media (prefers-reduced-motion: reduce) { + .welcome b { + animation: none; + } +} + +.login-form .ant-form-item-label > label { + color: var(--color-text); + font-weight: 500; +} + +.login-form input.ant-input:-webkit-autofill, +.login-form input.ant-input:-webkit-autofill:hover, +.login-form input.ant-input:-webkit-autofill:focus { + -webkit-text-fill-color: var(--color-text) !important; + -webkit-box-shadow: 0 0 0 1000px var(--bg-card-solid) inset !important; + box-shadow: 0 0 0 1000px var(--bg-card-solid) inset !important; + transition: + background-color 9999s ease-in-out 0s, + color 9999s ease-in-out 0s; +} + +.submit-row { + margin-bottom: 0; +} diff --git a/src/pages/login/LoginPage.tsx b/src/pages/login/LoginPage.tsx new file mode 100644 index 0000000..31ba478 --- /dev/null +++ b/src/pages/login/LoginPage.tsx @@ -0,0 +1,257 @@ +import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Button, + ConfigProvider, + Form, + Input, + Layout, + Menu, + Popover, + Space, + Spin, + message, +} from 'antd'; +import { + KeyOutlined, + LockOutlined, + MoonFilled, + MoonOutlined, + SunOutlined, + TranslationOutlined, + UserOutlined, +} from '@ant-design/icons'; + +import { FormProvider, useForm } from 'react-hook-form'; +import { HttpUtil, LanguageManager } from '@/utils'; +import { FormField, rhfZodValidate } from '@/components/form/rhf'; +import { setMessageInstance } from '@/utils/messageBus'; +import { pauseAnimationsUntilLeave, useTheme } from '@/hooks/useTheme'; +import { LoginFormSchema, TwoFactorCodeSchema, type LoginFormValues } from '@/schemas/login'; +import './LoginPage.css'; + +const HEADLINE_INTERVAL_MS = 2000; + +type LoginForm = LoginFormValues; + +const basePath = window.ADMIN_UI_BASE_PATH || ''; + +export default function LoginPage() { + const { t } = useTranslation(); + const { isDark, isUltra, toggleTheme, toggleUltra, antdThemeConfig } = useTheme(); + const [messageApi, messageContextHolder] = message.useMessage(); + + useEffect(() => { + setMessageInstance(messageApi); + }, [messageApi]); + + const [fetched, setFetched] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [twoFactorEnable, setTwoFactorEnable] = useState(false); + const [headlineIndex, setHeadlineIndex] = useState(0); + const methods = useForm({ + defaultValues: { username: '', password: '', twoFactorCode: '' }, + }); + const [lang, setLang] = useState(() => LanguageManager.getLanguage()); + + const headlineWords = useMemo(() => [t('pages.login.hello'), t('pages.login.title')], [t]); + + useEffect(() => { + const timer = window.setInterval(() => { + setHeadlineIndex((i) => (i + 1) % headlineWords.length); + }, HEADLINE_INTERVAL_MS); + return () => window.clearInterval(timer); + }, [headlineWords.length]); + + useEffect(() => { + let cancelled = false; + (async () => { + const msg = await HttpUtil.post('/getTwoFactorEnable'); + if (cancelled) return; + if (msg.success) setTwoFactorEnable(!!msg.obj); + setFetched(true); + })(); + return () => { + cancelled = true; + }; + }, []); + + const onSubmit = useCallback(async (values: LoginForm) => { + setSubmitting(true); + try { + const msg = await HttpUtil.post('/login', values); + if (msg.success) window.location.href = basePath + 'panel/'; + } finally { + setSubmitting(false); + } + }, []); + + const onLangChange = useCallback((next: string) => { + setLang(next); + LanguageManager.setLanguage(next); + }, []); + + const cycleTheme = useCallback(() => { + pauseAnimationsUntilLeave('login-theme-cycle'); + if (!isDark) { + toggleTheme(); + if (isUltra) toggleUltra(); + } else if (!isUltra) { + toggleUltra(); + } else { + toggleUltra(); + toggleTheme(); + } + }, [isDark, isUltra, toggleTheme, toggleUltra]); + + const pageClass = useMemo(() => { + const classes = ['login-app']; + if (isDark) classes.push('is-dark'); + if (isUltra) classes.push('is-ultra'); + return classes.join(' '); + }, [isDark, isUltra]); + + const langMenuItems = useMemo( + () => + (LanguageManager.supportedLanguages as { value: string; name: string; icon: string }[]).map( + (l) => ({ + key: l.value, + label: ( + + + {l.name} + + ), + }), + ), + [], + ); + + const themeIcon = !isDark ? : !isUltra ? : ; + + return ( + + {messageContextHolder} + + +
+
+ +
+ {!fetched ? ( +
+ +
+ ) : ( +
+
+ 游戏运营后台 +
+

+ {headlineWords[headlineIndex]} +

+ + +
+ + } + autoComplete="username" + size="large" + placeholder={t('username')} + autoFocus + /> + + + + } + autoComplete="current-password" + size="large" + placeholder={t('password')} + /> + + + {twoFactorEnable && ( + + } + autoComplete="one-time-code" + size="large" + placeholder={t('twoFactorCode')} + /> + + )} + + + + +
+
+
+ )} +
+
+
+
+ ); +} diff --git a/src/pages/orders/OrdersPage.css b/src/pages/orders/OrdersPage.css new file mode 100644 index 0000000..066c2b5 --- /dev/null +++ b/src/pages/orders/OrdersPage.css @@ -0,0 +1,510 @@ +.game-orders-page { + --orders-page-bg: #e6e8ec; + --orders-card-bg: #ffffff; + --orders-border: var(--ant-color-border-secondary); + height: 100vh; + min-height: 0; + overflow: hidden; + background: var(--orders-page-bg); +} + +.game-orders-page.is-dark { + --orders-page-bg: #1a1b1f; + --orders-card-bg: #23252b; +} + +.game-orders-page.is-dark.is-ultra { + --orders-page-bg: #000000; + --orders-card-bg: #101013; +} + +.game-orders-page .ant-layout, +.game-orders-page .ant-layout-content, +.orders-content-shell { + background: transparent; +} + +.orders-content { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden; + padding: 20px; +} + +.orders-content-shell { + height: 100%; + min-height: 0; + overflow: hidden; +} + +.orders-content > .ant-spin, +.orders-content > .ant-spin > .ant-spin-container { + width: 100%; + min-height: 0; +} + +.orders-content > .ant-spin { + display: flex; + flex: 1 1 auto; +} + +.orders-content > .ant-spin > .ant-spin-container { + height: 100%; +} + +.orders-loading-spacer { + height: 100%; + min-height: 0; +} + +.orders-shell { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + height: 100%; + min-height: 0; +} + +.orders-summary-card, +.orders-filter-card { + flex: 0 0 auto; +} + +.orders-summary-card, +.orders-filter-card, +.orders-table-card { + overflow: hidden; + border-radius: 8px; + background: var(--orders-card-bg); +} + +.orders-table-card { + display: flex; + flex: 1 1 0; + min-height: 0; +} + +.orders-summary-card .ant-card-body { + padding: 0; +} + +.orders-metric-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + background: var(--orders-card-bg); +} + +.orders-metric { + min-width: 0; + padding: 16px 20px; + background: var(--orders-card-bg); +} + +.orders-metric-heading { + overflow: hidden; + color: var(--ant-color-text-secondary); + font-size: 12px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +.orders-metric-value-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + margin-top: 8px; +} + +.orders-metric-icon { + display: inline-flex; + flex: 0 0 auto; + color: var(--metric-accent); + font-size: 16px; +} + +.orders-metric-value { + min-width: 0; + overflow-wrap: anywhere; + color: var(--ant-color-text); + font-size: 22px; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1; + white-space: nowrap; +} + +.orders-filter-card .ant-card-body, +.orders-table-card .ant-card-body { + min-width: 0; + padding: 0; +} + +.orders-table-card .ant-card-body { + display: flex; + flex: 1 1 auto; + min-height: 0; +} + +.orders-table-viewport { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} + +.orders-filter-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; +} + +.orders-filter-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + min-width: 0; + margin-inline-start: auto; +} + +.orders-search { + display: flex; + width: min(410px, 38vw); +} + +.orders-search .ant-input-affix-wrapper { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.orders-search .ant-btn { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.orders-filter-select { + width: 126px; +} + +.orders-channel-select { + width: 138px; +} + +.orders-server-select { + width: 150px; +} + +.orders-filter-refresh { + flex: 0 0 auto; +} + +.orders-table-card .ant-table-wrapper { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.orders-table-card .ant-table-wrapper > .ant-spin, +.orders-table-card .ant-table-wrapper > .ant-spin > .ant-spin-container, +.orders-table-card .ant-table { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.orders-table-card .ant-table-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.orders-table-card .ant-table-header { + flex: 0 0 auto; +} + +.orders-table-card .ant-table-body { + flex: 1 1 auto; + min-height: 0; + max-height: none !important; + overflow-y: auto !important; +} + +.orders-table-card .ant-table-thead .ant-table-cell { + text-align: left !important; + vertical-align: middle; +} + +.orders-table-card .ant-table-tbody > tr > .ant-table-cell { + text-align: left; + vertical-align: middle; +} + +.orders-table-card .ant-table-tbody > tr > .orders-cell-start { + text-align: left; +} + +.orders-table-card .ant-pagination { + flex: 0 0 auto; + margin-block: 10px; + padding-inline: 16px; +} + +.orders-identity, +.orders-stack { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.orders-identity .ant-typography, +.orders-stack .ant-typography { + overflow: hidden; + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.orders-stack .ant-typography-secondary { + font-size: 12px; +} + +.orders-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; +} + +.orders-numeric, +.orders-money, +.orders-time-range { + font-variant-numeric: tabular-nums; +} + +.orders-money { + font-weight: 500; +} + +.orders-time-range { + white-space: nowrap; +} + +.orders-row-actions { + min-width: 0; + justify-content: flex-start; +} + +.orders-row-actions .ant-btn { + width: 32px; + height: 32px; +} + +.orders-table-card .orders-actions-cell { + padding-inline: 8px !important; +} + +.orders-detail { + display: flex; + flex-direction: column; + gap: 18px; +} + +.orders-detail-heading { + display: grid; + grid-template-columns: 42px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; +} + +.orders-detail-heading .ant-typography { + margin: 0; +} + +.orders-detail-heading h4.ant-typography { + overflow-wrap: anywhere; + font-size: 17px; +} + +.orders-detail-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 6px; + background: var(--ant-color-primary-bg); + color: var(--ant-color-primary); + font-size: 20px; +} + +.orders-detail .ant-descriptions-item-label { + width: 116px; + white-space: nowrap; +} + +.orders-detail .ant-descriptions-item-content { + overflow-wrap: anywhere; +} + +.orders-detail-items { + display: flex; + flex-direction: column; + gap: 6px; +} + +.orders-detail-items > div { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.orders-detail-drawer .ant-drawer-content-wrapper { + height: 100dvh !important; + max-height: 100dvh; +} + +.orders-detail-drawer .ant-drawer-content { + max-height: 100dvh; + overflow: hidden; +} + +.orders-detail-drawer .ant-drawer-body { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +.orders-refund-modal .ant-modal-body { + max-height: calc(100dvh - 220px); + overflow-y: auto; + overscroll-behavior: contain; +} + +.orders-confirm-copy { + margin-bottom: 0; +} + +@media (max-width: 1180px) { + .orders-filter-bar { + flex-wrap: wrap; + } + + .orders-filter-actions { + width: 100%; + margin-inline-start: 0; + flex-wrap: wrap; + } + + .orders-search { + flex: 1 1 300px; + width: min(100%, 450px); + } +} + +@media (max-width: 900px) { + .orders-metric { + padding-inline: 14px; + } + + .orders-metric-value-row { + gap: 6px; + } + + .orders-metric-value { + font-size: 20px; + } +} + +@media (max-width: 768px) { + .orders-content { + padding: 8px; + } + + .orders-shell { + gap: 8px; + } + + .orders-metric { + padding: 12px 8px; + } + + .orders-metric-heading { + font-size: 11px; + } + + .orders-metric-value-row { + gap: 5px; + margin-top: 7px; + } + + .orders-metric-icon { + font-size: 14px; + } + + .orders-metric-value { + font-size: 18px; + } + + .orders-filter-bar { + padding: 10px 12px; + } + + .orders-filter-actions { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)) 32px; + } + + .orders-search { + grid-column: 1 / -1; + width: 100%; + } + + .orders-filter-select, + .orders-channel-select, + .orders-server-select { + width: 100%; + } + + .orders-filter-refresh { + width: 32px; + padding-inline: 0; + } + + .orders-table-card .ant-pagination { + padding-inline: 12px; + } + + .orders-table-card .ant-tag { + margin-inline-end: 0; + padding-inline: 4px; + font-size: 12px; + } + + .orders-detail-heading { + grid-template-columns: 38px minmax(0, 1fr); + } + + .orders-detail-heading > .ant-tag { + grid-column: 2; + justify-self: start; + } + + .orders-detail-icon { + width: 38px; + height: 38px; + } + + .orders-detail .ant-descriptions-item-label { + width: 96px; + } +} diff --git a/src/pages/orders/OrdersPage.tsx b/src/pages/orders/OrdersPage.tsx new file mode 100644 index 0000000..e56e81b --- /dev/null +++ b/src/pages/orders/OrdersPage.tsx @@ -0,0 +1,696 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import dayjs from 'dayjs'; +import { + Button, + Card, + ConfigProvider, + Descriptions, + Drawer, + Form, + Input, + Layout, + message, + Modal, + Result, + Select, + Space, + Spin, + Table, + Tag, + Tooltip, + Typography, +} from 'antd'; +import type { TableProps } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import { + ClockCircleOutlined, + DollarOutlined, + EyeOutlined, + GiftOutlined, + ReloadOutlined, + RollbackOutlined, + SearchOutlined, + ShoppingCartOutlined, + SyncOutlined, +} from '@ant-design/icons'; + +import { + useDeliverGameOrderMutation, + useGameOrdersQuery, + useRefundGameOrderMutation, +} from '@/api/queries/useGameOrdersQuery'; +import type { GameOrderFilters } from '@/api/queries/useGameOrdersQuery'; +import { useTheme } from '@/hooks/useTheme'; +import AppSidebar from '@/layouts/AppSidebar'; +import WorkspaceTabs from '@/layouts/WorkspaceTabs'; +import type { GameOrder, GameOrderChannel, GameOrderStatus } from '@/schemas/gameOrder'; +import { setMessageInstance } from '@/utils/messageBus'; +import './OrdersPage.css'; + +const integerFormatter = new Intl.NumberFormat('zh-CN', { + useGrouping: false, + maximumFractionDigits: 0, +}); +const moneyFormatter = new Intl.NumberFormat('zh-CN', { + useGrouping: false, + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); +const DEFAULT_TABLE_SCROLL_HEIGHT = 240; +const MIN_TABLE_SCROLL_HEIGHT = 96; + +const statusMeta: Record = { + pending: { color: 'warning', label: '待支付' }, + paid: { color: 'processing', label: '待发货' }, + delivered: { color: 'success', label: '已完成' }, + refunding: { color: 'orange', label: '退款中' }, + refunded: { color: 'purple', label: '已退款' }, + closed: { color: 'default', label: '已关闭' }, +}; + +const channelMeta: Record = { + official: { color: 'blue', label: '官方支付' }, + app_store: { color: 'geekblue', label: 'App Store' }, + google_play: { color: 'green', label: 'Google Play' }, + steam: { color: 'cyan', label: 'Steam' }, + third_party: { color: 'orange', label: '第三方支付' }, +}; + +const statusOptions = [ + { value: 'all', label: '全部状态' }, + { value: 'pending', label: '待支付' }, + { value: 'paid', label: '待发货' }, + { value: 'delivered', label: '已完成' }, + { value: 'refunding', label: '退款中' }, + { value: 'refunded', label: '已退款' }, + { value: 'closed', label: '已关闭' }, +] satisfies Array<{ value: GameOrderStatus | 'all'; label: string }>; + +const channelOptions = [ + { value: 'all', label: '全部渠道' }, + { value: 'official', label: '官方支付' }, + { value: 'app_store', label: 'App Store' }, + { value: 'google_play', label: 'Google Play' }, + { value: 'steam', label: 'Steam' }, + { value: 'third_party', label: '第三方支付' }, +] satisfies Array<{ value: GameOrderChannel | 'all'; label: string }>; + +interface RefundFormValues { + reason: string; +} + +function formatDateTime(value: string | null) { + return value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'; +} + +function formatMoney(amount: number, currency: GameOrder['currency']) { + return `${currency === 'CNY' ? '¥' : '$'}${moneyFormatter.format(amount)}`; +} + +function StatusTag({ status }: { status: GameOrderStatus }) { + const meta = statusMeta[status]; + return ( + + {meta.label} + + ); +} + +function ChannelTag({ channel }: { channel: GameOrderChannel }) { + const meta = channelMeta[channel]; + return {meta.label}; +} + +function SummaryMetric({ + label, + value, + icon, + accent, +}: { + label: string; + value: string; + icon: ReactNode; + accent: string; +}) { + return ( +
+
{label}
+
+ {icon} + {value} +
+
+ ); +} + +function itemSummary(order: GameOrder) { + return order.items + .map((item) => `${item.itemName} ${integerFormatter.format(item.quantity)}`) + .join(' / '); +} + +export default function OrdersPage() { + const { isDark, isUltra, antdThemeConfig } = useTheme(); + const [messageApi, messageContextHolder] = message.useMessage(); + useEffect(() => { + setMessageInstance(messageApi); + }, [messageApi]); + + const [filters, setFilters] = useState({ + page: 1, + pageSize: 20, + keyword: '', + status: 'all', + channel: 'all', + serverId: 'all', + }); + const [keywordInput, setKeywordInput] = useState(''); + const [selectedOrder, setSelectedOrder] = useState(null); + const [deliveryTarget, setDeliveryTarget] = useState(null); + const [refundTarget, setRefundTarget] = useState(null); + const tableViewportRef = useRef(null); + const [tableScrollHeight, setTableScrollHeight] = useState(DEFAULT_TABLE_SCROLL_HEIGHT); + const [refundForm] = Form.useForm(); + const deliveryMutation = useDeliverGameOrderMutation(); + const refundMutation = useRefundGameOrderMutation(); + const { orders, total, summary, serverOptions, loading, fetched, fetchError, refresh } = + useGameOrdersQuery(filters); + + useLayoutEffect(() => { + const viewport = tableViewportRef.current; + if (!viewport || !fetched || fetchError) return; + + const measure = () => { + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + const paginationStyle = pagination ? window.getComputedStyle(pagination) : null; + const paginationOuterHeight = pagination + ? pagination.getBoundingClientRect().height + + Number.parseFloat(paginationStyle?.marginTop || '0') + + Number.parseFloat(paginationStyle?.marginBottom || '0') + : 0; + const fixedHeight = (header?.getBoundingClientRect().height ?? 0) + paginationOuterHeight; + const nextHeight = Math.max( + MIN_TABLE_SCROLL_HEIGHT, + Math.floor(viewport.clientHeight - fixedHeight), + ); + + if (viewport.clientHeight > 0) { + setTableScrollHeight((current) => (current === nextHeight ? current : nextHeight)); + } + }; + + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(viewport); + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + if (header) resizeObserver.observe(header); + if (pagination) resizeObserver.observe(pagination); + measure(); + return () => resizeObserver.disconnect(); + }, [fetchError, fetched, total]); + + const pageClass = useMemo(() => { + const classes = ['game-orders-page']; + if (isDark) classes.push('is-dark'); + if (isUltra) classes.push('is-ultra'); + return classes.join(' '); + }, [isDark, isUltra]); + + const updateFilters = useCallback((patch: Partial) => { + setFilters((current) => ({ ...current, ...patch, page: patch.page ?? 1 })); + }, []); + + const openRefund = useCallback( + (order: GameOrder) => { + refundForm.setFieldsValue({ reason: '' }); + setRefundTarget(order); + }, + [refundForm], + ); + + const closeRefund = useCallback(() => { + setRefundTarget(null); + refundForm.resetFields(); + }, [refundForm]); + + const submitDelivery = async () => { + if (!deliveryTarget) return; + try { + const updated = await deliveryMutation.mutateAsync({ + orderId: deliveryTarget.id, + operatorNote: '运营后台手动补发', + }); + setSelectedOrder((current) => (current?.id === updated.id ? updated : current)); + messageApi.success('订单商品已补发'); + setDeliveryTarget(null); + } catch { + // The HTTP layer reports the server error and keeps the confirmation open. + } + }; + + const submitRefund = async (values: RefundFormValues) => { + if (!refundTarget) return; + try { + const updated = await refundMutation.mutateAsync({ + orderId: refundTarget.id, + reason: values.reason.trim(), + }); + setSelectedOrder((current) => (current?.id === updated.id ? updated : current)); + messageApi.success('退款申请已提交'); + closeRefund(); + } catch { + // The HTTP layer reports the server error and keeps the form open for correction. + } + }; + + const columns = useMemo['columns']>( + () => [ + { + title: '订单号', + key: 'order', + width: 218, + className: 'orders-cell-start', + render: (_, order) => ( +
+ + {order.id} + + + {order.providerOrderId} + +
+ ), + }, + { + title: '玩家账号', + key: 'player', + width: 174, + className: 'orders-cell-start', + render: (_, order) => ( +
+ {order.playerName} + + {order.playerId} / {order.accountName} + +
+ ), + }, + { + title: '区服', + dataIndex: 'serverName', + key: 'serverName', + width: 132, + className: 'orders-cell-start', + }, + { + title: '商品内容', + key: 'product', + width: 220, + className: 'orders-cell-start', + render: (_, order) => ( +
+ + {order.productName} + {order.quantity > 1 ? ` × ${integerFormatter.format(order.quantity)}` : ''} + + + {itemSummary(order)} + +
+ ), + }, + { + title: '实付金额', + key: 'amount', + width: 108, + className: 'orders-cell-start', + render: (_, order) => ( + {formatMoney(order.amount, order.currency)} + ), + }, + { + title: '支付渠道', + dataIndex: 'channel', + key: 'channel', + width: 124, + className: 'orders-cell-start', + render: (channel: GameOrderChannel) => , + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 92, + className: 'orders-cell-start', + render: (status: GameOrderStatus) => , + }, + { + title: '订单时间', + key: 'createdAt', + width: 190, + className: 'orders-cell-start', + render: (_, order) => ( +
+ 创建 {formatDateTime(order.createdAt)} + 支付 {formatDateTime(order.paidAt)} +
+ ), + }, + { + title: '操作', + key: 'actions', + width: 136, + fixed: 'right', + className: 'orders-cell-start orders-actions-cell', + render: (_, order) => ( + + + + } + /> + ) : ( +
+ +
+ } + accent="#1677ff" + /> + } + accent="#52c41a" + /> + } + accent="#faad14" + /> + } + accent="#13a8a8" + /> + } + accent="#ff4d4f" + /> +
+
+ + +
+
+
+ { + const value = event.target.value; + setKeywordInput(value); + if (value === '' && filters.keyword !== '') { + updateFilters({ keyword: '' }); + } + }} + onPressEnter={() => updateFilters({ keyword: keywordInput.trim() })} + /> + +
+ updateFilters({ channel })} + /> + ({ + value: server.id, + label: server.name, + }))} + onChange={setServerId} + /> + setRange(value as OverviewRange)} + /> +
+ } + > +
+ } + accent="#1677ff" + /> + } + accent="#13a8a8" + /> + } + accent="#52c41a" + /> + } + accent="#722ed1" + /> + } + accent="#fa8c16" + formatter={(value) => currencyFormatter.format(value)} + /> + } + accent="#eb2f96" + /> +
+ + +
+ + 当前在线 {integerFormatter.format(overview.metrics.onlinePlayers.value)} + + } + > + integerFormatter.format(Math.round(value))} + tooltipFormatter={(value) => integerFormatter.format(Math.round(value))} + /> + + + + {currencyFormatter.format(overview.metrics.revenue.value)} + + } + > + `¥${Math.round(value / 1000)}k`} + tooltipFormatter={(value) => currencyFormatter.format(Math.round(value))} + /> + +
+ +
+ + + 区服状态 + + } + extra={ + + {overview.servers.length} 个区服 + + } + > + + rowKey="id" + size="small" + columns={serverColumns} + dataSource={overview.servers} + pagination={false} + scroll={{ x: 890 }} + locale={{ emptyText: '暂无区服数据' }} + /> + + + + {overview.alerts.reduce((sum, item) => sum + item.count, 0)} 项 + + } + > + {overview.alerts.length === 0 ? ( + + ) : ( + ( + +
+ +
+ {item.title} + + {item.description} + +
+ +
+
+ )} + /> + )} +
+
+
+ ) : null} + + + + + + ); +} diff --git a/src/pages/players/PlayersPage.css b/src/pages/players/PlayersPage.css new file mode 100644 index 0000000..6711ef0 --- /dev/null +++ b/src/pages/players/PlayersPage.css @@ -0,0 +1,500 @@ +.game-players-page { + --players-page-bg: #e6e8ec; + --players-card-bg: #ffffff; + --players-border: var(--ant-color-border-secondary); + height: 100vh; + min-height: 0; + overflow: hidden; + background: var(--players-page-bg); +} + +.game-players-page.is-dark { + --players-page-bg: #1a1b1f; + --players-card-bg: #23252b; +} + +.game-players-page.is-dark.is-ultra { + --players-page-bg: #000000; + --players-card-bg: #101013; +} + +.game-players-page .ant-layout, +.game-players-page .ant-layout-content, +.players-content-shell { + background: transparent; +} + +.players-content { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + overflow: hidden; + padding: 20px; +} + +.players-content-shell { + height: 100%; + min-height: 0; + overflow: hidden; +} + +.players-content > .ant-spin, +.players-content > .ant-spin > .ant-spin-container { + width: 100%; + min-height: 0; +} + +.players-content > .ant-spin { + display: flex; + flex: 1 1 auto; +} + +.players-content > .ant-spin > .ant-spin-container { + height: 100%; +} + +.players-loading-spacer { + height: 100%; + min-height: 0; +} + +.players-shell { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + height: 100%; + min-height: 0; +} + +.players-summary-card, +.players-filter-card { + flex: 0 0 auto; +} + +.players-summary-card, +.players-filter-card, +.players-table-card { + overflow: hidden; + border-radius: 8px; + background: var(--players-card-bg); +} + +.players-table-card { + display: flex; + flex: 1 1 0; + min-height: 0; +} + +.players-summary-card .ant-card-body { + padding: 0; +} + +.players-metric-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + background: var(--players-card-bg); +} + +.players-metric { + min-width: 0; + padding: 16px 20px; + background: var(--players-card-bg); +} + +.players-metric-heading { + overflow: hidden; + color: var(--ant-color-text-secondary); + font-size: 12px; + line-height: 1.4; + text-overflow: ellipsis; + white-space: nowrap; +} + +.players-metric-value-row { + display: flex; + align-items: center; + gap: 8px; + min-width: 0; + margin-top: 8px; +} + +.players-metric-icon { + display: inline-flex; + flex: 0 0 auto; + color: var(--metric-accent); + font-size: 16px; +} + +.players-metric-value { + min-width: 0; + overflow-wrap: anywhere; + color: var(--ant-color-text); + font-size: 22px; + font-weight: 500; + font-variant-numeric: tabular-nums; + line-height: 1; + white-space: nowrap; +} + +.players-filter-card .ant-card-body, +.players-table-card .ant-card-body { + min-width: 0; + padding: 0; +} + +.players-table-card .ant-card-body { + display: flex; + flex: 1 1 auto; + min-height: 0; +} + +.players-table-viewport { + display: flex; + flex: 1 1 auto; + min-width: 0; + min-height: 0; +} + +.players-filter-bar { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; +} + +.players-filter-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 8px; + min-width: 0; + margin-inline-start: auto; +} + +.players-search { + display: flex; + width: min(410px, 38vw); +} + +.players-search .ant-input-affix-wrapper { + border-start-end-radius: 0; + border-end-end-radius: 0; +} + +.players-search .ant-btn { + border-start-start-radius: 0; + border-end-start-radius: 0; +} + +.players-filter-select { + width: 126px; +} + +.players-server-select { + width: 150px; +} + +.players-filter-refresh { + flex: 0 0 auto; +} + +.players-table-card .ant-table-wrapper { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-width: 0; + min-height: 0; +} + +.players-table-card .ant-table-wrapper > .ant-spin, +.players-table-card .ant-table-wrapper > .ant-spin > .ant-spin-container, +.players-table-card .ant-table { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.players-table-card .ant-table-container { + display: flex; + flex: 1 1 auto; + flex-direction: column; + min-height: 0; +} + +.players-table-card .ant-table-header { + flex: 0 0 auto; +} + +.players-table-card .ant-table-body { + flex: 1 1 auto; + min-height: 0; + max-height: none !important; + overflow-y: auto !important; +} + +.players-table-card .ant-table-thead .ant-table-cell { + text-align: left !important; + vertical-align: middle; +} + +.players-table-card .ant-table-tbody > tr > .ant-table-cell { + text-align: left; + vertical-align: middle; +} + +.players-table-card .ant-table-tbody > tr > .players-cell-start { + text-align: left; +} + +.players-table-card .ant-pagination { + flex: 0 0 auto; + margin-block: 10px; + padding-inline: 16px; +} + +.players-identity, +.players-stack { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.players-identity .ant-typography, +.players-stack .ant-typography { + overflow: hidden; + margin: 0; + text-overflow: ellipsis; + white-space: nowrap; +} + +.players-stack .ant-typography-secondary { + font-size: 12px; +} + +.players-mono { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace; + font-size: 12px; +} + +.players-numeric, +.players-time { + font-variant-numeric: tabular-nums; +} + +.players-time { + white-space: nowrap; +} + +.players-row-actions { + min-width: 0; + justify-content: flex-start; +} + +.players-row-actions .ant-btn { + width: 32px; + height: 32px; +} + +.players-table-card .players-actions-cell { + padding-inline: 8px !important; +} + +.players-detail { + display: flex; + flex-direction: column; + gap: 18px; +} + +.players-detail-heading { + display: grid; + grid-template-columns: 42px minmax(0, 1fr) auto; + align-items: center; + gap: 12px; +} + +.players-detail-heading .ant-typography { + margin: 0; +} + +.players-detail-heading h4.ant-typography { + overflow-wrap: anywhere; + font-size: 17px; +} + +.players-detail-icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 42px; + height: 42px; + border-radius: 6px; + background: var(--ant-color-primary-bg); + color: var(--ant-color-primary); + font-size: 20px; +} + +.players-detail .ant-descriptions-item-label { + width: 116px; + white-space: nowrap; +} + +.players-detail .ant-descriptions-item-content { + overflow-wrap: anywhere; +} + +.players-detail-drawer .ant-drawer-content-wrapper { + height: 100dvh !important; + max-height: 100dvh; +} + +.players-detail-drawer .ant-drawer-content { + max-height: 100dvh; + overflow: hidden; +} + +.players-detail-drawer .ant-drawer-body { + min-height: 0; + overflow-y: auto; + overscroll-behavior: contain; +} + +.players-form-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 0 14px; +} + +.players-form-grid .ant-input-number { + width: 100%; +} + +.players-resource-modal .ant-modal-body { + max-height: calc(100dvh - 220px); + overflow-y: auto; + overscroll-behavior: contain; +} + +.players-confirm-copy { + margin-bottom: 0; +} + +@media (max-width: 1180px) { + .players-filter-bar { + flex-wrap: wrap; + } + + .players-filter-actions { + width: 100%; + margin-inline-start: 0; + } + + .players-search { + flex: 1 1 300px; + width: min(100%, 450px); + } +} + +@media (max-width: 900px) { + .players-metric { + padding-inline: 14px; + } + + .players-metric-value-row { + gap: 6px; + } + + .players-metric-value { + font-size: 20px; + } +} + +@media (max-width: 768px) { + .players-content { + padding: 8px; + } + + .players-shell { + gap: 8px; + } + + .players-metric { + padding: 12px 8px; + } + + .players-metric-heading { + font-size: 11px; + } + + .players-metric-value-row { + gap: 5px; + margin-top: 7px; + } + + .players-metric-icon { + font-size: 14px; + } + + .players-metric-value { + font-size: 18px; + } + + .players-filter-bar { + padding: 10px 12px; + } + + .players-filter-actions { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 32px; + } + + .players-search { + grid-column: 1 / -1; + width: 100%; + } + + .players-filter-select, + .players-server-select { + width: 100%; + } + + .players-filter-refresh { + width: 32px; + padding-inline: 0; + } + + .players-table-card .ant-pagination { + padding-inline: 12px; + } + + .players-table-card .ant-tag { + margin-inline-end: 0; + padding-inline: 4px; + font-size: 12px; + } + + .players-detail-heading { + grid-template-columns: 38px minmax(0, 1fr); + } + + .players-detail-heading > .ant-tag { + grid-column: 2; + justify-self: start; + } + + .players-detail-icon { + width: 38px; + height: 38px; + } + + .players-detail .ant-descriptions-item-label { + width: 96px; + } + + .players-form-grid { + grid-template-columns: minmax(0, 1fr); + } +} diff --git a/src/pages/players/PlayersPage.tsx b/src/pages/players/PlayersPage.tsx new file mode 100644 index 0000000..2b7b4b5 --- /dev/null +++ b/src/pages/players/PlayersPage.tsx @@ -0,0 +1,774 @@ +import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'; +import type { CSSProperties, ReactNode } from 'react'; +import dayjs from 'dayjs'; +import { + Button, + Card, + ConfigProvider, + Descriptions, + Drawer, + Form, + Input, + InputNumber, + Layout, + message, + Modal, + Result, + Select, + Space, + Spin, + Table, + Tag, + Tooltip, + Typography, +} from 'antd'; +import type { TableProps } from 'antd'; +import zhCN from 'antd/locale/zh_CN'; +import { + CheckCircleOutlined, + EditOutlined, + EyeOutlined, + PlayCircleOutlined, + ReloadOutlined, + SearchOutlined, + StopOutlined, + TeamOutlined, + ThunderboltOutlined, + UserAddOutlined, + UserOutlined, +} from '@ant-design/icons'; + +import { + useGamePlayerResourcesMutation, + useGamePlayersQuery, + useGamePlayerStatusMutation, +} from '@/api/queries/useGamePlayersQuery'; +import type { GamePlayerFilters } from '@/api/queries/useGamePlayersQuery'; +import { useTheme } from '@/hooks/useTheme'; +import AppSidebar from '@/layouts/AppSidebar'; +import WorkspaceTabs from '@/layouts/WorkspaceTabs'; +import type { GamePlayer, GamePlayerStatus } from '@/schemas/gamePlayer'; +import { setMessageInstance } from '@/utils/messageBus'; +import './PlayersPage.css'; + +const integerFormatter = new Intl.NumberFormat('zh-CN', { + useGrouping: false, + maximumFractionDigits: 0, +}); +const currencyFormatter = new Intl.NumberFormat('zh-CN', { + style: 'currency', + currency: 'CNY', + useGrouping: false, + maximumFractionDigits: 0, +}); +const DEFAULT_TABLE_SCROLL_HEIGHT = 240; +const MIN_TABLE_SCROLL_HEIGHT = 96; + +const statusMeta: Record = { + online: { color: 'success', label: '在线' }, + offline: { color: 'default', label: '离线' }, + banned: { color: 'error', label: '已封禁' }, +}; + +const statusOptions = [ + { value: 'all', label: '全部状态' }, + { value: 'online', label: '在线' }, + { value: 'offline', label: '离线' }, + { value: 'banned', label: '已封禁' }, +] satisfies Array<{ value: GamePlayerStatus | 'all'; label: string }>; + +const banDurationOptions = [ + { value: 1, label: '1 天' }, + { value: 3, label: '3 天' }, + { value: 7, label: '7 天' }, + { value: 30, label: '30 天' }, + { value: 0, label: '永久' }, +]; + +interface ResourceFormValues { + level: number; + vipLevel: number; + combatPower: number; + gold: number; + diamonds: number; +} + +interface BanFormValues { + reason: string; + durationDays: number; +} + +function formatDateTime(value: string | null) { + return value ? dayjs(value).format('YYYY-MM-DD HH:mm:ss') : '—'; +} + +function StatusTag({ status }: { status: GamePlayerStatus }) { + const meta = statusMeta[status]; + return ( + + {meta.label} + + ); +} + +function SummaryMetric({ + label, + value, + icon, + accent, +}: { + label: string; + value: number; + icon: ReactNode; + accent: string; +}) { + return ( +
+
{label}
+
+ {icon} + {integerFormatter.format(value)} +
+
+ ); +} + +export default function PlayersPage() { + const { isDark, isUltra, antdThemeConfig } = useTheme(); + const [messageApi, messageContextHolder] = message.useMessage(); + useEffect(() => { + setMessageInstance(messageApi); + }, [messageApi]); + + const [filters, setFilters] = useState({ + page: 1, + pageSize: 20, + keyword: '', + status: 'all', + serverId: 'all', + }); + const [keywordInput, setKeywordInput] = useState(''); + const [selectedPlayer, setSelectedPlayer] = useState(null); + const [resourceTarget, setResourceTarget] = useState(null); + const [banTarget, setBanTarget] = useState(null); + const [recoverTarget, setRecoverTarget] = useState(null); + const tableViewportRef = useRef(null); + const [tableScrollHeight, setTableScrollHeight] = useState(DEFAULT_TABLE_SCROLL_HEIGHT); + const [resourceForm] = Form.useForm(); + const [banForm] = Form.useForm(); + const resourceMutation = useGamePlayerResourcesMutation(); + const statusMutation = useGamePlayerStatusMutation(); + const { players, total, summary, serverOptions, loading, fetched, fetchError, refresh } = + useGamePlayersQuery(filters); + + useLayoutEffect(() => { + const viewport = tableViewportRef.current; + if (!viewport || !fetched || fetchError) return; + + const measure = () => { + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + const paginationStyle = pagination ? window.getComputedStyle(pagination) : null; + const paginationOuterHeight = pagination + ? pagination.getBoundingClientRect().height + + Number.parseFloat(paginationStyle?.marginTop || '0') + + Number.parseFloat(paginationStyle?.marginBottom || '0') + : 0; + const fixedHeight = (header?.getBoundingClientRect().height ?? 0) + paginationOuterHeight; + const nextHeight = Math.max( + MIN_TABLE_SCROLL_HEIGHT, + Math.floor(viewport.clientHeight - fixedHeight), + ); + + if (viewport.clientHeight > 0) { + setTableScrollHeight((current) => (current === nextHeight ? current : nextHeight)); + } + }; + + const resizeObserver = new ResizeObserver(measure); + resizeObserver.observe(viewport); + const header = viewport.querySelector('.ant-table-header'); + const pagination = viewport.querySelector('.ant-pagination'); + if (header) resizeObserver.observe(header); + if (pagination) resizeObserver.observe(pagination); + measure(); + return () => resizeObserver.disconnect(); + }, [fetchError, fetched, total]); + + const pageClass = useMemo(() => { + const classes = ['game-players-page']; + if (isDark) classes.push('is-dark'); + if (isUltra) classes.push('is-ultra'); + return classes.join(' '); + }, [isDark, isUltra]); + + const updateFilters = useCallback((patch: Partial) => { + setFilters((current) => ({ ...current, ...patch, page: patch.page ?? 1 })); + }, []); + + const openResourceEditor = useCallback( + (player: GamePlayer) => { + setResourceTarget(player); + resourceForm.setFieldsValue({ + level: player.level, + vipLevel: player.vipLevel, + combatPower: player.combatPower, + gold: player.gold, + diamonds: player.diamonds, + }); + }, + [resourceForm], + ); + + const submitResources = async (values: ResourceFormValues) => { + if (!resourceTarget) return; + try { + const updated = await resourceMutation.mutateAsync({ + playerId: resourceTarget.id, + ...values, + }); + setSelectedPlayer((current) => (current?.id === updated.id ? updated : current)); + messageApi.success('玩家数值已更新'); + setResourceTarget(null); + resourceForm.resetFields(); + } catch { + // The HTTP layer reports the server error and keeps the editor open for correction. + } + }; + + const openBanModal = useCallback( + (player: GamePlayer) => { + setBanTarget(player); + banForm.setFieldsValue({ reason: '', durationDays: 7 }); + }, + [banForm], + ); + + const submitBan = async (values: BanFormValues) => { + if (!banTarget) return; + try { + const updated = await statusMutation.mutateAsync({ + playerId: banTarget.id, + status: 'banned', + reason: values.reason, + durationDays: values.durationDays, + }); + setSelectedPlayer((current) => (current?.id === updated.id ? updated : current)); + messageApi.success('玩家已封禁'); + setBanTarget(null); + banForm.resetFields(); + } catch { + // The HTTP layer reports the server error and keeps the modal open for correction. + } + }; + + const submitRecover = async () => { + if (!recoverTarget) return; + try { + const updated = await statusMutation.mutateAsync({ + playerId: recoverTarget.id, + status: 'offline', + }); + setSelectedPlayer((current) => (current?.id === updated.id ? updated : current)); + messageApi.success('玩家已解封'); + setRecoverTarget(null); + } catch { + // The HTTP layer reports the server error and keeps the confirmation open. + } + }; + + const columns = useMemo['columns']>( + () => [ + { + title: '角色', + key: 'player', + width: 180, + className: 'players-cell-start', + render: (_, player) => ( +
+ {player.name} + + ID {player.id} + +
+ ), + }, + { + title: '所属账号', + key: 'account', + width: 176, + className: 'players-cell-start', + render: (_, player) => ( +
+ {player.accountName} + + ID {player.accountId} + +
+ ), + }, + { + title: '所在区服', + key: 'server', + width: 152, + className: 'players-cell-start', + render: (_, player) => ( +
+ {player.serverName} + + {player.serverId} + +
+ ), + }, + { + title: '状态', + dataIndex: 'status', + key: 'status', + width: 92, + className: 'players-cell-start', + render: (status: GamePlayerStatus) => , + }, + { + title: '等级 / VIP', + key: 'level', + width: 104, + className: 'players-cell-start', + render: (_, player) => ( +
+ Lv. {integerFormatter.format(player.level)} + + VIP {integerFormatter.format(player.vipLevel)} + +
+ ), + }, + { + title: '战力', + dataIndex: 'combatPower', + key: 'combatPower', + width: 116, + className: 'players-cell-start', + render: (value: number) => ( + {integerFormatter.format(value)} + ), + }, + { + title: '金币 / 钻石', + key: 'resources', + width: 164, + className: 'players-cell-start', + render: (_, player) => ( +
+ 金币 {integerFormatter.format(player.gold)} + + 钻石 {integerFormatter.format(player.diamonds)} + +
+ ), + }, + { + title: '累计充值', + dataIndex: 'totalRecharge', + key: 'totalRecharge', + width: 126, + className: 'players-cell-start', + render: (value: number) => ( + {currencyFormatter.format(value)} + ), + }, + { + title: '最后登录', + key: 'lastLogin', + width: 190, + className: 'players-cell-start', + render: (_, player) => ( +
+ {formatDateTime(player.lastLoginTime)} + + {player.lastLoginIp} + +
+ ), + }, + { + title: '操作', + key: 'actions', + width: 112, + fixed: 'right', + className: 'players-cell-start players-actions-cell', + render: (_, player) => ( + + + + } + /> + ) : ( +
+ +
+ } + accent="#1677ff" + /> + } + accent="#52c41a" + /> + } + accent="#13a8a8" + /> + } + accent="#722ed1" + /> + } + accent="#ff4d4f" + /> +
+
+ + +
+
+
+ { + const value = event.target.value; + setKeywordInput(value); + if (value === '' && filters.keyword !== '') { + updateFilters({ keyword: '' }); + } + }} + onPressEnter={() => updateFilters({ keyword: keywordInput.trim() })} + /> + +
+ updateFilters({ serverId })} + /> +
+
+
+ + +
+ + rowKey="id" + size="middle" + columns={columns} + dataSource={players} + loading={loading} + scroll={{ x: 1420, y: tableScrollHeight }} + locale={{ emptyText: '暂无符合条件的玩家' }} + pagination={{ + current: filters.page, + pageSize: filters.pageSize, + total, + showSizeChanger: true, + pageSizeOptions: [10, 20, 50], + locale: { items_per_page: '/ 页' }, + onChange: (page, pageSize) => + setFilters((current) => ({ ...current, page, pageSize })), + }} + /> +
+
+
+ )} + + + + + setSelectedPlayer(null)} + > + {selectedPlayer && ( +
+
+ + + +
+ {selectedPlayer.name} + + ID {selectedPlayer.id} + +
+ +
+ + + + {selectedPlayer.accountName} / {selectedPlayer.accountId} + + {selectedPlayer.serverName} + + {integerFormatter.format(selectedPlayer.level)} + + + {integerFormatter.format(selectedPlayer.vipLevel)} + + + {integerFormatter.format(selectedPlayer.combatPower)} + + + {integerFormatter.format(selectedPlayer.gold)} + + + {integerFormatter.format(selectedPlayer.diamonds)} + + + {currencyFormatter.format(selectedPlayer.totalRecharge)} + + + {formatDateTime(selectedPlayer.createdAt)} + + + {formatDateTime(selectedPlayer.lastLoginTime)} + + + {selectedPlayer.lastLoginIp} + + {selectedPlayer.status === 'banned' && ( + <> + + {selectedPlayer.banReason || '—'} + + + {formatDateTime(selectedPlayer.banUntil)} + + + )} + +
+ )} +
+ + resourceForm.submit()} + onCancel={() => { + setResourceTarget(null); + resourceForm.resetFields(); + }} + > + + form={resourceForm} + layout="vertical" + requiredMark={false} + onFinish={submitResources} + > +
+ + + + + + + + + + + + + + + +
+ +
+ + banForm.submit()} + onCancel={() => { + setBanTarget(null); + banForm.resetFields(); + }} + > + + form={banForm} + layout="vertical" + requiredMark={false} + initialValues={{ durationDays: 7 }} + onFinish={submitBan} + > + + + + + { + const value = event.target.value; + setKeywordInput(value); + if (value === '' && filters.keyword !== '') { + updateFilters({ keyword: '' }); + } + }} + onPressEnter={() => updateFilters({ keyword: keywordInput.trim() })} + /> + +
+ updateFilters({ region })} + /> + + + + ); +} + +describe('FormField', () => { + it('submits values normalized from the Ant Design inputs', async () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.change(screen.getByLabelText('email'), { target: { value: 'a@b.com' } }); + fireEvent.click(screen.getByLabelText('enabled')); + fireEvent.change(screen.getByLabelText('slug'), { target: { value: 'HELLO' } }); + fireEvent.click(screen.getByText('Save')); + + await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + expect(onSubmit.mock.calls[0][0]).toMatchObject({ + email: 'a@b.com', + enabled: true, + slug: 'hello', + bytes: 2 * GB, + }); + }); + + it('applies the transform output before storing the value', async () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.change(screen.getByLabelText('email'), { target: { value: 'x' } }); + const bytes = screen.getByLabelText('bytes'); + fireEvent.change(bytes, { target: { value: '5' } }); + fireEvent.blur(bytes); + fireEvent.click(screen.getByText('Save')); + + await waitFor(() => expect(onSubmit).toHaveBeenCalledTimes(1)); + expect(onSubmit.mock.calls[0][0].bytes).toBe(5 * GB); + }); + + it('surfaces a resolver validation error as help text and blocks submit', async () => { + const onSubmit = vi.fn(); + render(); + + fireEvent.click(screen.getByText('Save')); + + await screen.findByText('email-required'); + expect(onSubmit).not.toHaveBeenCalled(); + }); +}); diff --git a/src/test/setup.components.ts b/src/test/setup.components.ts new file mode 100644 index 0000000..44d209a --- /dev/null +++ b/src/test/setup.components.ts @@ -0,0 +1,100 @@ +import { afterEach, vi } from 'vitest'; +import { cleanup } from '@testing-library/react'; +import i18next from 'i18next'; +import { initReactI18next } from 'react-i18next'; + +import enUS from '../i18n/locales/en-US.json'; + +vi.mock('persian-calendar-suite', () => ({ + PersianDateTimePicker: () => null, +})); + +if (typeof globalThis.localStorage === 'undefined') { + const store = new Map(); + const storage = { + getItem: (k: string) => (store.has(k) ? store.get(k)! : null), + setItem: (k: string, v: string) => { + store.set(k, String(v)); + }, + removeItem: (k: string) => { + store.delete(k); + }, + clear: () => { + store.clear(); + }, + key: (i: number) => Array.from(store.keys())[i] ?? null, + get length() { + return store.size; + }, + } as Storage; + Object.defineProperty(globalThis, 'localStorage', { value: storage, configurable: true }); + Object.defineProperty(globalThis, 'sessionStorage', { value: storage, configurable: true }); +} + +if (!window.matchMedia) { + window.matchMedia = ((query: string) => ({ + matches: false, + media: query, + onchange: null, + addListener: () => {}, + removeListener: () => {}, + addEventListener: () => {}, + removeEventListener: () => {}, + dispatchEvent: () => false, + })) as unknown as typeof window.matchMedia; +} + +if (typeof globalThis.ResizeObserver === 'undefined') { + globalThis.ResizeObserver = class { + observe() {} + unobserve() {} + disconnect() {} + } as unknown as typeof ResizeObserver; +} + +if (!Element.prototype.scrollIntoView) { + Element.prototype.scrollIntoView = () => {}; +} + +// jsdom does not implement pseudo-element styles or Range geometry. Ant +// Design and CodeMirror use these APIs for layout, so supply harmless test +// fallbacks instead of emitting noisy "Not implemented" errors. +const nativeGetComputedStyle = window.getComputedStyle.bind(window); +window.getComputedStyle = ((element: Element) => + nativeGetComputedStyle(element)) as typeof window.getComputedStyle; + +if (!Range.prototype.getClientRects) { + Range.prototype.getClientRects = () => [] as unknown as DOMRectList; +} + +if (!i18next.isInitialized) { + void i18next.use(initReactI18next).init({ + lng: 'en-US', + fallbackLng: 'en-US', + resources: { 'en-US': { translation: enUS } }, + interpolation: { escapeValue: false, prefix: '{', suffix: '}' }, + returnNull: false, + }); +} + +afterEach(async () => { + cleanup(); + document.body.innerHTML = ''; + /* + * React 19 defers passive-effect flushes onto a macrotask (setImmediate), + * whose callback reads `window.event`. If one is still queued when vitest + * tears down the jsdom environment, it fires after `window` is gone and + * throws "window is not defined". Drain a few macrotask ticks here so any + * pending callback runs while `window` still exists. Several ticks are used + * because a microtask resolving mid-drain (rc-trigger/AntD) can queue a new + * one behind the first. + */ + for (let i = 0; i < 3; i += 1) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } +}); + +import { HttpUtil, Msg } from '@/utils'; + +vi.spyOn(HttpUtil, 'post').mockResolvedValue(new Msg(true, '', {})); +vi.spyOn(HttpUtil, 'get').mockResolvedValue(new Msg(true, '', {})); diff --git a/src/test/setup.msw.ts b/src/test/setup.msw.ts new file mode 100644 index 0000000..dc08aeb --- /dev/null +++ b/src/test/setup.msw.ts @@ -0,0 +1,7 @@ +import { afterAll, afterEach, beforeAll } from 'vitest'; + +import { server } from './msw/server'; + +beforeAll(() => server.listen({ onUnhandledRequest: 'bypass' })); +afterEach(() => server.resetHandlers()); +afterAll(() => server.close()); diff --git a/src/test/setup.ts b/src/test/setup.ts new file mode 100644 index 0000000..0795147 --- /dev/null +++ b/src/test/setup.ts @@ -0,0 +1,8 @@ +// Vitest setup. The frontend's Base64 utility (used by link generators) +// reaches for `window.btoa` directly. Node 16+ ships btoa/atob on +// globalThis, so we just alias `window` to `globalThis` instead of +// pulling in jsdom — keeps the test env light and avoids a new dep. + +if (typeof globalThis.window === 'undefined') { + (globalThis as unknown as { window: typeof globalThis }).window = globalThis; +} diff --git a/src/test/storybook-theme.test.tsx b/src/test/storybook-theme.test.tsx new file mode 100644 index 0000000..5ebd6aa --- /dev/null +++ b/src/test/storybook-theme.test.tsx @@ -0,0 +1,57 @@ +import { render } from '@testing-library/react'; +import { afterEach, expect, test } from 'vitest'; + +import { withTheme } from '../../.storybook/preview'; +import { ThemeProvider } from '@/hooks/useTheme'; + +function Story() { + return
Story
; +} + +function StorybookTheme({ theme }: { theme: 'light' | 'dark' }) { + return withTheme(Story, { globals: { theme } } as Partial< + Parameters[1] + > as Parameters[1]); +} + +afterEach(() => { + document.body.className = ''; + document.documentElement.removeAttribute('data-theme'); +}); + +test('preserves unrelated body classes when applying the Storybook theme', () => { + document.body.className = 'storybook-fixture dark'; + document.documentElement.setAttribute('data-theme', 'ultra-dark'); + const { rerender } = render(); + + expect(document.body.classList.contains('storybook-fixture')).toBe(true); + expect(document.body.classList.contains('light')).toBe(true); + expect(document.body.classList.contains('dark')).toBe(false); + expect(document.documentElement.hasAttribute('data-theme')).toBe(false); + + rerender(); + + expect(document.body.classList.contains('storybook-fixture')).toBe(true); + expect(document.body.classList.contains('dark')).toBe(true); + expect(document.body.classList.contains('light')).toBe(false); +}); + +test('preserves unrelated body classes when applying the panel theme', () => { + document.body.className = 'panel-fixture'; + const message = document.createElement('div'); + message.id = 'message'; + message.className = 'message-fixture'; + document.body.append(message); + + render( + +
Panel
+
, + ); + + expect(document.body.classList.contains('panel-fixture')).toBe(true); + expect(document.body.classList.contains('dark')).toBe(true); + expect(document.body.classList.contains('light')).toBe(false); + expect(message.classList.contains('message-fixture')).toBe(true); + expect(message.classList.contains('dark')).toBe(true); +}); diff --git a/src/test/test-utils.tsx b/src/test/test-utils.tsx new file mode 100644 index 0000000..f1c4e54 --- /dev/null +++ b/src/test/test-utils.tsx @@ -0,0 +1,64 @@ +import type { ReactElement } from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import { ThemeProvider } from '@/hooks/useTheme'; + +export function makeTestQueryClient() { + return new QueryClient({ defaultOptions: { queries: { retry: false } } }); +} + +export function renderWithProviders(ui: ReactElement, options?: { queryClient?: QueryClient }) { + const queryClient = options?.queryClient ?? makeTestQueryClient(); + return render( + + {ui} + , + ); +} + +export function fieldLabels(): string[] { + return Array.from(document.querySelectorAll('.ant-form-item-label label')) + .map((el) => (el.textContent ?? '').trim()) + .filter(Boolean); +} + +function selectRootForField(fieldId: string): HTMLElement { + const control = document.getElementById(fieldId); + const select = control?.closest('.ant-select') as HTMLElement | null; + if (!select) throw new Error(`Select not found for field id: ${fieldId}`); + return select; +} + +function openSelect(select: HTMLElement) { + const target = (select.querySelector('.ant-select-selector') ?? select) as HTMLElement; + fireEvent.mouseDown(target); +} + +function openDropdownOptions(): string[] { + return Array.from( + document.querySelectorAll( + '.ant-select-dropdown:not(.ant-select-dropdown-hidden) .ant-select-item-option', + ), + ) + .map((o) => (o.getAttribute('title') ?? o.textContent ?? '').trim()) + .filter(Boolean); +} + +export function listSelectOptions(fieldId: string): string[] { + const select = selectRootForField(fieldId); + openSelect(select); + const opts = openDropdownOptions(); + fireEvent.keyDown(select, { key: 'Escape' }); + return opts; +} + +export function chooseSelectOption(fieldId: string, optionText: string) { + const select = selectRootForField(fieldId); + openSelect(select); + const option = Array.from(document.querySelectorAll('.ant-select-item-option')).find( + (o) => (o.getAttribute('title') ?? o.textContent ?? '').trim() === optionText, + ); + if (!option) throw new Error(`Option '${optionText}' not found for field '${fieldId}'`); + fireEvent.click(option); +} diff --git a/src/test/workspace-tabs.test.tsx b/src/test/workspace-tabs.test.tsx new file mode 100644 index 0000000..0b23a80 --- /dev/null +++ b/src/test/workspace-tabs.test.tsx @@ -0,0 +1,140 @@ +import { fireEvent, screen } from '@testing-library/react'; +import { MemoryRouter, Route, Routes, useLocation } from 'react-router'; +import { afterEach, expect, test } from 'vitest'; + +import WorkspaceTabs from '@/layouts/WorkspaceTabs'; +import { renderWithProviders } from './test-utils'; + +function LocationProbe() { + const location = useLocation(); + return {location.pathname}; +} + +function renderTabs(initialPath: string) { + return renderWithProviders( + + + + + + + } + /> + + , + ); +} + +afterEach(() => { + sessionStorage.clear(); +}); + +test('opens the current route beside the pinned overview tab', () => { + renderTabs('/accounts'); + + expect(screen.getByRole('tab', { name: '总览' }).getAttribute('aria-selected')).toBe('false'); + expect(screen.getByRole('tab', { name: '账号管理' }).getAttribute('aria-selected')).toBe('true'); + expect(screen.queryByRole('button', { name: '关闭 总览' })).toBeNull(); + expect(screen.getByRole('button', { name: '关闭 账号管理' })).not.toBeNull(); + const themeButton = screen.getByRole('button', { name: '主题' }); + const logoutButton = screen.getByRole('button', { name: '退出登录' }); + expect(themeButton).not.toBeNull(); + expect(logoutButton).not.toBeNull(); + expect(themeButton.compareDocumentPosition(logoutButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBe( + Node.DOCUMENT_POSITION_FOLLOWING, + ); +}); + +test('cycles the theme from the workspace toolbar', () => { + renderTabs('/accounts'); + + const before = `${document.body.className}|${document.documentElement.getAttribute('data-theme')}`; + const themeButton = screen.getByRole('button', { name: '主题' }); + fireEvent.click(themeButton); + const after = `${document.body.className}|${document.documentElement.getAttribute('data-theme')}`; + fireEvent.mouseLeave(themeButton); + + expect(after).not.toBe(before); +}); + +test('switches between opened routes', () => { + renderTabs('/accounts'); + + fireEvent.click(screen.getByRole('tab', { name: '总览' })); + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); +}); + +test('closing the active tab returns to the pinned overview', () => { + renderTabs('/accounts'); + + fireEvent.click(screen.getByRole('button', { name: '关闭 账号管理' })); + + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); + expect(screen.queryByRole('tab', { name: '账号管理' })).toBeNull(); + expect(sessionStorage.getItem('game-admin-open-tabs')).toBe('["/"]'); +}); + +test('opens and closes the server management tab', () => { + renderTabs('/servers'); + + expect(screen.getByRole('tab', { name: '区服管理' }).getAttribute('aria-selected')).toBe('true'); + expect(screen.getByRole('button', { name: '关闭 区服管理' })).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '关闭 区服管理' })); + + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); + expect(screen.queryByRole('tab', { name: '区服管理' })).toBeNull(); +}); + +test('opens and closes the player management tab', () => { + renderTabs('/players'); + + expect(screen.getByRole('tab', { name: '玩家管理' }).getAttribute('aria-selected')).toBe('true'); + expect(screen.getByRole('button', { name: '关闭 玩家管理' })).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '关闭 玩家管理' })); + + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); + expect(screen.queryByRole('tab', { name: '玩家管理' })).toBeNull(); +}); + +test('opens and closes the CDK management tab', () => { + renderTabs('/cdks'); + + expect(screen.getByRole('tab', { name: 'CDK管理' }).getAttribute('aria-selected')).toBe('true'); + expect(screen.getByRole('button', { name: '关闭 CDK管理' })).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '关闭 CDK管理' })); + + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); + expect(screen.queryByRole('tab', { name: 'CDK管理' })).toBeNull(); +}); + +test('opens announcement management as a single workspace tab', () => { + renderTabs('/announcements'); + + expect(screen.getByRole('tab', { name: '公告管理' }).getAttribute('aria-selected')).toBe('true'); + expect(screen.getByRole('button', { name: '关闭 公告管理' })).not.toBeNull(); + expect(screen.queryByText('公告分组')).toBeNull(); + expect(screen.queryByText('公告发布')).toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '关闭 公告管理' })); + + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); + expect(screen.queryByRole('tab', { name: '公告管理' })).toBeNull(); +}); + +test('opens and closes the order management tab', () => { + renderTabs('/orders'); + + expect(screen.getByRole('tab', { name: '订单管理' }).getAttribute('aria-selected')).toBe('true'); + expect(screen.getByRole('button', { name: '关闭 订单管理' })).not.toBeNull(); + + fireEvent.click(screen.getByRole('button', { name: '关闭 订单管理' })); + + expect(screen.getByLabelText('当前路由').textContent).toBe('/'); + expect(screen.queryByRole('tab', { name: '订单管理' })).toBeNull(); +}); diff --git a/src/test/zodValidate.test.ts b/src/test/zodValidate.test.ts new file mode 100644 index 0000000..e9427fa --- /dev/null +++ b/src/test/zodValidate.test.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { z } from 'zod'; + +import { Msg } from '@/utils'; +import { parseMsg } from '@/utils/zodValidate'; + +afterEach(() => { + vi.restoreAllMocks(); +}); + +describe('parseMsg', () => { + it('rejects a successful response whose payload violates its schema', () => { + const msg = new Msg(true, '', { id: 'not-a-number' }); + const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + expect(() => + parseMsg(msg, z.object({ id: z.number() }), 'test/value', { strict: true }), + ).toThrow('test/value response failed validation'); + expect(warning).toHaveBeenCalledWith( + '[zod] test/value response failed validation', + expect.arrayContaining([expect.objectContaining({ code: 'invalid_type', path: ['id'] })]), + ); + }); + + it('preserves a missing successful payload for callers that handle empty values', () => { + expect( + parseMsg(new Msg(true, '', null), z.object({ id: z.number() }), 'test/value').obj, + ).toBeNull(); + }); + + it('rejects malformed paged game payloads', () => { + const schema = z.object({ + items: z.array(z.object({ id: z.string() })), + total: z.number(), + page: z.number(), + pageSize: z.number(), + }); + const payload = { items: [], total: 'one', page: 1, pageSize: 20 }; + const warning = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + + expect(() => + parseMsg(new Msg(true, '', payload), schema, 'game/list', { + strict: true, + }), + ).toThrow('game/list response failed validation'); + expect(warning).toHaveBeenCalledWith( + '[zod] game/list response failed validation', + expect.arrayContaining([expect.objectContaining({ code: 'invalid_type', path: ['total'] })]), + ); + }); +}); diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..f282351 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,193 @@ +import { httpRequest } from '@/api/http-init'; +import type { HttpResponse } from '@/api/http-init'; +import { getMessage } from './messageBus'; + +type RespEnvelope = { success?: unknown; msg?: unknown; obj?: unknown }; + +export class Msg { + success: boolean; + msg: string; + obj: T | null; + + constructor(success: boolean = false, msg: string = '', obj: T | null = null) { + this.success = success; + this.msg = msg; + this.obj = obj; + } +} + +export interface HttpOptions { + headers?: Record | Headers; + params?: unknown; + timeout?: number; + signal?: AbortSignal; + silent?: boolean; + silentSuccess?: boolean; +} + +export class HttpUtil { + static _handleMsg(msg: unknown, silentSuccess = false): void { + if (!(msg instanceof Msg) || msg.msg === '') return; + if (msg.success) { + if (!silentSuccess) getMessage().success(msg.msg); + return; + } + getMessage().error(msg.msg); + } + + static _respToMsg(resp: HttpResponse | undefined): Msg { + if (!resp || !resp.data) return new Msg(false, 'No response data'); + const { data } = resp; + if (typeof data === 'object' && data !== null && 'success' in data) { + const envelope = data as RespEnvelope; + return new Msg( + Boolean(envelope.success), + typeof envelope.msg === 'string' ? envelope.msg : '', + envelope.obj ?? null, + ); + } + return typeof data === 'object' ? (data as Msg) : new Msg(false, 'Unknown response data', data); + } + + private static errorMsg(error: unknown): Msg { + const err = error as { + response?: { data?: { msg?: string; message?: string } }; + message?: string; + }; + const data = err.response?.data; + return new Msg(false, data?.msg || data?.message || err.message || 'Request failed'); + } + + static async get( + url: string, + params?: unknown, + options: HttpOptions = {}, + ): Promise> { + const { silent, silentSuccess, ...rest } = options; + try { + const resp = await httpRequest('GET', url, undefined, { ...rest, params }); + const msg = this._respToMsg(resp) as Msg; + if (!silent) this._handleMsg(msg, silentSuccess); + return msg; + } catch (error) { + const msg = this.errorMsg(error); + if (!silent) { + console.error('GET request failed:', error); + this._handleMsg(msg); + } + return msg; + } + } + + static async post( + url: string, + data?: unknown, + options: HttpOptions = {}, + ): Promise> { + const { silent, silentSuccess, ...rest } = options; + try { + const resp = await httpRequest('POST', url, data, rest); + const msg = this._respToMsg(resp) as Msg; + if (!silent) this._handleMsg(msg, silentSuccess); + return msg; + } catch (error) { + const msg = this.errorMsg(error); + if (!silent) { + console.error('POST request failed:', error); + this._handleMsg(msg); + } + return msg; + } + } + + static async delete(url: string, options: HttpOptions = {}): Promise> { + const { silent, silentSuccess, ...rest } = options; + try { + const resp = await httpRequest('DELETE', url, undefined, rest); + const msg = this._respToMsg(resp) as Msg; + if (!silent) this._handleMsg(msg, silentSuccess); + return msg; + } catch (error) { + const msg = this.errorMsg(error); + if (!silent) { + console.error('DELETE request failed:', error); + this._handleMsg(msg); + } + return msg; + } + } +} + +export function applyDocumentTitle(): void { + const host = window.location.hostname; + if (!host) return; + const current = document.title.trim(); + document.title = current ? `${host} - ${current}` : host; +} + +export class CookieManager { + static getCookie(name: string): string { + const prefix = `${name}=`; + for (const rawCookie of document.cookie.split(';')) { + const cookie = rawCookie.trim(); + if (cookie.startsWith(prefix)) return decodeURIComponent(cookie.slice(prefix.length)); + } + return ''; + } + + static setCookie(name: string, value: string, days?: number): void { + let expires = ''; + if (days) { + const date = new Date(); + date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000); + expires = `expires=${date.toUTCString()};`; + } + document.cookie = `${name}=${encodeURIComponent(value)};${expires}path=/`; + } +} + +export interface SupportedLanguage { + name: string; + value: string; + icon: string; +} + +export class LanguageManager { + static readonly supportedLanguages: readonly SupportedLanguage[] = [ + { name: 'العربية', value: 'ar-EG', icon: '🇪🇬' }, + { name: 'English', value: 'en-US', icon: '🇺🇸' }, + { name: 'فارسی', value: 'fa-IR', icon: '🇮🇷' }, + { name: '简体中文', value: 'zh-CN', icon: '🇨🇳' }, + { name: '繁體中文', value: 'zh-TW', icon: '🇹🇼' }, + { name: '日本語', value: 'ja-JP', icon: '🇯🇵' }, + { name: 'Русский', value: 'ru-RU', icon: '🇷🇺' }, + { name: 'Tiếng Việt', value: 'vi-VN', icon: '🇻🇳' }, + { name: 'Español', value: 'es-ES', icon: '🇪🇸' }, + { name: 'Indonesian', value: 'id-ID', icon: '🇮🇩' }, + { name: 'Український', value: 'uk-UA', icon: '🇺🇦' }, + { name: 'Türkçe', value: 'tr-TR', icon: '🇹🇷' }, + { name: 'Português', value: 'pt-BR', icon: '🇧🇷' }, + ]; + + static getLanguage(): string { + const stored = CookieManager.getCookie('lang'); + if (this.isSupportLanguage(stored)) return stored; + + const browserLanguage = window.navigator?.language || 'en-US'; + const exact = this.isSupportLanguage(browserLanguage) ? browserLanguage : ''; + const prefix = browserLanguage.split('-')[0]; + const matched = this.supportedLanguages.find((language) => language.value.startsWith(prefix)); + const language = exact || matched?.value || 'en-US'; + CookieManager.setCookie('lang', language, 365); + return language; + } + + static setLanguage(language: string): void { + CookieManager.setCookie('lang', this.isSupportLanguage(language) ? language : 'en-US', 365); + window.location.reload(); + } + + static isSupportLanguage(language: string): boolean { + return this.supportedLanguages.some((item) => item.value === language); + } +} diff --git a/src/utils/messageBus.ts b/src/utils/messageBus.ts new file mode 100644 index 0000000..a249bf3 --- /dev/null +++ b/src/utils/messageBus.ts @@ -0,0 +1,12 @@ +import { message as staticMessage } from 'antd'; +import type { MessageInstance } from 'antd/es/message/interface'; + +let current: MessageInstance | typeof staticMessage = staticMessage; + +export function setMessageInstance(instance: MessageInstance) { + current = instance; +} + +export function getMessage(): MessageInstance | typeof staticMessage { + return current; +} diff --git a/src/utils/zodValidate.ts b/src/utils/zodValidate.ts new file mode 100644 index 0000000..2b9cd46 --- /dev/null +++ b/src/utils/zodValidate.ts @@ -0,0 +1,24 @@ +import type { z } from 'zod'; +import { Msg } from '@/utils'; + +interface ParseMsgOptions { + strict?: boolean; +} + +export function parseMsg( + msg: Msg, + schema: T, + context: string, + options: ParseMsgOptions = {}, +): Msg> { + if (!msg.success || msg.obj == null) { + return msg as Msg>; + } + const result = schema.safeParse(msg.obj); + if (!result.success) { + console.warn(`[zod] ${context} response failed validation`, result.error.issues); + if (options.strict) throw new Error(`${context} response failed validation`); + return msg as Msg>; + } + return new Msg>(msg.success, msg.msg, result.data); +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..8253291 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "moduleResolution": "bundler", + "allowImportingTsExtensions": false, + "resolveJsonModule": true, + "isolatedModules": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "esModuleInterop": true, + "skipLibCheck": true, + "allowJs": true, + "checkJs": false, + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src/**/*.ts", "src/**/*.tsx"], + "exclude": ["node_modules", "dist"] +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..d8d16aa --- /dev/null +++ b/vite.config.js @@ -0,0 +1,152 @@ +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; +import path from 'node:path'; + +const projectRoot = import.meta.dirname; +const outDir = path.resolve(projectRoot, 'dist'); + +function normalizeBasePath(value) { + const trimmed = String(value || '/').trim(); + const withLeadingSlash = trimmed.startsWith('/') ? trimmed : `/${trimmed}`; + return withLeadingSlash.endsWith('/') ? withLeadingSlash : `${withLeadingSlash}/`; +} + +function rewritePageRequest(request, basePath) { + if (request.method !== 'GET' || !request.url) return; + + const [pathname, query = ''] = request.url.split('?'); + const suffix = query ? `?${query}` : ''; + const panelPath = `${basePath}panel`.replace(/\/+/g, '/'); + const apiPath = `${panelPath}/api`; + + if (pathname === basePath || pathname === basePath.slice(0, -1)) { + request.url = `/login.html${suffix}`; + return; + } + + if ( + pathname !== apiPath && + !pathname.startsWith(`${apiPath}/`) && + (pathname === panelPath || pathname.startsWith(`${panelPath}/`)) + ) { + request.url = `/index.html${suffix}`; + } +} + +function adminPagesPlugin(basePath) { + const configure = (server) => { + server.middlewares.use((request, _response, next) => { + rewritePageRequest(request, basePath); + next(); + }); + }; + + return { + name: 'admin-ui-pages', + apply: 'serve', + transformIndexHtml(html) { + const escaped = basePath.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); + const tag = ``; + return html.replace('', `${tag}`); + }, + configureServer: configure, + configurePreviewServer: configure, + }; +} + +function createProxy(backendTarget) { + if (!backendTarget) return undefined; + return { + '^/(?:[^/]+/)?(?:login|logout|getTwoFactorEnable|csrf-token|panel/api)(?:/|$)': { + target: backendTarget, + changeOrigin: true, + }, + }; +} + +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, projectRoot, ''); + const basePath = normalizeBasePath(env.VITE_ADMIN_BASE_PATH); + const backendTarget = env.VITE_BACKEND_TARGET?.trim(); + + return { + plugins: [react(), adminPagesPlugin(basePath)], + resolve: { + alias: { + '@': path.resolve(projectRoot, 'src'), + }, + }, + experimental: { + renderBuiltUrl(filename, { hostType }) { + if (hostType === 'js') { + return { + runtime: `((window.ADMIN_UI_BASE_PATH||'/')+${JSON.stringify(filename)})`, + }; + } + return undefined; + }, + }, + build: { + outDir, + emptyOutDir: true, + sourcemap: env.VITE_SOURCEMAP === 'true', + target: 'es2020', + chunkSizeWarningLimit: 1500, + rollupOptions: { + input: { + index: path.resolve(projectRoot, 'index.html'), + login: path.resolve(projectRoot, 'login.html'), + }, + output: { + manualChunks(id) { + if (!id.includes('node_modules')) return undefined; + if (id.includes('/node_modules/antd/')) return 'vendor-antd'; + if (id.includes('/@ant-design/icons/') || id.includes('/@ant-design/icons-svg/')) { + return 'vendor-icons'; + } + if ( + id.includes('/node_modules/@rc-component/') || + id.includes('/node_modules/rc-') || + id.includes('/@ant-design/cssinjs') || + id.includes('/@ant-design/colors') || + id.includes('/@ant-design/fast-color') || + id.includes('/@ant-design/react-slick') || + id.includes('/@ctrl/tinycolor') + ) { + return 'vendor-antd'; + } + if ( + id.includes('/node_modules/react-i18next/') || + id.includes('/node_modules/i18next/') + ) { + return 'vendor-i18next'; + } + if ( + id.includes('/node_modules/react/') || + id.includes('/node_modules/react-dom/') || + id.includes('/node_modules/scheduler/') + ) { + return 'vendor-react'; + } + if (id.includes('/node_modules/@tanstack/')) return 'vendor-tanstack'; + if (id.includes('/node_modules/react-hook-form/')) return 'vendor-react-hook-form'; + if (id.includes('/node_modules/react-router')) return 'vendor-router'; + if (id.includes('/node_modules/dayjs/')) return 'vendor-dayjs'; + return 'vendor'; + }, + }, + }, + }, + server: { + host: '0.0.0.0', + port: 19999, + strictPort: true, + proxy: createProxy(backendTarget), + }, + preview: { + host: '0.0.0.0', + port: 19999, + strictPort: true, + }, + }; +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..2e1da94 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,58 @@ +import path from 'node:path'; + +import react from '@vitejs/plugin-react'; +import { storybookTest } from '@storybook/addon-vitest/vitest-plugin'; +import { playwright } from '@vitest/browser-playwright'; +import { defineConfig } from 'vitest/config'; + +const dirname = import.meta.dirname; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + '@': path.resolve(dirname, 'src'), + }, + }, + test: { + globals: false, + // Keep jsdom-heavy form tests within the memory budget of local and CI runners. + maxWorkers: 2, + projects: [ + { + extends: true, + test: { + name: 'unit', + include: ['src/test/**/*.test.ts'], + environment: 'node', + setupFiles: ['./src/test/setup.ts', './src/test/setup.msw.ts'], + }, + }, + { + extends: true, + test: { + name: 'components', + include: ['src/test/**/*.test.tsx'], + environment: 'jsdom', + setupFiles: ['./src/test/setup.ts', './src/test/setup.components.ts'], + }, + }, + { + extends: true, + optimizeDeps: { + include: ['aria-query', 'lz-string', 'pretty-format', 'dom-accessibility-api'], + }, + plugins: [storybookTest({ configDir: path.join(dirname, '.storybook') })], + test: { + name: 'storybook', + browser: { + enabled: true, + headless: true, + provider: playwright({}), + instances: [{ browser: 'chromium' }], + }, + }, + }, + ], + }, +}); diff --git a/vitest.shims.d.ts b/vitest.shims.d.ts new file mode 100644 index 0000000..7782f28 --- /dev/null +++ b/vitest.shims.d.ts @@ -0,0 +1 @@ +/// \ No newline at end of file