From e3337f26a6f5fd71d01caee061c4578665e2643a Mon Sep 17 00:00:00 2001 From: 170-carry Date: Fri, 1 May 2026 00:14:08 +0800 Subject: [PATCH] Initial hyapp admin platform --- .env.example | 1 + .gitignore | 6 + .prettierrc | 5 + AGENTS.md | 95 + README.md | 127 + design.md | 272 +++ eslint.config.js | 32 + index.html | 12 + package.json | 39 + pnpm-lock.yaml | 2880 +++++++++++++++++++++++ src/App.jsx | 132 ++ src/App.test.jsx | 27 + src/api/auth.js | 21 + src/api/dashboard.js | 5 + src/api/logs.js | 9 + src/api/navigation.js | 5 + src/api/notifications.js | 9 + src/api/request.js | 105 + src/api/roles.js | 25 + src/api/search.js | 5 + src/api/services.js | 25 + src/api/users.js | 33 + src/auth/AuthProvider.jsx | 105 + src/components/base/Button.jsx | 49 + src/components/base/Card.jsx | 9 + src/components/base/ConfirmProvider.jsx | 58 + src/components/base/DataState.jsx | 28 + src/components/base/FilterChip.jsx | 25 + src/components/base/IconButton.jsx | 43 + src/components/base/KpiCard.jsx | 19 + src/components/base/PaginationBar.jsx | 26 + src/components/base/SearchBox.jsx | 18 + src/components/base/SegmentControl.jsx | 53 + src/components/base/TimeRangeSelect.jsx | 23 + src/components/base/ToastProvider.jsx | 41 + src/components/charts/BarChart.jsx | 44 + src/components/charts/ChartCard.jsx | 15 + src/components/charts/EChart.jsx | 70 + src/components/charts/LineChart.jsx | 41 + src/components/charts/StatusDonut.jsx | 51 + src/components/layout/Header.jsx | 155 ++ src/components/layout/PageHead.jsx | 11 + src/components/layout/PageSkeleton.jsx | 36 + src/components/layout/Sidebar.jsx | 181 ++ src/components/search/SearchDialog.jsx | 76 + src/components/service/DetailPanel.jsx | 137 ++ src/components/service/ResourceCell.jsx | 12 + src/components/service/RowActions.jsx | 44 + src/components/service/ServerRail.jsx | 27 + src/components/service/ServiceTable.jsx | 55 + src/components/service/StatusBadge.jsx | 16 + src/components/user/UserStatusBadge.jsx | 16 + src/components/user/UserTable.jsx | 148 ++ src/config/env.js | 1 + src/constants/navigation.js | 105 + src/constants/status.js | 8 + src/data/services.js | 108 + src/data/users.js | 58 + src/main.jsx | 28 + src/pages/LoginPage.jsx | 76 + src/pages/LogsPage.jsx | 107 + src/pages/MenuManagementPage.jsx | 81 + src/pages/NotificationsPage.jsx | 76 + src/pages/OverviewPage.jsx | 173 ++ src/pages/RoleManagementPage.jsx | 194 ++ src/pages/ServicesPage.jsx | 232 ++ src/pages/UserManagementPage.jsx | 335 +++ src/styles/app.css | 2290 ++++++++++++++++++ src/styles/base.css | 40 + src/styles/tokens.css | 52 + src/test/setup.js | 1 + src/theme.js | 170 ++ src/utils/time.js | 8 + vite.config.js | 22 + 74 files changed, 9667 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .prettierrc create mode 100644 AGENTS.md create mode 100644 README.md create mode 100644 design.md create mode 100644 eslint.config.js create mode 100644 index.html create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 src/App.jsx create mode 100644 src/App.test.jsx create mode 100644 src/api/auth.js create mode 100644 src/api/dashboard.js create mode 100644 src/api/logs.js create mode 100644 src/api/navigation.js create mode 100644 src/api/notifications.js create mode 100644 src/api/request.js create mode 100644 src/api/roles.js create mode 100644 src/api/search.js create mode 100644 src/api/services.js create mode 100644 src/api/users.js create mode 100644 src/auth/AuthProvider.jsx create mode 100644 src/components/base/Button.jsx create mode 100644 src/components/base/Card.jsx create mode 100644 src/components/base/ConfirmProvider.jsx create mode 100644 src/components/base/DataState.jsx create mode 100644 src/components/base/FilterChip.jsx create mode 100644 src/components/base/IconButton.jsx create mode 100644 src/components/base/KpiCard.jsx create mode 100644 src/components/base/PaginationBar.jsx create mode 100644 src/components/base/SearchBox.jsx create mode 100644 src/components/base/SegmentControl.jsx create mode 100644 src/components/base/TimeRangeSelect.jsx create mode 100644 src/components/base/ToastProvider.jsx create mode 100644 src/components/charts/BarChart.jsx create mode 100644 src/components/charts/ChartCard.jsx create mode 100644 src/components/charts/EChart.jsx create mode 100644 src/components/charts/LineChart.jsx create mode 100644 src/components/charts/StatusDonut.jsx create mode 100644 src/components/layout/Header.jsx create mode 100644 src/components/layout/PageHead.jsx create mode 100644 src/components/layout/PageSkeleton.jsx create mode 100644 src/components/layout/Sidebar.jsx create mode 100644 src/components/search/SearchDialog.jsx create mode 100644 src/components/service/DetailPanel.jsx create mode 100644 src/components/service/ResourceCell.jsx create mode 100644 src/components/service/RowActions.jsx create mode 100644 src/components/service/ServerRail.jsx create mode 100644 src/components/service/ServiceTable.jsx create mode 100644 src/components/service/StatusBadge.jsx create mode 100644 src/components/user/UserStatusBadge.jsx create mode 100644 src/components/user/UserTable.jsx create mode 100644 src/config/env.js create mode 100644 src/constants/navigation.js create mode 100644 src/constants/status.js create mode 100644 src/data/services.js create mode 100644 src/data/users.js create mode 100644 src/main.jsx create mode 100644 src/pages/LoginPage.jsx create mode 100644 src/pages/LogsPage.jsx create mode 100644 src/pages/MenuManagementPage.jsx create mode 100644 src/pages/NotificationsPage.jsx create mode 100644 src/pages/OverviewPage.jsx create mode 100644 src/pages/RoleManagementPage.jsx create mode 100644 src/pages/ServicesPage.jsx create mode 100644 src/pages/UserManagementPage.jsx create mode 100644 src/styles/app.css create mode 100644 src/styles/base.css create mode 100644 src/styles/tokens.css create mode 100644 src/test/setup.js create mode 100644 src/theme.js create mode 100644 src/utils/time.js create mode 100644 vite.config.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..14ea4ad --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +VITE_API_BASE_URL=/api diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d978a13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.env +node_modules/ +dist/ +coverage/ +*.log diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..1b64a01 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "tabWidth": 4, + "useTabs": false, + "printWidth": 120 +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8676a71 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,95 @@ +# AGENTS.md + +## 项目约束 + +1. 写代码不要猜。遇到不确定的依赖版本、导出名称、组件 API、项目结构、样式约定,先查本地文件、`node_modules` 或实际命令输出。 +2. 写前端或客户端项目时,不要添加无意义的描述文案。 +3. 包管理器固定使用 `pnpm`,不要生成 npm/yarn 锁文件。 +4. 基础 UI 使用 MUI,但视觉风格必须保持当前白色 HYApp 管理平台风格。 +5. 修改 MUI 组件颜色、尺寸、hover、focus、active 状态时,优先通过 `src/theme.js` 或项目封装组件处理,避免在页面里硬写零散覆盖。 +6. 图表必须使用图表库。本项目使用 ECharts,不手写业务图表。 +7. 组件必须模块化。不要把页面、基础组件、图表、业务表格、抽屉混在单个文件里。 +8. 页面加载必须有动画占位图,并且占位图要铺满内容区域。 +9. 动画要覆盖页面切换、按钮、菜单、输入框、选择框、表格、图表、弹窗/抽屉等交互,同时保留 `prefers-reduced-motion` 降级。 +10. Header 右侧放操作组件,左侧只放菜单展开/收起按钮和当前页 tab。 +11. 时间筛选属于总览页面内容,不属于 Header。 +12. 顶部搜索框点击后打开大搜索弹窗,输入后展示列表,列表项必须可点击。 +13. 左侧菜单 active 样式优先级必须高于 hover 样式。 +14. 侧边栏展开/收起按钮必须跟随状态切换图标。 +15. 展开态 Logo 区域和侧边栏宽度保持 248px。 + +## 当前实现方式 + +- 框架:React + Vite。 +- UI 组件库:MUI。 +- 图表库:ECharts。 +- 包管理器:pnpm。 +- 页面:`src/pages/OverviewPage.jsx`、`src/pages/ServicesPage.jsx` 和 `src/pages/UserManagementPage.jsx`。 +- 应用壳:`src/App.jsx`,只做状态编排、懒加载页面、抽屉挂载和事件分发。 +- 页面加载占位:`src/components/layout/PageSkeleton.jsx`。 +- 顶部搜索弹窗:`src/components/search/SearchDialog.jsx`。 +- MUI 主题:`src/theme.js`。 +- CSS token:`src/styles/tokens.css`。 +- 主要样式和动画:`src/styles/app.css`。 + +## 目录规范 + +- `src/components/base/`:项目基础组件封装,内部可以使用 MUI。 +- `src/components/layout/`:布局组件。 +- `src/components/charts/`:ECharts 封装和图表组件。 +- `src/components/service/`:服务管理相关业务组件。 +- `src/pages/`:页面级模块。 +- `src/constants/`:导航、状态等常量。 +- `src/data/`:演示数据。 +- `src/utils/`:工具函数。 + +## 按需导入规则 + +- MUI 组件必须路径导入,例如: + +```js +import Button from "@mui/material/Button"; +``` + +- MUI 图标必须路径导入,例如: + +```js +import Refresh from "@mui/icons-material/Refresh"; +``` + +- 不要使用总入口: + +```js +import { Button } from "@mui/material"; +import { Refresh } from "@mui/icons-material"; +``` + +- ECharts 只在 `src/components/charts/` 内封装和加载,不要在页面里直接散落图表初始化逻辑。 +- 页面和抽屉继续使用 `React.lazy` 做拆分加载。 +- 顶部搜索弹窗继续使用 lazy 加载,不要静态塞进主入口。 + +## 样式规则 + +- 状态色不要随意改: + - `--success`:运行中 + - `--warning`:警告 + - `--danger`:异常/危险操作 + - `--stopped`:已停止 +- 新增颜色优先写入 token 或 theme,不要在多个组件里重复硬编码。 +- 基础组件的视觉兼容优先改封装组件,不要让页面直接依赖 MUI 细节。 +- 不添加卡片套卡片。 +- 不添加无业务意义的说明区、营销区、空泛介绍区。 + +## 验证要求 + +每次完成前端改动后至少运行: + +```bash +pnpm build +``` + +如果需要本地预览: + +```bash +pnpm dev +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..b55f435 --- /dev/null +++ b/README.md @@ -0,0 +1,127 @@ +# HYApp 管理平台前端 + +基于 `React + Vite + MUI + ECharts` 从 `deploy-platform` 抽离出的白色主题 HYApp 管理平台前端。包管理器统一使用 `pnpm`。 + +## 用户要求 + +- 写代码不要猜:不确定的依赖、API、文件结构、现有约定,先查本地项目或依赖实际导出。 +- 写前端或客户端项目时,不添加无意义的描述文案。 +- 使用 `pnpm`。 +- 页面包含:`总览`、`服务管理` 和 `用户管理`。 +- 所有组件都要模块化,包括基础组件、布局组件、业务组件、图表组件。 +- 基础 UI 使用 MUI,但整体风格必须统一为白色后台管理平台风格。 +- 修改 MUI 组件颜色和样式时注意兼容性,优先通过主题和封装组件统一处理。 +- 图表使用图表库,不手写业务图表。 +- MUI、图标、图表能力尽量按需导入。 +- 页面加载时需要占位图,占位图要有动画效果,并且刚好铺满内容区域。 +- 动画效果要覆盖页面切换、按钮、菜单、弹窗、输入框、选择框、表格、图表等交互。 +- Header 里的操作组件靠右,左侧只保留菜单收起按钮和当前页 tab。 +- 时间筛选放在总览页面内容区,不放在 Header。 +- 点击顶部搜索框打开大搜索弹窗,弹窗输入框展示可点击结果列表。 +- 左侧菜单 hover 不能覆盖 active 视觉状态。 +- 当前页 tab 需要有简单阴影效果。 +- 总览旁边的菜单按钮负责展开/收起侧边栏,并且图标要跟随状态变化。 +- Logo 区域和展开态侧边栏宽度要比初始版本更宽。 + +## 技术栈 + +- React 19 +- Vite 8 +- MUI 9 +- ECharts 6 +- pnpm + +## 运行 + +```bash +pnpm install +pnpm dev +``` + +默认访问: + +```text +http://localhost:7001 +``` + +构建: + +```bash +pnpm build +``` + +## 实现写法 + +- `src/App.jsx` 只负责应用状态、页面切换、抽屉挂载和数据分发。 +- 页面模块放在 `src/pages/`: + - `OverviewPage.jsx` + - `ServicesPage.jsx` + - `UserManagementPage.jsx` +- 基础组件放在 `src/components/base/`,对 MUI 做项目级封装: + - `Button` + - `IconButton` + - `Card` + - `SearchBox` + - `FilterChip` + - `KpiCard` + - `SegmentControl` +- 布局组件放在 `src/components/layout/`: + - `Header` + - `Sidebar` + - `PageHead` + - `PageSkeleton` +- 图表组件放在 `src/components/charts/`,统一封装 ECharts: + - `EChart` + - `LineChart` + - `BarChart` + - `StatusDonut` + - `ChartCard` +- 服务业务组件放在 `src/components/service/`: + - `ServiceTable` + - `ServerRail` + - `DetailPanel` + - `RowActions` + - `ResourceCell` + - `StatusBadge` +- 常量、模拟数据和工具函数分别放在: + - `src/constants/` + - `src/data/` + - `src/utils/` + +## 样式约定 + +- 全局设计 token 在 `src/styles/tokens.css`。 +- 基础 reset 在 `src/styles/base.css`。 +- 页面布局、模块样式、动画在 `src/styles/app.css`。 +- MUI 主题集中在 `src/theme.js`,优先在主题层统一颜色、圆角、hover、focus 和组件兼容样式。 +- 避免在页面里直接散落 MUI 组件样式;优先改项目封装组件。 +- 状态色必须保持语义一致: + - 运行中:`--success` + - 警告:`--warning` + - 异常:`--danger` + - 已停止:`--stopped` + +## 按需导入约定 + +- MUI 组件使用路径导入,例如 `@mui/material/Button`。 +- MUI 图标使用路径导入,例如 `@mui/icons-material/Refresh`。 +- 不使用 `@mui/material` 或 `@mui/icons-material` 总入口导入。 +- ECharts 通过封装组件按需加载所需图表和组件能力。 +- 页面使用 `React.lazy` 拆分: + - `OverviewPage` + - `ServicesPage` + - `DetailDrawer` + +## 动画与加载 + +- 页面切换、卡片进入、表格行进入、图表进入、抽屉显示/消失都有统一动效。 +- 按钮、菜单、输入框、选择框、筛选项、状态徽标、进度条都有 hover/focus/active 反馈。 +- `PageSkeleton` 是页面加载占位图,使用 MUI `Skeleton`,铺满 `workspace` 内容区域。 +- 支持 `prefers-reduced-motion: reduce` 降级。 +- 顶部搜索弹窗按需加载,点击结果后打开服务详情。 +- 侧边栏支持展开/收起状态,展开态宽度为 248px。 + +## 文案约定 + +- 不写无意义介绍、营销口号、功能解释文案。 +- 只保留运维用户完成工作所需要的标签、状态、数据和操作。 diff --git a/design.md b/design.md new file mode 100644 index 0000000..8913c05 --- /dev/null +++ b/design.md @@ -0,0 +1,272 @@ +HYApp 管理平台 UI 设计规范(v1.0) + +--- + +## 一、设计目标 + +本系统为 HYApp 管理平台,UI 设计需满足: + +- 高信息密度(服务器 / 服务监控) +- 强状态表达(运行 / 异常 / 告警) +- 快速操作(启停 / 重启 / 排查) +- 统一规范(组件可复用) + +--- + +## 二、整体布局(Layout) + +### 2.1 页面结构 + + +┌───────────────────────────────┐ +│ Header (64px) │ +├──────────────┬────────────────┤ +│ Sidebar │ Content │ +│ (224px) │ (自适应) │ +└──────────────┴────────────────┘ + + +--- + +### 2.2 尺寸规范 + +| 模块 | 尺寸 | +|------|------| +| Header | 64px | +| Sidebar | 224px | +| 页面 padding | 24px | +| 卡片间距 | 16px | + +--- + +### 2.3 响应式 + +| 分辨率 | 行为 | +|--------|------| +| ≥1440px | 标准布局 | +| <1280px | Sidebar 收缩 | + +--- + +## 三、设计 Token(CSS Variables) + +```css +:root { + /* 背景 */ + --bg-page: #F4F7FB; + --bg-sidebar: #FFFFFF; + --bg-header: #FFFFFF; + --bg-card: #FFFFFF; + + /* 边框 */ + --border: #D8E0EC; + + /* 主色 */ + --primary: #2563EB; + + /* 状态色 */ + --success: #16A34A; + --warning: #D97706; + --danger: #DC2626; + --info: #0284C7; + + /* 字体 */ + --text-primary: #0F172A; + --text-secondary: #475569; + --text-tertiary: #64748B; +} +四、颜色规范(语义化) +4.1 状态颜色 +状态 颜色 含义 +运行中 #16A34A 正常 +已停止 #64748B 不活跃 +异常 #DC2626 错误 +警告 #D97706 风险 + +👉 禁止随意更改状态颜色 + +4.2 图表颜色 +--chart-blue: #2563EB; +--chart-green: #16A34A; +--chart-red: #DC2626; +--chart-orange: #D97706; +五、字体规范 +5.1 字号 +类型 大小 +KPI 数值 32px +页面标题 20px +卡片标题 14px +表格文字 13px +辅助文字 12px +5.2 字体 +-apple-system, PingFang SC, Microsoft YaHei, sans-serif +六、组件规范 +6.1 Sidebar(侧边栏) +宽度:224px +item 高度:44px + +状态: + +状态 样式 +默认 浅色文字 +hover 背景高亮 +active 渐变蓝 +6.2 Header(顶部栏) + +包含: + +搜索框 +刷新按钮 +时间筛选 +通知 +用户信息 +6.3 卡片(Card) +基础样式 +background: #FFFFFF; +border-radius: 10px; +padding: 20px; +6.4 KPI 卡片 + +结构: + +图标 + 标题 +数值(大) +辅助信息(增长/状态) +6.5 图表卡片 + +组成: + +标题 +筛选器 +图表 + +规范: + +项目 规则 +折线 2px +网格线 低透明 +字体 12px +七、表格规范 +7.1 表格样式 +background: #FFFFFF; +border-radius: 10px; +7.2 表头 +height: 44px; +background: #F1F5F9; +7.3 行状态 +状态 样式 +默认 无背景 +hover 高亮 +7.4 列规范 +服务列 + +包含: + +图标 +名称 +描述 +资源列(CPU / 内存) + +必须包含: + +数值 +mini 图表 或 进度条 +操作列 + +按钮顺序: + +启动 / 重启 / 停止 / 更多 +八、按钮规范 +8.1 类型 +类型 用途 +Primary 主操作 +Default 普通 +Danger 危险操作 +Success 启动 +8.2 样式示例 +.btn-primary { + background: var(--primary); + color: #FFFFFF; +} + +.btn-danger { + color: #DC2626; +} +九、状态组件 +9.1 状态点 +width: 8px; +height: 8px; +border-radius: 50%; +9.2 Badge +padding: 0 10px; +border-radius: 999px; +十、抽屉(详情面板) +10.1 尺寸 +属性 值 +宽度 360px +10.2 结构 +标题 +Tabs(基础信息 / 配置 / 监控 / 日志) +内容区 +操作区 +十一、页面规范 +11.1 总览页 + +模块顺序: + +KPI 卡片 +资源图表 +告警趋势 +服务列表 +11.2 服务管理页 + +模块: + +统计卡片 +趋势图 +筛选栏 +表格 +详情抽屉 +十二、动效规范 +场景 时长 +hover 150ms +抽屉 250ms +图表更新 渐变 +十三、前端实现建议 +13.1 技术栈 + +推荐: + +React + Ant Design / Arco +Vue + Element Plus / Naive UI +图表:ECharts +13.2 CSS 架构 +styles/ + ├── tokens.css + ├── base.css + ├── components/ + └── pages/ +13.3 命名规范(BEM) +.card {} +.card__header {} +.card__body {} + +.table {} +.table__row {} +十四、扩展能力 + +未来可扩展: + +🌙 深浅色模式 +📊 多主题 +🔔 实时告警(WebSocket) +📱 移动端适配 +结语 + +本规范为 HYApp 管理平台 UI 基础标准: + +所有页面必须遵循 Token +所有组件必须可复用 +状态颜色必须统一 + +如需新增组件,必须先扩展本规范后再开发。 diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..5619615 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,32 @@ +import js from "@eslint/js"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import globals from "globals"; + +export default [ + { ignores: ["dist", "node_modules"] }, + js.configs.recommended, + { + files: ["**/*.{js,jsx}"], + languageOptions: { + ecmaVersion: "latest", + globals: globals.browser, + parserOptions: { + ecmaFeatures: { jsx: true }, + sourceType: "module" + } + }, + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh + }, + rules: { + ...reactHooks.configs.recommended.rules, + "no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "react-hooks/exhaustive-deps": "off", + "react-hooks/set-state-in-effect": "off", + "react-hooks/use-memo": "off", + "react-refresh/only-export-components": "off" + } + } +]; diff --git a/index.html b/index.html new file mode 100644 index 0000000..f9644c2 --- /dev/null +++ b/index.html @@ -0,0 +1,12 @@ + + + + + + HYApp 管理平台 + + +
+ + + diff --git a/package.json b/package.json new file mode 100644 index 0000000..c7f6ae7 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "hyapp-admin-platform", + "version": "0.1.0", + "private": true, + "type": "module", + "packageManager": "pnpm@9.15.9", + "scripts": { + "dev": "vite --host 0.0.0.0", + "build": "vite build", + "lint": "eslint .", + "format": "prettier --write .", + "test": "vitest run", + "preview": "vite preview --host 0.0.0.0" + }, + "dependencies": { + "@emotion/react": "11.14.0", + "@emotion/styled": "11.14.1", + "@mui/icons-material": "9.0.0", + "@mui/material": "9.0.0", + "echarts": "6.0.0", + "react": "19.2.5", + "react-dom": "19.2.5", + "react-router-dom": "^7.14.2" + }, + "devDependencies": { + "@eslint/js": "^10.0.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@vitejs/plugin-react": "6.0.1", + "eslint": "^10.2.1", + "eslint-plugin-react-hooks": "^7.1.1", + "eslint-plugin-react-refresh": "^0.5.2", + "globals": "^17.5.0", + "jsdom": "^29.1.1", + "prettier": "^3.8.3", + "vite": "8.0.10", + "vitest": "^4.1.5" + } +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 0000000..200e7f5 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,2880 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@emotion/react': + specifier: 11.14.0 + version: 11.14.0(@types/react@19.2.14)(react@19.2.5) + '@emotion/styled': + specifier: 11.14.1 + version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5) + '@mui/icons-material': + specifier: 9.0.0 + version: 9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5) + '@mui/material': + specifier: 9.0.0 + version: 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + echarts: + specifier: 6.0.0 + version: 6.0.0 + react: + specifier: 19.2.5 + version: 19.2.5 + react-dom: + specifier: 19.2.5 + version: 19.2.5(react@19.2.5) + react-router-dom: + specifier: ^7.14.2 + version: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + devDependencies: + '@eslint/js': + specifier: ^10.0.1 + version: 10.0.1(eslint@10.2.1) + '@testing-library/jest-dom': + specifier: ^6.9.1 + version: 6.9.1 + '@testing-library/react': + specifier: ^16.3.2 + version: 16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@vitejs/plugin-react': + specifier: 6.0.1 + version: 6.0.1(vite@8.0.10) + eslint: + specifier: ^10.2.1 + version: 10.2.1 + eslint-plugin-react-hooks: + specifier: ^7.1.1 + version: 7.1.1(eslint@10.2.1) + eslint-plugin-react-refresh: + specifier: ^0.5.2 + version: 0.5.2(eslint@10.2.1) + globals: + specifier: ^17.5.0 + version: 17.5.0 + jsdom: + specifier: ^29.1.1 + version: 29.1.1 + prettier: + specifier: ^3.8.3 + version: 3.8.3 + vite: + specifier: 8.0.10 + version: 8.0.10 + vitest: + specifier: ^4.1.5 + version: 4.1.5(jsdom@29.1.1)(vite@8.0.10) + +packages: + + '@adobe/css-tools@4.4.4': + resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==} + + '@asamuzakjp/css-color@5.1.11': + resolution: {integrity: sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/dom-selector@7.1.1': + resolution: {integrity: sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/generational-cache@1.0.1': + resolution: {integrity: sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + '@asamuzakjp/nwsapi@2.3.9': + resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==} + + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.0': + resolution: {integrity: sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bramus/specificity@2.4.2': + resolution: {integrity: sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==} + hasBin: true + + '@csstools/color-helpers@6.0.2': + resolution: {integrity: sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==} + engines: {node: '>=20.19.0'} + + '@csstools/css-calc@3.2.0': + resolution: {integrity: sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-color-parser@4.1.0': + resolution: {integrity: sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-parser-algorithms': ^4.0.0 + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-parser-algorithms@4.0.0': + resolution: {integrity: sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==} + engines: {node: '>=20.19.0'} + peerDependencies: + '@csstools/css-tokenizer': ^4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3': + resolution: {integrity: sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==} + peerDependencies: + css-tree: ^3.2.1 + peerDependenciesMeta: + css-tree: + optional: true + + '@csstools/css-tokenizer@4.0.0': + resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} + engines: {node: '>=20.19.0'} + + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} + + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@emotion/babel-plugin@11.13.5': + resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==} + + '@emotion/cache@11.14.0': + resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==} + + '@emotion/hash@0.9.2': + resolution: {integrity: sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==} + + '@emotion/is-prop-valid@1.4.0': + resolution: {integrity: sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==} + + '@emotion/memoize@0.9.0': + resolution: {integrity: sha512-30FAj7/EoJ5mwVPOWhAyCX+FPfMDrVecJAM+Iw9NRoSl4BBAQeqj4cApHHUXOVvIPgLVDsCFoz/hGD+5QQD1GQ==} + + '@emotion/react@11.14.0': + resolution: {integrity: sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==} + peerDependencies: + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/serialize@1.3.3': + resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==} + + '@emotion/sheet@1.4.0': + resolution: {integrity: sha512-fTBW9/8r2w3dXWYM4HCB1Rdp8NLibOw2+XELH5m5+AkWiL/KqYX6dc0kKYlaYyKjrQ6ds33MCdMPEwgs2z1rqg==} + + '@emotion/styled@11.14.1': + resolution: {integrity: sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==} + peerDependencies: + '@emotion/react': ^11.0.0-rc.0 + '@types/react': '*' + react: '>=16.8.0' + peerDependenciesMeta: + '@types/react': + optional: true + + '@emotion/unitless@0.10.0': + resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0': + resolution: {integrity: sha512-yJMtVdH59sxi/aVJBpk9FQq+OR8ll5GT8oWd57UpeaKEVGab41JWaCFA7FRLoMLloOZF/c/wsPoe+bfGmRKgDg==} + peerDependencies: + react: '>=16.8.0' + + '@emotion/utils@1.4.2': + resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==} + + '@emotion/weak-memoize@0.4.0': + resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==} + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.23.5': + resolution: {integrity: sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/config-helpers@0.5.5': + resolution: {integrity: sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/core@1.2.1': + resolution: {integrity: sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/js@10.0.1': + resolution: {integrity: sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + peerDependencies: + eslint: ^10.0.0 + peerDependenciesMeta: + eslint: + optional: true + + '@eslint/object-schema@3.0.5': + resolution: {integrity: sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@eslint/plugin-kit@0.7.1': + resolution: {integrity: sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + '@exodus/bytes@1.15.0': + resolution: {integrity: sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==} + 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 + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@mui/core-downloads-tracker@9.0.0': + resolution: {integrity: sha512-uwQNGkhv0lf7ufxw6QXev77BW6pWbW+7uxYjU5+rfp4lBkFtMEgJCsarTM3Tn+i0lGx6+Ol2u88JdGXr0GDskA==} + + '@mui/icons-material@9.0.0': + resolution: {integrity: sha512-oDwyvI6LgjWRC9MBcSGvLkPud9S9ELgSBQFYxa1rYcZn6Br55dn22SyvsPDMsn0G8OndFk53iMT45W5mNqrogw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@mui/material': ^9.0.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/material@9.0.0': + resolution: {integrity: sha512-+VP/oQCDhDR87NQQgXnNBG8dwy6GNuQLnenS1pZvkbn2dKFSxRSRMybTpH9xUxXP+316mlYDy5CSbYtusnCWtw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@mui/material-pigment-css': ^9.0.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@mui/material-pigment-css': + optional: true + '@types/react': + optional: true + + '@mui/private-theming@9.0.0': + resolution: {integrity: sha512-JtuZoaiCqwD6vjgYu6Xp3T7DZkrxJlgtDz5yESzhI34fEX5hHMh2VJUbuL9UOg8xrfIFMrq6dcYoH/7Zi4G0RA==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/styled-engine@9.0.0': + resolution: {integrity: sha512-9RLGdX4Jg0aQPRuvqh/OLzYSPlgd5zyEw5/1HIRfdavSiOd03WtUaGZH9/w1RoTYuRKwpgy0hpIFaMHIqPVIWg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.4.1 + '@emotion/styled': ^11.3.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + + '@mui/system@9.0.0': + resolution: {integrity: sha512-YnC5Zg6j04IxiLc/boAKs0464jfZlLFVa7mf5E8lF0XOtZVUvG6R6gJK50lgUYdaaLdyLfxF6xR7LaPuEpeT/g==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@emotion/react': ^11.5.0 + '@emotion/styled': ^11.3.0 + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/react': + optional: true + '@emotion/styled': + optional: true + '@types/react': + optional: true + + '@mui/types@9.0.0': + resolution: {integrity: sha512-i1cuFCAWN44b3AJWO7mh7tuh1sqbQSeVr/94oG0TX5uXivac8XalgE4/6fQZcmGZigzbQ35IXxj/4jLpRIBYZg==} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@mui/utils@9.0.0': + resolution: {integrity: sha512-bQcqyg/gjULUqTuyUjSAFr6LQGLvtkNtDbJerAtoUn9kGZ0hg5QJiN1PLHMLbeFpe3te1831uq7GFl2ITokGdg==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@types/react': ^17.0.0 || ^18.0.0 || ^19.0.0 + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@oxc-project/types@0.127.0': + resolution: {integrity: sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==} + + '@popperjs/core@2.11.8': + resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.17': + resolution: {integrity: sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@testing-library/dom@10.4.1': + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} + + '@testing-library/jest-dom@6.9.1': + resolution: {integrity: sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} + + '@testing-library/react@16.3.2': + resolution: {integrity: sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==} + 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 + + '@tybys/wasm-util@0.10.1': + resolution: {integrity: sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==} + + '@types/aria-query@5.0.4': + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} + + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + + '@types/esrecurse@4.3.1': + resolution: {integrity: sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/parse-json@4.0.2': + resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-transition-group@4.4.12': + resolution: {integrity: sha512-8TV6R3h2j7a91c+1DXdJi3Syo69zzIZbz7Lg5tORM5LEJG7X/E6a1V3drRyBRZq7/utz7A+c4OgYLiLcYGHG6w==} + peerDependencies: + '@types/react': '*' + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@vitest/expect@4.1.5': + resolution: {integrity: sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==} + + '@vitest/mocker@4.1.5': + resolution: {integrity: sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.5': + resolution: {integrity: sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==} + + '@vitest/runner@4.1.5': + resolution: {integrity: sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==} + + '@vitest/snapshot@4.1.5': + resolution: {integrity: sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==} + + '@vitest/spy@4.1.5': + resolution: {integrity: sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==} + + '@vitest/utils@4.1.5': + resolution: {integrity: sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==} + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@5.2.0: + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} + + aria-query@5.3.0: + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} + + aria-query@5.3.2: + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} + + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + + babel-plugin-macros@3.1.0: + resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==} + engines: {node: '>=10', npm: '>=6'} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + baseline-browser-mapping@2.10.24: + resolution: {integrity: sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA==} + engines: {node: '>=6.0.0'} + hasBin: true + + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + caniuse-lite@1.0.30001791: + resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + convert-source-map@1.9.0: + resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cosmiconfig@7.1.0: + resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} + engines: {node: '>=10'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-tree@3.2.1: + resolution: {integrity: sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==} + engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0} + + css.escape@1.5.1: + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + data-urls@7.0.0: + resolution: {integrity: sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + dom-accessibility-api@0.5.16: + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} + + dom-accessibility-api@0.6.3: + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + echarts@6.0.0: + resolution: {integrity: sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==} + + electron-to-chromium@1.5.345: + resolution: {integrity: sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg==} + + entities@8.0.0: + resolution: {integrity: sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==} + engines: {node: '>=20.19.0'} + + error-ex@1.3.4: + resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + eslint-plugin-react-hooks@7.1.1: + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} + peerDependencies: + eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 + + eslint-plugin-react-refresh@0.5.2: + resolution: {integrity: sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==} + peerDependencies: + eslint: ^9 || ^10 + + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@10.2.1: + resolution: {integrity: sha512-wiyGaKsDgqXvF40P8mDwiUp/KQjE1FdrIEJsM8PZ3XCiniTMXS3OHWWUe5FI5agoCnr8x4xPrTDZuxsBlNHl+Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@11.2.0: + resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-root@1.1.0: + resolution: {integrity: sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + globals@17.5.0: + resolution: {integrity: sha512-qoV+HK2yFl/366t2/Cb3+xxPUo5BuMynomoDmiaZBIdbs+0pYbjfZU+twLhGKp4uCZ/+NbtpVepH5bGCxRyy2g==} + engines: {node: '>=18'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hermes-estree@0.25.1: + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} + + hermes-parser@0.25.1: + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + html-encoding-sniffer@6.0.0: + resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + indent-string@4.0.0: + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} + + is-arrayish@0.2.1: + resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} + + is-core-module@2.16.1: + resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==} + engines: {node: '>= 0.4'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsdom@29.1.1: + resolution: {integrity: sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24.0.0} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-parse-even-better-errors@2.3.1: + resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lines-and-columns@1.2.4: + resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@11.3.5: + resolution: {integrity: sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==} + engines: {node: 20 || >=22} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + lz-string@1.5.0: + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} + hasBin: true + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mdn-data@2.27.1: + resolution: {integrity: sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==} + + min-indent@1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoid@3.3.11: + resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + node-releases@2.0.38: + resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-json@5.2.0: + resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} + engines: {node: '>=8'} + + parse5@8.0.1: + resolution: {integrity: sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-type@4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + postcss@8.5.12: + resolution: {integrity: sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==} + engines: {node: ^10 || ^12 || >=14} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + prettier@3.8.3: + resolution: {integrity: sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==} + engines: {node: '>=14'} + hasBin: true + + pretty-format@27.5.1: + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-is@19.2.5: + resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} + + react-router-dom@7.14.2: + resolution: {integrity: sha512-YZcM5ES8jJSM+KrJ9BdvHHqlnGTg5tH3sC5ChFRj4inosKctdyzBDhOyyHdGk597q2OT6NTrCA1OvB/YDwfekQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.14.2: + resolution: {integrity: sha512-yCqNne6I8IB6rVCH7XUvlBK7/QKyqypBFGv+8dj4QBFJiiRX+FG7/nkdAvGElyvVZ/HQP5N19wzteuTARXi5Gw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-transition-group@4.4.5: + resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==} + peerDependencies: + react: '>=16.6.0' + react-dom: '>=16.6.0' + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + redent@3.0.0: + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + rolldown@1.0.0-rc.17: + resolution: {integrity: sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.5.7: + resolution: {integrity: sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + + strip-indent@3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + + stylis@4.2.0: + resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.1.2: + resolution: {integrity: sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==} + engines: {node: '>=18'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tldts-core@7.0.29: + resolution: {integrity: sha512-W99NuU7b1DcG3uJ3v9k9VztCH3WialNbBkBft5wCs8V8mexu0XQqaZEYb9l9RNNzK8+3EJ9PKWB0/RUtTQ/o+Q==} + + tldts@7.0.29: + resolution: {integrity: sha512-JIXCerhudr/N6OWLwLF1HVsTTUo7ry6qHa5eWZEkiMuxsIiAACL55tGLfqfHfoH7QaMQUW8fngD7u7TxWexYQg==} + hasBin: true + + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} + engines: {node: '>=16'} + + tr46@6.0.0: + resolution: {integrity: sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==} + engines: {node: '>=20'} + + tslib@2.3.0: + resolution: {integrity: sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + vite@8.0.10: + resolution: {integrity: sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + 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 + + vitest@4.1.5: + resolution: {integrity: sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + 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.5 + '@vitest/browser-preview': 4.1.5 + '@vitest/browser-webdriverio': 4.1.5 + '@vitest/coverage-istanbul': 4.1.5 + '@vitest/coverage-v8': 4.1.5 + '@vitest/ui': 4.1.5 + 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 + + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + + webidl-conversions@8.0.1: + resolution: {integrity: sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==} + engines: {node: '>=20'} + + whatwg-mimetype@5.0.0: + resolution: {integrity: sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==} + engines: {node: '>=20'} + + whatwg-url@16.0.1: + resolution: {integrity: sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@1.10.3: + resolution: {integrity: sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==} + engines: {node: '>= 6'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod-validation-error@4.0.2: + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + + zod@4.4.1: + resolution: {integrity: sha512-a6ENMBBGZBsnlSebQ/eKCguSBeGKSf4O7BPnqVPmYGtpBYI7VSqoVqw+QcB7kPRjbqPwhYTpFbVj/RqNz/CT0Q==} + + zrender@6.0.0: + resolution: {integrity: sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==} + +snapshots: + + '@adobe/css-tools@4.4.4': {} + + '@asamuzakjp/css-color@5.1.11': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-color-parser': 4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@asamuzakjp/dom-selector@7.1.1': + dependencies: + '@asamuzakjp/generational-cache': 1.0.1 + '@asamuzakjp/nwsapi': 2.3.9 + bidi-js: 1.0.3 + css-tree: 3.2.1 + is-potential-custom-element-name: 1.0.1 + + '@asamuzakjp/generational-cache@1.0.1': {} + + '@asamuzakjp/nwsapi@2.3.9': {} + + '@babel/code-frame@7.29.0': + dependencies: + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.0': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.1': + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-compilation-targets@7.28.6': + dependencies: + '@babel/compat-data': 7.29.0 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-globals@7.28.0': {} + + '@babel/helper-module-imports@7.28.6': + dependencies: + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': + dependencies: + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/helper-validator-option@7.27.1': {} + + '@babel/helpers@7.29.2': + dependencies: + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/runtime@7.29.2': {} + + '@babel/template@7.28.6': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + + '@babel/traverse@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.2 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bramus/specificity@2.4.2': + dependencies: + css-tree: 3.2.1 + + '@csstools/color-helpers@6.0.2': {} + + '@csstools/css-calc@3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-color-parser@4.1.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/color-helpers': 6.0.2 + '@csstools/css-calc': 3.2.0(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) + '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0)': + dependencies: + '@csstools/css-tokenizer': 4.0.0 + + '@csstools/css-syntax-patches-for-csstree@1.1.3(css-tree@3.2.1)': + optionalDependencies: + css-tree: 3.2.1 + + '@csstools/css-tokenizer@4.0.0': {} + + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emotion/babel-plugin@11.13.5': + dependencies: + '@babel/helper-module-imports': 7.28.6 + '@babel/runtime': 7.29.2 + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/serialize': 1.3.3 + babel-plugin-macros: 3.1.0 + convert-source-map: 1.9.0 + escape-string-regexp: 4.0.0 + find-root: 1.1.0 + source-map: 0.5.7 + stylis: 4.2.0 + transitivePeerDependencies: + - supports-color + + '@emotion/cache@11.14.0': + dependencies: + '@emotion/memoize': 0.9.0 + '@emotion/sheet': 1.4.0 + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + stylis: 4.2.0 + + '@emotion/hash@0.9.2': {} + + '@emotion/is-prop-valid@1.4.0': + dependencies: + '@emotion/memoize': 0.9.0 + + '@emotion/memoize@0.9.0': {} + + '@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/babel-plugin': 11.13.5 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.5) + '@emotion/utils': 1.4.2 + '@emotion/weak-memoize': 0.4.0 + hoist-non-react-statics: 3.3.2 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - supports-color + + '@emotion/serialize@1.3.3': + dependencies: + '@emotion/hash': 0.9.2 + '@emotion/memoize': 0.9.0 + '@emotion/unitless': 0.10.0 + '@emotion/utils': 1.4.2 + csstype: 3.2.3 + + '@emotion/sheet@1.4.0': {} + + '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/babel-plugin': 11.13.5 + '@emotion/is-prop-valid': 1.4.0 + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5) + '@emotion/serialize': 1.3.3 + '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.5) + '@emotion/utils': 1.4.2 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + transitivePeerDependencies: + - supports-color + + '@emotion/unitless@0.10.0': {} + + '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.5)': + dependencies: + react: 19.2.5 + + '@emotion/utils@1.4.2': {} + + '@emotion/weak-memoize@0.4.0': {} + + '@eslint-community/eslint-utils@4.9.1(eslint@10.2.1)': + dependencies: + eslint: 10.2.1 + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.23.5': + dependencies: + '@eslint/object-schema': 3.0.5 + debug: 4.4.3 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.5.5': + dependencies: + '@eslint/core': 1.2.1 + + '@eslint/core@1.2.1': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/js@10.0.1(eslint@10.2.1)': + optionalDependencies: + eslint: 10.2.1 + + '@eslint/object-schema@3.0.5': {} + + '@eslint/plugin-kit@0.7.1': + dependencies: + '@eslint/core': 1.2.1 + levn: 0.4.1 + + '@exodus/bytes@1.15.0': {} + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@mui/core-downloads-tracker@9.0.0': {} + + '@mui/icons-material@9.0.0(@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/material': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@mui/material@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/core-downloads-tracker': 9.0.0 + '@mui/system': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5) + '@mui/types': 9.0.0(@types/react@19.2.14) + '@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5) + '@popperjs/core': 2.11.8 + '@types/react-transition-group': 4.4.12(@types/react@19.2.14) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-is: 19.2.5 + react-transition-group: 4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5) + '@types/react': 19.2.14 + + '@mui/private-theming@9.0.0(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5) + prop-types: 15.8.1 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@mui/styled-engine@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@emotion/cache': 11.14.0 + '@emotion/serialize': 1.3.3 + '@emotion/sheet': 1.4.0 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 19.2.5 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5) + + '@mui/system@9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/private-theming': 9.0.0(@types/react@19.2.14)(react@19.2.5) + '@mui/styled-engine': 9.0.0(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5))(react@19.2.5) + '@mui/types': 9.0.0(@types/react@19.2.14) + '@mui/utils': 9.0.0(@types/react@19.2.14)(react@19.2.5) + clsx: 2.1.1 + csstype: 3.2.3 + prop-types: 15.8.1 + react: 19.2.5 + optionalDependencies: + '@emotion/react': 11.14.0(@types/react@19.2.14)(react@19.2.5) + '@emotion/styled': 11.14.1(@emotion/react@11.14.0(@types/react@19.2.14)(react@19.2.5))(@types/react@19.2.14)(react@19.2.5) + '@types/react': 19.2.14 + + '@mui/types@9.0.0(@types/react@19.2.14)': + dependencies: + '@babel/runtime': 7.29.2 + optionalDependencies: + '@types/react': 19.2.14 + + '@mui/utils@9.0.0(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@mui/types': 9.0.0(@types/react@19.2.14) + '@types/prop-types': 15.7.15 + clsx: 2.1.1 + prop-types: 15.8.1 + react: 19.2.5 + react-is: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.1 + optional: true + + '@oxc-project/types@0.127.0': {} + + '@popperjs/core@2.11.8': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.17': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@standard-schema/spec@1.1.0': {} + + '@testing-library/dom@10.4.1': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/runtime': 7.29.2 + '@types/aria-query': 5.0.4 + aria-query: 5.3.0 + dom-accessibility-api: 0.5.16 + lz-string: 1.5.0 + picocolors: 1.1.1 + pretty-format: 27.5.1 + + '@testing-library/jest-dom@6.9.1': + dependencies: + '@adobe/css-tools': 4.4.4 + aria-query: 5.3.2 + css.escape: 1.5.1 + dom-accessibility-api: 0.6.3 + picocolors: 1.1.1 + redent: 3.0.0 + + '@testing-library/react@16.3.2(@testing-library/dom@10.4.1)(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@testing-library/dom': 10.4.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@tybys/wasm-util@0.10.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/aria-query@5.0.4': {} + + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + + '@types/esrecurse@4.3.1': {} + + '@types/estree@1.0.8': {} + + '@types/json-schema@7.0.15': {} + + '@types/parse-json@4.0.2': {} + + '@types/prop-types@15.7.15': {} + + '@types/react-transition-group@4.4.12(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@vitejs/plugin-react@6.0.1(vite@8.0.10)': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.10 + + '@vitest/expect@4.1.5': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.5(vite@8.0.10)': + dependencies: + '@vitest/spy': 4.1.5 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.0.10 + + '@vitest/pretty-format@4.1.5': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.5': + dependencies: + '@vitest/utils': 4.1.5 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + '@vitest/utils': 4.1.5 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.5': {} + + '@vitest/utils@4.1.5': + dependencies: + '@vitest/pretty-format': 4.1.5 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ansi-regex@5.0.1: {} + + ansi-styles@5.2.0: {} + + aria-query@5.3.0: + dependencies: + dequal: 2.0.3 + + aria-query@5.3.2: {} + + assertion-error@2.0.1: {} + + babel-plugin-macros@3.1.0: + dependencies: + '@babel/runtime': 7.29.2 + cosmiconfig: 7.1.0 + resolve: 1.22.12 + + balanced-match@4.0.4: {} + + baseline-browser-mapping@2.10.24: {} + + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + browserslist@4.28.2: + dependencies: + baseline-browser-mapping: 2.10.24 + caniuse-lite: 1.0.30001791 + electron-to-chromium: 1.5.345 + node-releases: 2.0.38 + update-browserslist-db: 1.2.3(browserslist@4.28.2) + + callsites@3.1.0: {} + + caniuse-lite@1.0.30001791: {} + + chai@6.2.2: {} + + clsx@2.1.1: {} + + convert-source-map@1.9.0: {} + + convert-source-map@2.0.0: {} + + cookie@1.1.1: {} + + cosmiconfig@7.1.0: + dependencies: + '@types/parse-json': 4.0.2 + import-fresh: 3.3.1 + parse-json: 5.2.0 + path-type: 4.0.0 + yaml: 1.10.3 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-tree@3.2.1: + dependencies: + mdn-data: 2.27.1 + source-map-js: 1.2.1 + + css.escape@1.5.1: {} + + csstype@3.2.3: {} + + data-urls@7.0.0: + dependencies: + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js@10.6.0: {} + + deep-is@0.1.4: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + dom-accessibility-api@0.5.16: {} + + dom-accessibility-api@0.6.3: {} + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.2 + csstype: 3.2.3 + + echarts@6.0.0: + dependencies: + tslib: 2.3.0 + zrender: 6.0.0 + + electron-to-chromium@1.5.345: {} + + entities@8.0.0: {} + + error-ex@1.3.4: + dependencies: + is-arrayish: 0.2.1 + + es-errors@1.3.0: {} + + es-module-lexer@2.1.0: {} + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + eslint-plugin-react-hooks@7.1.1(eslint@10.2.1): + dependencies: + '@babel/core': 7.29.0 + '@babel/parser': 7.29.2 + eslint: 10.2.1 + hermes-parser: 0.25.1 + zod: 4.4.1 + zod-validation-error: 4.0.2(zod@4.4.1) + transitivePeerDependencies: + - supports-color + + eslint-plugin-react-refresh@0.5.2(eslint@10.2.1): + dependencies: + eslint: 10.2.1 + + eslint-scope@9.1.2: + dependencies: + '@types/esrecurse': 4.3.1 + '@types/estree': 1.0.8 + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@10.2.1: + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.23.5 + '@eslint/config-helpers': 0.5.5 + '@eslint/core': 1.2.1 + '@eslint/plugin-kit': 0.7.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 9.1.2 + eslint-visitor-keys: 5.0.1 + espree: 11.2.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + minimatch: 10.2.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + transitivePeerDependencies: + - supports-color + + espree@11.2.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 5.0.1 + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + expect-type@1.3.0: {} + + fast-deep-equal@3.1.3: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-root@1.1.0: {} + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + globals@17.5.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hermes-estree@0.25.1: {} + + hermes-parser@0.25.1: + dependencies: + hermes-estree: 0.25.1 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + html-encoding-sniffer@6.0.0: + dependencies: + '@exodus/bytes': 1.15.0 + transitivePeerDependencies: + - '@noble/hashes' + + ignore@5.3.2: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + imurmurhash@0.1.4: {} + + indent-string@4.0.0: {} + + is-arrayish@0.2.1: {} + + is-core-module@2.16.1: + dependencies: + hasown: 2.0.3 + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-potential-custom-element-name@1.0.1: {} + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + jsdom@29.1.1: + dependencies: + '@asamuzakjp/css-color': 5.1.11 + '@asamuzakjp/dom-selector': 7.1.1 + '@bramus/specificity': 2.4.2 + '@csstools/css-syntax-patches-for-csstree': 1.1.3(css-tree@3.2.1) + '@exodus/bytes': 1.15.0 + css-tree: 3.2.1 + data-urls: 7.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 6.0.0 + is-potential-custom-element-name: 1.0.1 + lru-cache: 11.3.5 + parse5: 8.0.1 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 6.0.1 + undici: 7.25.0 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 8.0.1 + whatwg-mimetype: 5.0.0 + whatwg-url: 16.0.1 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - '@noble/hashes' + + jsesc@3.1.0: {} + + json-buffer@3.0.1: {} + + json-parse-even-better-errors@2.3.1: {} + + json-schema-traverse@0.4.1: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + json5@2.2.3: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lines-and-columns@1.2.4: {} + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@11.3.5: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + lz-string@1.5.0: {} + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + mdn-data@2.27.1: {} + + min-indent@1.0.1: {} + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + ms@2.1.3: {} + + nanoid@3.3.11: {} + + natural-compare@1.4.0: {} + + node-releases@2.0.38: {} + + object-assign@4.1.1: {} + + obug@2.1.1: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-json@5.2.0: + dependencies: + '@babel/code-frame': 7.29.0 + error-ex: 1.3.4 + json-parse-even-better-errors: 2.3.1 + lines-and-columns: 1.2.4 + + parse5@8.0.1: + dependencies: + entities: 8.0.0 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-type@4.0.0: {} + + pathe@2.0.3: {} + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + postcss@8.5.12: + dependencies: + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + prelude-ls@1.2.1: {} + + prettier@3.8.3: {} + + pretty-format@27.5.1: + dependencies: + ansi-regex: 5.0.1 + ansi-styles: 5.2.0 + react-is: 17.0.2 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + punycode@2.3.1: {} + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-is@19.2.5: {} + + react-router-dom@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-router: 7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + + react-router@7.14.2(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react-transition-group@4.4.5(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + '@babel/runtime': 7.29.2 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + react@19.2.5: {} + + redent@3.0.0: + dependencies: + indent-string: 4.0.0 + strip-indent: 3.0.0 + + require-from-string@2.0.2: {} + + resolve-from@4.0.0: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.1 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + rolldown@1.0.0-rc.17: + dependencies: + '@oxc-project/types': 0.127.0 + '@rolldown/pluginutils': 1.0.0-rc.17 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.17 + '@rolldown/binding-darwin-x64': 1.0.0-rc.17 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.17 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.17 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.17 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.17 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.17 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.17 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.17 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.17 + + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + + scheduler@0.27.0: {} + + semver@6.3.1: {} + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + siginfo@2.0.0: {} + + source-map-js@1.2.1: {} + + source-map@0.5.7: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + + strip-indent@3.0.0: + dependencies: + min-indent: 1.0.1 + + stylis@4.2.0: {} + + supports-preserve-symlinks-flag@1.0.0: {} + + symbol-tree@3.2.4: {} + + tinybench@2.9.0: {} + + tinyexec@1.1.2: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + tinyrainbow@3.1.0: {} + + tldts-core@7.0.29: {} + + tldts@7.0.29: + dependencies: + tldts-core: 7.0.29 + + tough-cookie@6.0.1: + dependencies: + tldts: 7.0.29 + + tr46@6.0.0: + dependencies: + punycode: 2.3.1 + + tslib@2.3.0: {} + + tslib@2.8.1: + optional: true + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + undici@7.25.0: {} + + update-browserslist-db@1.2.3(browserslist@4.28.2): + dependencies: + browserslist: 4.28.2 + escalade: 3.2.0 + picocolors: 1.1.1 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + vite@8.0.10: + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.12 + rolldown: 1.0.0-rc.17 + tinyglobby: 0.2.16 + optionalDependencies: + fsevents: 2.3.3 + + vitest@4.1.5(jsdom@29.1.1)(vite@8.0.10): + dependencies: + '@vitest/expect': 4.1.5 + '@vitest/mocker': 4.1.5(vite@8.0.10) + '@vitest/pretty-format': 4.1.5 + '@vitest/runner': 4.1.5 + '@vitest/snapshot': 4.1.5 + '@vitest/spy': 4.1.5 + '@vitest/utils': 4.1.5 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.16 + tinyrainbow: 3.1.0 + vite: 8.0.10 + why-is-node-running: 2.3.0 + optionalDependencies: + jsdom: 29.1.1 + transitivePeerDependencies: + - msw + + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + + webidl-conversions@8.0.1: {} + + whatwg-mimetype@5.0.0: {} + + whatwg-url@16.0.1: + dependencies: + '@exodus/bytes': 1.15.0 + tr46: 6.0.0 + webidl-conversions: 8.0.1 + transitivePeerDependencies: + - '@noble/hashes' + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + + word-wrap@1.2.5: {} + + xml-name-validator@5.0.0: {} + + xmlchars@2.2.0: {} + + yallist@3.1.1: {} + + yaml@1.10.3: {} + + yocto-queue@0.1.0: {} + + zod-validation-error@4.0.2(zod@4.4.1): + dependencies: + zod: 4.4.1 + + zod@4.4.1: {} + + zrender@6.0.0: + dependencies: + tslib: 2.3.0 diff --git a/src/App.jsx b/src/App.jsx new file mode 100644 index 0000000..5a11b48 --- /dev/null +++ b/src/App.jsx @@ -0,0 +1,132 @@ +import { lazy, Suspense, useEffect, useMemo, useState } from "react"; +import { Navigate, Outlet, Route, Routes, useLocation, useNavigate } from "react-router-dom"; +import { getMenus } from "./api/navigation.js"; +import { useAuth } from "./auth/AuthProvider.jsx"; +import { Header } from "./components/layout/Header.jsx"; +import { PageSkeleton } from "./components/layout/PageSkeleton.jsx"; +import { Sidebar } from "./components/layout/Sidebar.jsx"; +import { findNavItemByPath, mapBackendMenus, navItems } from "./constants/navigation.js"; + +const LoginPage = lazy(() => import("./pages/LoginPage.jsx").then((module) => ({ default: module.LoginPage }))); +const OverviewPage = lazy(() => import("./pages/OverviewPage.jsx").then((module) => ({ default: module.OverviewPage }))); +const ServicesPage = lazy(() => import("./pages/ServicesPage.jsx").then((module) => ({ default: module.ServicesPage }))); +const UserManagementPage = lazy(() => + import("./pages/UserManagementPage.jsx").then((module) => ({ default: module.UserManagementPage })) +); +const RoleManagementPage = lazy(() => + import("./pages/RoleManagementPage.jsx").then((module) => ({ default: module.RoleManagementPage })) +); +const MenuManagementPage = lazy(() => + import("./pages/MenuManagementPage.jsx").then((module) => ({ default: module.MenuManagementPage })) +); +const LogsPage = lazy(() => import("./pages/LogsPage.jsx").then((module) => ({ default: module.LogsPage }))); +const NotificationsPage = lazy(() => + import("./pages/NotificationsPage.jsx").then((module) => ({ default: module.NotificationsPage })) +); + +function App() { + return ( + }> + + } /> + }> + }> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> + + + } /> + + + ); +} + +function RequireAuth() { + const { loading, user } = useAuth(); + const location = useLocation(); + + if (loading) { + return ; + } + + if (!user) { + return ; + } + + return ; +} + +function RequirePermission({ children, code }) { + const { can } = useAuth(); + + if (!can(code)) { + return ; + } + + return children; +} + +function AdminLayout() { + const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [refreshTick, setRefreshTick] = useState(0); + const [backendMenus, setBackendMenus] = useState([]); + const location = useLocation(); + const navigate = useNavigate(); + const activeNav = useMemo(() => findNavItemByPath(location.pathname, backendMenus.length ? backendMenus : navItems), [backendMenus, location.pathname]); + + useEffect(() => { + let mounted = true; + + async function loadMenus() { + try { + const data = await getMenus(); + if (mounted) { + setBackendMenus(mapBackendMenus(data)); + } + } catch { + if (mounted) { + setBackendMenus(navItems); + } + } + } + + loadMenus(); + return () => { + mounted = false; + }; + }, []); + + const menus = backendMenus.length ? backendMenus : navItems; + + return ( +
+
setRefreshTick((current) => current + 1)} + onToggleSidebar={() => setSidebarCollapsed((current) => !current)} + /> + +
+ + +
+ }> +
+ +
+
+
+
+
+ ); +} + +export default App; diff --git a/src/App.test.jsx b/src/App.test.jsx new file mode 100644 index 0000000..5a536dc --- /dev/null +++ b/src/App.test.jsx @@ -0,0 +1,27 @@ +import { render, screen } from "@testing-library/react"; +import { expect, test } from "vitest"; +import { ThemeProvider } from "@mui/material/styles"; +import { MemoryRouter } from "react-router-dom"; +import { AuthProvider } from "./auth/AuthProvider.jsx"; +import { ConfirmProvider } from "./components/base/ConfirmProvider.jsx"; +import { ToastProvider } from "./components/base/ToastProvider.jsx"; +import { theme } from "./theme.js"; +import App from "./App.jsx"; + +test("renders login route", async () => { + render( + + + + + + + + + + + + ); + + expect(await screen.findByText("HYApp 管理平台")).toBeInTheDocument(); +}); diff --git a/src/api/auth.js b/src/api/auth.js new file mode 100644 index 0000000..95c3037 --- /dev/null +++ b/src/api/auth.js @@ -0,0 +1,21 @@ +import { apiRequest } from "./request.js"; + +export function login(payload) { + return apiRequest("/v1/auth/login", { body: payload, skipAuth: true }); +} + +export function logout() { + return apiRequest("/v1/auth/logout", { method: "POST", skipAuth: true }); +} + +export function refreshSession() { + return apiRequest("/v1/auth/refresh", { method: "POST", skipAuth: true }); +} + +export function getMe() { + return apiRequest("/v1/auth/me"); +} + +export function changePassword(payload) { + return apiRequest("/v1/auth/change-password", { body: payload }); +} diff --git a/src/api/dashboard.js b/src/api/dashboard.js new file mode 100644 index 0000000..be78e80 --- /dev/null +++ b/src/api/dashboard.js @@ -0,0 +1,5 @@ +import { apiRequest } from "./request.js"; + +export function getDashboardOverview() { + return apiRequest("/v1/dashboard/overview"); +} diff --git a/src/api/logs.js b/src/api/logs.js new file mode 100644 index 0000000..1fbaffd --- /dev/null +++ b/src/api/logs.js @@ -0,0 +1,9 @@ +import { apiRequest } from "./request.js"; + +export function listLoginLogs(query) { + return apiRequest("/v1/logs/login", { query }); +} + +export function listOperationLogs(query) { + return apiRequest("/v1/logs/operations", { query }); +} diff --git a/src/api/navigation.js b/src/api/navigation.js new file mode 100644 index 0000000..c16c885 --- /dev/null +++ b/src/api/navigation.js @@ -0,0 +1,5 @@ +import { apiRequest } from "./request.js"; + +export function getMenus() { + return apiRequest("/v1/navigation/menus"); +} diff --git a/src/api/notifications.js b/src/api/notifications.js new file mode 100644 index 0000000..12d9e86 --- /dev/null +++ b/src/api/notifications.js @@ -0,0 +1,9 @@ +import { apiRequest } from "./request.js"; + +export function listNotifications() { + return apiRequest("/v1/notifications"); +} + +export function markNotificationRead(id) { + return apiRequest(`/v1/notifications/${id}/read`, { method: "PATCH", body: {} }); +} diff --git a/src/api/request.js b/src/api/request.js new file mode 100644 index 0000000..3bc1a7b --- /dev/null +++ b/src/api/request.js @@ -0,0 +1,105 @@ +import { API_BASE_URL } from "../config/env.js"; + +const TOKEN_KEY = "hyapp-admin.access-token"; + +let accessToken = window.localStorage.getItem(TOKEN_KEY) || ""; +let refreshHandler = null; +let unauthorizedHandler = null; + +export function getAccessToken() { + return accessToken; +} + +export function setAccessToken(token) { + accessToken = token || ""; + if (accessToken) { + window.localStorage.setItem(TOKEN_KEY, accessToken); + } else { + window.localStorage.removeItem(TOKEN_KEY); + } +} + +export function setRefreshHandler(handler) { + refreshHandler = handler; +} + +export function setUnauthorizedHandler(handler) { + unauthorizedHandler = handler; +} + +export async function apiRequest(path, options = {}) { + const { + body, + headers = {}, + method = body ? "POST" : "GET", + query, + retry = true, + raw = false, + skipAuth = false + } = options; + const url = buildURL(path, query); + const requestHeaders = { ...headers }; + + if (body !== undefined && !(body instanceof FormData)) { + requestHeaders["Content-Type"] = "application/json"; + } + if (accessToken && !skipAuth) { + requestHeaders.Authorization = `Bearer ${accessToken}`; + } + + const response = await fetch(url, { + method, + headers: requestHeaders, + body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body), + credentials: "include" + }); + + if (response.status === 401 && retry && refreshHandler && !skipAuth) { + const refreshed = await refreshHandler(); + if (refreshed) { + return apiRequest(path, { ...options, retry: false }); + } + } + + if (response.status === 401 && unauthorizedHandler && !skipAuth) { + unauthorizedHandler(); + } + + if (raw) { + if (!response.ok) { + throw new Error(response.statusText || "请求失败"); + } + return response; + } + + const payload = await readJSON(response); + if (!response.ok || payload.code !== 0) { + throw new Error(payload.message || response.statusText || "请求失败"); + } + + return payload.data; +} + +function buildURL(path, query) { + const url = new URL(`${API_BASE_URL}${path}`, window.location.origin); + if (query) { + Object.entries(query).forEach(([key, value]) => { + if (value !== undefined && value !== null && value !== "") { + url.searchParams.set(key, value); + } + }); + } + return url.toString(); +} + +async function readJSON(response) { + const text = await response.text(); + if (!text) { + return {}; + } + try { + return JSON.parse(text); + } catch { + return { code: response.ok ? 0 : response.status, message: text, data: text }; + } +} diff --git a/src/api/roles.js b/src/api/roles.js new file mode 100644 index 0000000..c0178b4 --- /dev/null +++ b/src/api/roles.js @@ -0,0 +1,25 @@ +import { apiRequest } from "./request.js"; + +export function listRoles() { + return apiRequest("/v1/roles"); +} + +export function createRole(payload) { + return apiRequest("/v1/roles", { body: payload }); +} + +export function updateRole(id, payload) { + return apiRequest(`/v1/roles/${id}`, { method: "PATCH", body: payload }); +} + +export function deleteRole(id) { + return apiRequest(`/v1/roles/${id}`, { method: "DELETE" }); +} + +export function replaceRolePermissions(id, permissionIds) { + return apiRequest(`/v1/roles/${id}/permissions`, { method: "PUT", body: { permissionIds } }); +} + +export function listPermissions() { + return apiRequest("/v1/permissions"); +} diff --git a/src/api/search.js b/src/api/search.js new file mode 100644 index 0000000..f1dd1cc --- /dev/null +++ b/src/api/search.js @@ -0,0 +1,5 @@ +import { apiRequest } from "./request.js"; + +export function searchGlobal(keyword) { + return apiRequest("/v1/search", { query: { keyword } }); +} diff --git a/src/api/services.js b/src/api/services.js new file mode 100644 index 0000000..8f3675f --- /dev/null +++ b/src/api/services.js @@ -0,0 +1,25 @@ +import { apiRequest } from "./request.js"; + +export function listServices(query) { + return apiRequest("/v1/services", { query }); +} + +export function getService(id) { + return apiRequest(`/v1/services/${id}`); +} + +export function createService(payload) { + return apiRequest("/v1/services", { body: payload }); +} + +export function updateService(id, payload) { + return apiRequest(`/v1/services/${id}`, { method: "PATCH", body: payload }); +} + +export function updateServiceStatus(id, status) { + return apiRequest(`/v1/services/${id}/status`, { method: "PATCH", body: { status } }); +} + +export function restartService(id) { + return apiRequest(`/v1/services/${id}/restart`, { body: {} }); +} diff --git a/src/api/users.js b/src/api/users.js new file mode 100644 index 0000000..17309cf --- /dev/null +++ b/src/api/users.js @@ -0,0 +1,33 @@ +import { apiRequest } from "./request.js"; + +export function listUsers(query) { + return apiRequest("/v1/users", { query }); +} + +export function getUser(id) { + return apiRequest(`/v1/users/${id}`); +} + +export function createUser(payload) { + return apiRequest("/v1/users", { body: payload }); +} + +export function updateUser(id, payload) { + return apiRequest(`/v1/users/${id}`, { method: "PATCH", body: payload }); +} + +export function updateUserStatus(id, status) { + return apiRequest(`/v1/users/${id}/status`, { method: "PATCH", body: { status } }); +} + +export function resetUserPassword(id, password) { + return apiRequest(`/v1/users/${id}/reset-password`, { body: { password } }); +} + +export function batchUpdateUserStatus(ids, status) { + return apiRequest("/v1/users/batch/status", { body: { ids, status } }); +} + +export function exportUsers(query) { + return apiRequest("/v1/users/export", { query, raw: true }); +} diff --git a/src/auth/AuthProvider.jsx b/src/auth/AuthProvider.jsx new file mode 100644 index 0000000..8ef3a71 --- /dev/null +++ b/src/auth/AuthProvider.jsx @@ -0,0 +1,105 @@ +import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; +import * as authApi from "../api/auth.js"; +import { getAccessToken, setAccessToken, setRefreshHandler, setUnauthorizedHandler } from "../api/request.js"; + +const AuthContext = createContext(null); + +export function AuthProvider({ children }) { + const [user, setUser] = useState(null); + const [permissions, setPermissions] = useState([]); + const [loading, setLoading] = useState(true); + + const applySession = useCallback((session) => { + if (session?.accessToken) { + setAccessToken(session.accessToken); + } + setUser(session?.user || null); + setPermissions(session?.permissions || []); + }, []); + + const clearSession = useCallback(() => { + setAccessToken(""); + setUser(null); + setPermissions([]); + }, []); + + const refresh = useCallback(async () => { + try { + const session = await authApi.refreshSession(); + applySession(session); + return true; + } catch { + clearSession(); + return false; + } + }, [applySession, clearSession]); + + useEffect(() => { + setRefreshHandler(refresh); + setUnauthorizedHandler(clearSession); + return () => { + setRefreshHandler(null); + setUnauthorizedHandler(null); + }; + }, [clearSession, refresh]); + + useEffect(() => { + let mounted = true; + + async function bootstrap() { + try { + if (getAccessToken()) { + const session = await authApi.getMe(); + if (mounted) { + applySession(session); + } + return; + } + await refresh(); + } finally { + if (mounted) { + setLoading(false); + } + } + } + + bootstrap(); + return () => { + mounted = false; + }; + }, [applySession, refresh]); + + const login = useCallback( + async (payload) => { + const session = await authApi.login(payload); + applySession(session); + return session; + }, + [applySession] + ); + + const logout = useCallback(async () => { + try { + await authApi.logout(); + } finally { + clearSession(); + } + }, [clearSession]); + + const can = useCallback((code) => permissions.includes(code), [permissions]); + + const value = useMemo( + () => ({ user, permissions, loading, login, logout, refresh, can }), + [can, loading, login, logout, permissions, refresh, user] + ); + + return {children}; +} + +export function useAuth() { + const context = useContext(AuthContext); + if (!context) { + throw new Error("useAuth must be used inside AuthProvider"); + } + return context; +} diff --git a/src/components/base/Button.jsx b/src/components/base/Button.jsx new file mode 100644 index 0000000..5a2bcea --- /dev/null +++ b/src/components/base/Button.jsx @@ -0,0 +1,49 @@ +import MuiButton from "@mui/material/Button"; + +export function Button({ children, className = "", variant, ...props }) { + const { sx, ...buttonProps } = props; + const variantClass = variant ? `button--${variant}` : ""; + const muiVariant = variant === "primary" ? "contained" : "outlined"; + const color = variant === "success" ? "success" : variant === "danger" ? "error" : "primary"; + const toneSx = getToneSx(variant); + + return ( + + {children} + + ); +} + +function getToneSx(variant) { + if (variant === "success") { + return { + color: "var(--success)", + borderColor: "rgba(46, 219, 95, 0.38)", + "&:hover": { + borderColor: "rgba(46, 219, 95, 0.72)", + backgroundColor: "rgba(46, 219, 95, 0.1)" + } + }; + } + + if (variant === "danger") { + return { + color: "var(--danger)", + borderColor: "rgba(255, 77, 79, 0.38)", + "&:hover": { + borderColor: "rgba(255, 77, 79, 0.72)", + backgroundColor: "rgba(255, 77, 79, 0.1)" + } + }; + } + + return {}; +} diff --git a/src/components/base/Card.jsx b/src/components/base/Card.jsx new file mode 100644 index 0000000..6be4ffd --- /dev/null +++ b/src/components/base/Card.jsx @@ -0,0 +1,9 @@ +import MuiCard from "@mui/material/Card"; + +export function Card({ children, className = "", component = "div", ...props }) { + return ( + + {children} + + ); +} diff --git a/src/components/base/ConfirmProvider.jsx b/src/components/base/ConfirmProvider.jsx new file mode 100644 index 0000000..7116ba2 --- /dev/null +++ b/src/components/base/ConfirmProvider.jsx @@ -0,0 +1,58 @@ +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogTitle from "@mui/material/DialogTitle"; +import { createContext, useCallback, useContext, useMemo, useRef, useState } from "react"; +import { Button } from "./Button.jsx"; + +const ConfirmContext = createContext(null); + +export function ConfirmProvider({ children }) { + const resolverRef = useRef(null); + const [state, setState] = useState(null); + + const confirm = useCallback((options) => { + setState({ + title: options.title || "确认操作", + message: options.message || "该操作提交后会立即生效。", + confirmText: options.confirmText || "确认", + tone: options.tone || "primary" + }); + + return new Promise((resolve) => { + resolverRef.current = resolve; + }); + }, []); + + const close = useCallback((result) => { + resolverRef.current?.(result); + resolverRef.current = null; + setState(null); + }, []); + + const value = useMemo(() => ({ confirm }), [confirm]); + + return ( + + {children} + close(false)} open={Boolean(state)} fullWidth> + {state?.title} + {state?.message} + + + + + + + ); +} + +export function useConfirm() { + const context = useContext(ConfirmContext); + if (!context) { + throw new Error("useConfirm must be used inside ConfirmProvider"); + } + return context.confirm; +} diff --git a/src/components/base/DataState.jsx b/src/components/base/DataState.jsx new file mode 100644 index 0000000..bfe2010 --- /dev/null +++ b/src/components/base/DataState.jsx @@ -0,0 +1,28 @@ +import Refresh from "@mui/icons-material/Refresh"; +import { Button } from "./Button.jsx"; + +export function DataState({ error, loading, children, onRetry }) { + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ {onRetry ? ( + + ) : null} +
+ ); + } + + return children; +} diff --git a/src/components/base/FilterChip.jsx b/src/components/base/FilterChip.jsx new file mode 100644 index 0000000..a9491fd --- /dev/null +++ b/src/components/base/FilterChip.jsx @@ -0,0 +1,25 @@ +import Chip from "@mui/material/Chip"; + +export function FilterChip({ active, label, onClick }) { + return ( + + ); +} diff --git a/src/components/base/IconButton.jsx b/src/components/base/IconButton.jsx new file mode 100644 index 0000000..b0dc05b --- /dev/null +++ b/src/components/base/IconButton.jsx @@ -0,0 +1,43 @@ +import MuiIconButton from "@mui/material/IconButton"; + +export function IconButton({ children, className = "", label, notice, tone, ...props }) { + const { sx, ...buttonProps } = props; + + return ( + + {children} + {notice ? {notice} : null} + + ); +} + +function getToneSx(tone) { + if (tone === "success") { + return { + color: "var(--success)", + "&:hover": { borderColor: "rgba(46, 219, 95, 0.72)", backgroundColor: "rgba(46, 219, 95, 0.1)" } + }; + } + + if (tone === "danger") { + return { + color: "var(--danger)", + "&:hover": { borderColor: "rgba(255, 77, 79, 0.72)", backgroundColor: "rgba(255, 77, 79, 0.1)" } + }; + } + + return {}; +} diff --git a/src/components/base/KpiCard.jsx b/src/components/base/KpiCard.jsx new file mode 100644 index 0000000..f8aa25b --- /dev/null +++ b/src/components/base/KpiCard.jsx @@ -0,0 +1,19 @@ +import { Card } from "./Card.jsx"; + +export function KpiCard({ icon: Icon, label, sub, tone = "primary", unit, value }) { + return ( + +
+
{label}
+
+ +
+
+
+ {value} + {unit ? {unit} : null} +
+
{sub}
+
+ ); +} diff --git a/src/components/base/PaginationBar.jsx b/src/components/base/PaginationBar.jsx new file mode 100644 index 0000000..f5ebffe --- /dev/null +++ b/src/components/base/PaginationBar.jsx @@ -0,0 +1,26 @@ +import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft"; +import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight"; +import { IconButton } from "./IconButton.jsx"; + +export function PaginationBar({ page, pageSize, total, onPageChange }) { + const totalPages = Math.max(1, Math.ceil(total / pageSize)); + const start = total === 0 ? 0 : (page - 1) * pageSize + 1; + const end = Math.min(total, page * pageSize); + + return ( +
+ + {start}-{end} / 共 {total} 条 + +
+ onPageChange(page - 1)}> + + + {page} / {totalPages} + = totalPages} label="下一页" onClick={() => onPageChange(page + 1)}> + + +
+
+ ); +} diff --git a/src/components/base/SearchBox.jsx b/src/components/base/SearchBox.jsx new file mode 100644 index 0000000..9d4ceea --- /dev/null +++ b/src/components/base/SearchBox.jsx @@ -0,0 +1,18 @@ +import InputBase from "@mui/material/InputBase"; +import Search from "@mui/icons-material/Search"; + +export function SearchBox({ autoFocus = false, className = "", onChange, placeholder, value }) { + return ( + + ); +} diff --git a/src/components/base/SegmentControl.jsx b/src/components/base/SegmentControl.jsx new file mode 100644 index 0000000..13d7744 --- /dev/null +++ b/src/components/base/SegmentControl.jsx @@ -0,0 +1,53 @@ +import ToggleButton from "@mui/material/ToggleButton"; +import ToggleButtonGroup from "@mui/material/ToggleButtonGroup"; +import { useState } from "react"; + +export function SegmentControl({ value = "avg" }) { + const [selected, setSelected] = useState(value); + + return ( + { + if (nextValue) { + setSelected(nextValue); + } + }} + sx={{ + border: "1px solid var(--border)", + backgroundColor: "var(--bg-card-strong)", + "& .MuiToggleButtonGroup-grouped": { + border: 0, + margin: 0 + } + }} + > + + 平均值 + + + 峰值 + + + ); +} + +const segmentSx = { + minWidth: 52, + height: 24, + padding: "0 10px", + borderRadius: "6px !important", + color: "var(--text-tertiary)", + fontSize: 12, + lineHeight: 1, + "&.Mui-selected": { + backgroundColor: "rgba(37, 99, 235, 0.1)", + color: "var(--text-primary)" + }, + "&.Mui-selected:hover": { + backgroundColor: "rgba(37, 99, 235, 0.14)" + } +}; diff --git a/src/components/base/TimeRangeSelect.jsx b/src/components/base/TimeRangeSelect.jsx new file mode 100644 index 0000000..b297cb5 --- /dev/null +++ b/src/components/base/TimeRangeSelect.jsx @@ -0,0 +1,23 @@ +import MenuItem from "@mui/material/MenuItem"; +import Select from "@mui/material/Select"; + +const ranges = ["近 1 小时", "近 24 小时", "近 7 天"]; + +export function TimeRangeSelect({ className = "", onChange, value }) { + return ( + + ); +} diff --git a/src/components/base/ToastProvider.jsx b/src/components/base/ToastProvider.jsx new file mode 100644 index 0000000..474cd89 --- /dev/null +++ b/src/components/base/ToastProvider.jsx @@ -0,0 +1,41 @@ +import Alert from "@mui/material/Alert"; +import Snackbar from "@mui/material/Snackbar"; +import { createContext, useCallback, useContext, useMemo, useState } from "react"; + +const ToastContext = createContext(null); + +export function ToastProvider({ children }) { + const [toast, setToast] = useState(null); + + const showToast = useCallback((message, severity = "info") => { + setToast({ message, severity }); + }, []); + + const value = useMemo(() => ({ showToast }), [showToast]); + + return ( + + {children} + setToast(null)} + open={Boolean(toast)} + > + {toast ? ( + setToast(null)} severity={toast.severity} variant="filled"> + {toast.message} + + ) : null} + + + ); +} + +export function useToast() { + const context = useContext(ToastContext); + if (!context) { + throw new Error("useToast must be used inside ToastProvider"); + } + return context; +} diff --git a/src/components/charts/BarChart.jsx b/src/components/charts/BarChart.jsx new file mode 100644 index 0000000..505175b --- /dev/null +++ b/src/components/charts/BarChart.jsx @@ -0,0 +1,44 @@ +import { EChart } from "./EChart.jsx"; + +export function BarChart({ series }) { + return ; +} + +function createBarOption(series) { + return { + animationDuration: 450, + grid: { left: 8, right: 8, top: 16, bottom: 10, containLabel: false }, + tooltip: { + trigger: "axis", + backgroundColor: "#ffffff", + borderColor: "#d8e0ec", + textStyle: { color: "#0f172a" } + }, + xAxis: { + type: "category", + data: series.map((_, index) => index + 1), + show: false + }, + yAxis: { + type: "value", + show: false, + splitLine: { show: true, lineStyle: { color: "rgba(100, 116, 139, 0.14)" } } + }, + series: [ + { + name: "严重", + type: "bar", + data: series.map((value) => Math.max(1, Math.round(value * 0.45))), + barWidth: 8, + itemStyle: { color: "#dc2626", borderRadius: [4, 4, 0, 0] } + }, + { + name: "警告", + type: "bar", + data: series, + barWidth: 8, + itemStyle: { color: "#d97706", borderRadius: [4, 4, 0, 0] } + } + ] + }; +} diff --git a/src/components/charts/ChartCard.jsx b/src/components/charts/ChartCard.jsx new file mode 100644 index 0000000..b04ff9b --- /dev/null +++ b/src/components/charts/ChartCard.jsx @@ -0,0 +1,15 @@ +import { Card } from "../base/Card.jsx"; +import { SegmentControl } from "../base/SegmentControl.jsx"; +import { LineChart } from "./LineChart.jsx"; + +export function ChartCard({ color, compact = false, series, title }) { + return ( + +
+
{title}
+ +
+ +
+ ); +} diff --git a/src/components/charts/EChart.jsx b/src/components/charts/EChart.jsx new file mode 100644 index 0000000..bbd94c8 --- /dev/null +++ b/src/components/charts/EChart.jsx @@ -0,0 +1,70 @@ +import { useEffect, useRef } from "react"; + +let echartsLoader; + +function loadEcharts() { + if (!echartsLoader) { + echartsLoader = Promise.all([ + import("echarts/lib/echarts"), + import("echarts/lib/chart/line"), + import("echarts/lib/chart/bar"), + import("echarts/lib/chart/pie"), + import("echarts/lib/component/grid"), + import("echarts/lib/component/tooltip"), + import("echarts/lib/component/legend") + ]).then(([echarts]) => echarts); + } + + return echartsLoader; +} + +export function EChart({ className = "", option }) { + const elementRef = useRef(null); + const chartRef = useRef(null); + const optionRef = useRef(option); + + useEffect(() => { + optionRef.current = option; + chartRef.current?.setOption(option, true); + }, [option]); + + useEffect(() => { + const element = elementRef.current; + + if (!element) { + return undefined; + } + + let disposed = false; + let removeResize = () => {}; + + loadEcharts().then((echarts) => { + if (disposed) { + return; + } + + const chart = echarts.init(element, null, { renderer: "canvas" }); + chartRef.current = chart; + chart.setOption(optionRef.current); + + const resize = () => chart.resize(); + const observer = typeof ResizeObserver === "function" ? new ResizeObserver(resize) : null; + observer?.observe(element); + window.addEventListener("resize", resize); + + removeResize = () => { + observer?.disconnect(); + window.removeEventListener("resize", resize); + }; + }); + + return () => { + disposed = true; + removeResize(); + chartRef.current?.dispose(); + chartRef.current = null; + }; + }, []); + + return
; +} diff --git a/src/components/charts/LineChart.jsx b/src/components/charts/LineChart.jsx new file mode 100644 index 0000000..08288d8 --- /dev/null +++ b/src/components/charts/LineChart.jsx @@ -0,0 +1,41 @@ +import { EChart } from "./EChart.jsx"; + +export function LineChart({ color, series }) { + return ; +} + +function createLineOption(color, series) { + return { + animationDuration: 450, + grid: { left: 8, right: 8, top: 14, bottom: 8, containLabel: false }, + tooltip: { + trigger: "axis", + backgroundColor: "#ffffff", + borderColor: "#d8e0ec", + textStyle: { color: "#0f172a" } + }, + xAxis: { + type: "category", + boundaryGap: false, + data: series.map((_, index) => index + 1), + show: false + }, + yAxis: { + type: "value", + min: 0, + max: 100, + show: false, + splitLine: { show: true, lineStyle: { color: "rgba(100, 116, 139, 0.16)" } } + }, + series: [ + { + type: "line", + data: series, + smooth: true, + symbol: "none", + lineStyle: { width: 2, color }, + areaStyle: { color, opacity: 0.14 } + } + ] + }; +} diff --git a/src/components/charts/StatusDonut.jsx b/src/components/charts/StatusDonut.jsx new file mode 100644 index 0000000..e4aaf46 --- /dev/null +++ b/src/components/charts/StatusDonut.jsx @@ -0,0 +1,51 @@ +import { EChart } from "./EChart.jsx"; + +export function StatusDonut({ stats }) { + return ( +
+ +
+ + + +
+
+ ); +} + +function createPieOption(stats) { + return { + animationDuration: 450, + tooltip: { + trigger: "item", + backgroundColor: "#ffffff", + borderColor: "#d8e0ec", + textStyle: { color: "#0f172a" } + }, + series: [ + { + type: "pie", + radius: ["54%", "78%"], + center: ["50%", "50%"], + avoidLabelOverlap: true, + label: { show: false }, + labelLine: { show: false }, + data: [ + { name: "运行中", value: stats.running, itemStyle: { color: "#16a34a" } }, + { name: "异常/警告", value: stats.error + stats.warning, itemStyle: { color: "#dc2626" } }, + { name: "已停止", value: stats.stopped, itemStyle: { color: "#64748b" } } + ] + } + ] + }; +} + +function Legend({ color, label, value }) { + return ( +
+ + {label} + {value} +
+ ); +} diff --git a/src/components/layout/Header.jsx b/src/components/layout/Header.jsx new file mode 100644 index 0000000..3b773f1 --- /dev/null +++ b/src/components/layout/Header.jsx @@ -0,0 +1,155 @@ +import ExpandMore from "@mui/icons-material/ExpandMore"; +import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; +import Logout from "@mui/icons-material/Logout"; +import Menu from "@mui/icons-material/Menu"; +import MenuOpen from "@mui/icons-material/MenuOpen"; +import NotificationsNoneOutlined from "@mui/icons-material/NotificationsNoneOutlined"; +import Person from "@mui/icons-material/Person"; +import Refresh from "@mui/icons-material/Refresh"; +import Search from "@mui/icons-material/Search"; +import MuiMenu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import { lazy, Suspense, useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; +import { listNotifications } from "../../api/notifications.js"; +import { useAuth } from "../../auth/AuthProvider.jsx"; +import { IconButton } from "../base/IconButton.jsx"; +import { useToast } from "../base/ToastProvider.jsx"; + +const SearchDialog = lazy(() => import("../search/SearchDialog.jsx").then((module) => ({ default: module.SearchDialog }))); + +export function Header({ activeLabel, isSidebarCollapsed, onRefresh, onToggleSidebar }) { + const [searchOpen, setSearchOpen] = useState(false); + const [searchMounted, setSearchMounted] = useState(false); + const [userMenuAnchor, setUserMenuAnchor] = useState(null); + const [unread, setUnread] = useState(0); + const navigate = useNavigate(); + const { logout, user } = useAuth(); + const { showToast } = useToast(); + const ToggleIcon = isSidebarCollapsed ? MenuOpen : Menu; + const userMenuOpen = Boolean(userMenuAnchor); + + useEffect(() => { + if (searchOpen) { + setSearchMounted(true); + return undefined; + } + + const timeout = window.setTimeout(() => setSearchMounted(false), 280); + return () => window.clearTimeout(timeout); + }, [searchOpen]); + + const openSearch = () => { + setSearchMounted(true); + setSearchOpen(true); + }; + + const closeUserMenu = () => { + setUserMenuAnchor(null); + }; + + const handleLogout = async () => { + closeUserMenu(); + await logout(); + navigate("/login", { replace: true }); + }; + + const handleSearchSelect = (result) => { + if (result?.target) { + navigate(result.target); + } + }; + + const openNotifications = () => { + navigate("/notifications"); + }; + + useEffect(() => { + let mounted = true; + + async function loadUnread() { + try { + const data = await listNotifications(); + if (mounted) { + setUnread(data.unread || 0); + } + } catch { + if (mounted) { + setUnread(0); + } + } + } + + loadUnread(); + const timer = window.setInterval(loadUnread, 30000); + return () => { + mounted = false; + window.clearInterval(timer); + }; + }, []); + + const refresh = () => { + onRefresh(); + showToast("已刷新当前页面", "success"); + }; + + return ( +
+
+
+ +
+
+ HYApp 管理平台 + Admin Platform +
+
+ +
+
+ + + +
{activeLabel}
+
+ +
+ + + + + + + + + + + + 退出 + + +
+
+ + {searchMounted ? ( + + setSearchOpen(false)} onSelect={handleSearchSelect} /> + + ) : null} +
+ ); +} diff --git a/src/components/layout/PageHead.jsx b/src/components/layout/PageHead.jsx new file mode 100644 index 0000000..430ad4d --- /dev/null +++ b/src/components/layout/PageHead.jsx @@ -0,0 +1,11 @@ +export function PageHead({ children, meta, title }) { + return ( +
+
+

{title}

+ {meta ?

{meta}

: null} +
+
{children}
+
+ ); +} diff --git a/src/components/layout/PageSkeleton.jsx b/src/components/layout/PageSkeleton.jsx new file mode 100644 index 0000000..86c0c70 --- /dev/null +++ b/src/components/layout/PageSkeleton.jsx @@ -0,0 +1,36 @@ +import Skeleton from "@mui/material/Skeleton"; + +export function PageSkeleton() { + return ( + + ); +} + +function SkeletonBlock({ className = "" }) { + return ; +} diff --git a/src/components/layout/Sidebar.jsx b/src/components/layout/Sidebar.jsx new file mode 100644 index 0000000..53e7a2d --- /dev/null +++ b/src/components/layout/Sidebar.jsx @@ -0,0 +1,181 @@ +import ExpandMore from "@mui/icons-material/ExpandMore"; +import { useEffect, useRef, useState } from "react"; + +export function Sidebar({ activePath, isCollapsed, items, onNavigate }) { + const [expandedGroupId, setExpandedGroupId] = useState(() => findParentGroupId(activePath, items)); + const [flyoutGroupId, setFlyoutGroupId] = useState(null); + const sidebarRef = useRef(null); + + useEffect(() => { + setExpandedGroupId(findParentGroupId(activePath, items)); + setFlyoutGroupId(null); + }, [activePath, items]); + + useEffect(() => { + setFlyoutGroupId(null); + }, [isCollapsed]); + + useEffect(() => { + if (!flyoutGroupId) { + return undefined; + } + + const closeFlyout = (event) => { + if (sidebarRef.current?.contains(event.target)) { + return; + } + + setFlyoutGroupId(null); + }; + + const closeOnEscape = (event) => { + if (event.key === "Escape") { + setFlyoutGroupId(null); + } + }; + + document.addEventListener("pointerdown", closeFlyout); + document.addEventListener("keydown", closeOnEscape); + + return () => { + document.removeEventListener("pointerdown", closeFlyout); + document.removeEventListener("keydown", closeOnEscape); + }; + }, [flyoutGroupId]); + + const handleNavigate = (path) => { + if (path) { + onNavigate(path); + } + setFlyoutGroupId(null); + }; + + return ( + + ); +} + +function NavGroup({ + activePath, + isCollapsed, + isExpanded, + isFlyoutOpen, + item, + onNavigate, + onToggleExpanded, + onToggleFlyout +}) { + const Icon = item.icon; + const children = item.children || []; + const hasChildren = children.length > 0; + const isActive = Boolean(item.path && activePath.startsWith(item.path)); + const isChildActive = children.some((child) => child.path && activePath.startsWith(child.path)); + const isOpen = isCollapsed ? isFlyoutOpen : isExpanded; + + const handleParentClick = () => { + if (hasChildren && isCollapsed) { + onToggleFlyout(); + return; + } + + if (hasChildren) { + onToggleExpanded(); + return; + } + + onNavigate(item.path); + }; + + return ( +
+ + + {hasChildren && isOpen && !isCollapsed ? ( +
+ {children.map((child) => { + const ChildIcon = child.icon; + + return ( + + ); + })} +
+ ) : null} + + {hasChildren && isCollapsed && isFlyoutOpen ? ( +
+
{item.label}
+ {children.map((child) => { + const ChildIcon = child.icon; + + return ( + + ); + })} +
+ ) : null} +
+ ); +} + +function findParentGroupId(activePath, items) { + for (const item of items) { + if (item.children?.some((child) => child.path && activePath.startsWith(child.path))) { + return item.id; + } + } + + return null; +} diff --git a/src/components/search/SearchDialog.jsx b/src/components/search/SearchDialog.jsx new file mode 100644 index 0000000..6afad7c --- /dev/null +++ b/src/components/search/SearchDialog.jsx @@ -0,0 +1,76 @@ +import Dialog from "@mui/material/Dialog"; +import List from "@mui/material/List"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemText from "@mui/material/ListItemText"; +import { useEffect, useState } from "react"; +import { searchGlobal } from "../../api/search.js"; +import { SearchBox } from "../base/SearchBox.jsx"; + +export function SearchDialog({ onClose, onSelect, open }) { + const [keyword, setKeyword] = useState(""); + const [results, setResults] = useState([]); + const [loading, setLoading] = useState(false); + + useEffect(() => { + if (!open) { + setKeyword(""); + setResults([]); + } + }, [open]); + + useEffect(() => { + if (!open) { + return undefined; + } + + const timer = window.setTimeout(async () => { + setLoading(true); + try { + const data = await searchGlobal(keyword); + setResults(data || []); + } catch { + setResults([]); + } finally { + setLoading(false); + } + }, 180); + + return () => window.clearTimeout(timer); + }, [keyword, open]); + + const selectResult = (item) => { + onSelect(item); + onClose(); + }; + + return ( + +
+ +
+ + + {results.map((item) => ( + selectResult(item)}> + + + ))} + {!results.length ?
{loading ? "搜索中..." : "暂无匹配结果"}
: null} +
+
+ ); +} diff --git a/src/components/service/DetailPanel.jsx b/src/components/service/DetailPanel.jsx new file mode 100644 index 0000000..dd6a252 --- /dev/null +++ b/src/components/service/DetailPanel.jsx @@ -0,0 +1,137 @@ +import Close from "@mui/icons-material/Close"; +import PlayArrow from "@mui/icons-material/PlayArrow"; +import Refresh from "@mui/icons-material/Refresh"; +import Stop from "@mui/icons-material/Stop"; +import { Button } from "../base/Button.jsx"; +import { Card } from "../base/Card.jsx"; +import { IconButton } from "../base/IconButton.jsx"; +import { detailTabs } from "../../constants/status.js"; +import { StatusBadge } from "./StatusBadge.jsx"; + +export function InlineInspector({ canOperate = true, restartService, service, setServiceStatus }) { + if (!service) { + return null; + } + + return ( + + + + + + + ); +} + +export function DetailDrawer({ activeTab, canOperate = true, isOpen, onClose, restartService, service, setActiveTab, setServiceStatus }) { + if (!service) { + return null; + } + + return ( + + ); +} + +function PanelHead({ service }) { + return ( +
+ +
+ ); +} + +function PanelTitle({ service }) { + return ( +
+

{service.name}

+ +
+ ); +} + +function StaticTabs() { + return ( +
+ {detailTabs.map((tab, index) => ( + + ))} +
+ ); +} + +function DetailBody({ activeTab, service }) { + return ( +
+ {activeTab === "日志" ? ( +
    + {service.logs.map((log) => ( +
  • {log}
  • + ))} +
+ ) : ( +
+ + + + + + + + +
+ )} +
+ ); +} + +function DetailRow({ label, value }) { + return ( +
+ {label} + {value} +
+ ); +} + +function DrawerActions({ canOperate, restartService, service, setServiceStatus }) { + return ( +
+ + + +
+ ); +} diff --git a/src/components/service/ResourceCell.jsx b/src/components/service/ResourceCell.jsx new file mode 100644 index 0000000..96fa73b --- /dev/null +++ b/src/components/service/ResourceCell.jsx @@ -0,0 +1,12 @@ +export function ResourceCell({ memory = false, value }) { + return ( +
+
+ {value}% +
+
+ +
+
+ ); +} diff --git a/src/components/service/RowActions.jsx b/src/components/service/RowActions.jsx new file mode 100644 index 0000000..be609ba --- /dev/null +++ b/src/components/service/RowActions.jsx @@ -0,0 +1,44 @@ +import MoreHoriz from "@mui/icons-material/MoreHoriz"; +import PlayArrow from "@mui/icons-material/PlayArrow"; +import Refresh from "@mui/icons-material/Refresh"; +import Stop from "@mui/icons-material/Stop"; +import { IconButton } from "../base/IconButton.jsx"; + +export function RowActions({ canOperate = true, restartService, service, setServiceStatus }) { + const isRunning = service.status === "running"; + const isStopped = service.status === "stopped"; + + return ( +
event.stopPropagation()}> + setServiceStatus(service.id, "running")} + label="启动" + tone="success" + > + + + restartService(service.id)} + label="重启" + > + + + setServiceStatus(service.id, "stopped")} + label="停止" + tone="danger" + > + + + + + +
+ ); +} diff --git a/src/components/service/ServerRail.jsx b/src/components/service/ServerRail.jsx new file mode 100644 index 0000000..2d21a51 --- /dev/null +++ b/src/components/service/ServerRail.jsx @@ -0,0 +1,27 @@ +import { Card } from "../base/Card.jsx"; +import { StatusBadge } from "./StatusBadge.jsx"; + +export function ServerRail({ openDetail, services }) { + return ( + +
+
服务器列表
+ +
+
+ {services.slice(0, 4).map((service) => ( + + ))} +
+
+ ); +} diff --git a/src/components/service/ServiceTable.jsx b/src/components/service/ServiceTable.jsx new file mode 100644 index 0000000..c2655a2 --- /dev/null +++ b/src/components/service/ServiceTable.jsx @@ -0,0 +1,55 @@ +import Storage from "@mui/icons-material/Storage"; +import { Card } from "../base/Card.jsx"; +import { ResourceCell } from "./ResourceCell.jsx"; +import { RowActions } from "./RowActions.jsx"; +import { StatusBadge } from "./StatusBadge.jsx"; + +export function ServiceTable({ canOperate = true, openDetail, restartService, services, setServiceStatus, title }) { + return ( + +
+
{title}
+
共 {services.length} 条
+
+
+
+
+
服务名称
+
状态
+
部署
+
CPU
+
内存
+
操作
+
+ {services.length ? ( + services.map((service) => ( +
openDetail(service.id)} role="button" tabIndex={0}> +
+
+ +
+
+
{service.name}
+
{service.desc}
+
+
+
+ +
+
+ {service.deploy} +
{service.version}
+
+ + + +
+ )) + ) : ( +
暂无匹配服务
+ )} +
+
+
+ ); +} diff --git a/src/components/service/StatusBadge.jsx b/src/components/service/StatusBadge.jsx new file mode 100644 index 0000000..8c92577 --- /dev/null +++ b/src/components/service/StatusBadge.jsx @@ -0,0 +1,16 @@ +import Chip from "@mui/material/Chip"; +import { statusMeta } from "../../constants/status.js"; + +export function StatusBadge({ service }) { + const meta = statusMeta[service.status]; + + return ( + } + label={service.statusLabel} + size="small" + variant="outlined" + /> + ); +} diff --git a/src/components/user/UserStatusBadge.jsx b/src/components/user/UserStatusBadge.jsx new file mode 100644 index 0000000..e78449e --- /dev/null +++ b/src/components/user/UserStatusBadge.jsx @@ -0,0 +1,16 @@ +import Chip from "@mui/material/Chip"; +import { userStatusMeta } from "../../data/users.js"; + +export function UserStatusBadge({ status }) { + const meta = userStatusMeta[status]; + + return ( + } + label={meta.label} + size="small" + variant="outlined" + /> + ); +} diff --git a/src/components/user/UserTable.jsx b/src/components/user/UserTable.jsx new file mode 100644 index 0000000..91319b7 --- /dev/null +++ b/src/components/user/UserTable.jsx @@ -0,0 +1,148 @@ +import EditOutlined from "@mui/icons-material/EditOutlined"; +import LockOutlined from "@mui/icons-material/LockOutlined"; +import LockOpenOutlined from "@mui/icons-material/LockOpenOutlined"; +import PasswordOutlined from "@mui/icons-material/PasswordOutlined"; +import Person from "@mui/icons-material/Person"; +import PersonRemoveOutlined from "@mui/icons-material/PersonRemoveOutlined"; +import VerifiedUserOutlined from "@mui/icons-material/VerifiedUserOutlined"; +import Checkbox from "@mui/material/Checkbox"; +import { Card } from "../base/Card.jsx"; +import { IconButton } from "../base/IconButton.jsx"; +import { UserStatusBadge } from "./UserStatusBadge.jsx"; + +export function UserTable({ + canEdit, + canResetPassword, + canStatus, + onEdit, + onResetPassword, + onToggleDisabled, + onToggleLocked, + selectedIds, + setSelectedIds, + total, + users +}) { + const allChecked = users.length > 0 && users.every((user) => selectedIds.includes(user.id)); + + const toggleAll = (checked) => { + if (!checked) { + setSelectedIds([]); + return; + } + setSelectedIds(users.map((user) => user.id)); + }; + + return ( + +
+
用户清单
+
共 {total} 条
+
+
+
+
+
+ toggleAll(event.target.checked)} size="small" /> +
+
用户
+
角色
+
团队
+
状态
+
MFA
+
最后登录
+
操作
+
+ {users.length ? ( + users.map((user) => ( +
+
+ { + setSelectedIds((current) => + event.target.checked ? [...current, user.id] : current.filter((id) => id !== user.id) + ); + }} + size="small" + /> +
+
+
+ +
+
+
{user.name}
+
{user.account}
+
+
+
{user.role || "-"}
+
{user.team || "-"}
+
+ +
+
{user.mfa}
+
{user.lastLogin}
+ +
+ )) + ) : ( +
暂无匹配用户
+ )} +
+
+
+ ); +} + +function UserRowActions({ + canEdit, + canResetPassword, + canStatus, + onEdit, + onResetPassword, + onToggleDisabled, + onToggleLocked, + user +}) { + const isDisabled = user.status === "disabled"; + const isLocked = user.status === "locked"; + + return ( +
+ onEdit(user)}> + + + onResetPassword(user)}> + + + onToggleLocked(user)} + tone={isLocked ? "success" : undefined} + > + {isLocked ? : } + + onToggleDisabled(user)} + tone={isDisabled ? "success" : "danger"} + > + {isDisabled ? : } + +
+ ); +} diff --git a/src/config/env.js b/src/config/env.js new file mode 100644 index 0000000..024e625 --- /dev/null +++ b/src/config/env.js @@ -0,0 +1 @@ +export const API_BASE_URL = import.meta.env.VITE_API_BASE_URL || "/api"; diff --git a/src/constants/navigation.js b/src/constants/navigation.js new file mode 100644 index 0000000..f72c2b5 --- /dev/null +++ b/src/constants/navigation.js @@ -0,0 +1,105 @@ +import DashboardOutlined from "@mui/icons-material/DashboardOutlined"; +import HistoryOutlined from "@mui/icons-material/HistoryOutlined"; +import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; +import LoginOutlined from "@mui/icons-material/LoginOutlined"; +import ManageAccountsOutlined from "@mui/icons-material/ManageAccountsOutlined"; +import MenuOpenOutlined from "@mui/icons-material/MenuOpenOutlined"; +import NotificationsNoneOutlined from "@mui/icons-material/NotificationsNoneOutlined"; +import ReceiptLongOutlined from "@mui/icons-material/ReceiptLongOutlined"; +import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; +import SettingsApplicationsOutlined from "@mui/icons-material/SettingsApplicationsOutlined"; + +export const navItems = [ + { id: "overview", code: "overview", label: "总览", path: "/overview", icon: DashboardOutlined }, + { id: "services", code: "services", label: "服务管理", path: "/services", icon: Inventory2Outlined }, + { + id: "system", + code: "system", + label: "系统管理", + icon: SettingsApplicationsOutlined, + children: [ + { id: "system-users", code: "system-users", label: "用户管理", path: "/system/users", icon: ManageAccountsOutlined }, + { id: "system-roles", code: "system-roles", label: "角色配置", path: "/system/roles", icon: ShieldOutlined }, + { id: "system-menus", code: "system-menus", label: "菜单权限", path: "/system/menus", icon: MenuOpenOutlined } + ] + }, + { + id: "logs", + code: "logs", + label: "日志审计", + icon: HistoryOutlined, + children: [ + { id: "logs-login", code: "logs-login", label: "登录日志", path: "/logs/login", icon: LoginOutlined }, + { id: "logs-operations", code: "logs-operations", label: "操作日志", path: "/logs/operations", icon: ReceiptLongOutlined } + ] + }, + { + id: "notifications", + code: "notifications", + label: "通知中心", + path: "/notifications", + icon: NotificationsNoneOutlined + } +]; + +const iconMap = { + dashboard: DashboardOutlined, + history: HistoryOutlined, + inventory: Inventory2Outlined, + login: LoginOutlined, + menu: MenuOpenOutlined, + notifications: NotificationsNoneOutlined, + receipt: ReceiptLongOutlined, + settings: SettingsApplicationsOutlined, + shield: ShieldOutlined, + users: ManageAccountsOutlined +}; + +export function mapBackendMenus(menus = []) { + return menus.map((item) => ({ + id: item.code || String(item.id), + code: item.code, + label: item.label, + path: item.path, + icon: iconMap[item.icon] || MenuOpenOutlined, + permissionCode: item.permissionCode, + children: item.children?.length ? mapBackendMenus(item.children) : undefined + })); +} + +export function findNavItemById(id, items = navItems) { + for (const item of items) { + if (item.id === id) { + return item; + } + + if (item.children?.length) { + const child = findNavItemById(id, item.children); + + if (child) { + return child; + } + } + } + + return null; +} + +export function findNavItemByPath(pathname, items = navItems) { + let fallback = null; + + for (const item of items) { + if (item.path && pathname.startsWith(item.path)) { + fallback = item; + } + + if (item.children?.length) { + const child = findNavItemByPath(pathname, item.children); + if (child) { + return child; + } + } + } + + return fallback; +} diff --git a/src/constants/status.js b/src/constants/status.js new file mode 100644 index 0000000..3893948 --- /dev/null +++ b/src/constants/status.js @@ -0,0 +1,8 @@ +export const statusMeta = { + running: { label: "运行中", className: "running" }, + warning: { label: "警告", className: "warning" }, + error: { label: "异常", className: "error" }, + stopped: { label: "已停止", className: "stopped" } +}; + +export const detailTabs = ["基础信息", "配置", "监控", "日志"]; diff --git a/src/data/services.js b/src/data/services.js new file mode 100644 index 0000000..73b6e24 --- /dev/null +++ b/src/data/services.js @@ -0,0 +1,108 @@ +export const initialServices = [ + { + id: "svc-gateway", + name: "API Gateway", + desc: "流量入口 / 鉴权 / 限流", + status: "running", + statusLabel: "运行中", + cpu: 36, + memory: 58, + deploy: "prod-a", + version: "v2.8.4", + port: "8080", + replicas: "6/6", + response: "42ms", + updated: "14:22", + trend: [34, 38, 35, 42, 39, 44, 36], + logs: ["14:22 健康检查通过", "14:18 配置热更新完成", "13:55 副本扩容至 6"] + }, + { + id: "svc-auth", + name: "Auth Service", + desc: "账号 / 权限 / Token", + status: "warning", + statusLabel: "警告", + cpu: 68, + memory: 74, + deploy: "prod-a", + version: "v1.14.0", + port: "7001", + replicas: "4/4", + response: "96ms", + updated: "14:19", + trend: [48, 52, 55, 62, 71, 66, 68], + logs: ["14:19 P95 延迟超过阈值", "14:07 慢查询 8 条", "13:42 健康检查通过"] + }, + { + id: "svc-order", + name: "Order Worker", + desc: "订单队列 / 任务分发", + status: "running", + statusLabel: "运行中", + cpu: 52, + memory: 63, + deploy: "prod-b", + version: "v3.2.1", + port: "9100", + replicas: "8/8", + response: "61ms", + updated: "14:21", + trend: [41, 44, 46, 49, 52, 56, 52], + logs: ["14:21 消费速率 2.4k/min", "14:10 队列积压清零", "13:36 副本滚动更新完成"] + }, + { + id: "svc-billing", + name: "Billing Core", + desc: "计费 / 对账 / 回调", + status: "error", + statusLabel: "异常", + cpu: 84, + memory: 81, + deploy: "prod-b", + version: "v2.4.9", + port: "7300", + replicas: "2/4", + response: "318ms", + updated: "14:16", + trend: [56, 64, 73, 78, 82, 89, 84], + logs: ["14:16 2 个副本健康检查失败", "14:12 回调超时率 6.3%", "13:58 自动重试已触发"] + }, + { + id: "svc-cache", + name: "Redis Cluster", + desc: "缓存 / 分布式锁", + status: "running", + statusLabel: "运行中", + cpu: 29, + memory: 47, + deploy: "infra", + version: "v7.2.5", + port: "6379", + replicas: "3/3", + response: "5ms", + updated: "14:23", + trend: [24, 28, 27, 31, 29, 32, 29], + logs: ["14:23 主从延迟 8ms", "14:00 快照完成", "13:30 内存碎片率正常"] + }, + { + id: "svc-search", + name: "Search Indexer", + desc: "索引构建 / 查询同步", + status: "stopped", + statusLabel: "已停止", + cpu: 0, + memory: 12, + deploy: "staging", + version: "v0.9.7", + port: "9200", + replicas: "0/2", + response: "-", + updated: "13:45", + trend: [23, 18, 14, 8, 0, 0, 0], + logs: ["13:45 手动停止", "13:40 索引任务完成", "13:20 灰度环境同步完成"] + } +]; + +export const trafficSeries = [36, 42, 39, 54, 49, 62, 58, 73, 68, 76, 71, 82]; +export const cpuSeries = [42, 44, 48, 52, 50, 57, 61, 58, 64, 69, 66, 72]; +export const alertSeries = [4, 5, 3, 8, 6, 9, 7, 12, 10, 14, 11, 9]; diff --git a/src/data/users.js b/src/data/users.js new file mode 100644 index 0000000..6954d53 --- /dev/null +++ b/src/data/users.js @@ -0,0 +1,58 @@ +export const userStatusMeta = { + active: { label: "启用", tone: "running" }, + locked: { label: "锁定", tone: "warning" }, + disabled: { label: "停用", tone: "stopped" } +}; + +export const initialUsers = [ + { + id: "u-001", + name: "admin", + account: "admin", + role: "平台管理员", + team: "平台运维", + status: "active", + mfa: "已开启", + lastLogin: "14:22" + }, + { + id: "u-002", + name: "陈一", + account: "chenyi", + role: "运维管理员", + team: "应用运维", + status: "active", + mfa: "已开启", + lastLogin: "13:48" + }, + { + id: "u-003", + name: "李安", + account: "lian", + role: "审计员", + team: "安全合规", + status: "locked", + mfa: "已开启", + lastLogin: "昨日 18:31" + }, + { + id: "u-004", + name: "王北", + account: "wangbei", + role: "只读用户", + team: "研发协作", + status: "disabled", + mfa: "未开启", + lastLogin: "04-26 09:15" + }, + { + id: "u-005", + name: "赵敏", + account: "zhaomin", + role: "运维管理员", + team: "数据库运维", + status: "active", + mfa: "已开启", + lastLogin: "12:05" + } +]; diff --git a/src/main.jsx b/src/main.jsx new file mode 100644 index 0000000..c1df0b9 --- /dev/null +++ b/src/main.jsx @@ -0,0 +1,28 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { ThemeProvider } from "@mui/material/styles"; +import { BrowserRouter } from "react-router-dom"; +import { AuthProvider } from "./auth/AuthProvider.jsx"; +import { ConfirmProvider } from "./components/base/ConfirmProvider.jsx"; +import { ToastProvider } from "./components/base/ToastProvider.jsx"; +import App from "./App.jsx"; +import { theme } from "./theme.js"; +import "./styles/tokens.css"; +import "./styles/base.css"; +import "./styles/app.css"; + +createRoot(document.getElementById("app")).render( + + + + + + + + + + + + + +); diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx new file mode 100644 index 0000000..c481437 --- /dev/null +++ b/src/pages/LoginPage.jsx @@ -0,0 +1,76 @@ +import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; +import LoginOutlined from "@mui/icons-material/LoginOutlined"; +import TextField from "@mui/material/TextField"; +import { useState } from "react"; +import { Navigate, useLocation, useNavigate } from "react-router-dom"; +import { useAuth } from "../auth/AuthProvider.jsx"; +import { Button } from "../components/base/Button.jsx"; +import { useToast } from "../components/base/ToastProvider.jsx"; + +export function LoginPage() { + const [username, setUsername] = useState("admin"); + const [password, setPassword] = useState(""); + const [submitting, setSubmitting] = useState(false); + const { login, user } = useAuth(); + const { showToast } = useToast(); + const navigate = useNavigate(); + const location = useLocation(); + const from = location.state?.from?.pathname || "/overview"; + + if (user) { + return ; + } + + const submit = async (event) => { + event.preventDefault(); + setSubmitting(true); + try { + await login({ username, password }); + showToast("登录成功", "success"); + navigate(from, { replace: true }); + } catch (err) { + showToast(err.message || "登录失败", "error"); + } finally { + setSubmitting(false); + } + }; + + return ( +
+
+
+
+ +
+
+

HYApp 管理平台

+

Admin Platform

+
+
+ +
+ setUsername(event.target.value)} + required + value={username} + /> + setPassword(event.target.value)} + required + type="password" + value={password} + /> + +
+
+
+ ); +} diff --git a/src/pages/LogsPage.jsx b/src/pages/LogsPage.jsx new file mode 100644 index 0000000..e37ec6d --- /dev/null +++ b/src/pages/LogsPage.jsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from "react"; +import { useOutletContext } from "react-router-dom"; +import { listLoginLogs, listOperationLogs } from "../api/logs.js"; +import { Card } from "../components/base/Card.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { PaginationBar } from "../components/base/PaginationBar.jsx"; +import { SearchBox } from "../components/base/SearchBox.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; + +export function LogsPage({ type }) { + const [query, setQuery] = useState(""); + const [page, setPage] = useState(1); + const [data, setData] = useState({ items: [], total: 0, page: 1, pageSize: 10 }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const { refreshTick } = useOutletContext(); + const isLogin = type === "login"; + + const load = async () => { + setLoading(true); + setError(""); + try { + const loader = isLogin ? listLoginLogs : listOperationLogs; + const result = await loader({ page, page_size: 10, keyword: query }); + setData(result); + } catch (err) { + setError(err.message || "加载日志失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [page, query, refreshTick, type]); + + return ( + <> + +
+ { setQuery(value); setPage(1); }} placeholder="搜索用户、IP、动作、资源..." /> +
+ + + +
+
{isLogin ? "登录记录" : "操作记录"}
+
共 {data.total} 条
+
+
+
+ {isLogin ? ( + <> +
用户
+
IP
+
状态
+
消息
+
时间
+ + ) : ( + <> +
用户
+
动作
+
资源
+
状态
+
详情
+
时间
+ + )} +
+ {(data.items || []).map((item) => ( +
+ {isLogin ? ( + <> +
{item.username || "-"}
+
{item.ip || "-"}
+
{item.status}
+
{item.message || "-"}
+
{formatDate(item.createdAt)}
+ + ) : ( + <> +
{item.username || "-"}
+
{item.action}
+
{item.resource || "-"}
+
{item.status}
+
{item.detail || "-"}
+
{formatDate(item.createdAt)}
+ + )} +
+ ))} + {!data.items?.length ?
暂无日志
: null} +
+
+ +
+ + ); +} + +function formatDate(value) { + if (!value) { + return "-"; + } + return new Date(value).toLocaleString("zh-CN", { hour12: false }); +} diff --git a/src/pages/MenuManagementPage.jsx b/src/pages/MenuManagementPage.jsx new file mode 100644 index 0000000..904a354 --- /dev/null +++ b/src/pages/MenuManagementPage.jsx @@ -0,0 +1,81 @@ +import MenuOpenOutlined from "@mui/icons-material/MenuOpenOutlined"; +import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; +import { useEffect, useMemo, useState } from "react"; +import { useOutletContext } from "react-router-dom"; +import { getMenus } from "../api/navigation.js"; +import { listPermissions } from "../api/roles.js"; +import { Card } from "../components/base/Card.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { KpiCard } from "../components/base/KpiCard.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; + +export function MenuManagementPage() { + const [menus, setMenus] = useState([]); + const [permissions, setPermissions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const { refreshTick } = useOutletContext(); + + const flatMenus = useMemo(() => flattenMenus(menus), [menus]); + + const load = async () => { + setLoading(true); + setError(""); + try { + const [menuData, permissionData] = await Promise.all([getMenus(), listPermissions()]); + setMenus(menuData || []); + setPermissions(permissionData || []); + } catch (err) { + setError(err.message || "加载菜单权限失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [refreshTick]); + + return ( + <> + + + +
+ 来自后端菜单树} /> + 菜单 / 按钮 / 接口} /> +
+ + +
+
菜单树
+
共 {flatMenus.length} 项
+
+
+
+
菜单
+
路由
+
权限码
+
图标
+
+ {flatMenus.map((menu) => ( +
+
{menu.label}
+
{menu.path || "-"}
+
{menu.permissionCode || "-"}
+
{menu.icon || "-"}
+
+ ))} +
+
+
+ + ); +} + +function flattenMenus(menus, depth = 0) { + return menus.flatMap((menu) => [ + { ...menu, depth }, + ...flattenMenus(menu.children || [], depth + 1) + ]); +} diff --git a/src/pages/NotificationsPage.jsx b/src/pages/NotificationsPage.jsx new file mode 100644 index 0000000..2ce1a31 --- /dev/null +++ b/src/pages/NotificationsPage.jsx @@ -0,0 +1,76 @@ +import DoneAllOutlined from "@mui/icons-material/DoneAllOutlined"; +import { useEffect, useState } from "react"; +import { useOutletContext } from "react-router-dom"; +import { listNotifications, markNotificationRead } from "../api/notifications.js"; +import { Button } from "../components/base/Button.jsx"; +import { Card } from "../components/base/Card.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { useToast } from "../components/base/ToastProvider.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; + +export function NotificationsPage() { + const [data, setData] = useState({ items: [], unread: 0 }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const { refreshTick } = useOutletContext(); + const { showToast } = useToast(); + + const load = async () => { + setLoading(true); + setError(""); + try { + setData(await listNotifications()); + } catch (err) { + setError(err.message || "加载通知失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [refreshTick]); + + const read = async (id) => { + await markNotificationRead(id); + showToast("通知已标记为已读", "success"); + load(); + }; + + return ( + <> + + + +
+ {(data.items || []).map((item) => ( +
+
+
{item.title}
+
{item.content}
+
{formatDate(item.createdAt)}
+
+ {!item.readAt ? ( + + ) : ( + 已读 + )} +
+ ))} + {!data.items?.length ?
暂无通知
: null} +
+
+
+ + ); +} + +function formatDate(value) { + if (!value) { + return "-"; + } + return new Date(value).toLocaleString("zh-CN", { hour12: false }); +} diff --git a/src/pages/OverviewPage.jsx b/src/pages/OverviewPage.jsx new file mode 100644 index 0000000..7f36ea9 --- /dev/null +++ b/src/pages/OverviewPage.jsx @@ -0,0 +1,173 @@ +import Add from "@mui/icons-material/Add"; +import DeveloperBoardOutlined from "@mui/icons-material/DeveloperBoardOutlined"; +import DnsOutlined from "@mui/icons-material/DnsOutlined"; +import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; +import Refresh from "@mui/icons-material/Refresh"; +import SecurityOutlined from "@mui/icons-material/SecurityOutlined"; +import StorageOutlined from "@mui/icons-material/StorageOutlined"; +import { useEffect, useMemo, useState } from "react"; +import { useOutletContext } from "react-router-dom"; +import { getDashboardOverview } from "../api/dashboard.js"; +import { listServices, restartService as restartServiceApi, updateServiceStatus } from "../api/services.js"; +import { useAuth } from "../auth/AuthProvider.jsx"; +import { Button } from "../components/base/Button.jsx"; +import { Card } from "../components/base/Card.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { KpiCard } from "../components/base/KpiCard.jsx"; +import { TimeRangeSelect } from "../components/base/TimeRangeSelect.jsx"; +import { useConfirm } from "../components/base/ConfirmProvider.jsx"; +import { useToast } from "../components/base/ToastProvider.jsx"; +import { BarChart } from "../components/charts/BarChart.jsx"; +import { ChartCard } from "../components/charts/ChartCard.jsx"; +import { StatusDonut } from "../components/charts/StatusDonut.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; +import { ServerRail } from "../components/service/ServerRail.jsx"; +import { ServiceTable } from "../components/service/ServiceTable.jsx"; + +export function OverviewPage() { + const [range, setRange] = useState("近 1 小时"); + const [overview, setOverview] = useState(null); + const [services, setServices] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const { refreshTick } = useOutletContext(); + const { can } = useAuth(); + const confirm = useConfirm(); + const { showToast } = useToast(); + + const load = async () => { + setLoading(true); + setError(""); + try { + const [overviewData, serviceData] = await Promise.all([ + getDashboardOverview(), + listServices({ page: 1, page_size: 8 }) + ]); + setOverview(overviewData); + setServices(serviceData.items || []); + } catch (err) { + setError(err.message || "加载总览失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [refreshTick]); + + const stats = useMemo(() => { + if (!overview) { + return { running: 0, stopped: 0, warning: 0, error: 0, avgCpu: 0, avgMemory: 0 }; + } + return { + running: overview.running, + stopped: overview.stopped, + warning: overview.warning, + error: overview.error, + avgCpu: overview.avgCpu, + avgMemory: overview.avgMemory + }; + }, [overview]); + + const setServiceStatus = async (id, status) => { + if (status === "stopped") { + const ok = await confirm({ + title: "停止服务", + message: "停止后该服务会立即进入不可用状态。", + confirmText: "停止", + tone: "danger" + }); + if (!ok) { + return; + } + } + await updateServiceStatus(id, status); + showToast("服务状态已更新", "success"); + load(); + }; + + const restartService = async (id) => { + const ok = await confirm({ + title: "重启服务", + message: "重启会写入操作日志并刷新服务状态。", + confirmText: "重启" + }); + if (!ok) { + return; + } + await restartServiceApi(id); + showToast("服务已重启", "success"); + load(); + }; + + return ( + <> + + + + {can("service:create") ? ( + + ) : null} + + + +
+ 在线 {overview?.serversOnline || 0}} /> + 运行中 {stats.running} / 已停止 {stats.stopped}} /> + 来自服务资产} /> + 来自服务资产} /> + 严重 {stats.error} / 警告 {stats.warning}} /> +
+ +
+ +
+
服务器状态分布
+
+ +
+ + + +
+
告警趋势
+
+ +
+
+ +
+ {}} /> + {}} + restartService={restartService} + setServiceStatus={setServiceStatus} + title="服务列表" + /> +
+ +
+ {(overview?.alerts || []).map((alert) => ( +
+ +
+
{alert.name}
+
{alert.desc}
+
+
{alert.time}
+
+ ))} +
+
+ + ); +} diff --git a/src/pages/RoleManagementPage.jsx b/src/pages/RoleManagementPage.jsx new file mode 100644 index 0000000..bb2ad99 --- /dev/null +++ b/src/pages/RoleManagementPage.jsx @@ -0,0 +1,194 @@ +import Add from "@mui/icons-material/Add"; +import DeleteOutlineOutlined from "@mui/icons-material/DeleteOutlineOutlined"; +import EditOutlined from "@mui/icons-material/EditOutlined"; +import Drawer from "@mui/material/Drawer"; +import TextField from "@mui/material/TextField"; +import { useEffect, useMemo, useState } from "react"; +import { useOutletContext } from "react-router-dom"; +import { createRole, deleteRole, listPermissions, listRoles, updateRole } from "../api/roles.js"; +import { useAuth } from "../auth/AuthProvider.jsx"; +import { Button } from "../components/base/Button.jsx"; +import { Card } from "../components/base/Card.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { IconButton } from "../components/base/IconButton.jsx"; +import { useConfirm } from "../components/base/ConfirmProvider.jsx"; +import { useToast } from "../components/base/ToastProvider.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; + +const emptyRole = { name: "", code: "", description: "", permissionIds: [] }; + +export function RoleManagementPage() { + const [roles, setRoles] = useState([]); + const [permissions, setPermissions] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [drawerOpen, setDrawerOpen] = useState(false); + const [editingRole, setEditingRole] = useState(null); + const [form, setForm] = useState(emptyRole); + const { refreshTick } = useOutletContext(); + const { can } = useAuth(); + const confirm = useConfirm(); + const { showToast } = useToast(); + + const groupedPermissions = useMemo(() => { + return permissions.reduce((groups, permission) => { + const key = permission.kind || "other"; + groups[key] = groups[key] || []; + groups[key].push(permission); + return groups; + }, {}); + }, [permissions]); + + const load = async () => { + setLoading(true); + setError(""); + try { + const [roleData, permissionData] = await Promise.all([listRoles(), listPermissions()]); + setRoles(roleData || []); + setPermissions(permissionData || []); + } catch (err) { + setError(err.message || "加载角色失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [refreshTick]); + + const openCreate = () => { + setEditingRole(null); + setForm(emptyRole); + setDrawerOpen(true); + }; + + const openEdit = (role) => { + setEditingRole(role); + setForm({ + name: role.name, + code: role.code, + description: role.description || "", + permissionIds: role.permissions?.map((permission) => permission.id) || [] + }); + setDrawerOpen(true); + }; + + const submit = async (event) => { + event.preventDefault(); + try { + if (editingRole) { + await updateRole(editingRole.id, form); + showToast("角色已更新", "success"); + } else { + await createRole(form); + showToast("角色已创建", "success"); + } + setDrawerOpen(false); + load(); + } catch (err) { + showToast(err.message || "保存角色失败", "error"); + } + }; + + const remove = async (role) => { + const ok = await confirm({ + title: "删除角色", + message: `${role.name} 删除后会影响已绑定用户的权限。`, + confirmText: "删除", + tone: "danger" + }); + if (!ok) { + return; + } + await deleteRole(role.id); + showToast("角色已删除", "success"); + load(); + }; + + const togglePermission = (permissionId, checked) => { + setForm((current) => ({ + ...current, + permissionIds: checked + ? [...current.permissionIds, permissionId] + : current.permissionIds.filter((id) => id !== permissionId) + })); + }; + + return ( + <> + + {can("role:manage") ? ( + + ) : null} + + + + +
+
角色清单
+
共 {roles.length} 条
+
+
+
+
角色
+
编码
+
权限数量
+
说明
+
操作
+
+ {roles.map((role) => ( +
+
{role.name}
+
{role.code}
+
{role.permissions?.length || 0}
+
{role.description || "-"}
+
+ openEdit(role)}> + + + remove(role)} tone="danger"> + + +
+
+ ))} +
+
+
+ + setDrawerOpen(false)}> +
+

{editingRole ? "编辑角色" : "新增角色"}

+ setForm({ ...form, name: event.target.value })} /> + setForm({ ...form, code: event.target.value })} /> + setForm({ ...form, description: event.target.value })} /> + {Object.entries(groupedPermissions).map(([kind, items]) => ( +
+
{kind}
+
+ {items.map((permission) => ( + + ))} +
+
+ ))} +
+ + +
+ +
+ + ); +} diff --git a/src/pages/ServicesPage.jsx b/src/pages/ServicesPage.jsx new file mode 100644 index 0000000..4d0f76f --- /dev/null +++ b/src/pages/ServicesPage.jsx @@ -0,0 +1,232 @@ +import Add from "@mui/icons-material/Add"; +import Inventory2Outlined from "@mui/icons-material/Inventory2Outlined"; +import PlayCircleOutlined from "@mui/icons-material/PlayCircleOutlined"; +import StopCircleOutlined from "@mui/icons-material/StopCircleOutlined"; +import WarningAmberOutlined from "@mui/icons-material/WarningAmberOutlined"; +import Drawer from "@mui/material/Drawer"; +import TextField from "@mui/material/TextField"; +import { useEffect, useMemo, useState } from "react"; +import { useOutletContext, useSearchParams } from "react-router-dom"; +import { + createService, + listServices, + restartService as restartServiceApi, + updateServiceStatus +} from "../api/services.js"; +import { useAuth } from "../auth/AuthProvider.jsx"; +import { Button } from "../components/base/Button.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { FilterChip } from "../components/base/FilterChip.jsx"; +import { KpiCard } from "../components/base/KpiCard.jsx"; +import { PaginationBar } from "../components/base/PaginationBar.jsx"; +import { SearchBox } from "../components/base/SearchBox.jsx"; +import { useConfirm } from "../components/base/ConfirmProvider.jsx"; +import { useToast } from "../components/base/ToastProvider.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; +import { DetailDrawer, InlineInspector } from "../components/service/DetailPanel.jsx"; +import { ServiceTable } from "../components/service/ServiceTable.jsx"; +import { detailTabs } from "../constants/status.js"; + +const filterItems = [ + ["all", "全部状态"], + ["running", "运行中"], + ["warning", "警告"], + ["error", "异常"], + ["stopped", "已停止"] +]; + +const emptyForm = { + name: "", + desc: "", + deploy: "prod-a", + version: "v1.0.0", + port: "8080", + replicas: "1/1", + response: "-", + cpu: 0, + memory: 0, + status: "running" +}; + +export function ServicesPage() { + const [query, setQuery] = useState(""); + const [statusFilter, setStatusFilter] = useState("all"); + const [page, setPage] = useState(1); + const [data, setData] = useState({ items: [], total: 0, page: 1, pageSize: 10 }); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [selectedId, setSelectedId] = useState(null); + const [drawerOpen, setDrawerOpen] = useState(false); + const [activeTab, setActiveTab] = useState(detailTabs[0]); + const [formOpen, setFormOpen] = useState(false); + const [form, setForm] = useState(emptyForm); + const { refreshTick } = useOutletContext(); + const [searchParams] = useSearchParams(); + const { can } = useAuth(); + const confirm = useConfirm(); + const { showToast } = useToast(); + + const load = async () => { + setLoading(true); + setError(""); + try { + const result = await listServices({ page, page_size: 10, keyword: query, status: statusFilter }); + setData(result); + if (!selectedId && result.items?.length) { + setSelectedId(Number(searchParams.get("service")) || result.items[0].id); + } + } catch (err) { + setError(err.message || "加载服务失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [page, query, statusFilter, refreshTick]); + + const stats = useMemo(() => { + const items = data.items || []; + return { + running: items.filter((service) => service.status === "running").length, + stopped: items.filter((service) => service.status === "stopped").length, + error: items.filter((service) => service.status === "error").length + }; + }, [data.items]); + + const selectedService = (data.items || []).find((service) => service.id === selectedId) || data.items?.[0]; + + const openDetail = (id) => { + setSelectedId(id); + setActiveTab(detailTabs[0]); + setDrawerOpen(true); + }; + + const setServiceStatus = async (id, status) => { + if (status === "stopped") { + const ok = await confirm({ + title: "停止服务", + message: "停止服务会写入操作日志,并让该服务进入不可用状态。", + confirmText: "停止", + tone: "danger" + }); + if (!ok) { + return; + } + } + await updateServiceStatus(id, status); + showToast("服务状态已更新", "success"); + load(); + }; + + const restartService = async (id) => { + const ok = await confirm({ title: "重启服务", message: "重启会刷新服务状态并记录操作日志。", confirmText: "重启" }); + if (!ok) { + return; + } + await restartServiceApi(id); + showToast("服务已重启", "success"); + load(); + }; + + const submitService = async (event) => { + event.preventDefault(); + try { + await createService({ ...form, cpu: Number(form.cpu), memory: Number(form.memory) }); + showToast("服务已创建", "success"); + setFormOpen(false); + setForm(emptyForm); + load(); + } catch (err) { + showToast(err.message || "创建服务失败", "error"); + } + }; + + return ( + <> + + {can("service:create") ? ( + + ) : null} + + +
+ 来自后端资产} /> + 当前页统计} /> + 当前页统计} /> + 当前页统计} /> +
+ +
+ { setQuery(value); setPage(1); }} placeholder="搜索服务名称、IP、端口..." /> +
+ {filterItems.map(([value, label]) => ( + { + setStatusFilter(value); + setPage(1); + }} + /> + ))} +
+
+ + +
+
+ + +
+ +
+
+ + setDrawerOpen(false)} + restartService={restartService} + service={selectedService} + setActiveTab={setActiveTab} + setServiceStatus={setServiceStatus} + /> + + setFormOpen(false)}> +
+

创建服务

+ setForm({ ...form, name: event.target.value })} /> + setForm({ ...form, desc: event.target.value })} /> + setForm({ ...form, deploy: event.target.value })} /> + setForm({ ...form, version: event.target.value })} /> + setForm({ ...form, port: event.target.value })} /> + setForm({ ...form, cpu: event.target.value })} /> + setForm({ ...form, memory: event.target.value })} /> +
+ + +
+ +
+ + ); +} diff --git a/src/pages/UserManagementPage.jsx b/src/pages/UserManagementPage.jsx new file mode 100644 index 0000000..2b4330b --- /dev/null +++ b/src/pages/UserManagementPage.jsx @@ -0,0 +1,335 @@ +import Add from "@mui/icons-material/Add"; +import BlockOutlined from "@mui/icons-material/BlockOutlined"; +import DownloadOutlined from "@mui/icons-material/DownloadOutlined"; +import ManageAccountsOutlined from "@mui/icons-material/ManageAccountsOutlined"; +import ShieldOutlined from "@mui/icons-material/ShieldOutlined"; +import VerifiedUserOutlined from "@mui/icons-material/VerifiedUserOutlined"; +import Drawer from "@mui/material/Drawer"; +import FormControlLabel from "@mui/material/FormControlLabel"; +import Switch from "@mui/material/Switch"; +import TextField from "@mui/material/TextField"; +import { useEffect, useMemo, useState } from "react"; +import { useNavigate, useOutletContext } from "react-router-dom"; +import { listRoles } from "../api/roles.js"; +import { + batchUpdateUserStatus, + createUser, + exportUsers, + listUsers, + resetUserPassword, + updateUser, + updateUserStatus +} from "../api/users.js"; +import { useAuth } from "../auth/AuthProvider.jsx"; +import { Button } from "../components/base/Button.jsx"; +import { DataState } from "../components/base/DataState.jsx"; +import { FilterChip } from "../components/base/FilterChip.jsx"; +import { KpiCard } from "../components/base/KpiCard.jsx"; +import { PaginationBar } from "../components/base/PaginationBar.jsx"; +import { SearchBox } from "../components/base/SearchBox.jsx"; +import { useConfirm } from "../components/base/ConfirmProvider.jsx"; +import { useToast } from "../components/base/ToastProvider.jsx"; +import { PageHead } from "../components/layout/PageHead.jsx"; +import { UserTable } from "../components/user/UserTable.jsx"; +import { userStatusMeta } from "../data/users.js"; + +const statusFilters = [ + ["all", "全部状态"], + ["active", "启用"], + ["locked", "锁定"], + ["disabled", "停用"] +]; + +const emptyForm = { + username: "", + name: "", + team: "", + password: "", + status: "active", + mfaEnabled: false, + roleIds: [] +}; + +export function UserManagementPage() { + const [users, setUsers] = useState({ items: [], total: 0, page: 1, pageSize: 10 }); + const [roles, setRoles] = useState([]); + const [query, setQuery] = useState(""); + const [roleFilter, setRoleFilter] = useState("all"); + const [statusFilter, setStatusFilter] = useState("all"); + const [page, setPage] = useState(1); + const [selectedIds, setSelectedIds] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(""); + const [formOpen, setFormOpen] = useState(false); + const [editingUser, setEditingUser] = useState(null); + const [form, setForm] = useState(emptyForm); + const { refreshTick } = useOutletContext(); + const { can } = useAuth(); + const navigate = useNavigate(); + const confirm = useConfirm(); + const { showToast } = useToast(); + + const roleFilters = useMemo(() => [["all", "全部角色"], ...roles.map((role) => [role.name, role.name])], [roles]); + + const load = async () => { + setLoading(true); + setError(""); + try { + const [userData, roleData] = await Promise.all([ + listUsers({ page, page_size: 10, keyword: query, role: roleFilter, status: statusFilter }), + listRoles() + ]); + setUsers(userData); + setRoles(roleData || []); + setSelectedIds([]); + } catch (err) { + setError(err.message || "加载用户失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + load(); + }, [page, query, roleFilter, statusFilter, refreshTick]); + + const stats = useMemo(() => { + const items = users.items || []; + const active = items.filter((user) => user.status === "active").length; + const locked = items.filter((user) => user.status === "locked").length; + const admin = items.filter((user) => String(user.role).includes("管理员")).length; + return { active, admin, locked }; + }, [users.items]); + + const openCreate = () => { + setEditingUser(null); + setForm(emptyForm); + setFormOpen(true); + }; + + const openEdit = (user) => { + setEditingUser(user); + setForm({ + username: user.account, + name: user.name, + team: user.team || "", + password: "", + status: user.status, + mfaEnabled: user.mfaEnabled, + roleIds: user.roleIds || [] + }); + setFormOpen(true); + }; + + const submitUser = async (event) => { + event.preventDefault(); + try { + if (editingUser) { + await updateUser(editingUser.id, { + name: form.name, + team: form.team, + status: form.status, + mfaEnabled: form.mfaEnabled, + roleIds: form.roleIds + }); + showToast("用户已更新", "success"); + } else { + const result = await createUser(form); + showToast(`用户已创建,初始密码:${result.initialPassword}`, "success"); + } + setFormOpen(false); + load(); + } catch (err) { + showToast(err.message || "保存用户失败", "error"); + } + }; + + const toggleLocked = async (user) => { + const nextStatus = user.status === "locked" ? "active" : "locked"; + const ok = await confirm({ + title: user.status === "locked" ? "解锁用户" : "锁定用户", + message: `${user.name} 的登录状态会立即变更。`, + confirmText: user.status === "locked" ? "解锁" : "锁定", + tone: user.status === "locked" ? "primary" : "danger" + }); + if (!ok) { + return; + } + await updateUserStatus(user.id, nextStatus); + showToast("用户状态已更新", "success"); + load(); + }; + + const toggleDisabled = async (user) => { + const nextStatus = user.status === "disabled" ? "active" : "disabled"; + const ok = await confirm({ + title: user.status === "disabled" ? "启用用户" : "停用用户", + message: `${user.name} 的账号状态会立即变更。`, + confirmText: user.status === "disabled" ? "启用" : "停用", + tone: user.status === "disabled" ? "primary" : "danger" + }); + if (!ok) { + return; + } + await updateUserStatus(user.id, nextStatus); + showToast("用户状态已更新", "success"); + load(); + }; + + const resetPassword = async (user) => { + const ok = await confirm({ + title: "重置密码", + message: `${user.name} 的密码会重置为系统生成的初始密码。`, + confirmText: "重置", + tone: "danger" + }); + if (!ok) { + return; + } + const result = await resetUserPassword(user.id); + showToast(`已重置,初始密码:${result.initialPassword}`, "success"); + }; + + const batchStatus = async (status) => { + if (!selectedIds.length) { + showToast("请先选择用户", "warning"); + return; + } + const ok = await confirm({ + title: "批量更新状态", + message: `将更新 ${selectedIds.length} 个用户状态。`, + confirmText: "确认", + tone: status === "active" ? "primary" : "danger" + }); + if (!ok) { + return; + } + await batchUpdateUserStatus(selectedIds, status); + showToast("批量状态已更新", "success"); + load(); + }; + + const downloadCSV = async () => { + const response = await exportUsers({ keyword: query, role: roleFilter, status: statusFilter }); + const blob = await response.blob(); + const url = window.URL.createObjectURL(blob); + const link = document.createElement("a"); + link.href = url; + link.download = "hyapp-users.csv"; + link.click(); + window.URL.revokeObjectURL(url); + }; + + const setRoleChecked = (roleId, checked) => { + setForm((current) => ({ + ...current, + roleIds: checked ? [...current.roleIds, roleId] : current.roleIds.filter((id) => id !== roleId) + })); + }; + + return ( + <> + + {can("role:view") ? ( + + ) : null} + + {can("user:create") ? ( + + ) : null} + + +
+ {stats.active} 个启用账户} /> + 当前页统计} /> + 可登录控制台} /> + 需管理员处理} /> +
+ +
+ { setQuery(value); setPage(1); }} placeholder="搜索用户、账号、角色、团队..." /> +
+ {roleFilters.map(([value, label]) => ( + { setRoleFilter(value); setPage(1); }} /> + ))} +
+
+ {statusFilters.map(([value, label]) => ( + { setStatusFilter(value); setPage(1); }} /> + ))} +
+ {can("user:status") ? ( +
+ + +
+ ) : null} +
+ + + + + + + setFormOpen(false)}> +
+

{editingUser ? "编辑用户" : "新增用户"}

+ setForm({ ...form, username: event.target.value })} + /> + setForm({ ...form, name: event.target.value })} /> + setForm({ ...form, team: event.target.value })} /> + {!editingUser ? ( + setForm({ ...form, password: event.target.value })} /> + ) : null} +
角色
+
+ {roles.map((role) => ( + + ))} +
+ setForm({ ...form, mfaEnabled: event.target.checked })} />} + label="MFA 已开启" + /> +
+ + +
+ +
+ + ); +} diff --git a/src/styles/app.css b/src/styles/app.css new file mode 100644 index 0000000..c6410a2 --- /dev/null +++ b/src/styles/app.css @@ -0,0 +1,2290 @@ +.app-shell { + display: grid; + grid-template-rows: var(--header-height) 1fr; + min-height: 100vh; + background: var(--bg-page); +} + +@keyframes page-enter { + from { + opacity: 0; + transform: translateY(10px) scale(0.995); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes panel-enter { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +@keyframes row-enter { + from { + opacity: 0; + transform: translateX(-8px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +@keyframes shimmer { + from { + background-position: 120% 0; + } + to { + background-position: -120% 0; + } +} + +@keyframes notice-pop { + 0%, + 100% { + transform: scale(1); + } + 45% { + transform: scale(1.18); + } +} + +@keyframes progress-fill { + from { + transform: scaleX(0); + } + to { + transform: scaleX(1); + } +} + +.header { + display: grid; + grid-template-columns: 248px 1fr; + height: var(--header-height); + border-bottom: 1px solid var(--border); + background: var(--bg-header); + transition: grid-template-columns var(--motion-slow) var(--ease-emphasized); +} + +.brand { + display: flex; + align-items: center; + gap: 14px; + height: var(--header-height); + padding: 0 22px; + border-right: 1px solid var(--border); + background: var(--bg-sidebar); +} + +.brand-mark { + display: grid; + width: 38px; + height: 38px; + place-items: center; + border-radius: 8px; + background: linear-gradient(135deg, var(--primary), var(--brand-secondary)); + color: var(--active-contrast); + transition: + transform var(--motion-base) var(--ease-emphasized), + box-shadow var(--motion-base) var(--ease-standard); +} + +.brand:hover .brand-mark { + box-shadow: 0 10px 24px rgba(37, 99, 235, 0.22); + transform: rotate(-6deg) scale(1.05); +} + +.brand-title { + display: block; + color: var(--text-primary); + font-size: 16px; + font-weight: 650; +} + +.brand-meta { + display: block; + margin-top: 2px; + color: var(--text-tertiary); + font-size: 12px; +} + +.header-tools { + display: flex; + min-width: 0; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 0 24px; +} + +.header-left-tools, +.header-right-tools { + display: flex; + min-width: 0; + align-items: center; + gap: 12px; +} + +.header-right-tools { + margin-left: auto; + justify-content: flex-end; +} + +.header-location { + display: inline-flex; + min-width: 74px; + height: 36px; + align-items: center; + justify-content: center; + padding: 0 16px; + border: 1px solid rgba(37, 99, 235, 0.24); + border-radius: 8px; + background: rgba(37, 99, 235, 0.08); + box-shadow: + var(--shadow-soft), + inset 0 -1px 0 rgba(37, 99, 235, 0.22); + color: var(--text-primary); + font-size: 15px; + font-weight: 650; + white-space: nowrap; +} + +.search-box { + display: flex; + flex: 1 1 420px; + min-width: 180px; + max-width: 520px; + height: 38px; + align-items: center; + gap: 10px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-input); + color: var(--text-tertiary); + transition: + border-color var(--motion-base) var(--ease-standard), + background var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.search-box:focus-within { + border-color: rgba(37, 99, 235, 0.78); + background: var(--bg-input-strong); + box-shadow: 0 0 0 3px var(--focus-ring); + transform: translateY(-1px); +} + +.search-box svg { + transition: + color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.search-box:focus-within svg { + color: var(--primary); + transform: scale(1.08); +} + +.search-trigger { + display: flex; + width: min(360px, 34vw); + min-width: 260px; + height: 38px; + align-items: center; + gap: 10px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-input); + color: var(--text-tertiary); + cursor: pointer; + text-align: left; + transition: + border-color var(--motion-base) var(--ease-standard), + background var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.search-trigger span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.search-trigger:hover, +.search-trigger:focus-visible { + border-color: rgba(37, 99, 235, 0.78); + background: var(--bg-input-strong); + box-shadow: 0 0 0 3px var(--focus-ring); + color: var(--text-primary); + transform: translateY(-1px); +} + +.search-box input { + width: 100%; + min-width: 0; + border: 0; + background: transparent; + color: var(--text-primary); + outline: 0; +} + +.search-box input::placeholder { + color: var(--text-tertiary); +} + +.time-select { + width: 116px; + height: 38px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card); + color: var(--text-secondary); +} + +.icon-button { + display: inline-grid; + width: 38px; + height: 38px; + place-items: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + transition: + background var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + border-color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.icon-button:hover { + border-color: rgba(37, 99, 235, 0.7); + background: var(--bg-hover); + box-shadow: var(--shadow-soft); + color: var(--text-primary); + transform: translateY(-1px); +} + +.icon-button:active { + box-shadow: none; + transform: translateY(0) scale(0.96); +} + +.icon-button--notice { + position: relative; +} + +.icon-button--notice span { + position: absolute; + top: -5px; + right: -5px; + display: grid; + min-width: 17px; + height: 17px; + place-items: center; + border-radius: 999px; + background: var(--danger); + color: var(--active-contrast); + font-size: 10px; + font-weight: 700; + animation: notice-pop 1500ms var(--ease-emphasized) infinite; +} + +.user-pill { + display: flex; + height: 38px; + align-items: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + transition: + border-color var(--motion-base) var(--ease-standard), + background var(--motion-base) var(--ease-standard), + color var(--motion-base) var(--ease-standard); +} + +.user-pill:hover { + border-color: rgba(37, 99, 235, 0.52); + background: rgba(37, 99, 235, 0.08); + color: var(--text-primary); +} + +.user-pill:active { + transform: scale(0.98); +} + +.user-menu .MuiPaper-root { + min-width: 132px; + margin-top: 8px; +} + +.user-menu__item { + display: flex; + gap: 8px; + min-height: 38px; + color: var(--text-secondary); + font-size: 13px; +} + +.user-menu__item:hover { + color: var(--text-primary); +} + +.body-grid { + display: grid; + grid-template-columns: 248px 1fr; + min-height: 0; + transition: grid-template-columns var(--motion-slow) var(--ease-emphasized); +} + +.sidebar { + position: relative; + z-index: 25; + min-height: 0; + overflow: visible; + padding: 16px 12px; + border-right: 1px solid var(--border); + background: var(--bg-sidebar); +} + +.app-shell--sidebar-collapsed .header, +.app-shell--sidebar-collapsed .body-grid { + grid-template-columns: 84px 1fr; +} + +.app-shell--sidebar-collapsed .brand { + justify-content: center; + padding: 0; +} + +.app-shell--sidebar-collapsed .brand-text, +.sidebar--collapsed .nav-label { + display: none; +} + +.sidebar--collapsed { + padding: 16px 10px; +} + +.sidebar--collapsed .nav-item { + width: 44px; + justify-content: center; + justify-self: center; + padding: 0; +} + +.nav-list { + display: grid; + gap: 6px; +} + +.nav-group { + display: grid; + position: relative; + gap: 4px; +} + +.nav-item { + position: relative; + display: flex; + width: 100%; + height: 44px; + align-items: center; + gap: 10px; + padding: 0 12px; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + overflow: hidden; + transition: + background var(--motion-base) var(--ease-standard), + color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.nav-item--parent { + padding-right: 10px; +} + +.nav-item--open:not(.nav-item--active) { + background: rgba(37, 99, 235, 0.06); + color: var(--text-primary); +} + +.nav-expand { + margin-left: auto; + color: var(--text-tertiary); + transition: + color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.nav-item--open .nav-expand, +.nav-item:hover .nav-expand { + color: currentColor; +} + +.nav-item--expanded .nav-expand { + transform: rotate(180deg); +} + +.nav-children { + display: grid; + gap: 4px; + padding-left: 20px; +} + +.nav-item--child { + height: 38px; + padding-left: 12px; + font-size: 13px; +} + +.nav-item--child svg { + font-size: 18px; +} + +.sidebar--collapsed .nav-children, +.sidebar--collapsed .nav-item--parent .nav-expand { + display: none !important; +} + +.sidebar--collapsed .nav-item--open, +.sidebar--collapsed .nav-item--open:hover, +.sidebar--collapsed .nav-item--flyout-open, +.sidebar--collapsed .nav-item--flyout-open:hover { + background: linear-gradient(90deg, rgba(37, 99, 235, 0.96), rgba(14, 165, 233, 0.34)); + box-shadow: inset 0 0 0 1px rgba(14, 165, 233, 0.2); + color: var(--active-contrast); +} + +.nav-flyout { + position: absolute; + top: 0; + left: calc(100% + 12px); + z-index: 30; + display: grid; + width: 168px; + gap: 4px; + padding: 8px; + border: 1px solid rgba(37, 99, 235, 0.22); + border-radius: 8px; + background: var(--bg-flyout); + box-shadow: var(--shadow-panel); + animation: panel-enter var(--motion-base) var(--ease-emphasized) both; +} + +.nav-flyout::before { + position: absolute; + top: 16px; + left: -6px; + width: 10px; + height: 10px; + border-bottom: 1px solid rgba(37, 99, 235, 0.22); + border-left: 1px solid rgba(37, 99, 235, 0.22); + background: var(--bg-flyout); + content: ""; + transform: rotate(45deg); +} + +.nav-flyout__title { + padding: 4px 8px 6px; + color: var(--text-tertiary); + font-size: 12px; + font-weight: 650; +} + +.nav-flyout__item { + display: flex; + height: 38px; + align-items: center; + gap: 10px; + padding: 0 10px; + border-radius: 8px; + background: transparent; + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + text-align: left; + transition: + background var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.nav-flyout__item:hover, +.nav-flyout__item--active { + background: rgba(37, 99, 235, 0.1); + color: var(--text-primary); + transform: translateX(2px); +} + +.nav-item::before { + position: absolute; + inset: 8px auto 8px 0; + width: 3px; + border-radius: 0 999px 999px 0; + background: var(--primary); + content: ""; + opacity: 0; + transform: translateX(-4px); + transition: + opacity var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.nav-item:hover { + background: var(--bg-hover); + color: var(--text-primary); + transform: translateX(2px); +} + +.nav-item--active, +.nav-item--active:hover { + background: linear-gradient(90deg, rgba(37, 99, 235, 0.96), rgba(14, 165, 233, 0.34)); + box-shadow: inset 0 0 0 1px rgba(14, 165, 233, 0.2); + color: var(--active-contrast); +} + +.nav-item:hover::before, +.nav-item--active::before { + opacity: 1; + transform: translateX(0); +} + +.nav-label { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.workspace { + position: relative; + min-width: 0; + min-height: 0; + overflow: auto; + padding: 24px; +} + +.page-transition { + animation: page-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.page-skeleton { + display: grid; + height: calc(100vh - var(--header-height) - 48px); + height: calc(100dvh - var(--header-height) - 48px); + grid-template-rows: 38px minmax(96px, 0.7fr) minmax(160px, 1fr) minmax(200px, 1.2fr); + gap: 16px; + animation: page-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.page-skeleton__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.page-skeleton__title { + width: min(220px, 36%); + height: 30px; +} + +.page-skeleton__actions { + display: flex; + gap: 10px; +} + +.page-skeleton__actions .page-skeleton__block { + width: 96px; + height: 36px; +} + +.page-skeleton__kpis { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 16px; +} + +.page-skeleton__charts { + display: grid; + grid-template-columns: minmax(280px, 1.15fr) repeat(2, minmax(260px, 1fr)) minmax(260px, 0.95fr); + gap: 16px; + min-height: 0; +} + +.page-skeleton__content { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 16px; + min-height: 0; +} + +.page-skeleton__block { + position: relative; + width: 100%; + height: 100%; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 10px; + background-color: var(--skeleton-surface); + opacity: 1; + transform: translateZ(0); + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.page-skeleton__block::after { + background: linear-gradient(90deg, transparent, var(--skeleton-shine), transparent); +} + +.page-skeleton__kpi:nth-child(2), +.page-skeleton__charts .page-skeleton__block:nth-child(2), +.page-skeleton__content .page-skeleton__block:nth-child(2) { + animation-delay: 40ms; +} + +.page-skeleton__kpi:nth-child(3), +.page-skeleton__charts .page-skeleton__block:nth-child(3) { + animation-delay: 80ms; +} + +.page-skeleton__kpi:nth-child(4), +.page-skeleton__charts .page-skeleton__block:nth-child(4) { + animation-delay: 120ms; +} + +.page-skeleton__kpi:nth-child(5) { + animation-delay: 160ms; +} + +.search-dialog { + overflow: hidden; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--bg-card); + background-image: none; + box-shadow: var(--shadow-dialog); + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.search-dialog__head { + padding: 18px; + border-bottom: 1px solid var(--border); + background: var(--bg-card-strong); +} + +.search-dialog__input { + max-width: none; + width: 100%; + height: 44px; +} + +.search-dialog__list { + display: grid; + gap: 6px; + max-height: min(56vh, 520px); + overflow: auto; + padding: 10px; +} + +.search-dialog__item { + min-height: 64px; + border: 1px solid transparent; + border-radius: 10px; + color: var(--text-primary); + transition: + background var(--motion-base) var(--ease-standard), + border-color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.search-dialog__item:hover { + border-color: rgba(37, 99, 235, 0.34); + background: rgba(37, 99, 235, 0.08); + transform: translateX(3px); +} + +.search-dialog__primary { + color: var(--text-primary); + font-size: 14px; + font-weight: 650; +} + +.search-dialog__secondary { + color: var(--text-tertiary); + font-size: 12px; +} + +.search-dialog__empty { + display: grid; + min-height: 120px; + place-items: center; + color: var(--text-tertiary); + font-size: 13px; +} + +.page-head { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 16px; + margin-bottom: 18px; +} + +.page-title { + margin: 0; + font-size: 20px; + font-weight: 700; +} + +.page-meta { + margin: 6px 0 0; + color: var(--text-tertiary); + font-size: 12px; +} + +.page-actions { + display: flex; + align-items: center; + gap: 10px; +} + +.button { + display: inline-flex; + height: 36px; + align-items: center; + justify-content: center; + gap: 8px; + padding: 0 12px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card); + color: var(--text-secondary); + cursor: pointer; + font-size: 13px; + white-space: nowrap; + transition: + background var(--motion-fast) var(--ease-standard), + border-color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.button:hover { + border-color: rgba(37, 99, 235, 0.7); + background: var(--bg-hover); + box-shadow: var(--shadow-soft); + color: var(--text-primary); + transform: translateY(-1px); +} + +.button:active { + box-shadow: none; + transform: translateY(0) scale(0.985); +} + +.button:disabled, +.action-button:disabled { + cursor: not-allowed; + opacity: 0.42; +} + +.button--primary { + border-color: var(--primary); + background: var(--primary); + color: var(--active-contrast); +} + +.button--success { + color: var(--success); +} + +.button--danger { + color: var(--danger); +} + +.kpi-grid { + display: grid; + grid-template-columns: repeat(5, minmax(0, 1fr)); + gap: 16px; +} + +.card { + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-card); +} + +.kpi-card { + min-height: 132px; + padding: 20px; + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + border-color var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.kpi-grid .kpi-card:nth-child(2), +.service-kpi-row .kpi-card:nth-child(2) { + animation-delay: 40ms; +} + +.kpi-grid .kpi-card:nth-child(3), +.service-kpi-row .kpi-card:nth-child(3) { + animation-delay: 80ms; +} + +.kpi-grid .kpi-card:nth-child(4), +.service-kpi-row .kpi-card:nth-child(4) { + animation-delay: 120ms; +} + +.kpi-grid .kpi-card:nth-child(5), +.service-kpi-row .chart-card { + animation-delay: 160ms; +} + +.kpi-card:hover, +.chart-card:hover, +.table-card:hover, +.server-rail:hover, +.inline-inspector:hover { + border-color: rgba(37, 99, 235, 0.34); + box-shadow: var(--shadow-panel); + transform: translateY(-2px); +} + +.kpi-card__top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.kpi-card__label, +.card-title { + color: var(--text-secondary); + font-size: 14px; + font-weight: 650; +} + +.metric-icon { + display: grid; + width: 34px; + height: 34px; + place-items: center; + border-radius: 8px; + background: rgba(37, 99, 235, 0.1); + color: var(--primary); + transition: + background var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.kpi-card:hover .metric-icon { + transform: scale(1.08) rotate(-4deg); +} + +.metric-icon--success { + background: rgba(22, 163, 74, 0.1); + color: var(--success); +} + +.metric-icon--warning { + background: rgba(217, 119, 6, 0.1); + color: var(--warning); +} + +.metric-icon--info { + background: rgba(2, 132, 199, 0.1); + color: var(--info); +} + +.kpi-value { + margin-top: 18px; + color: var(--text-primary); + font-size: 32px; + font-weight: 760; + line-height: 1; +} + +.kpi-value span { + margin-left: 5px; + font-size: 14px; + font-weight: 600; +} + +.kpi-sub { + margin-top: 12px; + color: var(--text-tertiary); + font-size: 12px; +} + +.status-ok { + color: var(--success); +} + +.status-warn { + color: var(--warning); +} + +.status-danger { + color: var(--danger); +} + +.panel-grid { + display: grid; + grid-template-columns: minmax(0, 1.35fr) minmax(340px, 0.65fr); + gap: 16px; + margin-top: 16px; +} + +.overview-charts { + display: grid; + grid-template-columns: minmax(280px, 1.15fr) repeat(2, minmax(260px, 1fr)) minmax(260px, 0.95fr); + gap: 16px; + margin-top: 16px; +} + +.chart-card { + min-height: 304px; + padding: 20px; + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + border-color var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.chart-card--compact { + min-height: 150px; +} + +.chart-card--compact .line-chart { + height: 78px; +} + +.chart-card--compact .card-head { + margin-bottom: 6px; +} + +.chart-card--compact .segmented { + display: none; +} + +.card-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + margin-bottom: 18px; +} + +.segmented { + display: inline-flex; + height: 30px; + padding: 2px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card-strong); +} + +.segment { + min-width: 52px; + padding: 0 10px; + border-radius: 6px; + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + font-size: 12px; + transition: + background var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.segment:hover { + transform: translateY(-1px); +} + +.segment--active { + background: rgba(37, 99, 235, 0.1); + color: var(--text-primary); +} + +.line-chart { + width: 100%; + height: 216px; + opacity: 0; + animation: panel-enter 420ms var(--ease-emphasized) 120ms both; +} + +.echart { + width: 100%; + min-width: 0; +} + +.line-chart__grid { + stroke: rgba(100, 116, 139, 0.16); + stroke-width: 1; +} + +.line-chart__area { + opacity: 0.18; +} + +.line-chart__line { + fill: none; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; +} + +.donut-wrap { + display: grid; + grid-template-columns: 140px 1fr; + align-items: center; + gap: 22px; + min-height: 216px; +} + +.donut-echart { + width: 140px; + height: 140px; + opacity: 0; + animation: panel-enter 420ms var(--ease-emphasized) 120ms both; +} + +.donut { + display: grid; + width: 140px; + height: 140px; + place-items: center; + border-radius: 50%; +} + +.donut-core { + display: grid; + width: 78px; + height: 78px; + place-items: center; + border-radius: 50%; + background: var(--bg-card); + color: var(--text-secondary); + font-size: 12px; +} + +.donut-core strong { + display: block; + color: var(--text-primary); + font-size: 24px; + line-height: 1; +} + +.donut-legend { + display: grid; + gap: 14px; +} + +.legend-row { + display: grid; + grid-template-columns: 8px 1fr auto; + align-items: center; + gap: 10px; + color: var(--text-secondary); + font-size: 13px; +} + +.legend-row strong { + color: var(--text-primary); +} + +.legend-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.legend-dot--success { + background: var(--success); +} + +.legend-dot--danger { + background: var(--danger); +} + +.legend-dot--stopped { + background: var(--stopped); +} + +.bar-chart { + display: block; + width: 100%; + height: 216px; + opacity: 0; + animation: panel-enter 420ms var(--ease-emphasized) 120ms both; +} + +.alert-list { + display: grid; + gap: 12px; +} + +.alert-item { + display: grid; + grid-template-columns: 8px 1fr auto; + align-items: center; + gap: 12px; + min-height: 42px; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + background var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.alert-item:nth-child(2) { + animation-delay: 50ms; +} + +.alert-item:nth-child(3) { + animation-delay: 100ms; +} + +.alert-item:hover { + transform: translateX(2px); +} + +.alert-dot { + width: 8px; + height: 8px; + border-radius: 50%; +} + +.alert-dot--danger { + background: var(--danger); +} + +.alert-dot--warning { + background: var(--warning); +} + +.alert-dot--info { + background: var(--info); +} + +.alert-name { + color: var(--text-primary); + font-size: 13px; + font-weight: 620; +} + +.alert-desc, +.alert-time { + color: var(--text-tertiary); + font-size: 12px; +} + +.table-card { + margin-top: 16px; + overflow: hidden; + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + border-color var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.split-grid { + display: grid; + grid-template-columns: 280px minmax(0, 1fr); + gap: 16px; + margin-top: 16px; +} + +.server-rail { + overflow: hidden; + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + border-color var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.server-list { + display: grid; + gap: 0; + padding: 8px; +} + +.server-item { + display: grid; + grid-template-columns: 8px 1fr auto; + align-items: center; + gap: 12px; + width: 100%; + min-height: 64px; + padding: 10px 12px; + border-radius: 8px; + background: transparent; + color: var(--text-primary); + cursor: pointer; + text-align: left; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + background var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.server-item:hover { + background: var(--bg-hover); + transform: translateX(3px); +} + +.server-item:nth-child(2) { + animation-delay: 40ms; +} + +.server-item:nth-child(3) { + animation-delay: 80ms; +} + +.server-item:nth-child(4) { + animation-delay: 120ms; +} + +.server-item strong, +.server-item small { + display: block; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.server-item small { + margin-top: 4px; + color: var(--text-tertiary); + font-size: 12px; +} + +.server-status { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--stopped); +} + +.server-status--running { + background: var(--success); +} + +.server-status--warning { + background: var(--warning); +} + +.server-status--error { + background: var(--danger); +} + +.link-button { + background: transparent; + color: var(--primary); + cursor: pointer; + font-size: 12px; +} + +.alert-strip { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 16px; + margin-top: 16px; + padding-bottom: 8px; +} + +.table-toolbar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 14px; + padding: 18px 20px; + border-bottom: 1px solid var(--border); +} + +.filter-row { + display: flex; + align-items: center; + gap: 8px; +} + +.filter-chip { + height: 30px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 999px; + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + font-size: 12px; + transition: + background var(--motion-fast) var(--ease-standard), + border-color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.filter-chip:hover { + box-shadow: var(--shadow-soft); + transform: translateY(-1px); +} + +.filter-chip--active { + border-color: rgba(37, 99, 235, 0.7); + background: rgba(37, 99, 235, 0.1); + color: var(--text-primary); +} + +.service-kpi-row { + display: grid; + grid-template-columns: repeat(4, minmax(170px, 1fr)); + gap: 16px; +} + +.service-kpi-row .kpi-card { + min-height: 150px; +} + +.filters-panel { + display: flex; + align-items: center; + gap: 14px; + margin-top: 16px; +} + +.filters-panel .search-box { + flex: 0 0 280px; +} + +.service-page-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) 320px; + gap: 16px; + margin-top: 12px; +} + +.service-page-grid .table-card { + margin-top: 0; +} + +.user-kpi-row { + display: grid; + grid-template-columns: repeat(4, minmax(170px, 1fr)); + gap: 16px; +} + +.user-filters-panel { + flex-wrap: wrap; +} + +.user-filters-panel .search-box { + flex: 0 0 320px; +} + +.user-table-card { + margin-top: 16px; +} + +.user-table { + min-width: 1040px; +} + +.user-row { + display: grid; + grid-template-columns: 1.4fr 1fr 1fr 0.72fr 0.72fr 0.9fr 0.72fr; + align-items: center; + min-height: 64px; + padding: 0 20px; + border-bottom: 1px solid var(--row-border); + color: var(--text-secondary); + font-size: 13px; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + background var(--motion-base) var(--ease-standard), + border-color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.user-table--selectable { + min-width: 1180px; +} + +.user-table--selectable .user-row { + grid-template-columns: 48px 1.4fr 1fr 1fr 0.72fr 0.72fr 0.9fr 1.2fr; +} + +.user-row--head { + min-height: 44px; + background: var(--bg-card-strong); + color: var(--text-tertiary); + font-size: 12px; + font-weight: 650; +} + +.user-row:not(.user-row--head):hover { + background: rgba(37, 99, 235, 0.06); + border-color: rgba(37, 99, 235, 0.2); + transform: translateX(3px); +} + +.user-row:nth-child(3) { + animation-delay: 35ms; +} + +.user-row:nth-child(4) { + animation-delay: 70ms; +} + +.user-row:nth-child(5) { + animation-delay: 105ms; +} + +.user-row:nth-child(6) { + animation-delay: 140ms; +} + +.user-cell { + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.user-main { + display: flex; + min-width: 0; + align-items: center; + gap: 12px; +} + +.user-avatar { + display: grid; + flex: 0 0 auto; + width: 34px; + height: 34px; + place-items: center; + border-radius: 8px; + background: rgba(37, 99, 235, 0.1); + color: var(--primary); + transition: + background var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.user-row:hover .user-avatar { + background: rgba(37, 99, 235, 0.14); + transform: scale(1.08); +} + +.service-table { + min-width: 980px; +} + +.service-row { + display: grid; + grid-template-columns: 2.1fr 0.85fr 1fr 1fr 0.9fr 1.35fr; + align-items: center; + min-height: 64px; + padding: 0 20px; + border-bottom: 1px solid var(--row-border); + font-size: 13px; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + background var(--motion-base) var(--ease-standard), + border-color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.service-row--head { + min-height: 44px; + background: var(--bg-card-strong); + color: var(--text-tertiary); + font-size: 12px; + font-weight: 650; +} + +.service-row:not(.service-row--head) { + cursor: pointer; +} + +.service-row:not(.service-row--head):hover { + background: rgba(37, 99, 235, 0.06); + border-color: rgba(37, 99, 235, 0.2); + transform: translateX(3px); +} + +.service-row:nth-child(3) { + animation-delay: 35ms; +} + +.service-row:nth-child(4) { + animation-delay: 70ms; +} + +.service-row:nth-child(5) { + animation-delay: 105ms; +} + +.service-row:nth-child(6) { + animation-delay: 140ms; +} + +.service-row:nth-child(7) { + animation-delay: 175ms; +} + +.service-row:nth-child(8) { + animation-delay: 210ms; +} + +.service-cell { + min-width: 0; +} + +.service-main { + display: flex; + min-width: 0; + align-items: center; + gap: 12px; +} + +.service-icon { + display: grid; + flex: 0 0 auto; + width: 34px; + height: 34px; + place-items: center; + border-radius: 8px; + background: rgba(2, 132, 199, 0.1); + color: var(--info); + transition: + background var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.service-row:hover .service-icon { + background: rgba(2, 132, 199, 0.14); + transform: scale(1.08); +} + +.service-name { + overflow: hidden; + color: var(--text-primary); + font-weight: 650; + text-overflow: ellipsis; + white-space: nowrap; +} + +.service-desc { + overflow: hidden; + margin-top: 4px; + color: var(--text-tertiary); + font-size: 12px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.status-badge { + display: inline-flex; + height: 24px; + align-items: center; + gap: 7px; + padding: 0 10px; + border-radius: 999px; + background: rgba(100, 116, 139, 0.1); + color: var(--text-secondary); + font-size: 12px; + transition: + background var(--motion-base) var(--ease-standard), + border-color var(--motion-base) var(--ease-standard), + color var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.status-badge:hover { + transform: translateY(-1px); +} + +.status-point { + width: 8px; + height: 8px; + border-radius: 50%; + transition: + background var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.status-badge:hover .status-point { + transform: scale(1.18); +} + +.status-badge--running .status-point { + background: var(--success); + box-shadow: 0 0 12px rgba(22, 163, 74, 0.28); +} + +.status-badge--running { + color: var(--success); +} + +.status-badge--warning .status-point { + background: var(--warning); + box-shadow: 0 0 12px rgba(217, 119, 6, 0.28); +} + +.status-badge--warning { + color: var(--warning); +} + +.status-badge--error .status-point { + background: var(--danger); + box-shadow: 0 0 12px rgba(220, 38, 38, 0.28); +} + +.status-badge--error { + color: var(--danger); +} + +.status-badge--stopped .status-point { + background: var(--stopped); +} + +.resource-cell { + display: grid; + gap: 7px; + width: min(144px, 92%); +} + +.resource-line { + display: flex; + justify-content: space-between; + color: var(--text-secondary); + font-size: 12px; +} + +.progress { + height: 6px; + overflow: hidden; + border-radius: 999px; + background: rgba(100, 116, 139, 0.16); +} + +.progress span { + display: block; + height: 100%; + border-radius: inherit; + background: var(--primary); + transform-origin: left center; + animation: progress-fill 650ms var(--ease-emphasized) both; + transition: + background var(--motion-base) var(--ease-standard), + width var(--motion-slow) var(--ease-emphasized); +} + +.progress--memory span { + background: var(--info); +} + +.deploy-cell { + color: var(--text-secondary); +} + +.muted { + color: var(--text-tertiary); +} + +.row-actions { + display: flex; + align-items: center; + gap: 8px; +} + +.action-button { + display: inline-grid; + width: 32px; + height: 32px; + place-items: center; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card-strong); + color: var(--text-secondary); + cursor: pointer; + transition: + background var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + border-color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-fast) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.action-button:hover { + border-color: rgba(37, 99, 235, 0.7); + background: var(--bg-hover); + box-shadow: var(--shadow-soft); + color: var(--text-primary); + transform: translateY(-1px); +} + +.action-button:active { + box-shadow: none; + transform: translateY(0) scale(0.94); +} + +.action-button--success { + color: var(--success); +} + +.action-button--danger { + color: var(--danger); +} + +.drawer-scrim { + position: fixed; + inset: var(--header-height) 0 0; + z-index: 19; + display: block; + background: rgba(15, 23, 42, 0); + opacity: 0; + transition: + background var(--motion-slow) var(--ease-standard), + opacity var(--motion-slow) var(--ease-standard); +} + +.drawer-scrim--open { + background: var(--overlay-scrim); + opacity: 1; +} + +.drawer { + position: fixed; + top: var(--header-height); + right: 0; + bottom: 0; + z-index: 20; + display: grid; + width: 360px; + grid-template-rows: auto auto 1fr auto; + border-left: 1px solid var(--border); + background: var(--bg-card); + box-shadow: var(--shadow-drawer); + transform: translateX(100%); + transition: + opacity 280ms var(--ease-standard), + transform 280ms var(--ease-emphasized); + opacity: 0.86; +} + +.drawer--open { + transform: translateX(0); + opacity: 1; +} + +.drawer-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 18px 18px 14px; + border-bottom: 1px solid var(--border); +} + +.panel-title-line { + display: flex; + min-width: 0; + align-items: center; + gap: 10px; +} + +.drawer-title { + margin: 0; + color: var(--text-primary); + font-size: 16px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.drawer-tabs { + display: grid; + grid-template-columns: repeat(4, 1fr); + border-bottom: 1px solid var(--border); +} + +.drawer-tab { + height: 42px; + background: transparent; + color: var(--text-tertiary); + cursor: pointer; + font-size: 12px; + transition: + background var(--motion-fast) var(--ease-standard), + color var(--motion-fast) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard); +} + +.drawer-tab:hover { + background: rgba(37, 99, 235, 0.08); + color: var(--text-primary); +} + +.drawer-tab--active { + color: var(--primary); + box-shadow: inset 0 -2px 0 var(--primary); +} + +.drawer-body { + min-height: 0; + overflow: auto; + padding: 18px; +} + +.detail-grid { + display: grid; + gap: 12px; +} + +.detail-row { + display: flex; + justify-content: space-between; + gap: 16px; + padding-bottom: 12px; + border-bottom: 1px solid var(--detail-border); + color: var(--text-secondary); + font-size: 13px; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.detail-row span:first-child { + color: var(--text-tertiary); +} + +.log-list { + display: grid; + gap: 10px; + margin: 0; + padding: 0; + list-style: none; +} + +.log-list li { + padding-bottom: 10px; + border-bottom: 1px solid var(--detail-border); + color: var(--text-secondary); + font-size: 12px; + line-height: 1.6; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.drawer-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 18px; + border-top: 1px solid var(--border); +} + +.inline-inspector { + display: grid; + min-height: 420px; + grid-template-rows: auto auto 1fr auto; + overflow: hidden; + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; + transition: + border-color var(--motion-base) var(--ease-standard), + box-shadow var(--motion-base) var(--ease-standard), + transform var(--motion-base) var(--ease-emphasized); +} + +.inline-inspector .drawer-body { + max-height: none; +} + +.empty-state { + padding: 34px 20px; + color: var(--text-tertiary); + text-align: center; +} + +.login-page { + display: grid; + min-height: 100vh; + place-items: center; + padding: 24px; + background: + linear-gradient(180deg, rgba(37, 99, 235, 0.08), rgba(37, 99, 235, 0)), + var(--bg-page); +} + +.login-panel { + display: grid; + width: min(420px, 100%); + gap: 24px; + padding: 28px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-card); + box-shadow: var(--shadow-panel); + animation: panel-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.login-brand { + display: flex; + align-items: center; + gap: 14px; +} + +.login-brand h1 { + margin: 0; + color: var(--text-primary); + font-size: 20px; +} + +.login-brand p { + margin: 4px 0 0; + color: var(--text-tertiary); + font-size: 13px; +} + +.login-form, +.form-drawer { + display: grid; + gap: 16px; +} + +.form-drawer { + width: min(420px, 100vw); + padding: 22px; +} + +.form-drawer--wide { + width: min(560px, 100vw); +} + +.form-drawer h2 { + margin: 0 0 4px; + color: var(--text-primary); + font-size: 18px; +} + +.form-drawer__actions { + display: flex; + justify-content: flex-end; + gap: 10px; + padding-top: 8px; +} + +.form-section-title { + color: var(--text-secondary); + font-size: 13px; + font-weight: 700; +} + +.permission-section { + display: grid; + gap: 10px; +} + +.permission-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.permission-check { + display: flex; + align-items: center; + gap: 8px; + min-height: 34px; + padding: 0 10px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card-strong); + color: var(--text-secondary); + font-size: 12px; +} + +.batch-actions { + display: flex; + gap: 8px; +} + +.pagination-bar { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + margin-top: 12px; + color: var(--text-tertiary); + font-size: 12px; +} + +.pagination-bar__actions { + display: flex; + align-items: center; + gap: 8px; +} + +.data-state { + display: grid; + min-height: 260px; + place-items: center; + gap: 14px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg-card); + color: var(--text-tertiary); +} + +.data-state--loading { + padding: 16px; +} + +.data-state--loading .page-skeleton__block { + min-height: 220px; +} + +.data-state__title { + color: var(--text-secondary); + font-size: 14px; + font-weight: 650; +} + +.admin-table { + min-width: 900px; +} + +.admin-row { + display: grid; + align-items: center; + min-height: 58px; + padding: 0 20px; + border-bottom: 1px solid var(--row-border); + color: var(--text-secondary); + font-size: 13px; + animation: row-enter var(--motion-slow) var(--ease-emphasized) both; +} + +.admin-row--head { + min-height: 44px; + background: var(--bg-card-strong); + color: var(--text-tertiary); + font-size: 12px; + font-weight: 650; +} + +.admin-table--roles .admin-row { + grid-template-columns: 1fr 1fr 0.7fr 1.6fr 0.8fr; +} + +.admin-table--menus .admin-row { + grid-template-columns: 1.2fr 1.4fr 1.2fr 0.8fr; +} + +.admin-table--login-logs .admin-row { + grid-template-columns: 1fr 1fr 0.7fr 1.6fr 1.2fr; +} + +.admin-table--operation-logs .admin-row { + grid-template-columns: 0.9fr 1fr 1fr 0.7fr 1.6fr 1.2fr; +} + +.notification-list { + display: grid; +} + +.notification-item { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 14px; + min-height: 86px; + padding: 16px 20px; + border-bottom: 1px solid var(--row-border); +} + +.notification-title { + color: var(--text-primary); + font-size: 14px; + font-weight: 700; +} + +.notification-content { + margin: 6px 0; + color: var(--text-secondary); + font-size: 13px; +} + +@media (max-width: 1080px) { + body { + overflow: auto; + } + + .app-shell, + .body-grid { + min-height: auto; + } + + .workspace { + overflow: visible; + } + + .kpi-grid, + .panel-grid, + .overview-charts, + .service-kpi-row, + .user-kpi-row, + .page-skeleton__kpis, + .page-skeleton__charts { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .table-scroll { + overflow-x: auto; + } + + .user-pill { + display: none; + } + + .split-grid, + .service-page-grid, + .page-skeleton__content { + grid-template-columns: 1fr; + } + + .inline-inspector { + display: none; + } +} + +@media (max-width: 760px) { + .header-tools { + gap: 8px; + padding: 0 12px; + } + + .header-location { + min-width: auto; + padding: 0 12px; + } + + .search-trigger { + min-width: 0; + width: 42px; + justify-content: center; + padding: 0; + } + + .search-trigger span { + display: none; + } + + .workspace { + padding: 16px; + } + + .page-skeleton { + height: calc(100vh - var(--header-height) - 32px); + height: calc(100dvh - var(--header-height) - 32px); + grid-template-rows: 80px minmax(96px, 0.55fr) minmax(132px, 0.75fr) minmax(180px, 1fr); + } + + .page-skeleton__head { + align-items: flex-start; + flex-direction: column; + } + + .page-skeleton__title { + width: 58%; + } + + .page-head, + .table-toolbar { + align-items: flex-start; + flex-direction: column; + } + + .kpi-grid, + .panel-grid, + .overview-charts, + .service-kpi-row, + .user-kpi-row, + .alert-strip, + .page-skeleton__kpis, + .page-skeleton__charts { + grid-template-columns: 1fr; + } + + .page-skeleton__kpis .page-skeleton__kpi:nth-child(n + 3), + .page-skeleton__charts .page-skeleton__block:nth-child(n + 2), + .page-skeleton__rail { + display: none; + } + + .page-skeleton__kpis { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + .filters-panel { + align-items: stretch; + flex-direction: column; + } + + .filters-panel .search-box { + flex-basis: auto; + width: 100%; + } + + .filter-row { + flex-wrap: wrap; + } + + .drawer { + width: min(360px, 100vw); + } +} + +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 1ms !important; + animation-iteration-count: 1 !important; + scroll-behavior: auto !important; + transition-duration: 1ms !important; + } +} diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..f0898fb --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,40 @@ +* { + box-sizing: border-box; +} + +html, +body { + width: 100%; + min-width: 320px; + min-height: 100%; + margin: 0; +} + +body { + overflow: hidden; + background: var(--bg-page); + color: var(--text-primary); + font-family: var(--font-system); + letter-spacing: 0; +} + +button, +input, +select { + font: inherit; +} + +button { + border: 0; +} + +button:focus-visible, +input:focus-visible, +select:focus-visible { + outline: 2px solid rgba(37, 99, 235, 0.7); + outline-offset: 2px; +} + +#app { + min-height: 100vh; +} diff --git a/src/styles/tokens.css b/src/styles/tokens.css new file mode 100644 index 0000000..05fa1df --- /dev/null +++ b/src/styles/tokens.css @@ -0,0 +1,52 @@ +:root { + --bg-page: #f4f7fb; + --bg-sidebar: #ffffff; + --bg-header: #ffffff; + --bg-card: #ffffff; + --bg-card-strong: #f1f5f9; + --bg-hover: rgba(37, 99, 235, 0.08); + --bg-input: rgba(248, 250, 252, 0.92); + --bg-input-strong: #ffffff; + --bg-flyout: rgba(255, 255, 255, 0.98); + + --border: #d8e0ec; + --border-soft: rgba(100, 116, 139, 0.22); + --row-border: rgba(216, 224, 236, 0.8); + --detail-border: rgba(216, 224, 236, 0.75); + + --primary: #2563eb; + --primary-strong: #1d4ed8; + --brand-secondary: #0ea5e9; + + --success: #16a34a; + --warning: #d97706; + --danger: #dc2626; + --info: #0284c7; + --stopped: #64748b; + + --chart-blue: #2563eb; + --chart-green: #16a34a; + --chart-red: #dc2626; + --chart-orange: #d97706; + + --text-primary: #0f172a; + --text-secondary: #475569; + --text-tertiary: #64748b; + --active-contrast: #ffffff; + + --shadow-soft: 0 10px 24px rgba(15, 23, 42, 0.08); + --shadow-panel: 0 18px 44px rgba(15, 23, 42, 0.1); + --shadow-dialog: 0 28px 80px rgba(15, 23, 42, 0.18); + --shadow-drawer: 0 24px 60px rgba(15, 23, 42, 0.18); + --overlay-scrim: rgba(15, 23, 42, 0.24); + --focus-ring: rgba(37, 99, 235, 0.16); + --skeleton-surface: rgba(255, 255, 255, 0.86); + --skeleton-shine: rgba(37, 99, 235, 0.1); + --motion-fast: 150ms; + --motion-base: 220ms; + --motion-slow: 320ms; + --ease-standard: cubic-bezier(0.2, 0, 0, 1); + --ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1); + --font-system: -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif; + --header-height: 72px; +} diff --git a/src/test/setup.js b/src/test/setup.js new file mode 100644 index 0000000..f149f27 --- /dev/null +++ b/src/test/setup.js @@ -0,0 +1 @@ +import "@testing-library/jest-dom/vitest"; diff --git a/src/theme.js b/src/theme.js new file mode 100644 index 0000000..4d1b10a --- /dev/null +++ b/src/theme.js @@ -0,0 +1,170 @@ +import { createTheme } from "@mui/material/styles"; + +export const theme = createTheme({ + palette: { + mode: "light", + primary: { + main: "#2563eb", + contrastText: "#ffffff" + }, + success: { + main: "#16a34a" + }, + warning: { + main: "#d97706" + }, + error: { + main: "#dc2626" + }, + background: { + default: "#f4f7fb", + paper: "#ffffff" + }, + text: { + primary: "#0f172a", + secondary: "#475569", + disabled: "#94a3b8" + }, + divider: "#d8e0ec" + }, + typography: { + fontFamily: '-apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif', + button: { + textTransform: "none", + fontWeight: 650 + } + }, + shape: { + borderRadius: 8 + }, + components: { + MuiButton: { + defaultProps: { + disableRipple: false + }, + styleOverrides: { + root: { + minWidth: 0, + height: 36, + borderRadius: 8, + padding: "0 12px", + fontSize: 13, + transition: + "background-color 150ms cubic-bezier(0.2, 0, 0, 1), border-color 150ms cubic-bezier(0.2, 0, 0, 1), box-shadow 150ms cubic-bezier(0.2, 0, 0, 1), transform 220ms cubic-bezier(0.16, 1, 0.3, 1)" + }, + outlined: { + borderColor: "#d8e0ec", + color: "#475569", + backgroundColor: "#ffffff", + "&:hover": { + borderColor: "rgba(37, 99, 235, 0.7)", + backgroundColor: "rgba(37, 99, 235, 0.08)", + color: "#0f172a" + } + }, + containedPrimary: { + backgroundColor: "#2563eb", + "&:hover": { + backgroundColor: "#1d4ed8" + } + } + } + }, + MuiCard: { + styleOverrides: { + root: { + border: "1px solid #d8e0ec", + borderRadius: 10, + backgroundColor: "#ffffff", + backgroundImage: "none", + color: "#0f172a", + transition: + "border-color 220ms cubic-bezier(0.2, 0, 0, 1), box-shadow 220ms cubic-bezier(0.2, 0, 0, 1), transform 220ms cubic-bezier(0.16, 1, 0.3, 1)" + } + } + }, + MuiChip: { + styleOverrides: { + root: { + height: 24, + borderRadius: 999, + fontSize: 12, + backgroundColor: "rgba(100, 116, 139, 0.1)", + transition: + "background-color 150ms cubic-bezier(0.2, 0, 0, 1), border-color 150ms cubic-bezier(0.2, 0, 0, 1), color 150ms cubic-bezier(0.2, 0, 0, 1), transform 220ms cubic-bezier(0.16, 1, 0.3, 1)" + }, + icon: { + marginLeft: 8, + marginRight: -2 + }, + label: { + paddingLeft: 8, + paddingRight: 10 + } + } + }, + MuiIconButton: { + styleOverrides: { + root: { + borderRadius: 8, + color: "#475569", + transition: + "background-color 150ms cubic-bezier(0.2, 0, 0, 1), border-color 150ms cubic-bezier(0.2, 0, 0, 1), color 150ms cubic-bezier(0.2, 0, 0, 1), transform 220ms cubic-bezier(0.16, 1, 0.3, 1)" + } + } + }, + MuiInputBase: { + styleOverrides: { + input: { + "&::placeholder": { + color: "#94a3b8", + opacity: 1 + } + } + } + }, + MuiMenu: { + styleOverrides: { + paper: { + border: "1px solid #d8e0ec", + backgroundImage: "none", + backgroundColor: "#ffffff", + transformOrigin: "top right" + } + } + }, + MuiSelect: { + styleOverrides: { + root: { + color: "#475569", + fontSize: 13 + }, + icon: { + color: "#475569" + } + } + }, + MuiOutlinedInput: { + styleOverrides: { + notchedOutline: { + borderColor: "#d8e0ec" + }, + root: { + backgroundColor: "#ffffff", + transition: + "background-color 150ms cubic-bezier(0.2, 0, 0, 1), box-shadow 220ms cubic-bezier(0.2, 0, 0, 1), transform 220ms cubic-bezier(0.16, 1, 0.3, 1)", + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: "rgba(37, 99, 235, 0.7)" + }, + "&.Mui-focused .MuiOutlinedInput-notchedOutline": { + borderColor: "#2563eb" + }, + "&.Mui-focused": { + boxShadow: "0 0 0 3px rgba(37, 99, 235, 0.16)", + transform: "translateY(-1px)" + } + } + } + } + } +}); diff --git a/src/utils/time.js b/src/utils/time.js new file mode 100644 index 0000000..4e32e5c --- /dev/null +++ b/src/utils/time.js @@ -0,0 +1,8 @@ +export function formatTime(date = new Date()) { + return date.toLocaleTimeString("zh-CN", { + hour12: false, + hour: "2-digit", + minute: "2-digit", + second: "2-digit" + }); +} diff --git a/vite.config.js b/vite.config.js new file mode 100644 index 0000000..18d24d5 --- /dev/null +++ b/vite.config.js @@ -0,0 +1,22 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +export default defineConfig({ + plugins: [react()], + server: { + port: 7001, + proxy: { + "/api": { + target: "http://localhost:13100", + changeOrigin: true + } + } + }, + preview: { + port: 7001 + }, + test: { + environment: "jsdom", + setupFiles: "./src/test/setup.js" + } +});