Ver Fonte

first commit

周玉环 há 2 semanas atrás
pai
commit
e7dda8d2c8
59 ficheiros alterados com 8931 adições e 0 exclusões
  1. 10 0
      .env.development
  2. 9 0
      .env.production
  3. 26 0
      .gitignore
  4. 474 0
      docs/API_GUIDE.md
  5. 198 0
      docs/AXIOS_MIGRATION.md
  6. 0 0
      docs/COOKIE_GUIDE.md
  7. 138 0
      docs/HTTP_COMPARISON.md
  8. 248 0
      docs/PERMISSION_GUIDE.md
  9. 202 0
      docs/PROJECT_STRUCTURE.md
  10. 385 0
      docs/PROXY_GUIDE.md
  11. 287 0
      docs/QUICK_START.md
  12. 23 0
      eslint.config.js
  13. 13 0
      index.html
  14. 39 0
      package.json
  15. 4139 0
      pnpm-lock.yaml
  16. 1 0
      public/vite.svg
  17. 42 0
      src/App.css
  18. 58 0
      src/App.tsx
  19. 0 0
      src/assets/react.svg
  20. 28 0
      src/components/common/PermissionButton.tsx
  21. 34 0
      src/components/common/PermissionWrapper.tsx
  22. 12 0
      src/config/api.ts
  23. 23 0
      src/config/menuIds.ts
  24. 67 0
      src/contexts/AuthContext.tsx
  25. 46 0
      src/contexts/MenuContext.tsx
  26. 20 0
      src/hooks/usePermission.ts
  27. 220 0
      src/http/Axios.ts
  28. 49 0
      src/http/axiosTransform.ts
  29. 60 0
      src/http/checkStatus.ts
  30. 57 0
      src/http/helper.ts
  31. 235 0
      src/http/index.ts
  32. 69 0
      src/index.css
  33. 143 0
      src/layouts/Header/index.module.css
  34. 122 0
      src/layouts/Header/index.tsx
  35. 18 0
      src/layouts/MainLayout/index.module.css
  36. 21 0
      src/layouts/MainLayout/index.tsx
  37. 37 0
      src/layouts/Sidebar/index.module.css
  38. 59 0
      src/layouts/Sidebar/index.tsx
  39. 10 0
      src/main.tsx
  40. 178 0
      src/pages/content-model/index.module.css
  41. 128 0
      src/pages/content-model/index.tsx
  42. 0 0
      src/pages/content/index.module.css
  43. 0 0
      src/pages/content/index.tsx
  44. 197 0
      src/router/index.tsx
  45. 103 0
      src/services/BaseService.ts
  46. 23 0
      src/services/api.ts
  47. 21 0
      src/services/index.ts
  48. 67 0
      src/services/modules/contentModel.ts
  49. 92 0
      src/services/modules/system.ts
  50. 78 0
      src/services/modules/user.ts
  51. 43 0
      src/types/axios.d.ts
  52. 39 0
      src/types/menu.ts
  53. 46 0
      src/utils/message.ts
  54. 102 0
      src/utils/sign.ts
  55. 68 0
      src/utils/tenant.ts
  56. 28 0
      tsconfig.app.json
  57. 7 0
      tsconfig.json
  58. 26 0
      tsconfig.node.json
  59. 63 0
      vite.config.ts

+ 10 - 0
.env.development

@@ -0,0 +1,10 @@
+# 开发环境配置
+
+# API 基础地址
+VITE_API_BASE_URL=/
+
+# 应用标题
+VITE_APP_TITLE=Content Management System (Dev)
+
+# 是否启用 Mock 数据
+VITE_USE_MOCK=false

+ 9 - 0
.env.production

@@ -0,0 +1,9 @@
+# 生产环境配置
+
+# API 基础地址(生产环境后端地址)
+VITE_API_BASE_URL=/
+# 应用标题
+VITE_APP_TITLE=Content Management System
+
+# 是否启用 Mock 数据
+VITE_USE_MOCK=false

+ 26 - 0
.gitignore

@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+node_modules

+ 474 - 0
docs/API_GUIDE.md

@@ -0,0 +1,474 @@
+# API 请求封装使用指南
+
+本项目参考 JeecgBoot 前端的请求封装方案,提供了完整的 HTTP 请求工具和服务层封装。
+
+## 目录结构
+
+```
+src/
+├── utils/
+│   └── request.ts              # 核心请求工具
+├── services/
+│   ├── api.ts                  # 系统 API
+│   ├── BaseService.ts          # 基础服务类
+│   ├── index.ts                # 统一导出
+│   └── modules/                # 业务模块服务
+│       ├── user.ts
+│       ├── role.ts
+│       └── content.ts
+└── examples/
+    └── ApiUsage.tsx            # 使用示例
+```
+
+## 核心特性
+
+### 1. 请求拦截器
+- 自动添加 Token
+- GET 请求自动添加时间戳防缓存
+- 统一请求头配置
+
+### 2. 响应拦截器
+- 自动处理 JeecgBoot 标准响应格式
+- 支持文件下载
+- 统一错误处理
+
+### 3. 错误处理
+- Token 过期自动跳转登录
+- 权限不足提示
+- 服务器错误提示
+- 支持跳过全局错误处理
+
+### 4. 超时控制
+- 默认 30 秒超时
+- 可自定义超时时间
+
+## 快速开始
+
+### 方式一:直接使用 http 工具
+
+```typescript
+import http from "@/utils/request";
+
+// GET 请求
+const users = await http.get("/sys/user/list", { pageNo: 1, pageSize: 10 });
+
+// POST 请求
+const result = await http.post("/sys/user/add", { username: "test" });
+
+// PUT 请求
+await http.put("/sys/user/edit", { id: "1", username: "updated" });
+
+// DELETE 请求
+await http.delete("/sys/user/delete", { id: "1" });
+
+// 文件上传
+const formData = new FormData();
+formData.append("file", file);
+const uploadResult = await http.upload("/sys/common/upload", formData);
+
+// 文件下载
+await http.download("/sys/user/exportXls", "用户列表.xls", { status: 1 });
+```
+
+### 方式二:使用预定义的 API 服务
+
+```typescript
+import { sysApi, dictApi, uploadApi } from "@/services";
+
+// 系统 API
+const menus = await sysApi.getUserMenus();
+const permissions = await sysApi.getUserPermissions();
+await sysApi.login({ username: "admin", password: "123456" });
+
+// 字典 API
+const dictItems = await dictApi.getDictItems("sex");
+
+// 上传 API
+const result = await uploadApi.upload(file);
+```
+
+### 方式三:使用 BaseService 封装的服务
+
+```typescript
+import { userService, roleService, contentService } from "@/services";
+
+// 分页查询
+const userList = await userService.list({
+  pageNo: 1,
+  pageSize: 10,
+  column: "createTime",
+  order: "desc",
+  username: "admin", // 查询条件
+});
+
+// CRUD 操作
+const user = await userService.getById("1");
+await userService.add({ username: "newuser" });
+await userService.edit({ id: "1", username: "updated" });
+await userService.delete("1");
+await userService.deleteBatch(["1", "2", "3"]);
+
+// 导入导出
+await userService.exportXls({ status: 1 }, "用户列表.xls");
+await userService.importExcel(file);
+```
+
+### 方式四:动态创建服务
+
+```typescript
+import { createService } from "@/services";
+
+// 为任意模块创建服务
+const customService = createService("/api/custom");
+
+const list = await customService.list({ pageNo: 1, pageSize: 10 });
+const item = await customService.getById("1");
+await customService.add({ name: "test" });
+```
+
+## 创建自定义服务
+
+### 1. 继承 BaseService
+
+```typescript
+// src/services/modules/department.ts
+import { BaseService } from "../BaseService";
+import http from "../../utils/request";
+
+export interface Department {
+  id: string;
+  departName: string;
+  parentId?: string;
+  orgCode?: string;
+}
+
+class DepartmentService extends BaseService<Department> {
+  constructor() {
+    super("/sys/sysDepart"); // 基础路径
+  }
+
+  // 自定义方法
+  queryTreeList() {
+    return http.get<Department[]>("/sys/sysDepart/queryTreeList");
+  }
+
+  searchBy(keyword: string) {
+    return http.get<Department[]>("/sys/sysDepart/searchBy", { keyword });
+  }
+}
+
+export const departmentService = new DepartmentService();
+export default departmentService;
+```
+
+### 2. 在 services/index.ts 中导出
+
+```typescript
+export { default as departmentService } from "./modules/department";
+export type { Department } from "./modules/department";
+```
+
+## 高级用法
+
+### 1. 自定义请求配置
+
+```typescript
+// 跳过全局错误处理
+const data = await http.get(
+  "/api/check",
+  { username: "test" },
+  { skipErrorHandler: true }
+);
+
+// 自定义超时时间
+const data = await http.post(
+  "/api/long-task",
+  { data: "..." },
+  { timeout: 60000 } // 60秒
+);
+
+// 自定义请求头
+const data = await http.get(
+  "/api/data",
+  {},
+  {
+    headers: {
+      "Custom-Header": "value",
+    },
+  }
+);
+```
+
+### 2. Token 管理
+
+```typescript
+import { TokenManager } from "@/services";
+
+// 获取 Token
+const token = TokenManager.getToken();
+
+// 设置 Token
+TokenManager.setToken("your-token");
+
+// 删除 Token
+TokenManager.removeToken();
+```
+
+### 3. 文件操作
+
+```typescript
+// 单文件上传
+import { uploadApi } from "@/services";
+
+const handleUpload = async (file: File) => {
+  const result = await uploadApi.upload(file);
+  console.log("文件地址:", result.url);
+};
+
+// 批量上传
+const handleBatchUpload = async (files: File[]) => {
+  const result = await uploadApi.uploadBatch(files);
+  console.log("文件地址列表:", result.urls);
+};
+
+// 导出 Excel
+await userService.exportXls(
+  {
+    status: 1,
+    createTime_begin: "2024-01-01",
+    createTime_end: "2024-12-31",
+  },
+  "用户列表.xls"
+);
+
+// 导入 Excel
+const handleImport = async (file: File) => {
+  const result = await userService.importExcel(file);
+  console.log("导入结果:", result);
+};
+```
+
+### 4. 分页查询
+
+```typescript
+import type { PageParams, PageResult } from "@/services";
+
+const queryUsers = async () => {
+  const params: Partial<PageParams> = {
+    pageNo: 1,
+    pageSize: 10,
+    column: "createTime", // 排序字段
+    order: "desc", // 排序方式
+    // 查询条件
+    username: "admin",
+    status: 1,
+    createTime_begin: "2024-01-01",
+    createTime_end: "2024-12-31",
+  };
+
+  const result: PageResult<User> = await userService.list(params);
+
+  console.log("总记录数:", result.total);
+  console.log("总页数:", result.pages);
+  console.log("当前页:", result.current);
+  console.log("每页大小:", result.size);
+  console.log("数据列表:", result.records);
+};
+```
+
+## 响应格式
+
+### JeecgBoot 标准响应格式
+
+```json
+{
+  "success": true,
+  "message": "操作成功",
+  "code": 200,
+  "result": {
+    // 实际数据
+  },
+  "timestamp": 1638888888888
+}
+```
+
+### 分页响应格式
+
+```json
+{
+  "success": true,
+  "message": "查询成功",
+  "code": 200,
+  "result": {
+    "records": [...],
+    "total": 100,
+    "size": 10,
+    "current": 1,
+    "pages": 10
+  }
+}
+```
+
+## 错误处理
+
+### 全局错误处理
+
+系统会自动处理以下错误:
+
+- **401 Token 过期**:弹窗提示并跳转登录页
+- **403 权限不足**:弹窗提示
+- **500 服务器错误**:弹窗提示
+- **其他错误**:弹窗显示错误信息
+
+### 自定义错误处理
+
+```typescript
+try {
+  const data = await http.get("/api/data", {}, { skipErrorHandler: true });
+  // 处理成功
+} catch (error) {
+  // 自定义错误处理
+  console.error("请求失败:", error.message);
+  // 显示自定义提示
+  alert("操作失败,请重试");
+}
+```
+
+## 最佳实践
+
+### 1. 服务层组织
+
+```
+services/
+├── api.ts                  # 系统级 API(登录、权限等)
+├── BaseService.ts          # 基础服务类
+├── index.ts                # 统一导出
+└── modules/                # 按业务模块组织
+    ├── user.ts            # 用户模块
+    ├── role.ts            # 角色模块
+    ├── department.ts      # 部门模块
+    └── content.ts         # 内容模块
+```
+
+### 2. 类型定义
+
+```typescript
+// 定义接口类型
+export interface User {
+  id: string;
+  username: string;
+  realname: string;
+  // ...
+}
+
+// 使用泛型
+class UserService extends BaseService<User> {
+  // TypeScript 会自动推断返回类型
+}
+```
+
+### 3. 错误处理策略
+
+```typescript
+// 需要用户感知的错误:使用全局错误处理
+await userService.delete(id);
+
+// 不需要用户感知的错误:跳过全局错误处理
+try {
+  await http.get("/api/check", { username }, { skipErrorHandler: true });
+  // 用户名可用
+} catch {
+  // 用户名已存在
+}
+```
+
+### 4. Loading 状态管理
+
+```typescript
+const [loading, setLoading] = useState(false);
+
+const handleSubmit = async () => {
+  try {
+    setLoading(true);
+    await userService.add(formData);
+    // 成功提示
+  } catch (error) {
+    // 错误已被全局处理
+  } finally {
+    setLoading(false);
+  }
+};
+```
+
+## 环境配置
+
+在 `.env` 文件中配置 API 基础地址:
+
+```bash
+# 开发环境
+VITE_API_BASE_URL=http://localhost:8080
+
+# 生产环境
+VITE_API_BASE_URL=https://api.example.com
+```
+
+## 常见问题
+
+### 1. Token 如何存储?
+
+Token 存储在 localStorage 中,key 为 `ACCESS_TOKEN`。
+
+### 2. 如何修改 Token Header 名称?
+
+在 `src/utils/request.ts` 中修改 `TokenManager.TOKEN_HEADER`。
+
+### 3. 如何自定义超时时间?
+
+```typescript
+const data = await http.get("/api/data", {}, { timeout: 60000 });
+```
+
+### 4. 如何处理文件流响应?
+
+系统会自动检测 `content-type`,如果是 `application/octet-stream` 会返回 Blob。
+
+### 5. 如何添加请求日志?
+
+在 `src/utils/request.ts` 的 `requestInterceptor` 中添加:
+
+```typescript
+console.log("请求:", config.url, config);
+```
+
+## 迁移指南
+
+### 从 Axios 迁移
+
+```typescript
+// Axios
+axios.get("/api/users", { params: { id: 1 } });
+axios.post("/api/users", { username: "test" });
+
+// 本项目
+http.get("/api/users", { id: 1 });
+http.post("/api/users", { username: "test" });
+```
+
+### 从 JeecgBoot Vue 版本迁移
+
+大部分 API 保持一致,只需要:
+
+1. 将 `this.$http` 改为 `http`
+2. 将 `this.url.xxx` 改为服务方法调用
+3. 使用 async/await 替代 Promise.then
+
+```typescript
+// Vue 版本
+this.$http.get(this.url.list, { params: query }).then((res) => {
+  this.dataSource = res.result.records;
+});
+
+// React 版本
+const result = await userService.list(query);
+setDataSource(result.records);
+```

+ 198 - 0
docs/AXIOS_MIGRATION.md

@@ -0,0 +1,198 @@
+# Axios 迁移完成
+
+## ✅ 已完成
+
+已成功将请求封装从 fetch 迁移到 axios,完整复刻 JeecgBoot 前端的请求封装。
+
+### 核心文件
+
+```
+src/utils/http/
+├── index.ts              # 主入口,配置 transform
+├── Axios.ts              # Axios 封装类
+├── axiosTransform.ts     # 转换器接口定义
+├── checkStatus.ts        # HTTP 状态码检查
+└── helper.ts             # 辅助函数
+
+src/utils/
+├── sign.ts               # 请求签名(MD5)
+└── tenant.ts             # 多租户管理
+
+src/types/
+└── axios.d.ts            # 类型定义
+```
+
+### 核心功能
+
+1. **请求签名** - MD5 签名,防篡改
+2. **多租户支持** - 自动添加租户ID
+3. **请求/响应拦截** - 完整的拦截器链
+4. **错误处理** - 统一的错误提示
+5. **参数处理** - GET/POST 参数自动处理
+6. **文件上传** - 支持单文件和批量上传
+7. **时间戳防缓存** - GET 请求自动添加时间戳
+
+### 使用方式
+
+#### 1. 基础请求
+
+```typescript
+import { defHttp } from "@/utils/http";
+
+// GET 请求
+const data = await defHttp.get({ url: "/api/users", params: { id: 1 } });
+
+// POST 请求
+const result = await defHttp.post({ url: "/api/users", data: { name: "张三" } });
+
+// PUT 请求
+await defHttp.put({ url: "/api/users/1", data: { name: "李四" } });
+
+// DELETE 请求
+await defHttp.delete({ url: "/api/users/1" });
+```
+
+#### 2. 文件上传
+
+```typescript
+import { defHttp } from "@/utils/http";
+
+// 单文件上传
+const file = document.querySelector('input[type="file"]').files[0];
+const result = await defHttp.uploadFile(
+  { url: "/api/upload" },
+  { file, name: "file" }
+);
+```
+
+#### 3. 使用服务层
+
+```typescript
+import { userService } from "@/services";
+
+// 分页查询
+const users = await userService.list({ pageNo: 1, pageSize: 10 });
+
+// CRUD 操作
+await userService.add({ username: "test" });
+await userService.edit({ id: "1", username: "updated" });
+await userService.delete("1");
+```
+
+#### 4. 自定义选项
+
+```typescript
+// 跳过错误提示
+const data = await defHttp.get(
+  { url: "/api/check" },
+  { errorMessageMode: "none" }
+);
+
+// 显示成功提示
+await defHttp.post(
+  { url: "/api/save", data: {} },
+  { successMessageMode: "success" }
+);
+
+// 返回原生响应
+const response = await defHttp.get(
+  { url: "/api/data" },
+  { isReturnNativeResponse: true }
+);
+```
+
+### 自动添加的请求头
+
+所有请求会自动添加以下请求头:
+
+```typescript
+{
+  "X-Timestamp": "1234567890",        // 时间戳
+  "X-Sign": "abc123...",              // MD5 签名
+  "X-Version": "v3",                  // 版本标识
+  "X-Tenant-ID": "0",                 // 租户ID
+  "Content-Type": "application/json"  // 内容类型
+}
+```
+
+### 签名算法
+
+请求签名使用 MD5 算法:
+
+```
+签名字符串 = URL路径 + 排序后的参数 + 签名密钥
+签名 = MD5(签名字符串)
+```
+
+示例:
+```
+URL: /api/user/list
+参数: { name: "张三", age: 20 }
+密钥: DD05F1C54D63749EEEB10B4F8B6830B1
+
+签名字符串: /api/user/listaage=20&name=张三DD05F1C54D63749EEEB10B4F8B6830B1
+签名: MD5(签名字符串)
+```
+
+### 多租户
+
+系统支持多租户,会自动从 localStorage 读取租户ID:
+
+```typescript
+// 设置租户ID
+import { setTenantId } from "@/utils/tenant";
+setTenantId("123");
+
+// 设置临时租户ID(优先级更高)
+import { setShareTenantId } from "@/utils/tenant";
+setShareTenantId("456");
+```
+
+### 与 JeecgBoot 的差异
+
+| 功能 | JeecgBoot | 当前实现 | 说明 |
+|------|-----------|----------|------|
+| 基础库 | axios | axios | ✅ 相同 |
+| 请求签名 | ✅ | ✅ | ✅ 已实现 |
+| 多租户 | ✅ | ✅ | ✅ 已实现 |
+| Token 认证 | ✅ | ❌ | 使用 Cookie 代替 |
+| 请求取消 | ✅ | ❌ | 可选功能 |
+| 流式数据 | ✅ | ❌ | 可选功能 |
+| 低代码应用ID | ✅ | ❌ | 特定场景 |
+| 乾坤微前端 | ✅ | ❌ | 特定场景 |
+
+### 配置修改
+
+如需修改签名密钥或其他配置,编辑以下文件:
+
+```typescript
+// src/utils/sign.ts
+const SIGN_KEY = "DD05F1C54D63749EEEB10B4F8B6830B1"; // 修改签名密钥
+
+// src/config/api.ts
+export const API_PREFIX = "/sohoyw-som"; // 修改API前缀
+export const TIMEOUT = 30000; // 修改超时时间
+```
+
+### 注意事项
+
+1. **签名密钥** - 需要与后端约定一致
+2. **租户ID** - 多租户系统必须设置
+3. **Cookie** - 使用 Cookie 认证,确保后端支持
+4. **跨域** - 开发环境使用 Vite 代理
+
+### 迁移检查清单
+
+- [x] 安装 axios 和 crypto-js
+- [x] 创建 axios 封装类
+- [x] 实现请求/响应拦截器
+- [x] 实现请求签名
+- [x] 实现多租户支持
+- [x] 更新所有服务层调用
+- [x] 测试基础 CRUD 操作
+- [x] 测试文件上传
+- [x] 测试错误处理
+
+## 🎉 完成
+
+现在项目已经完全使用 axios + JeecgBoot 风格的请求封装!

+ 0 - 0
docs/COOKIE_GUIDE.md


+ 138 - 0
docs/HTTP_COMPARISON.md

@@ -0,0 +1,138 @@
+# HTTP 请求封装对比
+
+## 当前实现 vs JeecgBoot
+
+### ✅ 已实现的核心功能
+
+| 功能 | JeecgBoot | 当前实现 | 说明 |
+|------|-----------|----------|------|
+| 请求拦截 | ✅ | ✅ | 添加通用请求头、处理参数 |
+| 响应拦截 | ✅ | ✅ | 统一处理响应数据 |
+| 错误处理 | ✅ | ✅ | HTTP 状态码错误提示 |
+| 超时控制 | ✅ | ✅ | 默认 30 秒超时 |
+| Cookie 认证 | ❌ | ✅ | 使用 Cookie 而非 Token |
+| GET 防缓存 | ✅ | ✅ | 自动添加时间戳 |
+| API 前缀 | ✅ | ✅ | 统一添加 `/sohoyw-som` |
+| 消息提示 | ✅ | ✅ | 使用 Notification 组件 |
+
+### 🔄 可选增强功能
+
+以下是 JeecgBoot 中的高级功能,可根据需要添加:
+
+#### 1. 请求签名(MD5)
+```typescript
+// JeecgBoot 实现
+config.headers[ConfigEnum.TIMESTAMP] = signMd5Utils.getTimestamp();
+config.headers[ConfigEnum.Sign] = signMd5Utils.getSign(config.url, config.params, config.data);
+```
+
+**是否需要:** 取决于后端是否要求签名验证
+
+#### 2. 多租户支持
+```typescript
+// JeecgBoot 实现
+config.headers[ConfigEnum.TENANT_ID] = tenantId;
+```
+
+**是否需要:** 如果系统支持多租户,需要添加
+
+#### 3. 请求取消(防重复)
+```typescript
+// JeecgBoot 使用 axios 的 CancelToken
+// 当前可以使用 AbortController 实现
+```
+
+**是否需要:** 防止用户快速点击导致重复请求
+
+#### 4. 流式数据支持
+```typescript
+// JeecgBoot 支持 SSE 流式响应
+config.responseType = 'stream';
+```
+
+**是否需要:** 如果有 AI 对话等流式场景
+
+#### 5. 文件下载增强
+```typescript
+// JeecgBoot 支持下载进度、文件名提取等
+```
+
+**是否需要:** 如果有大文件下载需求
+
+#### 6. 请求重试
+```typescript
+// 失败后自动重试
+retryCount: 3,
+retryDelay: 1000
+```
+
+**是否需要:** 提高网络不稳定时的成功率
+
+#### 7. 请求日志
+```typescript
+// 记录所有请求到 errorLog store
+errorLogStore.addAjaxErrorInfo(error);
+```
+
+**是否需要:** 用于问题排查和监控
+
+## 建议
+
+### 当前实现已足够的场景
+- ✅ 标准的 CRUD 操作
+- ✅ 基于 Cookie 的认证
+- ✅ 简单的错误提示
+- ✅ 文件上传下载
+
+### 需要增强的场景
+- ❌ 需要请求签名验证
+- ❌ 多租户系统
+- ❌ 需要防止重复请求
+- ❌ 有流式数据需求
+- ❌ 需要详细的请求日志
+
+## 快速增强方案
+
+如果需要添加某个功能,可以参考以下步骤:
+
+### 1. 添加请求签名
+```typescript
+// src/utils/sign.ts
+export function generateSign(url: string, params: any, data: any): string {
+  // 实现签名逻辑
+}
+
+// src/utils/request.ts
+config.headers['X-Sign'] = generateSign(url, params, data);
+config.headers['X-Timestamp'] = Date.now();
+```
+
+### 2. 添加多租户
+```typescript
+// src/utils/request.ts
+const tenantId = getTenantId(); // 从 localStorage 或 context 获取
+config.headers['X-Tenant-ID'] = tenantId;
+```
+
+### 3. 添加请求取消
+```typescript
+// src/utils/request.ts
+const pendingRequests = new Map<string, AbortController>();
+
+// 在请求前检查并取消重复请求
+const requestKey = `${method}:${url}`;
+if (pendingRequests.has(requestKey)) {
+  pendingRequests.get(requestKey)?.abort();
+}
+```
+
+## 总结
+
+当前的 `request.ts` 实现已经覆盖了大部分常用场景,代码简洁易维护。
+
+**建议:**
+1. 先使用当前实现
+2. 根据实际需求逐步添加功能
+3. 避免过度设计,保持代码简洁
+
+如果后端明确要求某些功能(如签名、租户ID),再添加对应的实现。

+ 248 - 0
docs/PERMISSION_GUIDE.md

@@ -0,0 +1,248 @@
+# JeecgBoot 权限接入指南
+
+## 概述
+
+本项目已集成 JeecgBoot 后台权限系统,支持动态菜单渲染和细粒度的权限控制。
+
+## 目录结构
+
+```
+src/
+├── contexts/
+│   └── AuthContext.tsx          # 权限上下文,管理菜单和权限数据
+├── hooks/
+│   └── usePermission.ts         # 权限检查 Hook
+├── services/
+│   └── api.ts                   # API 请求封装
+├── types/
+│   └── menu.ts                  # 类型定义
+├── components/
+│   └── common/
+│       ├── PermissionButton.tsx    # 权限按钮组件
+│       └── PermissionWrapper.tsx   # 权限包装组件
+└── examples/
+    └── PermissionUsage.tsx      # 使用示例
+```
+
+## 快速开始
+
+### 1. 配置环境变量
+
+复制 `.env.example` 为 `.env`,配置后端 API 地址:
+
+```bash
+VITE_API_BASE_URL=http://localhost:8080
+```
+
+### 2. 在应用中使用 AuthProvider
+
+```tsx
+import { AuthProvider } from "./contexts/AuthContext";
+
+function App() {
+  return (
+    <AuthProvider>
+      {/* 你的应用组件 */}
+    </AuthProvider>
+  );
+}
+```
+
+### 3. 使用权限控制
+
+#### 方式一:使用 PermissionButton 组件
+
+```tsx
+import PermissionButton from "@/components/common/PermissionButton";
+
+<PermissionButton menuId="user-management" action="add" variant="primary">
+  新增用户
+</PermissionButton>
+```
+
+#### 方式二:使用 PermissionWrapper 组件
+
+```tsx
+import PermissionWrapper from "@/components/common/PermissionWrapper";
+
+<PermissionWrapper menuId="user-management" action="delete">
+  <Button variant="negative">删除</Button>
+</PermissionWrapper>
+```
+
+#### 方式三:使用 usePermission Hook
+
+```tsx
+import { usePermission } from "@/hooks/usePermission";
+
+function MyComponent() {
+  const permission = usePermission("user-management");
+
+  return (
+    <>
+      {permission.canAdd && <Button>新增</Button>}
+      {permission.canEdit && <Button>编辑</Button>}
+      {permission.canDelete && <Button>删除</Button>}
+    </>
+  );
+}
+```
+
+## 权限类型
+
+系统支持以下标准权限类型:
+
+- `view` - 查看权限
+- `add` - 新增权限
+- `edit` - 编辑权限
+- `delete` - 删除权限
+- `export` - 导出权限
+- `import` - 导入权限
+
+你也可以使用自定义权限标识:
+
+```tsx
+permission.hasAction("custom-action")
+```
+
+## API 接口
+
+### 获取用户菜单
+
+```typescript
+GET /sys/permission/getUserPermissionByToken
+```
+
+返回格式:
+```json
+[
+  {
+    "id": "menu-1",
+    "name": "user-management",
+    "path": "/user",
+    "meta": {
+      "title": "用户管理",
+      "icon": "user"
+    },
+    "children": []
+  }
+]
+```
+
+### 获取用户权限
+
+```typescript
+GET /sys/permission/getPermissionRuleByUser
+```
+
+返回格式:
+```json
+[
+  {
+    "id": "perm-1",
+    "menuId": "menu-1",
+    "actions": ["view", "add", "edit", "delete"]
+  }
+]
+```
+
+## 最佳实践
+
+### 1. 菜单 ID 管理
+
+建议在配置文件中统一管理菜单 ID:
+
+```typescript
+// src/config/menuIds.ts
+export const MENU_IDS = {
+  USER_MANAGEMENT: "user-management",
+  ROLE_MANAGEMENT: "role-management",
+  CONTENT_MANAGEMENT: "content-management",
+} as const;
+```
+
+使用时:
+
+```tsx
+import { MENU_IDS } from "@/config/menuIds";
+
+<PermissionButton menuId={MENU_IDS.USER_MANAGEMENT} action="add">
+  新增用户
+</PermissionButton>
+```
+
+### 2. 路由级权限控制
+
+```tsx
+import { Navigate } from "react-router-dom";
+import { usePermission } from "@/hooks/usePermission";
+
+function ProtectedRoute({ menuId, children }) {
+  const { canView } = usePermission(menuId);
+
+  if (!canView) {
+    return <Navigate to="/403" />;
+  }
+
+  return children;
+}
+```
+
+### 3. 表格操作列权限控制
+
+```tsx
+function TableActions({ record, menuId }) {
+  const permission = usePermission(menuId);
+
+  return (
+    <>
+      {permission.canEdit && (
+        <Button onClick={() => handleEdit(record)}>编辑</Button>
+      )}
+      {permission.canDelete && (
+        <Button onClick={() => handleDelete(record)}>删除</Button>
+      )}
+    </>
+  );
+}
+```
+
+## 注意事项
+
+1. **Token 管理**:确保在登录后将 token 存储到 localStorage
+2. **错误处理**:API 请求失败时会抛出异常,建议添加全局错误处理
+3. **权限刷新**:用户权限变更后,调用 `refreshAuth()` 刷新权限数据
+4. **性能优化**:权限数据使用 Map 存储,查询性能为 O(1)
+
+## 故障排查
+
+### 菜单不显示
+
+1. 检查 API 地址是否正确
+2. 检查 token 是否有效
+3. 检查后端返回的菜单数据格式
+
+### 权限按钮不显示
+
+1. 确认 menuId 是否正确
+2. 确认后端返回的权限数据中包含对应的 action
+3. 检查浏览器控制台是否有错误信息
+
+## 扩展
+
+### 自定义权限检查逻辑
+
+如果需要更复杂的权限检查逻辑,可以扩展 `hasPermission` 函数:
+
+```typescript
+// src/contexts/AuthContext.tsx
+const hasPermission = (menuId: string, action: string): boolean => {
+  // 超级管理员拥有所有权限
+  if (userRole === "admin") {
+    return true;
+  }
+
+  const actions = permissions.get(menuId);
+  return actions ? actions.includes(action) : false;
+};
+```

+ 202 - 0
docs/PROJECT_STRUCTURE.md

@@ -0,0 +1,202 @@
+# 项目结构说明
+
+## 目录结构
+
+```
+src/
+├── assets/                 # 静态资源
+│   └── react.svg
+├── components/             # 通用组件
+│   └── common/
+│       ├── PermissionButton.tsx    # 权限按钮
+│       └── PermissionWrapper.tsx   # 权限包装器
+├── config/                 # 配置文件
+│   ├── api.ts             # API 配置(前缀、超时等)
+│   └── menuIds.ts         # 菜单 ID 常量
+├── contexts/               # React Context
+│   └── AuthContext.tsx    # 权限上下文
+├── hooks/                  # 自定义 Hooks
+│   └── usePermission.ts   # 权限检查 Hook
+├── layouts/                # 布局组件
+│   ├── Header.tsx         # 顶部导航
+│   ├── MainLayout.tsx     # 主布局
+│   └── Sidebar.tsx        # 侧边栏
+├── pages/                  # 页面组件
+│   └── ContentPage.tsx    # 内容页面
+├── services/               # API 服务层
+│   ├── api.ts             # API 统一导出
+│   ├── BaseService.ts     # 基础服务类
+│   ├── index.ts           # 服务总导出
+│   └── modules/           # 业务模块
+│       ├── content.ts     # 内容管理
+│       ├── role.ts        # 角色管理
+│       ├── system.ts      # 系统管理
+│       └── user.ts        # 用户管理
+├── types/                  # 类型定义
+│   ├── axios.d.ts         # Axios 类型
+│   └── menu.ts            # 菜单类型
+├── utils/                  # 工具函数
+│   ├── http/              # HTTP 请求封装
+│   │   ├── Axios.ts       # Axios 封装类
+│   │   ├── axiosTransform.ts  # 转换器接口
+│   │   ├── checkStatus.ts     # 状态码检查
+│   │   ├── helper.ts          # 辅助函数
+│   │   └── index.ts           # 主入口
+│   ├── message.ts         # 消息提示
+│   ├── sign.ts            # 请求签名
+│   └── tenant.ts          # 多租户管理
+├── App.css
+├── App.tsx                 # 应用根组件
+├── index.css
+└── main.tsx                # 应用入口
+
+docs/                       # 文档目录
+├── API_GUIDE.md           # API 使用指南
+├── AXIOS_MIGRATION.md     # Axios 迁移说明
+├── HTTP_COMPARISON.md     # HTTP 封装对比
+├── PERMISSION_GUIDE.md    # 权限系统指南
+├── PROJECT_STRUCTURE.md   # 项目结构说明(本文件)
+├── PROXY_GUIDE.md         # 代理配置指南
+└── QUICK_START.md         # 快速开始
+
+public/                     # 公共资源
+└── vite.svg
+
+.env.example               # 环境变量示例
+.env.development           # 开发环境配置
+.env.production            # 生产环境配置
+vite.config.ts             # Vite 配置
+tsconfig.json              # TypeScript 配置
+package.json               # 项目依赖
+```
+
+## 核心模块说明
+
+### 1. HTTP 请求层 (`src/utils/http/`)
+
+完整复刻 JeecgBoot 的 axios 封装:
+
+- **Axios.ts** - VAxios 封装类,提供请求方法
+- **axiosTransform.ts** - 定义转换器接口
+- **checkStatus.ts** - HTTP 状态码检查和错误提示
+- **helper.ts** - 辅助函数(时间戳、参数格式化等)
+- **index.ts** - 主入口,配置拦截器和转换器
+
+### 2. 服务层 (`src/services/`)
+
+按业务模块组织的 API 服务:
+
+- **BaseService.ts** - 基础服务类,提供通用 CRUD
+- **api.ts** - 统一导出所有 API
+- **modules/** - 业务模块
+  - **system.ts** - 登录、权限、字典、上传
+  - **user.ts** - 用户管理
+  - **role.ts** - 角色管理
+  - **content.ts** - 内容管理
+
+### 3. 权限系统 (`src/contexts/`, `src/hooks/`, `src/components/common/`)
+
+完整的权限管理方案:
+
+- **AuthContext.tsx** - 权限上下文,管理菜单和权限数据
+- **usePermission.ts** - 权限检查 Hook
+- **PermissionButton.tsx** - 带权限控制的按钮
+- **PermissionWrapper.tsx** - 权限包装器组件
+
+### 4. 布局系统 (`src/layouts/`)
+
+三栏布局结构:
+
+- **MainLayout.tsx** - 主布局容器
+- **Header.tsx** - 顶部导航,动态菜单
+- **Sidebar.tsx** - 侧边栏,树形菜单
+
+### 5. 工具函数 (`src/utils/`)
+
+- **message.ts** - 消息提示(使用 Notification)
+- **sign.ts** - 请求签名(MD5)
+- **tenant.ts** - 多租户管理
+
+### 6. 配置文件 (`src/config/`)
+
+- **api.ts** - API 配置(前缀、超时、BASE_URL)
+- **menuIds.ts** - 菜单 ID 常量
+
+## 技术栈
+
+- **React 19** - UI 框架
+- **TypeScript** - 类型安全
+- **Vite** - 构建工具
+- **Axios** - HTTP 请求
+- **Contentful F36** - UI 组件库
+- **Crypto-JS** - 加密库(MD5 签名)
+
+## 核心特性
+
+1. ✅ **Axios 封装** - 完整复刻 JeecgBoot
+2. ✅ **请求签名** - MD5 签名防篡改
+3. ✅ **多租户支持** - 自动添加租户 ID
+4. ✅ **权限管理** - 动态菜单 + 按钮权限
+5. ✅ **Cookie 认证** - 自动携带 Cookie
+6. ✅ **错误处理** - 统一的错误提示
+7. ✅ **代理配置** - 开发环境跨域处理
+
+## 开发规范
+
+### 1. 组件组织
+
+- `layouts/` - 布局组件
+- `pages/` - 页面组件
+- `components/common/` - 通用组件
+
+### 2. 服务层组织
+
+- `services/api.ts` - 系统级 API
+- `services/modules/` - 按业务模块组织
+
+### 3. 命名规范
+
+- 组件:PascalCase(如 `UserList.tsx`)
+- 文件:camelCase(如 `userService.ts`)
+- 常量:UPPER_SNAKE_CASE(如 `API_PREFIX`)
+
+### 4. 类型定义
+
+- 所有接口都应该有类型定义
+- 类型文件放在 `src/types/` 目录
+
+## 环境变量
+
+```bash
+# .env.development
+VITE_API_BASE_URL=http://localhost:8080
+
+# .env.production
+VITE_API_BASE_URL=https://api.example.com
+```
+
+## 常用命令
+
+```bash
+# 安装依赖
+pnpm install
+
+# 启动开发服务器
+pnpm dev
+
+# 构建生产版本
+pnpm build
+
+# 预览生产版本
+pnpm preview
+
+# 代码检查
+pnpm lint
+```
+
+## 相关文档
+
+- [API 使用指南](./API_GUIDE.md)
+- [权限系统指南](./PERMISSION_GUIDE.md)
+- [代理配置指南](./PROXY_GUIDE.md)
+- [快速开始](./QUICK_START.md)

+ 385 - 0
docs/PROXY_GUIDE.md

@@ -0,0 +1,385 @@
+# Vite 代理配置指南
+
+## 概述
+
+本项目使用 Vite 的代理功能来解决开发环境的跨域问题。
+
+## 代理配置
+
+### 配置文件
+
+代理配置在 `vite.config.ts` 中:
+
+```typescript
+server: {
+  proxy: {
+    // 代理所有 /api 开头的请求
+    "/api": {
+      target: "http://localhost:8080",
+      changeOrigin: true,
+      rewrite: (path) => path.replace(/^\/api/, ""),
+    },
+
+    // 代理系统接口
+    "/sys": {
+      target: "http://localhost:8080",
+      changeOrigin: true,
+    },
+
+    // 代理 WebSocket
+    "/websocket": {
+      target: "http://localhost:8080",
+      changeOrigin: true,
+      ws: true,
+    },
+  },
+}
+```
+
+## 工作原理
+
+### 开发环境
+
+在开发环境中(`npm run dev`):
+
+1. 前端运行在 `http://localhost:3000`
+2. 后端运行在 `http://localhost:8080`
+3. 所有以 `/api`、`/sys` 开头的请求会被代理到后端
+
+**请求流程:**
+
+```
+浏览器请求: http://localhost:3000/sys/user/list
+    ↓
+Vite 代理: http://localhost:8080/sys/user/list
+    ↓
+后端响应
+```
+
+### 生产环境
+
+在生产环境中(`npm run build`):
+
+1. 前端打包后部署到服务器
+2. 使用完整的 API 地址(从环境变量读取)
+3. 不经过代理,直接请求后端
+
+**请求流程:**
+
+```
+浏览器请求: https://api.example.com/sys/user/list
+    ↓
+后端响应
+```
+
+## 环境变量配置
+
+### 开发环境 (`.env.development`)
+
+```bash
+# 开发环境使用本地后端
+VITE_API_BASE_URL=http://localhost:8080
+```
+
+### 生产环境 (`.env.production`)
+
+```bash
+# 生产环境使用实际后端地址
+VITE_API_BASE_URL=https://api.example.com
+```
+
+## 代理规则说明
+
+### 1. `/api` 路径代理
+
+```typescript
+"/api": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+  rewrite: (path) => path.replace(/^\/api/, ""),
+}
+```
+
+**作用:** 将 `/api` 前缀去掉后转发
+
+**示例:**
+- 请求:`/api/users/list`
+- 转发:`http://localhost:8080/users/list`
+
+### 2. `/sys` 路径代理
+
+```typescript
+"/sys": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+}
+```
+
+**作用:** 保留 `/sys` 前缀转发
+
+**示例:**
+- 请求:`/sys/user/list`
+- 转发:`http://localhost:8080/sys/user/list`
+
+### 3. WebSocket 代理
+
+```typescript
+"/websocket": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+  ws: true,
+}
+```
+
+**作用:** 代理 WebSocket 连接
+
+## 常见问题
+
+### 1. 代理不生效
+
+**原因:** 请求路径不匹配代理规则
+
+**解决:** 确保请求路径以配置的前缀开头(如 `/api`、`/sys`)
+
+```typescript
+// ✅ 正确 - 会被代理
+http.get("/sys/user/list");
+
+// ❌ 错误 - 不会被代理
+http.get("http://localhost:8080/sys/user/list");
+```
+
+### 2. 跨域问题
+
+**原因:** `changeOrigin` 未设置为 `true`
+
+**解决:** 确保代理配置中有 `changeOrigin: true`
+
+```typescript
+"/api": {
+  target: "http://localhost:8080",
+  changeOrigin: true, // 必须设置
+}
+```
+
+### 3. Cookie 无法携带
+
+**原因:** 跨域请求默认不携带 Cookie
+
+**解决:** 在代理配置中添加 Cookie 相关配置
+
+```typescript
+"/api": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+  cookieDomainRewrite: "localhost",
+}
+```
+
+### 4. HTTPS 证书问题
+
+**原因:** 后端使用自签名证书
+
+**解决:** 添加 `secure: false`
+
+```typescript
+"/api": {
+  target: "https://localhost:8443",
+  changeOrigin: true,
+  secure: false, // 忽略证书验证
+}
+```
+
+## 高级配置
+
+### 1. 多个后端服务代理
+
+```typescript
+server: {
+  proxy: {
+    // 用户服务
+    "/api/user": {
+      target: "http://localhost:8081",
+      changeOrigin: true,
+    },
+    // 订单服务
+    "/api/order": {
+      target: "http://localhost:8082",
+      changeOrigin: true,
+    },
+  },
+}
+```
+
+### 2. 根据环境变量动态配置
+
+```typescript
+import { loadEnv } from "vite";
+
+export default defineConfig(({ mode }) => {
+  const env = loadEnv(mode, process.cwd(), "");
+
+  return {
+    server: {
+      proxy: {
+        "/api": {
+          target: env.VITE_API_BASE_URL,
+          changeOrigin: true,
+        },
+      },
+    },
+  };
+});
+```
+
+### 3. 请求日志
+
+```typescript
+"/api": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+  configure: (proxy, options) => {
+    proxy.on("proxyReq", (proxyReq, req, res) => {
+      console.log("代理请求:", req.method, req.url);
+    });
+    proxy.on("proxyRes", (proxyRes, req, res) => {
+      console.log("代理响应:", proxyRes.statusCode, req.url);
+    });
+  },
+}
+```
+
+### 4. 请求重写
+
+```typescript
+"/api": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+  rewrite: (path) => {
+    // 移除 /api 前缀
+    const newPath = path.replace(/^\/api/, "");
+    console.log(`重写路径: ${path} -> ${newPath}`);
+    return newPath;
+  },
+}
+```
+
+## 部署注意事项
+
+### 1. Nginx 反向代理
+
+生产环境通常使用 Nginx 做反向代理:
+
+```nginx
+server {
+    listen 80;
+    server_name example.com;
+
+    # 前端静态资源
+    location / {
+        root /var/www/html;
+        try_files $uri $uri/ /index.html;
+    }
+
+    # 后端 API 代理
+    location /api {
+        proxy_pass http://localhost:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+
+    location /sys {
+        proxy_pass http://localhost:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+    }
+}
+```
+
+### 2. 前后端分离部署
+
+如果前后端部署在不同域名:
+
+**前端:** `https://www.example.com`  
+**后端:** `https://api.example.com`
+
+需要在 `.env.production` 中配置完整的后端地址:
+
+```bash
+VITE_API_BASE_URL=https://api.example.com
+```
+
+同时后端需要配置 CORS:
+
+```java
+@Configuration
+public class CorsConfig {
+    @Bean
+    public CorsFilter corsFilter() {
+        CorsConfiguration config = new CorsConfiguration();
+        config.addAllowedOrigin("https://www.example.com");
+        config.addAllowedMethod("*");
+        config.addAllowedHeader("*");
+        config.setAllowCredentials(true);
+        
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config);
+        return new CorsFilter(source);
+    }
+}
+```
+
+## 调试技巧
+
+### 1. 查看代理日志
+
+在 `vite.config.ts` 中添加日志:
+
+```typescript
+server: {
+  proxy: {
+    "/api": {
+      target: "http://localhost:8080",
+      changeOrigin: true,
+      configure: (proxy) => {
+        proxy.on("error", (err) => {
+          console.log("代理错误:", err);
+        });
+        proxy.on("proxyReq", (proxyReq, req) => {
+          console.log("发送请求:", req.method, req.url);
+        });
+        proxy.on("proxyRes", (proxyRes, req) => {
+          console.log("收到响应:", proxyRes.statusCode, req.url);
+        });
+      },
+    },
+  },
+}
+```
+
+### 2. 使用浏览器开发者工具
+
+1. 打开 Network 面板
+2. 查看请求的 URL
+3. 检查请求头和响应头
+4. 确认是否被代理
+
+### 3. 测试代理配置
+
+```bash
+# 启动开发服务器
+pnpm dev
+
+# 在浏览器中访问
+http://localhost:3000
+
+# 在控制台测试请求
+fetch('/sys/user/list').then(r => r.json()).then(console.log)
+```
+
+## 参考资料
+
+- [Vite 官方文档 - server.proxy](https://vitejs.dev/config/server-options.html#server-proxy)
+- [http-proxy-middleware 文档](https://github.com/chimurai/http-proxy-middleware)

+ 287 - 0
docs/QUICK_START.md

@@ -0,0 +1,287 @@
+# 快速开始指南
+
+## 1. 环境准备
+
+### 必需环境
+
+- Node.js >= 20.19+
+- pnpm >= 8.0
+
+### 安装 pnpm
+
+```bash
+npm install -g pnpm
+```
+
+## 2. 项目初始化
+
+### 克隆项目
+
+```bash
+git clone <repository-url>
+cd content-frontend
+```
+
+### 安装依赖
+
+```bash
+pnpm install
+```
+
+## 3. 配置后端地址
+
+### 复制环境变量文件
+
+```bash
+cp .env.example .env.development
+```
+
+### 修改后端地址
+
+编辑 `.env.development`:
+
+```bash
+# 修改为你的后端地址
+VITE_API_BASE_URL=http://localhost:8080
+```
+
+## 4. 启动开发服务器
+
+```bash
+pnpm dev
+```
+
+项目将在 `http://localhost:3000` 启动。
+
+## 5. 代理配置说明
+
+### 开发环境
+
+开发环境下,所有 API 请求会通过 Vite 代理转发到后端:
+
+```
+前端: http://localhost:3000
+后端: http://localhost:8080
+
+请求流程:
+http://localhost:3000/sys/user/list
+    ↓ (Vite 代理)
+http://localhost:8080/sys/user/list
+```
+
+### 代理规则
+
+在 `vite.config.ts` 中配置:
+
+```typescript
+proxy: {
+  "/api": {
+    target: "http://localhost:8080",
+    changeOrigin: true,
+    rewrite: (path) => path.replace(/^\/api/, ""),
+  },
+  "/sys": {
+    target: "http://localhost:8080",
+    changeOrigin: true,
+  },
+}
+```
+
+## 6. 测试接口连接
+
+### 方式一:浏览器控制台
+
+打开浏览器控制台,执行:
+
+```javascript
+fetch('/sys/user/list')
+  .then(r => r.json())
+  .then(console.log)
+```
+
+### 方式二:使用示例页面
+
+访问示例页面测试 API:
+
+```
+http://localhost:3000/examples/api-usage
+```
+
+## 7. 常见问题
+
+### 问题 1: 端口被占用
+
+**错误信息:**
+```
+Port 3000 is in use
+```
+
+**解决方案:**
+
+修改 `vite.config.ts` 中的端口:
+
+```typescript
+server: {
+  port: 3001, // 改为其他端口
+}
+```
+
+### 问题 2: 代理不生效
+
+**原因:** 请求路径不匹配代理规则
+
+**解决方案:**
+
+确保 API 请求路径以 `/api` 或 `/sys` 开头:
+
+```typescript
+// ✅ 正确
+http.get("/sys/user/list");
+
+// ❌ 错误
+http.get("http://localhost:8080/sys/user/list");
+```
+
+### 问题 3: 跨域问题
+
+**原因:** 后端未配置 CORS
+
+**解决方案:**
+
+1. 使用 Vite 代理(推荐)
+2. 或在后端配置 CORS
+
+### 问题 4: Token 无法携带
+
+**原因:** Cookie 配置问题
+
+**解决方案:**
+
+在代理配置中添加:
+
+```typescript
+"/api": {
+  target: "http://localhost:8080",
+  changeOrigin: true,
+  cookieDomainRewrite: "localhost",
+}
+```
+
+## 8. 构建生产版本
+
+### 构建
+
+```bash
+pnpm build
+```
+
+构建产物在 `dist` 目录。
+
+### 预览
+
+```bash
+pnpm preview
+```
+
+### 生产环境配置
+
+编辑 `.env.production`:
+
+```bash
+# 生产环境后端地址
+VITE_API_BASE_URL=https://api.example.com
+```
+
+## 9. 部署
+
+### 方式一:静态文件部署
+
+将 `dist` 目录部署到任意静态文件服务器(Nginx、Apache 等)。
+
+### 方式二:Docker 部署
+
+```dockerfile
+FROM nginx:alpine
+COPY dist /usr/share/nginx/html
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+EXPOSE 80
+CMD ["nginx", "-g", "daemon off;"]
+```
+
+### Nginx 配置示例
+
+```nginx
+server {
+    listen 80;
+    server_name example.com;
+
+    root /usr/share/nginx/html;
+    index index.html;
+
+    # 前端路由
+    location / {
+        try_files $uri $uri/ /index.html;
+    }
+
+    # API 代理
+    location /api {
+        proxy_pass http://backend:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+
+    location /sys {
+        proxy_pass http://backend:8080;
+        proxy_set_header Host $host;
+        proxy_set_header X-Real-IP $remote_addr;
+    }
+}
+```
+
+## 10. 开发建议
+
+### 目录结构
+
+```
+src/
+├── layouts/      # 布局组件
+├── pages/        # 页面组件
+├── components/   # 通用组件
+├── services/     # API 服务
+├── hooks/        # 自定义 Hooks
+├── contexts/     # React Context
+├── utils/        # 工具函数
+└── types/        # 类型定义
+```
+
+### 代码规范
+
+- 使用 TypeScript 严格模式
+- 组件使用函数式组件
+- 使用 Hooks 管理状态
+- API 请求使用封装的服务层
+
+### Git 提交规范
+
+```bash
+feat: 新功能
+fix: 修复 bug
+docs: 文档更新
+style: 代码格式调整
+refactor: 重构
+test: 测试相关
+chore: 构建/工具相关
+```
+
+## 11. 下一步
+
+- 阅读 [权限系统使用指南](./PERMISSION_GUIDE.md)
+- 阅读 [API 请求封装指南](./API_GUIDE.md)
+- 阅读 [Vite 代理配置指南](./PROXY_GUIDE.md)
+- 查看示例代码 `src/examples/`
+
+## 12. 获取帮助
+
+- 查看项目文档
+- 提交 Issue
+- 联系开发团队

+ 23 - 0
eslint.config.js

@@ -0,0 +1,23 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+  globalIgnores(['dist']),
+  {
+    files: ['**/*.{ts,tsx}'],
+    extends: [
+      js.configs.recommended,
+      tseslint.configs.recommended,
+      reactHooks.configs.flat.recommended,
+      reactRefresh.configs.vite,
+    ],
+    languageOptions: {
+      ecmaVersion: 2020,
+      globals: globals.browser,
+    },
+  },
+])

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!doctype html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8" />
+    <link rel="icon" type="image/svg+xml" href="/vite.svg" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>content-frontend</title>
+  </head>
+  <body>
+    <div id="root"></div>
+    <script type="module" src="/src/main.tsx"></script>
+  </body>
+</html>

+ 39 - 0
package.json

@@ -0,0 +1,39 @@
+{
+  "name": "content-frontend",
+  "private": true,
+  "version": "0.0.0",
+  "type": "module",
+  "scripts": {
+    "dev": "vite",
+    "build": "tsc -b && vite build",
+    "lint": "eslint .",
+    "preview": "vite preview"
+  },
+  "dependencies": {
+    "@contentful/f36-components": "^5.9.0",
+    "@contentful/f36-icons": "^5.9.0",
+    "@contentful/f36-tokens": "^5.1.0",
+    "@emotion/react": "^11.14.0",
+    "@emotion/styled": "^11.14.1",
+    "axios": "^1.13.2",
+    "crypto-js": "^4.2.0",
+    "react": "^19.2.0",
+    "react-dom": "^19.2.0",
+    "react-router-dom": "^7.9.6"
+  },
+  "devDependencies": {
+    "@eslint/js": "^9.39.1",
+    "@types/crypto-js": "^4.2.2",
+    "@types/node": "^24.10.1",
+    "@types/react": "^19.2.5",
+    "@types/react-dom": "^19.2.3",
+    "@vitejs/plugin-react": "^5.1.1",
+    "eslint": "^9.39.1",
+    "eslint-plugin-react-hooks": "^7.0.1",
+    "eslint-plugin-react-refresh": "^0.4.24",
+    "globals": "^16.5.0",
+    "typescript": "~5.9.3",
+    "typescript-eslint": "^8.46.4",
+    "vite": "^7.2.4"
+  }
+}

+ 4139 - 0
pnpm-lock.yaml

@@ -0,0 +1,4139 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@contentful/f36-components':
+        specifier: ^5.9.0
+        version: 5.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons':
+        specifier: ^5.9.0
+        version: 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens':
+        specifier: ^5.1.0
+        version: 5.1.0
+      '@emotion/react':
+        specifier: ^11.14.0
+        version: 11.14.0(@types/react@19.2.7)(react@19.2.0)
+      '@emotion/styled':
+        specifier: ^11.14.1
+        version: 11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)
+      axios:
+        specifier: ^1.13.2
+        version: 1.13.2
+      crypto-js:
+        specifier: ^4.2.0
+        version: 4.2.0
+      react:
+        specifier: ^19.2.0
+        version: 19.2.0
+      react-dom:
+        specifier: ^19.2.0
+        version: 19.2.0(react@19.2.0)
+      react-router-dom:
+        specifier: ^7.9.6
+        version: 7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+    devDependencies:
+      '@eslint/js':
+        specifier: ^9.39.1
+        version: 9.39.1
+      '@types/crypto-js':
+        specifier: ^4.2.2
+        version: 4.2.2
+      '@types/node':
+        specifier: ^24.10.1
+        version: 24.10.1
+      '@types/react':
+        specifier: ^19.2.5
+        version: 19.2.7
+      '@types/react-dom':
+        specifier: ^19.2.3
+        version: 19.2.3(@types/react@19.2.7)
+      '@vitejs/plugin-react':
+        specifier: ^5.1.1
+        version: 5.1.1(vite@7.2.6(@types/node@24.10.1))
+      eslint:
+        specifier: ^9.39.1
+        version: 9.39.1
+      eslint-plugin-react-hooks:
+        specifier: ^7.0.1
+        version: 7.0.1(eslint@9.39.1)
+      eslint-plugin-react-refresh:
+        specifier: ^0.4.24
+        version: 0.4.24(eslint@9.39.1)
+      globals:
+        specifier: ^16.5.0
+        version: 16.5.0
+      typescript:
+        specifier: ~5.9.3
+        version: 5.9.3
+      typescript-eslint:
+        specifier: ^8.46.4
+        version: 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      vite:
+        specifier: ^7.2.4
+        version: 7.2.6(@types/node@24.10.1)
+
+packages:
+
+  '@babel/code-frame@7.27.1':
+    resolution: {integrity: sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/compat-data@7.28.5':
+    resolution: {integrity: sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/core@7.28.5':
+    resolution: {integrity: sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/generator@7.28.5':
+    resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-compilation-targets@7.27.2':
+    resolution: {integrity: sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==}
+    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.27.1':
+    resolution: {integrity: sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/helper-module-transforms@7.28.3':
+    resolution: {integrity: sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0
+
+  '@babel/helper-plugin-utils@7.27.1':
+    resolution: {integrity: sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==}
+    engines: {node: '>=6.9.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.28.4':
+    resolution: {integrity: sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/parser@7.28.5':
+    resolution: {integrity: sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+
+  '@babel/plugin-transform-react-jsx-self@7.27.1':
+    resolution: {integrity: sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/plugin-transform-react-jsx-source@7.27.1':
+    resolution: {integrity: sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==}
+    engines: {node: '>=6.9.0'}
+    peerDependencies:
+      '@babel/core': ^7.0.0-0
+
+  '@babel/runtime@7.28.4':
+    resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/template@7.27.2':
+    resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/traverse@7.28.5':
+    resolution: {integrity: sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@babel/types@7.28.5':
+    resolution: {integrity: sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==}
+    engines: {node: '>=6.9.0'}
+
+  '@contentful/f36-accordion@5.9.0':
+    resolution: {integrity: sha512-dMxqIdII+TzS+GCx8i7ZYCFUmJVyKYet32WAPJibkw6KLfkWNyJNnYNqmegVBxrll795JSWk8GUZYsoYeGyPRw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-asset@5.9.0':
+    resolution: {integrity: sha512-FvQjsrHQe3Vyhug4EzcUdR4ty+VdUuDmm0uDH3XZPHtpII4kxuiuFUbxG2LMllU8ZUGHhHI+3E+aJxtOgXO+fQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-autocomplete@5.9.0':
+    resolution: {integrity: sha512-7q/bCrIQyArfe2mU+0KIIqD5cmmyBJDbmH02P0qrwbgBLYNXrds8KEhx8TOffKOg0v+AZU6U50Yb6PhZFpnz+Q==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-avatar@5.9.0':
+    resolution: {integrity: sha512-526lo4QtwIs7aYeME/U55GkVQhypSl8WtH2NtKVV+y35S5mpiMHCVjZNipZUBVwGw8Dkp01gzJTWEYw1SOt4kA==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-badge@5.9.0':
+    resolution: {integrity: sha512-bdGDTlrPWUNG5iaAKY4l6eFHQaY9PlUY+mlAky6wVq8YKUEWeccYFUd3sbMhCw+q2R+IKn1Ya0GadNKTJc9kyw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-button@5.9.0':
+    resolution: {integrity: sha512-e6qyaSFbyIhyTlrzcqS8tIj1F/A4WCNxC+KNz5sMXjhiXeB8gK2+WL8DGwQm+MSwtXysh02K1eYtJ1AQMu4lFQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-card@5.9.0':
+    resolution: {integrity: sha512-LDLXLnjYK0AnADbF8oTcjcba4+wfUiWgjyg6ijPdMrc4/OXV4gqOwHjnhZjjVEgUUiWgbV5ism/db84opai+ZQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-collapse@5.9.0':
+    resolution: {integrity: sha512-QvxIzhwBUePWY0a85ezkBHwCstGYDUEVWWCmGR/Z/lMR/yM6ZHNcZ8A/KFrRCOFrgNlx13j3VW1G2AMfhhz64A==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-components@5.9.0':
+    resolution: {integrity: sha512-PiRQla7eJjTKJiF81evoMYxZtd2K8nnjmpdBJF2H2woOBzZBr7XJ8QcZYbOHWDOuOPccImeItw2zeD4VQXqUkQ==}
+    peerDependencies:
+      '@types/react': '>=16.8'
+      '@types/react-dom': '>=16.8'
+      react: '>=16.8'
+      react-dom: '>=16.8'
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@contentful/f36-copybutton@5.9.0':
+    resolution: {integrity: sha512-gcgaaz9LIebMvu/uJzuKuzBeq9TWe9LwNo6XQW/AbikBSkIJRMrVI4BZUcg606ttKT+DiksKNeblr2LM2pCC9Q==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-core@5.9.0':
+    resolution: {integrity: sha512-6uFPzFaoVX3wwYaHcFWy9UK5/nA0xPBgJk49uZd1yDoEkL8B+QEdYp/McmkF0Sit6G9DDHXmSZE0+jjeA7c5LA==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-datepicker@5.9.0':
+    resolution: {integrity: sha512-XZt5ttaG05tp7wMfnHYWAmol8ySDdzB9LUlE/EqYQBDoKccSLwC8EFCk0dCoy3a8moHC0eVcNYpzHsggC/7jpw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-datetime@5.9.0':
+    resolution: {integrity: sha512-PrrvlumV0GdCyppwuWoSxaftl5Y/wd/aK/zLXkFjJVxjiG+pbkiDMuqQAgocYE67tmtRESatKxXwoHqO3mhqXQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-drag-handle@5.9.0':
+    resolution: {integrity: sha512-mUvpxZagh2DsOwoA/cOBV2HrimM8TQwrP6vGkUC1E0KRiBXM+h6WVwE1GkWeuVEWdgsvQ59NKRbUgZvKbFKU3g==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-empty-state@5.9.0':
+    resolution: {integrity: sha512-8l0lq4HMjXaDCjM55CTGcopiUsUu8HgOnAk70TDB/spxo0FWJGY9D6Cu01o0fuFZDX09+5+VnfgD0Ae04Mq9Xw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-entity-list@5.9.0':
+    resolution: {integrity: sha512-O7fGFUhIP1pKSLxq5Wb2QItObrDs6918hyIW4KHFSOzy7SXTE6686y0cpnwJ9txLmXJ0OT4oRH4NuJUr+i3Cmg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-forms@5.9.0':
+    resolution: {integrity: sha512-C68tODPeAFGWPyISnkoil276E/wxrmE7SWOPmlDrj3nH2aZ3J3pfrnjosPUBjioB1X0YSDZ/U5mQr9tWgkIqtQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-header@5.9.0':
+    resolution: {integrity: sha512-+5yyiSjJAbVSqi2UqAY2UITqNo/7Xqvzw944MuyVx4xawfEhNdBfJSu2NrF8yKzx2GscswMtiMUcGbvQ9bY1zQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-icon@5.9.0':
+    resolution: {integrity: sha512-uiMqdWZDDyd/2fizqMiUpBlVhFOLeurGJYazyeKyctAwqOV6D/osyXy1nLKK4WH48ucTlNT8NnHotyBYjj8JRw==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-icons@5.9.0':
+    resolution: {integrity: sha512-TjLdcGjiIxdNvfQUHpWI29Q8CoYsWl/JMDvZ5//G8Vc66uZVVOWRV8EmIx/56WmcQ9mu+qFNYju6QLUtjwJI7w==}
+    engines: {node: '>=16'}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-image@5.9.0':
+    resolution: {integrity: sha512-EI+Ct06VPxeP8z7+lvHrxw0OgwoppTY6T12NsvL9yzdVVTcPJ36Ws6xA+D+ffccduVmnVVo929IoGxzYxUw/oA==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-layout@5.9.0':
+    resolution: {integrity: sha512-OUnzwwOYgQNLZIcOjoP33CmsQbm1LriQwVKULoHdTvDETULbujnxUjGESWkMvW6wpWt+wlVIvArtFT+kd9QrIQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-list@5.9.0':
+    resolution: {integrity: sha512-VVC8UXaF+WkE1FMQKkhrLGRsZ14N7g9cx8KgpuVpo9Qd0Ydwbxho7w1egRTghIUzBOKqxV1j++UF+WW8I5E+DA==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-menu@5.9.0':
+    resolution: {integrity: sha512-6Xy/Urj6gccVAqsheoTuNuyj0U9sYNL82wqTavpnQu3lOq1M98rCYeCHhIA/SXT2YsIy554SmLSQRjtV37xKFg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-modal@5.9.0':
+    resolution: {integrity: sha512-h0K4c3Tr44ODbgfxx+KP55fnmonBFRCrolVds6DVZJUy4QERaJI6V0Msv6wSNnTbLqLfnHkq8CM5VRz+yL5E9w==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-multiselect@5.9.0':
+    resolution: {integrity: sha512-GnKUzTQ386zFA0mZaAW11p6eDafFG2aZ+U2j0CaDPl0rH+249Xmvm0XALp6GlKpKt2H1s/FGqgYIlLct3syDYg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-navlist@5.9.0':
+    resolution: {integrity: sha512-d9D5B6aJu01ITN99GH4hKGtMk09zkOlwdm95sGVNv8WftBhrTACGgdIYBmp6KO+1xbBRnf9yqyRzcBvK2Xq1Rw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-note@5.9.0':
+    resolution: {integrity: sha512-Gf0OvrYA50YCw6F9n7kSRM9x5XdSpTKieifbKsMwDGe4b1ZArrOHth9ga06fEfajAfnhHCzgiRJ1Fiq9lrw9KA==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-notification@5.9.0':
+    resolution: {integrity: sha512-Xz8KFKMxa560gJM+OSVpHoM7bHpD3HaDhweh0qH3voy5w07OeOYLlM7Wtl4u9jg1lvI8MHlw3UCVgkLqaPRQ5Q==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-pagination@5.9.0':
+    resolution: {integrity: sha512-OERldOBFx1tvFxbS9ajG3qOGwa129Jt9IaXqmVezwaEo02ZjvfaeVuo9djJosTiiKl81+eaRX1juCewlVicsWg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-pill@5.9.0':
+    resolution: {integrity: sha512-Xf76tRd9cjL2QR8DieJ7nvoXddCgRLFQvjiI3TRL3J/AV80g4sSzhRNhN4+WmNuydhIrOl/chCLBtoY/ZONJPw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-popover@5.9.0':
+    resolution: {integrity: sha512-1sz0YWAaxcyQE7thhsnv00bzfgDTR+gi/i11L4m0eQD9nddRWhflH+pGQfur1+YV/FIxGaFtKB950qpMqwlqGQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-progress-stepper@5.9.0':
+    resolution: {integrity: sha512-ZA1mmvmg7ktTHnVg1Pn8haxwRgFAkjWrHl/+ykXBpW6s8xutv8EFPcyXcBGIStOmf4acNJe2t8a4cmJJYCImBg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-skeleton@5.9.0':
+    resolution: {integrity: sha512-3vcHXApsXS3/8XypBv1MEajiwa0JfRgWYtk+7V5MrsPSqFONqomck99KtfotPULxdLyanO65QUfqbRQNzbOO6A==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-spinner@5.9.0':
+    resolution: {integrity: sha512-cmXZR8NBnjvC1MenuGR+woBUhdiZV/PwMgOytR77SgUCyoQVvVCpkbI31XVTJgJx0Qe1K8cDA+mIn+g+aUNpkg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-table@5.9.0':
+    resolution: {integrity: sha512-nMSyJUq6MgwE/gspG5xYpZnhnWUmTMA0Ca62s6ddt8APGhd5TR/6V1ngtkaDsKOKFd8An+NGBlG2VgZkEKbC9g==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-tabs@5.9.0':
+    resolution: {integrity: sha512-r9eVjeiQJZ8eYcmuosdI40CEI/DLg7iR0AOdT9TyGSQRDE+lbCt+jwXLmF5HbauNiBEPNusFfDowFcO7KNcmIQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-text-link@5.9.0':
+    resolution: {integrity: sha512-fLGqd5wtMmTj4oThK96Ao693ei5zjBSLfWK0Y5qyFxMEUPnfzBDX+g3RedXeHn7SdIgVXhD6vlAUCAVe7Z+G0g==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-tokens@5.1.0':
+    resolution: {integrity: sha512-7++MPQiizK6qLhPkilWIKqM5Mhdr199tK7dL4iZ689iTwA82uvXXOojJr+0wVOurG2Odb+6/3WLe1XdveDZHEA==}
+
+  '@contentful/f36-tooltip@5.9.0':
+    resolution: {integrity: sha512-YRQ7iFWTFfAcQSP62w9a5lmpAjsyRXLyymBqw9mXc9t79CqX3leClfBmYp4VyOmDsp3McuaJKAhx+AULtIJmWQ==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-typography@5.9.0':
+    resolution: {integrity: sha512-ai6+xx8hdaDI8l27GJNbQ+AN1gOoWxQAKosAdvt7rnMyWegBwl0YK8vQ8OY6tQ5ba7yekSvMSHv7Pq+Ov29WJg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-usage-card@5.9.0':
+    resolution: {integrity: sha512-YwX426bGwVBfzjdYCiWtcgYzX2t1PKU5e/x8o74fsbmag1dddezOH6AApQn13U24wmuYlKPa6J6PdcVO9wecLw==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-usage-count@5.9.0':
+    resolution: {integrity: sha512-3emhW48a93HSJxQVoUgCRJOOjkRWJNhEN6eQ1N5QLS+Qj9YLvkqMSyhPKOHfgQib3FMtxBhJeaUhanA4gptJAg==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@contentful/f36-utils@5.2.0':
+    resolution: {integrity: sha512-0YWsKAdU2HeNz5DwuOCR0lnlHVhlXiDkqtYaH8hHqT9HlQKHDSBPQUI9Vk5OrPDRJ3uLVoyGOJcvUDf27tTopA==}
+    peerDependencies:
+      react: '>=16.8'
+      react-dom: '>=16.8'
+
+  '@emotion/babel-plugin@11.13.5':
+    resolution: {integrity: sha512-pxHCpT2ex+0q+HH91/zsdHkw/lXd468DIN2zvfvLtPKLLMo6gQj7oLObq8PhkrxOZb/gGCq03S3Z7PDhS8pduQ==}
+
+  '@emotion/cache@10.0.29':
+    resolution: {integrity: sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==}
+
+  '@emotion/cache@11.14.0':
+    resolution: {integrity: sha512-L/B1lc/TViYk4DcpGxtAVbx0ZyiKM5ktoIyafGkH6zg/tj+mA+NE//aPYKG0k8kCHSHVJrpLpcAlOBEXQ3SavA==}
+
+  '@emotion/core@10.3.1':
+    resolution: {integrity: sha512-447aUEjPIm0MnE6QYIaFz9VQOHSXf4Iu6EWOIqq11EAPqinkSZmfymPTmlOE3QjLv846lH4JVZBUOtwGbuQoww==}
+    peerDependencies:
+      react: '>=16.3.0'
+
+  '@emotion/css@10.0.27':
+    resolution: {integrity: sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==}
+
+  '@emotion/hash@0.8.0':
+    resolution: {integrity: sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==}
+
+  '@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.7.4':
+    resolution: {integrity: sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==}
+
+  '@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@0.11.16':
+    resolution: {integrity: sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==}
+
+  '@emotion/serialize@1.3.3':
+    resolution: {integrity: sha512-EISGqt7sSNWHGI76hC7x1CksiXPahbxEOrC5RjmFRJTqLyEK9/9hZvBbiYn70dw4wuwMKiEMCUlR6ZXTSWQqxA==}
+
+  '@emotion/sheet@0.9.4':
+    resolution: {integrity: sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==}
+
+  '@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/stylis@0.8.5':
+    resolution: {integrity: sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==}
+
+  '@emotion/unitless@0.10.0':
+    resolution: {integrity: sha512-dFoMUuQA20zvtVTuxZww6OHoJYgrzfKM1t52mVySDJnMSEa08ruEvdYQbhvyu6soU+NeLVd3yKfTfT0NeV6qGg==}
+
+  '@emotion/unitless@0.7.5':
+    resolution: {integrity: sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==}
+
+  '@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@0.11.3':
+    resolution: {integrity: sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==}
+
+  '@emotion/utils@1.4.2':
+    resolution: {integrity: sha512-3vLclRofFziIa3J2wDh9jjbkUz9qk5Vi3IZ/FSTKViB0k+ef0fPV7dYrUIugbgupYDx7v9ud/SjrtEP8Y4xLoA==}
+
+  '@emotion/weak-memoize@0.2.5':
+    resolution: {integrity: sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==}
+
+  '@emotion/weak-memoize@0.4.0':
+    resolution: {integrity: sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==}
+
+  '@esbuild/aix-ppc64@0.25.12':
+    resolution: {integrity: sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/android-arm64@0.25.12':
+    resolution: {integrity: sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/android-arm@0.25.12':
+    resolution: {integrity: sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/android-x64@0.25.12':
+    resolution: {integrity: sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/darwin-arm64@0.25.12':
+    resolution: {integrity: sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/darwin-x64@0.25.12':
+    resolution: {integrity: sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/freebsd-arm64@0.25.12':
+    resolution: {integrity: sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/freebsd-x64@0.25.12':
+    resolution: {integrity: sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/linux-arm64@0.25.12':
+    resolution: {integrity: sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/linux-arm@0.25.12':
+    resolution: {integrity: sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/linux-ia32@0.25.12':
+    resolution: {integrity: sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/linux-loong64@0.25.12':
+    resolution: {integrity: sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/linux-mips64el@0.25.12':
+    resolution: {integrity: sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/linux-ppc64@0.25.12':
+    resolution: {integrity: sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/linux-riscv64@0.25.12':
+    resolution: {integrity: sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/linux-s390x@0.25.12':
+    resolution: {integrity: sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/linux-x64@0.25.12':
+    resolution: {integrity: sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/netbsd-arm64@0.25.12':
+    resolution: {integrity: sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/netbsd-x64@0.25.12':
+    resolution: {integrity: sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/openbsd-arm64@0.25.12':
+    resolution: {integrity: sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/openbsd-x64@0.25.12':
+    resolution: {integrity: sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/openharmony-arm64@0.25.12':
+    resolution: {integrity: sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/sunos-x64@0.25.12':
+    resolution: {integrity: sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/win32-arm64@0.25.12':
+    resolution: {integrity: sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/win32-ia32@0.25.12':
+    resolution: {integrity: sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/win32-x64@0.25.12':
+    resolution: {integrity: sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/eslint-utils@4.9.0':
+    resolution: {integrity: sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==}
+    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.21.1':
+    resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/config-helpers@0.4.2':
+    resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/core@0.17.0':
+    resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/eslintrc@3.3.3':
+    resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/js@9.39.1':
+    resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/object-schema@2.1.7':
+    resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/plugin-kit@0.4.1':
+    resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@humanfs/core@0.19.1':
+    resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/node@0.16.7':
+    resolution: {integrity: sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==}
+    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==}
+
+  '@phosphor-icons/react@2.1.8':
+    resolution: {integrity: sha512-RxJlAkErO+t50DsY82ga9RGOULK6Jux0MdmXqvDjtOzG3PYQFz6rjdUU2q06lPMMbJTT+d+qurKYmF7i2Uv74A==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      react: '>= 16.8'
+      react-dom: '>= 16.8'
+
+  '@popperjs/core@2.11.8':
+    resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==}
+
+  '@radix-ui/primitive@1.1.3':
+    resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
+  '@radix-ui/react-collection@1.1.7':
+    resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-compose-refs@1.1.2':
+    resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-context@1.1.2':
+    resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-direction@1.1.1':
+    resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-id@1.1.1':
+    resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-presence@1.1.5':
+    resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-primitive@2.1.3':
+    resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-roving-focus@1.1.11':
+    resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-slot@1.2.3':
+    resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-tabs@1.1.13':
+    resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+
+  '@radix-ui/react-use-callback-ref@1.1.1':
+    resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-controllable-state@1.2.2':
+    resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-effect-event@0.0.2':
+    resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/react-use-layout-effect@1.1.1':
+    resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@rolldown/pluginutils@1.0.0-beta.47':
+    resolution: {integrity: sha512-8QagwMH3kNCuzD8EWL8R2YPW5e4OrHNSAHRFDdmFqEwEaD/KcNKjVoumo+gP2vW5eKB2UPbM6vTYiGZX0ixLnw==}
+
+  '@rollup/rollup-android-arm-eabi@4.53.3':
+    resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
+    cpu: [arm]
+    os: [android]
+
+  '@rollup/rollup-android-arm64@4.53.3':
+    resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==}
+    cpu: [arm64]
+    os: [android]
+
+  '@rollup/rollup-darwin-arm64@4.53.3':
+    resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@rollup/rollup-darwin-x64@4.53.3':
+    resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@rollup/rollup-freebsd-arm64@4.53.3':
+    resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@rollup/rollup-freebsd-x64@4.53.3':
+    resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.53.3':
+    resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
+    cpu: [arm]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm-musleabihf@4.53.3':
+    resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
+    cpu: [arm]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-arm64-gnu@4.53.3':
+    resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-arm64-musl@4.53.3':
+    resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
+    cpu: [arm64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-loong64-gnu@4.53.3':
+    resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
+    cpu: [loong64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-ppc64-gnu@4.53.3':
+    resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
+    cpu: [ppc64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-riscv64-gnu@4.53.3':
+    resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-riscv64-musl@4.53.3':
+    resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
+    cpu: [riscv64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-linux-s390x-gnu@4.53.3':
+    resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
+    cpu: [s390x]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-gnu@4.53.3':
+    resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
+    cpu: [x64]
+    os: [linux]
+    libc: [glibc]
+
+  '@rollup/rollup-linux-x64-musl@4.53.3':
+    resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
+    cpu: [x64]
+    os: [linux]
+    libc: [musl]
+
+  '@rollup/rollup-openharmony-arm64@4.53.3':
+    resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@rollup/rollup-win32-arm64-msvc@4.53.3':
+    resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@rollup/rollup-win32-ia32-msvc@4.53.3':
+    resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-gnu@4.53.3':
+    resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==}
+    cpu: [x64]
+    os: [win32]
+
+  '@rollup/rollup-win32-x64-msvc@4.53.3':
+    resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==}
+    cpu: [x64]
+    os: [win32]
+
+  '@types/babel__core@7.20.5':
+    resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
+
+  '@types/babel__generator@7.27.0':
+    resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==}
+
+  '@types/babel__template@7.4.4':
+    resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==}
+
+  '@types/babel__traverse@7.28.0':
+    resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==}
+
+  '@types/crypto-js@4.2.2':
+    resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
+
+  '@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/node@24.10.1':
+    resolution: {integrity: sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==}
+
+  '@types/parse-json@4.0.2':
+    resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+
+  '@types/react-dom@19.2.3':
+    resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==}
+    peerDependencies:
+      '@types/react': ^19.2.0
+
+  '@types/react-modal@3.16.3':
+    resolution: {integrity: sha512-xXuGavyEGaFQDgBv4UVm8/ZsG+qxeQ7f77yNrW3n+1J6XAstUy5rYHeIHPh1KzsGc6IkCIdu6lQ2xWzu1jBTLg==}
+
+  '@types/react@19.2.7':
+    resolution: {integrity: sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==}
+
+  '@typescript-eslint/eslint-plugin@8.48.0':
+    resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^8.48.0
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/parser@8.48.0':
+    resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/project-service@8.48.0':
+    resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/scope-manager@8.48.0':
+    resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/tsconfig-utils@8.48.0':
+    resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/type-utils@8.48.0':
+    resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/types@8.48.0':
+    resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/typescript-estree@8.48.0':
+    resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/utils@8.48.0':
+    resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/visitor-keys@8.48.0':
+    resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@vitejs/plugin-react@5.1.1':
+    resolution: {integrity: sha512-WQfkSw0QbQ5aJ2CHYw23ZGkqnRwqKHD/KYsMeTkZzPT4Jcf0DcBxBtwMJxnu6E7oxw5+JC6ZAiePgh28uJ1HBA==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    peerDependencies:
+      vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
+
+  acorn-jsx@5.3.2:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  acorn@8.15.0:
+    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  ajv@6.12.6:
+    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+  ansi-styles@4.3.0:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  argparse@2.0.1:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  asynckit@0.4.0:
+    resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
+
+  axios@1.13.2:
+    resolution: {integrity: sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==}
+
+  babel-plugin-emotion@10.2.2:
+    resolution: {integrity: sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==}
+
+  babel-plugin-macros@2.8.0:
+    resolution: {integrity: sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==}
+
+  babel-plugin-macros@3.1.0:
+    resolution: {integrity: sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==}
+    engines: {node: '>=10', npm: '>=6'}
+
+  babel-plugin-syntax-jsx@6.18.0:
+    resolution: {integrity: sha512-qrPaCSo9c8RHNRHIotaufGbuOBN8rtdC4QrrFFc43vyWCCz7Kl7GL1PGaXtMGQZUXrkCjNEgxDfmAuAabr/rlw==}
+
+  balanced-match@1.0.2:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  baseline-browser-mapping@2.8.32:
+    resolution: {integrity: sha512-OPz5aBThlyLFgxyhdwf/s2+8ab3OvT7AdTNvKHBwpXomIYeXqpUUuT8LrdtxZSsWJ4R4CU1un4XGh5Ez3nlTpw==}
+    hasBin: true
+
+  brace-expansion@1.1.12:
+    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+  brace-expansion@2.0.2:
+    resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+  browserslist@4.28.0:
+    resolution: {integrity: sha512-tbydkR/CxfMwelN0vwdP/pLkDwyAASZ+VfWm4EOwlB6SWhx1sYnWLqo8N5j0rAzPfzfRaxt0mM/4wPU/Su84RQ==}
+    engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
+    hasBin: true
+
+  call-bind-apply-helpers@1.0.2:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  callsites@3.1.0:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+    engines: {node: '>=6'}
+
+  caniuse-lite@1.0.30001757:
+    resolution: {integrity: sha512-r0nnL/I28Zi/yjk1el6ilj27tKcdjLsNqAOZr0yVjWPrSQyHgKI2INaEWw21bAQSv2LXRt1XuCS/GomNpWOxsQ==}
+
+  chalk@4.1.2:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  color-convert@2.0.1:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  color-name@1.1.4:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  combined-stream@1.0.8:
+    resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
+    engines: {node: '>= 0.8'}
+
+  compute-scroll-into-view@1.0.20:
+    resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==}
+
+  concat-map@0.0.1:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  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@6.0.0:
+    resolution: {integrity: sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==}
+    engines: {node: '>=8'}
+
+  cosmiconfig@7.1.0:
+    resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
+    engines: {node: '>=10'}
+
+  create-emotion@10.0.27:
+    resolution: {integrity: sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==}
+
+  cross-spawn@7.0.6:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
+  crypto-js@4.2.0:
+    resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
+
+  csstype@2.6.21:
+    resolution: {integrity: sha512-Z1PhmomIfypOpoMjRQB70jfvy/wxT50qW08YXO5lMIJkrdq4yOTR+AW7FqutScmB9NkLwxo+jU+kZLbofZZq/w==}
+
+  csstype@3.2.3:
+    resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==}
+
+  date-fns@2.30.0:
+    resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==}
+    engines: {node: '>=0.11'}
+
+  dayjs@1.11.19:
+    resolution: {integrity: sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==}
+
+  debug@4.4.3:
+    resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  deep-is@0.1.4:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+  delayed-stream@1.0.0:
+    resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
+    engines: {node: '>=0.4.0'}
+
+  detect-node-es@1.1.0:
+    resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
+  downshift@6.1.12:
+    resolution: {integrity: sha512-7XB/iaSJVS4T8wGFT3WRXmSF1UlBHAA40DshZtkrIscIN+VC+Lh363skLxFTvJwtNgHxAMDGEHT4xsyQFWL+UA==}
+    peerDependencies:
+      react: '>=16.12.0'
+
+  dunder-proto@1.0.1:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  electron-to-chromium@1.5.262:
+    resolution: {integrity: sha512-NlAsMteRHek05jRUxUR0a5jpjYq9ykk6+kO0yRaMi5moe7u0fVIOeQ3Y30A8dIiWFBNUoQGi1ljb1i5VtS9WQQ==}
+
+  emotion@10.0.27:
+    resolution: {integrity: sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==}
+
+  error-ex@1.3.4:
+    resolution: {integrity: sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==}
+
+  es-define-property@1.0.1:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  es-errors@1.3.0:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  es-object-atoms@1.1.1:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  es-set-tostringtag@2.1.0:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  esbuild@0.25.12:
+    resolution: {integrity: sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  escalade@3.2.0:
+    resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==}
+    engines: {node: '>=6'}
+
+  escape-string-regexp@1.0.5:
+    resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==}
+    engines: {node: '>=0.8.0'}
+
+  escape-string-regexp@4.0.0:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  eslint-plugin-react-hooks@7.0.1:
+    resolution: {integrity: sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==}
+    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
+
+  eslint-plugin-react-refresh@0.4.24:
+    resolution: {integrity: sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==}
+    peerDependencies:
+      eslint: '>=8.40'
+
+  eslint-scope@8.4.0:
+    resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  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@4.2.1:
+    resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  eslint@9.39.1:
+    resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    hasBin: true
+    peerDependencies:
+      jiti: '*'
+    peerDependenciesMeta:
+      jiti:
+        optional: true
+
+  espree@10.4.0:
+    resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  esquery@1.6.0:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+    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'}
+
+  esutils@2.0.3:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  exenv@1.2.2:
+    resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==}
+
+  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.3.3:
+    resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+  focus-lock@1.3.6:
+    resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==}
+    engines: {node: '>=10'}
+
+  follow-redirects@1.15.11:
+    resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+
+  form-data@4.0.5:
+    resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==}
+    engines: {node: '>= 6'}
+
+  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'}
+
+  get-intrinsic@1.3.0:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  get-proto@1.0.1:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
+  glob-parent@6.0.2:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  globals@14.0.0:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  globals@16.5.0:
+    resolution: {integrity: sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==}
+    engines: {node: '>=18'}
+
+  gopd@1.2.0:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  graphemer@1.4.0:
+    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+  has-flag@4.0.0:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  has-symbols@1.1.0:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  has-tostringtag@1.0.2:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
+  hasown@2.0.2:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    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==}
+
+  ignore@5.3.2:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  ignore@7.0.5:
+    resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+    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'}
+
+  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'}
+
+  isexe@2.0.0:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  js-tokens@4.0.0:
+    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+  js-yaml@4.1.1:
+    resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==}
+    hasBin: 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'}
+
+  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'}
+
+  lodash.merge@4.6.2:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  loose-envify@1.4.0:
+    resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+    hasBin: true
+
+  lru-cache@5.1.1:
+    resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+
+  math-intrinsics@1.1.0:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  mime-db@1.52.0:
+    resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==}
+    engines: {node: '>= 0.6'}
+
+  mime-types@2.1.35:
+    resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==}
+    engines: {node: '>= 0.6'}
+
+  minimatch@3.1.2:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+  minimatch@9.0.5:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  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.27:
+    resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
+
+  object-assign@4.1.1:
+    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+    engines: {node: '>=0.10.0'}
+
+  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'}
+
+  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'}
+
+  picocolors@1.1.1:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  picomatch@4.0.3:
+    resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+    engines: {node: '>=12'}
+
+  postcss@8.5.6:
+    resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  prelude-ls@1.2.1:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  prop-types@15.8.1:
+    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+  proxy-from-env@1.1.0:
+    resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
+
+  punycode@2.3.1:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  react-animate-height@3.2.3:
+    resolution: {integrity: sha512-R6DSvr7ud07oeCixScyvXWEMJY/Mt2+GyOWC1KMaRc69gOBw+SsCg4TJmrp4rKUM1hyd6p+YKw90brjPH93Y2A==}
+    engines: {node: '>= 12.0.0'}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+
+  react-clientside-effect@1.2.8:
+    resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+  react-day-picker@8.10.1:
+    resolution: {integrity: sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==}
+    peerDependencies:
+      date-fns: ^2.28.0 || ^3.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+
+  react-dom@19.2.0:
+    resolution: {integrity: sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==}
+    peerDependencies:
+      react: ^19.2.0
+
+  react-fast-compare@3.2.2:
+    resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==}
+
+  react-focus-lock@2.13.7:
+    resolution: {integrity: sha512-20lpZHEQrXPb+pp1tzd4ULL6DyO5D2KnR0G69tTDdydrmNhU7pdFmbQUYVyHUgp+xN29IuFR0PVuhOmvaZL9Og==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  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-lifecycles-compat@3.0.4:
+    resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==}
+
+  react-modal@3.16.3:
+    resolution: {integrity: sha512-yCYRJB5YkeQDQlTt17WGAgFJ7jr2QYcWa1SHqZ3PluDmnKJ/7+tVU+E6uKyZ0nODaeEj+xCpK4LcSnKXLMC0Nw==}
+    peerDependencies:
+      react: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19
+      react-dom: ^0.14.0 || ^15.0.0 || ^16 || ^17 || ^18 || ^19
+
+  react-popper@2.3.0:
+    resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==}
+    peerDependencies:
+      '@popperjs/core': ^2.0.0
+      react: ^16.8.0 || ^17 || ^18
+      react-dom: ^16.8.0 || ^17 || ^18
+
+  react-refresh@0.18.0:
+    resolution: {integrity: sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==}
+    engines: {node: '>=0.10.0'}
+
+  react-router-dom@7.9.6:
+    resolution: {integrity: sha512-2MkC2XSXq6HjGcihnx1s0DBWQETI4mlis4Ux7YTLvP67xnGxCvq+BcCQSO81qQHVUTM1V53tl4iVVaY5sReCOA==}
+    engines: {node: '>=20.0.0'}
+    peerDependencies:
+      react: '>=18'
+      react-dom: '>=18'
+
+  react-router@7.9.6:
+    resolution: {integrity: sha512-Y1tUp8clYRXpfPITyuifmSoE2vncSME18uVLgaqyxh9H35JWpIfzHo+9y3Fzh5odk/jxPW29IgLgzcdwxGqyNA==}
+    engines: {node: '>=20.0.0'}
+    peerDependencies:
+      react: '>=18'
+      react-dom: '>=18'
+    peerDependenciesMeta:
+      react-dom:
+        optional: true
+
+  react@19.2.0:
+    resolution: {integrity: sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==}
+    engines: {node: '>=0.10.0'}
+
+  resolve-from@4.0.0:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  resolve@1.22.11:
+    resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==}
+    engines: {node: '>= 0.4'}
+    hasBin: true
+
+  rollup@4.53.3:
+    resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
+    engines: {node: '>=18.0.0', npm: '>=8.0.0'}
+    hasBin: true
+
+  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
+
+  semver@7.7.3:
+    resolution: {integrity: sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==}
+    engines: {node: '>=10'}
+    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'}
+
+  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'}
+
+  strip-json-comments@3.1.1:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  stylis@4.2.0:
+    resolution: {integrity: sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==}
+
+  supports-color@7.2.0:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  supports-preserve-symlinks-flag@1.0.0:
+    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+    engines: {node: '>= 0.4'}
+
+  tinyglobby@0.2.15:
+    resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==}
+    engines: {node: '>=12.0.0'}
+
+  truncate@3.0.0:
+    resolution: {integrity: sha512-C+0Xojw7wZPl6MDq5UjMTuxZvBPK04mtdFet7k+GSZPINcvLZFCXg+15kWIL4wAqDB7CksIsKiRLbQ1wa7rKdw==}
+
+  ts-api-utils@2.1.0:
+    resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
+  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'}
+
+  typescript-eslint@8.48.0:
+    resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  typescript@5.9.3:
+    resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  undici-types@7.16.0:
+    resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==}
+
+  update-browserslist-db@1.1.4:
+    resolution: {integrity: sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==}
+    hasBin: true
+    peerDependencies:
+      browserslist: '>= 4.21.0'
+
+  uri-js@4.4.1:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  use-callback-ref@1.3.3:
+    resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  use-sidecar@1.1.3:
+    resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  vite@7.2.6:
+    resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==}
+    engines: {node: ^20.19.0 || >=22.12.0}
+    hasBin: true
+    peerDependencies:
+      '@types/node': ^20.19.0 || >=22.12.0
+      jiti: '>=1.21.0'
+      less: ^4.0.0
+      lightningcss: ^1.21.0
+      sass: ^1.70.0
+      sass-embedded: ^1.70.0
+      stylus: '>=0.54.8'
+      sugarss: ^5.0.0
+      terser: ^5.16.0
+      tsx: ^4.8.1
+      yaml: ^2.4.2
+    peerDependenciesMeta:
+      '@types/node':
+        optional: true
+      jiti:
+        optional: true
+      less:
+        optional: true
+      lightningcss:
+        optional: true
+      sass:
+        optional: true
+      sass-embedded:
+        optional: true
+      stylus:
+        optional: true
+      sugarss:
+        optional: true
+      terser:
+        optional: true
+      tsx:
+        optional: true
+      yaml:
+        optional: true
+
+  warning@4.0.3:
+    resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==}
+
+  which@2.0.2:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  word-wrap@1.2.5:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
+  yallist@3.1.1:
+    resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
+
+  yaml@1.10.2:
+    resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==}
+    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.1.13:
+    resolution: {integrity: sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==}
+
+snapshots:
+
+  '@babel/code-frame@7.27.1':
+    dependencies:
+      '@babel/helper-validator-identifier': 7.28.5
+      js-tokens: 4.0.0
+      picocolors: 1.1.1
+
+  '@babel/compat-data@7.28.5': {}
+
+  '@babel/core@7.28.5':
+    dependencies:
+      '@babel/code-frame': 7.27.1
+      '@babel/generator': 7.28.5
+      '@babel/helper-compilation-targets': 7.27.2
+      '@babel/helper-module-transforms': 7.28.3(@babel/core@7.28.5)
+      '@babel/helpers': 7.28.4
+      '@babel/parser': 7.28.5
+      '@babel/template': 7.27.2
+      '@babel/traverse': 7.28.5
+      '@babel/types': 7.28.5
+      '@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.28.5':
+    dependencies:
+      '@babel/parser': 7.28.5
+      '@babel/types': 7.28.5
+      '@jridgewell/gen-mapping': 0.3.13
+      '@jridgewell/trace-mapping': 0.3.31
+      jsesc: 3.1.0
+
+  '@babel/helper-compilation-targets@7.27.2':
+    dependencies:
+      '@babel/compat-data': 7.28.5
+      '@babel/helper-validator-option': 7.27.1
+      browserslist: 4.28.0
+      lru-cache: 5.1.1
+      semver: 6.3.1
+
+  '@babel/helper-globals@7.28.0': {}
+
+  '@babel/helper-module-imports@7.27.1':
+    dependencies:
+      '@babel/traverse': 7.28.5
+      '@babel/types': 7.28.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-module-transforms@7.28.3(@babel/core@7.28.5)':
+    dependencies:
+      '@babel/core': 7.28.5
+      '@babel/helper-module-imports': 7.27.1
+      '@babel/helper-validator-identifier': 7.28.5
+      '@babel/traverse': 7.28.5
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/helper-plugin-utils@7.27.1': {}
+
+  '@babel/helper-string-parser@7.27.1': {}
+
+  '@babel/helper-validator-identifier@7.28.5': {}
+
+  '@babel/helper-validator-option@7.27.1': {}
+
+  '@babel/helpers@7.28.4':
+    dependencies:
+      '@babel/template': 7.27.2
+      '@babel/types': 7.28.5
+
+  '@babel/parser@7.28.5':
+    dependencies:
+      '@babel/types': 7.28.5
+
+  '@babel/plugin-transform-react-jsx-self@7.27.1(@babel/core@7.28.5)':
+    dependencies:
+      '@babel/core': 7.28.5
+      '@babel/helper-plugin-utils': 7.27.1
+
+  '@babel/plugin-transform-react-jsx-source@7.27.1(@babel/core@7.28.5)':
+    dependencies:
+      '@babel/core': 7.28.5
+      '@babel/helper-plugin-utils': 7.27.1
+
+  '@babel/runtime@7.28.4': {}
+
+  '@babel/template@7.27.2':
+    dependencies:
+      '@babel/code-frame': 7.27.1
+      '@babel/parser': 7.28.5
+      '@babel/types': 7.28.5
+
+  '@babel/traverse@7.28.5':
+    dependencies:
+      '@babel/code-frame': 7.27.1
+      '@babel/generator': 7.28.5
+      '@babel/helper-globals': 7.28.0
+      '@babel/parser': 7.28.5
+      '@babel/template': 7.27.2
+      '@babel/types': 7.28.5
+      debug: 4.4.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@babel/types@7.28.5':
+    dependencies:
+      '@babel/helper-string-parser': 7.27.1
+      '@babel/helper-validator-identifier': 7.28.5
+
+  '@contentful/f36-accordion@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-collapse': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-asset@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icon': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-autocomplete@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-forms': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-popover': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-skeleton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      downshift: 6.1.12(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-avatar@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-image': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-menu': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-badge@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-button@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-spinner': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-card@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-asset': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-badge': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-drag-handle': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icon': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-menu': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-skeleton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      truncate: 3.0.0
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-collapse@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-components@5.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-accordion': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-asset': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-autocomplete': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-avatar': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-badge': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-card': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-collapse': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-copybutton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-datepicker': 5.9.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-datetime': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-drag-handle': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-empty-state': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-entity-list': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-forms': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-header': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icon': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-image': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-layout': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-list': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-menu': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-modal': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-multiselect': 5.9.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-navlist': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-note': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-notification': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-pagination': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-pill': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-popover': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-progress-stepper': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-skeleton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-spinner': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-table': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tabs': 5.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-text-link': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-usage-card': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-usage-count': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+      '@types/react-dom': 19.2.3(@types/react@19.2.7)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-copybutton@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-core@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-tokens': 5.1.0
+      '@emotion/core': 10.3.1(react@19.2.0)
+      '@emotion/is-prop-valid': 1.4.0
+      csstype: 3.2.3
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-datepicker@5.9.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-forms': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-popover': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      date-fns: 2.30.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-day-picker: 8.10.1(date-fns@2.30.0)(react@19.2.0)
+      react-dom: 19.2.0(react@19.2.0)
+      react-focus-lock: 2.13.7(@types/react@19.2.7)(react@19.2.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - supports-color
+
+  '@contentful/f36-datetime@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      dayjs: 1.11.19
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-drag-handle@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-empty-state@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-entity-list@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-badge': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-drag-handle': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icon': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-menu': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-skeleton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-forms@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-header@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-icon@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@phosphor-icons/react': 2.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-icons@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icon': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@phosphor-icons/react': 2.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-image@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-skeleton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-layout@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-header': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-list@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-menu@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-popover': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-modal@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@types/react-modal': 3.16.3
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-modal: 3.16.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-multiselect@5.9.0(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@babel/runtime': 7.28.4
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-forms': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-popover': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-skeleton': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-focus-lock: 2.13.7(@types/react@19.2.7)(react@19.2.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - supports-color
+
+  '@contentful/f36-navlist@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-note@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icon': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-notification@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-text-link': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-animate-height: 3.2.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-pagination@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-forms': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-pill@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-drag-handle': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-popover@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@popperjs/core': 2.11.8
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-progress-stepper@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-button': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-skeleton@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-table': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-spinner@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-table@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-tabs@5.9.0(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - '@types/react'
+      - '@types/react-dom'
+      - supports-color
+
+  '@contentful/f36-text-link@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-tokens@5.1.0': {}
+
+  '@contentful/f36-tooltip@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@popperjs/core': 2.11.8
+      csstype: 3.2.3
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-typography@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-utils': 5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-usage-card@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-card': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-icons': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-text-link': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-tooltip': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-usage-count@5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-core': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@contentful/f36-tokens': 5.1.0
+      '@contentful/f36-typography': 5.9.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@contentful/f36-utils@5.2.0(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@contentful/f36-tokens': 5.1.0
+      emotion: 10.0.27
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    transitivePeerDependencies:
+      - supports-color
+
+  '@emotion/babel-plugin@11.13.5':
+    dependencies:
+      '@babel/helper-module-imports': 7.27.1
+      '@babel/runtime': 7.28.4
+      '@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@10.0.29':
+    dependencies:
+      '@emotion/sheet': 0.9.4
+      '@emotion/stylis': 0.8.5
+      '@emotion/utils': 0.11.3
+      '@emotion/weak-memoize': 0.2.5
+
+  '@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/core@10.3.1(react@19.2.0)':
+    dependencies:
+      '@babel/runtime': 7.28.4
+      '@emotion/cache': 10.0.29
+      '@emotion/css': 10.0.27
+      '@emotion/serialize': 0.11.16
+      '@emotion/sheet': 0.9.4
+      '@emotion/utils': 0.11.3
+      react: 19.2.0
+    transitivePeerDependencies:
+      - supports-color
+
+  '@emotion/css@10.0.27':
+    dependencies:
+      '@emotion/serialize': 0.11.16
+      '@emotion/utils': 0.11.3
+      babel-plugin-emotion: 10.2.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@emotion/hash@0.8.0': {}
+
+  '@emotion/hash@0.9.2': {}
+
+  '@emotion/is-prop-valid@1.4.0':
+    dependencies:
+      '@emotion/memoize': 0.9.0
+
+  '@emotion/memoize@0.7.4': {}
+
+  '@emotion/memoize@0.9.0': {}
+
+  '@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      '@babel/runtime': 7.28.4
+      '@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.0)
+      '@emotion/utils': 1.4.2
+      '@emotion/weak-memoize': 0.4.0
+      hoist-non-react-statics: 3.3.2
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+    transitivePeerDependencies:
+      - supports-color
+
+  '@emotion/serialize@0.11.16':
+    dependencies:
+      '@emotion/hash': 0.8.0
+      '@emotion/memoize': 0.7.4
+      '@emotion/unitless': 0.7.5
+      '@emotion/utils': 0.11.3
+      csstype: 2.6.21
+
+  '@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@0.9.4': {}
+
+  '@emotion/sheet@1.4.0': {}
+
+  '@emotion/styled@11.14.1(@emotion/react@11.14.0(@types/react@19.2.7)(react@19.2.0))(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      '@babel/runtime': 7.28.4
+      '@emotion/babel-plugin': 11.13.5
+      '@emotion/is-prop-valid': 1.4.0
+      '@emotion/react': 11.14.0(@types/react@19.2.7)(react@19.2.0)
+      '@emotion/serialize': 1.3.3
+      '@emotion/use-insertion-effect-with-fallbacks': 1.2.0(react@19.2.0)
+      '@emotion/utils': 1.4.2
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+    transitivePeerDependencies:
+      - supports-color
+
+  '@emotion/stylis@0.8.5': {}
+
+  '@emotion/unitless@0.10.0': {}
+
+  '@emotion/unitless@0.7.5': {}
+
+  '@emotion/use-insertion-effect-with-fallbacks@1.2.0(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+
+  '@emotion/utils@0.11.3': {}
+
+  '@emotion/utils@1.4.2': {}
+
+  '@emotion/weak-memoize@0.2.5': {}
+
+  '@emotion/weak-memoize@0.4.0': {}
+
+  '@esbuild/aix-ppc64@0.25.12':
+    optional: true
+
+  '@esbuild/android-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/android-arm@0.25.12':
+    optional: true
+
+  '@esbuild/android-x64@0.25.12':
+    optional: true
+
+  '@esbuild/darwin-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/darwin-x64@0.25.12':
+    optional: true
+
+  '@esbuild/freebsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/freebsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-arm@0.25.12':
+    optional: true
+
+  '@esbuild/linux-ia32@0.25.12':
+    optional: true
+
+  '@esbuild/linux-loong64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-mips64el@0.25.12':
+    optional: true
+
+  '@esbuild/linux-ppc64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-riscv64@0.25.12':
+    optional: true
+
+  '@esbuild/linux-s390x@0.25.12':
+    optional: true
+
+  '@esbuild/linux-x64@0.25.12':
+    optional: true
+
+  '@esbuild/netbsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/netbsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/openbsd-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/openbsd-x64@0.25.12':
+    optional: true
+
+  '@esbuild/openharmony-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/sunos-x64@0.25.12':
+    optional: true
+
+  '@esbuild/win32-arm64@0.25.12':
+    optional: true
+
+  '@esbuild/win32-ia32@0.25.12':
+    optional: true
+
+  '@esbuild/win32-x64@0.25.12':
+    optional: true
+
+  '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1)':
+    dependencies:
+      eslint: 9.39.1
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/regexpp@4.12.2': {}
+
+  '@eslint/config-array@0.21.1':
+    dependencies:
+      '@eslint/object-schema': 2.1.7
+      debug: 4.4.3
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/config-helpers@0.4.2':
+    dependencies:
+      '@eslint/core': 0.17.0
+
+  '@eslint/core@0.17.0':
+    dependencies:
+      '@types/json-schema': 7.0.15
+
+  '@eslint/eslintrc@3.3.3':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.4.3
+      espree: 10.4.0
+      globals: 14.0.0
+      ignore: 5.3.2
+      import-fresh: 3.3.1
+      js-yaml: 4.1.1
+      minimatch: 3.1.2
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/js@9.39.1': {}
+
+  '@eslint/object-schema@2.1.7': {}
+
+  '@eslint/plugin-kit@0.4.1':
+    dependencies:
+      '@eslint/core': 0.17.0
+      levn: 0.4.1
+
+  '@humanfs/core@0.19.1': {}
+
+  '@humanfs/node@0.16.7':
+    dependencies:
+      '@humanfs/core': 0.19.1
+      '@humanwhocodes/retry': 0.4.3
+
+  '@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
+
+  '@phosphor-icons/react@2.1.8(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+
+  '@popperjs/core@2.11.8': {}
+
+  '@radix-ui/primitive@1.1.3': {}
+
+  '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+      '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+  '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-context@1.1.2(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-direction@1.1.1(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-id@1.1.1(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+      '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+  '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-slot': 1.2.3(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+      '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+  '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+      '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+  '@radix-ui/react-slot@1.2.3(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-context': 1.1.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-direction': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-id': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.7))(@types/react@19.2.7)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+      '@types/react-dom': 19.2.3(@types/react@19.2.7)
+
+  '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.7)(react@19.2.0)
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.7)(react@19.2.0)
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.7)(react@19.2.0)':
+    dependencies:
+      react: 19.2.0
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  '@rolldown/pluginutils@1.0.0-beta.47': {}
+
+  '@rollup/rollup-android-arm-eabi@4.53.3':
+    optional: true
+
+  '@rollup/rollup-android-arm64@4.53.3':
+    optional: true
+
+  '@rollup/rollup-darwin-arm64@4.53.3':
+    optional: true
+
+  '@rollup/rollup-darwin-x64@4.53.3':
+    optional: true
+
+  '@rollup/rollup-freebsd-arm64@4.53.3':
+    optional: true
+
+  '@rollup/rollup-freebsd-x64@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-arm-gnueabihf@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-arm-musleabihf@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-arm64-musl@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-loong64-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-ppc64-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-riscv64-musl@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-s390x-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-x64-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-linux-x64-musl@4.53.3':
+    optional: true
+
+  '@rollup/rollup-openharmony-arm64@4.53.3':
+    optional: true
+
+  '@rollup/rollup-win32-arm64-msvc@4.53.3':
+    optional: true
+
+  '@rollup/rollup-win32-ia32-msvc@4.53.3':
+    optional: true
+
+  '@rollup/rollup-win32-x64-gnu@4.53.3':
+    optional: true
+
+  '@rollup/rollup-win32-x64-msvc@4.53.3':
+    optional: true
+
+  '@types/babel__core@7.20.5':
+    dependencies:
+      '@babel/parser': 7.28.5
+      '@babel/types': 7.28.5
+      '@types/babel__generator': 7.27.0
+      '@types/babel__template': 7.4.4
+      '@types/babel__traverse': 7.28.0
+
+  '@types/babel__generator@7.27.0':
+    dependencies:
+      '@babel/types': 7.28.5
+
+  '@types/babel__template@7.4.4':
+    dependencies:
+      '@babel/parser': 7.28.5
+      '@babel/types': 7.28.5
+
+  '@types/babel__traverse@7.28.0':
+    dependencies:
+      '@babel/types': 7.28.5
+
+  '@types/crypto-js@4.2.2': {}
+
+  '@types/estree@1.0.8': {}
+
+  '@types/json-schema@7.0.15': {}
+
+  '@types/node@24.10.1':
+    dependencies:
+      undici-types: 7.16.0
+
+  '@types/parse-json@4.0.2': {}
+
+  '@types/react-dom@19.2.3(@types/react@19.2.7)':
+    dependencies:
+      '@types/react': 19.2.7
+
+  '@types/react-modal@3.16.3':
+    dependencies:
+      '@types/react': 19.2.7
+
+  '@types/react@19.2.7':
+    dependencies:
+      csstype: 3.2.3
+
+  '@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.2
+      '@typescript-eslint/parser': 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      '@typescript-eslint/scope-manager': 8.48.0
+      '@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.48.0
+      eslint: 9.39.1
+      graphemer: 1.4.0
+      ignore: 7.0.5
+      natural-compare: 1.4.0
+      ts-api-utils: 2.1.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.48.0
+      '@typescript-eslint/types': 8.48.0
+      '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
+      '@typescript-eslint/visitor-keys': 8.48.0
+      debug: 4.4.3
+      eslint: 9.39.1
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/project-service@8.48.0(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
+      '@typescript-eslint/types': 8.48.0
+      debug: 4.4.3
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/scope-manager@8.48.0':
+    dependencies:
+      '@typescript-eslint/types': 8.48.0
+      '@typescript-eslint/visitor-keys': 8.48.0
+
+  '@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)':
+    dependencies:
+      typescript: 5.9.3
+
+  '@typescript-eslint/type-utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/types': 8.48.0
+      '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      debug: 4.4.3
+      eslint: 9.39.1
+      ts-api-utils: 2.1.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/types@8.48.0': {}
+
+  '@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)':
+    dependencies:
+      '@typescript-eslint/project-service': 8.48.0(typescript@5.9.3)
+      '@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
+      '@typescript-eslint/types': 8.48.0
+      '@typescript-eslint/visitor-keys': 8.48.0
+      debug: 4.4.3
+      minimatch: 9.0.5
+      semver: 7.7.3
+      tinyglobby: 0.2.15
+      ts-api-utils: 2.1.0(typescript@5.9.3)
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/utils@8.48.0(eslint@9.39.1)(typescript@5.9.3)':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
+      '@typescript-eslint/scope-manager': 8.48.0
+      '@typescript-eslint/types': 8.48.0
+      '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
+      eslint: 9.39.1
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/visitor-keys@8.48.0':
+    dependencies:
+      '@typescript-eslint/types': 8.48.0
+      eslint-visitor-keys: 4.2.1
+
+  '@vitejs/plugin-react@5.1.1(vite@7.2.6(@types/node@24.10.1))':
+    dependencies:
+      '@babel/core': 7.28.5
+      '@babel/plugin-transform-react-jsx-self': 7.27.1(@babel/core@7.28.5)
+      '@babel/plugin-transform-react-jsx-source': 7.27.1(@babel/core@7.28.5)
+      '@rolldown/pluginutils': 1.0.0-beta.47
+      '@types/babel__core': 7.20.5
+      react-refresh: 0.18.0
+      vite: 7.2.6(@types/node@24.10.1)
+    transitivePeerDependencies:
+      - supports-color
+
+  acorn-jsx@5.3.2(acorn@8.15.0):
+    dependencies:
+      acorn: 8.15.0
+
+  acorn@8.15.0: {}
+
+  ajv@6.12.6:
+    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-styles@4.3.0:
+    dependencies:
+      color-convert: 2.0.1
+
+  argparse@2.0.1: {}
+
+  asynckit@0.4.0: {}
+
+  axios@1.13.2:
+    dependencies:
+      follow-redirects: 1.15.11
+      form-data: 4.0.5
+      proxy-from-env: 1.1.0
+    transitivePeerDependencies:
+      - debug
+
+  babel-plugin-emotion@10.2.2:
+    dependencies:
+      '@babel/helper-module-imports': 7.27.1
+      '@emotion/hash': 0.8.0
+      '@emotion/memoize': 0.7.4
+      '@emotion/serialize': 0.11.16
+      babel-plugin-macros: 2.8.0
+      babel-plugin-syntax-jsx: 6.18.0
+      convert-source-map: 1.9.0
+      escape-string-regexp: 1.0.5
+      find-root: 1.1.0
+      source-map: 0.5.7
+    transitivePeerDependencies:
+      - supports-color
+
+  babel-plugin-macros@2.8.0:
+    dependencies:
+      '@babel/runtime': 7.28.4
+      cosmiconfig: 6.0.0
+      resolve: 1.22.11
+
+  babel-plugin-macros@3.1.0:
+    dependencies:
+      '@babel/runtime': 7.28.4
+      cosmiconfig: 7.1.0
+      resolve: 1.22.11
+
+  babel-plugin-syntax-jsx@6.18.0: {}
+
+  balanced-match@1.0.2: {}
+
+  baseline-browser-mapping@2.8.32: {}
+
+  brace-expansion@1.1.12:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  brace-expansion@2.0.2:
+    dependencies:
+      balanced-match: 1.0.2
+
+  browserslist@4.28.0:
+    dependencies:
+      baseline-browser-mapping: 2.8.32
+      caniuse-lite: 1.0.30001757
+      electron-to-chromium: 1.5.262
+      node-releases: 2.0.27
+      update-browserslist-db: 1.1.4(browserslist@4.28.0)
+
+  call-bind-apply-helpers@1.0.2:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  callsites@3.1.0: {}
+
+  caniuse-lite@1.0.30001757: {}
+
+  chalk@4.1.2:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  color-convert@2.0.1:
+    dependencies:
+      color-name: 1.1.4
+
+  color-name@1.1.4: {}
+
+  combined-stream@1.0.8:
+    dependencies:
+      delayed-stream: 1.0.0
+
+  compute-scroll-into-view@1.0.20: {}
+
+  concat-map@0.0.1: {}
+
+  convert-source-map@1.9.0: {}
+
+  convert-source-map@2.0.0: {}
+
+  cookie@1.1.1: {}
+
+  cosmiconfig@6.0.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.2
+
+  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.2
+
+  create-emotion@10.0.27:
+    dependencies:
+      '@emotion/cache': 10.0.29
+      '@emotion/serialize': 0.11.16
+      '@emotion/sheet': 0.9.4
+      '@emotion/utils': 0.11.3
+
+  cross-spawn@7.0.6:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  crypto-js@4.2.0: {}
+
+  csstype@2.6.21: {}
+
+  csstype@3.2.3: {}
+
+  date-fns@2.30.0:
+    dependencies:
+      '@babel/runtime': 7.28.4
+
+  dayjs@1.11.19: {}
+
+  debug@4.4.3:
+    dependencies:
+      ms: 2.1.3
+
+  deep-is@0.1.4: {}
+
+  delayed-stream@1.0.0: {}
+
+  detect-node-es@1.1.0: {}
+
+  downshift@6.1.12(react@19.2.0):
+    dependencies:
+      '@babel/runtime': 7.28.4
+      compute-scroll-into-view: 1.0.20
+      prop-types: 15.8.1
+      react: 19.2.0
+      react-is: 17.0.2
+      tslib: 2.8.1
+
+  dunder-proto@1.0.1:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  electron-to-chromium@1.5.262: {}
+
+  emotion@10.0.27:
+    dependencies:
+      babel-plugin-emotion: 10.2.2
+      create-emotion: 10.0.27
+    transitivePeerDependencies:
+      - supports-color
+
+  error-ex@1.3.4:
+    dependencies:
+      is-arrayish: 0.2.1
+
+  es-define-property@1.0.1: {}
+
+  es-errors@1.3.0: {}
+
+  es-object-atoms@1.1.1:
+    dependencies:
+      es-errors: 1.3.0
+
+  es-set-tostringtag@2.1.0:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  esbuild@0.25.12:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.25.12
+      '@esbuild/android-arm': 0.25.12
+      '@esbuild/android-arm64': 0.25.12
+      '@esbuild/android-x64': 0.25.12
+      '@esbuild/darwin-arm64': 0.25.12
+      '@esbuild/darwin-x64': 0.25.12
+      '@esbuild/freebsd-arm64': 0.25.12
+      '@esbuild/freebsd-x64': 0.25.12
+      '@esbuild/linux-arm': 0.25.12
+      '@esbuild/linux-arm64': 0.25.12
+      '@esbuild/linux-ia32': 0.25.12
+      '@esbuild/linux-loong64': 0.25.12
+      '@esbuild/linux-mips64el': 0.25.12
+      '@esbuild/linux-ppc64': 0.25.12
+      '@esbuild/linux-riscv64': 0.25.12
+      '@esbuild/linux-s390x': 0.25.12
+      '@esbuild/linux-x64': 0.25.12
+      '@esbuild/netbsd-arm64': 0.25.12
+      '@esbuild/netbsd-x64': 0.25.12
+      '@esbuild/openbsd-arm64': 0.25.12
+      '@esbuild/openbsd-x64': 0.25.12
+      '@esbuild/openharmony-arm64': 0.25.12
+      '@esbuild/sunos-x64': 0.25.12
+      '@esbuild/win32-arm64': 0.25.12
+      '@esbuild/win32-ia32': 0.25.12
+      '@esbuild/win32-x64': 0.25.12
+
+  escalade@3.2.0: {}
+
+  escape-string-regexp@1.0.5: {}
+
+  escape-string-regexp@4.0.0: {}
+
+  eslint-plugin-react-hooks@7.0.1(eslint@9.39.1):
+    dependencies:
+      '@babel/core': 7.28.5
+      '@babel/parser': 7.28.5
+      eslint: 9.39.1
+      hermes-parser: 0.25.1
+      zod: 4.1.13
+      zod-validation-error: 4.0.2(zod@4.1.13)
+    transitivePeerDependencies:
+      - supports-color
+
+  eslint-plugin-react-refresh@0.4.24(eslint@9.39.1):
+    dependencies:
+      eslint: 9.39.1
+
+  eslint-scope@8.4.0:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  eslint-visitor-keys@3.4.3: {}
+
+  eslint-visitor-keys@4.2.1: {}
+
+  eslint@9.39.1:
+    dependencies:
+      '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1)
+      '@eslint-community/regexpp': 4.12.2
+      '@eslint/config-array': 0.21.1
+      '@eslint/config-helpers': 0.4.2
+      '@eslint/core': 0.17.0
+      '@eslint/eslintrc': 3.3.3
+      '@eslint/js': 9.39.1
+      '@eslint/plugin-kit': 0.4.1
+      '@humanfs/node': 0.16.7
+      '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.4.3
+      '@types/estree': 1.0.8
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.6
+      debug: 4.4.3
+      escape-string-regexp: 4.0.0
+      eslint-scope: 8.4.0
+      eslint-visitor-keys: 4.2.1
+      espree: 10.4.0
+      esquery: 1.6.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
+      lodash.merge: 4.6.2
+      minimatch: 3.1.2
+      natural-compare: 1.4.0
+      optionator: 0.9.4
+    transitivePeerDependencies:
+      - supports-color
+
+  espree@10.4.0:
+    dependencies:
+      acorn: 8.15.0
+      acorn-jsx: 5.3.2(acorn@8.15.0)
+      eslint-visitor-keys: 4.2.1
+
+  esquery@1.6.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  esrecurse@4.3.0:
+    dependencies:
+      estraverse: 5.3.0
+
+  estraverse@5.3.0: {}
+
+  esutils@2.0.3: {}
+
+  exenv@1.2.2: {}
+
+  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.3):
+    optionalDependencies:
+      picomatch: 4.0.3
+
+  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.3.3
+      keyv: 4.5.4
+
+  flatted@3.3.3: {}
+
+  focus-lock@1.3.6:
+    dependencies:
+      tslib: 2.8.1
+
+  follow-redirects@1.15.11: {}
+
+  form-data@4.0.5:
+    dependencies:
+      asynckit: 0.4.0
+      combined-stream: 1.0.8
+      es-set-tostringtag: 2.1.0
+      hasown: 2.0.2
+      mime-types: 2.1.35
+
+  fsevents@2.3.3:
+    optional: true
+
+  function-bind@1.1.2: {}
+
+  gensync@1.0.0-beta.2: {}
+
+  get-intrinsic@1.3.0:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      function-bind: 1.1.2
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      math-intrinsics: 1.1.0
+
+  get-proto@1.0.1:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
+  glob-parent@6.0.2:
+    dependencies:
+      is-glob: 4.0.3
+
+  globals@14.0.0: {}
+
+  globals@16.5.0: {}
+
+  gopd@1.2.0: {}
+
+  graphemer@1.4.0: {}
+
+  has-flag@4.0.0: {}
+
+  has-symbols@1.1.0: {}
+
+  has-tostringtag@1.0.2:
+    dependencies:
+      has-symbols: 1.1.0
+
+  hasown@2.0.2:
+    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
+
+  ignore@5.3.2: {}
+
+  ignore@7.0.5: {}
+
+  import-fresh@3.3.1:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  imurmurhash@0.1.4: {}
+
+  is-arrayish@0.2.1: {}
+
+  is-core-module@2.16.1:
+    dependencies:
+      hasown: 2.0.2
+
+  is-extglob@2.1.1: {}
+
+  is-glob@4.0.3:
+    dependencies:
+      is-extglob: 2.1.1
+
+  isexe@2.0.0: {}
+
+  js-tokens@4.0.0: {}
+
+  js-yaml@4.1.1:
+    dependencies:
+      argparse: 2.0.1
+
+  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
+
+  lines-and-columns@1.2.4: {}
+
+  locate-path@6.0.0:
+    dependencies:
+      p-locate: 5.0.0
+
+  lodash.merge@4.6.2: {}
+
+  loose-envify@1.4.0:
+    dependencies:
+      js-tokens: 4.0.0
+
+  lru-cache@5.1.1:
+    dependencies:
+      yallist: 3.1.1
+
+  math-intrinsics@1.1.0: {}
+
+  mime-db@1.52.0: {}
+
+  mime-types@2.1.35:
+    dependencies:
+      mime-db: 1.52.0
+
+  minimatch@3.1.2:
+    dependencies:
+      brace-expansion: 1.1.12
+
+  minimatch@9.0.5:
+    dependencies:
+      brace-expansion: 2.0.2
+
+  ms@2.1.3: {}
+
+  nanoid@3.3.11: {}
+
+  natural-compare@1.4.0: {}
+
+  node-releases@2.0.27: {}
+
+  object-assign@4.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.27.1
+      error-ex: 1.3.4
+      json-parse-even-better-errors: 2.3.1
+      lines-and-columns: 1.2.4
+
+  path-exists@4.0.0: {}
+
+  path-key@3.1.1: {}
+
+  path-parse@1.0.7: {}
+
+  path-type@4.0.0: {}
+
+  picocolors@1.1.1: {}
+
+  picomatch@4.0.3: {}
+
+  postcss@8.5.6:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  prelude-ls@1.2.1: {}
+
+  prop-types@15.8.1:
+    dependencies:
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      react-is: 16.13.1
+
+  proxy-from-env@1.1.0: {}
+
+  punycode@2.3.1: {}
+
+  react-animate-height@3.2.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+    dependencies:
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+
+  react-clientside-effect@1.2.8(react@19.2.0):
+    dependencies:
+      '@babel/runtime': 7.28.4
+      react: 19.2.0
+
+  react-day-picker@8.10.1(date-fns@2.30.0)(react@19.2.0):
+    dependencies:
+      date-fns: 2.30.0
+      react: 19.2.0
+
+  react-dom@19.2.0(react@19.2.0):
+    dependencies:
+      react: 19.2.0
+      scheduler: 0.27.0
+
+  react-fast-compare@3.2.2: {}
+
+  react-focus-lock@2.13.7(@types/react@19.2.7)(react@19.2.0):
+    dependencies:
+      '@babel/runtime': 7.28.4
+      focus-lock: 1.3.6
+      prop-types: 15.8.1
+      react: 19.2.0
+      react-clientside-effect: 1.2.8(react@19.2.0)
+      use-callback-ref: 1.3.3(@types/react@19.2.7)(react@19.2.0)
+      use-sidecar: 1.1.3(@types/react@19.2.7)(react@19.2.0)
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  react-is@16.13.1: {}
+
+  react-is@17.0.2: {}
+
+  react-lifecycles-compat@3.0.4: {}
+
+  react-modal@3.16.3(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+    dependencies:
+      exenv: 1.2.2
+      prop-types: 15.8.1
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-lifecycles-compat: 3.0.4
+      warning: 4.0.3
+
+  react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+    dependencies:
+      '@popperjs/core': 2.11.8
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-fast-compare: 3.2.2
+      warning: 4.0.3
+
+  react-refresh@0.18.0: {}
+
+  react-router-dom@7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+    dependencies:
+      react: 19.2.0
+      react-dom: 19.2.0(react@19.2.0)
+      react-router: 7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
+
+  react-router@7.9.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
+    dependencies:
+      cookie: 1.1.1
+      react: 19.2.0
+      set-cookie-parser: 2.7.2
+    optionalDependencies:
+      react-dom: 19.2.0(react@19.2.0)
+
+  react@19.2.0: {}
+
+  resolve-from@4.0.0: {}
+
+  resolve@1.22.11:
+    dependencies:
+      is-core-module: 2.16.1
+      path-parse: 1.0.7
+      supports-preserve-symlinks-flag: 1.0.0
+
+  rollup@4.53.3:
+    dependencies:
+      '@types/estree': 1.0.8
+    optionalDependencies:
+      '@rollup/rollup-android-arm-eabi': 4.53.3
+      '@rollup/rollup-android-arm64': 4.53.3
+      '@rollup/rollup-darwin-arm64': 4.53.3
+      '@rollup/rollup-darwin-x64': 4.53.3
+      '@rollup/rollup-freebsd-arm64': 4.53.3
+      '@rollup/rollup-freebsd-x64': 4.53.3
+      '@rollup/rollup-linux-arm-gnueabihf': 4.53.3
+      '@rollup/rollup-linux-arm-musleabihf': 4.53.3
+      '@rollup/rollup-linux-arm64-gnu': 4.53.3
+      '@rollup/rollup-linux-arm64-musl': 4.53.3
+      '@rollup/rollup-linux-loong64-gnu': 4.53.3
+      '@rollup/rollup-linux-ppc64-gnu': 4.53.3
+      '@rollup/rollup-linux-riscv64-gnu': 4.53.3
+      '@rollup/rollup-linux-riscv64-musl': 4.53.3
+      '@rollup/rollup-linux-s390x-gnu': 4.53.3
+      '@rollup/rollup-linux-x64-gnu': 4.53.3
+      '@rollup/rollup-linux-x64-musl': 4.53.3
+      '@rollup/rollup-openharmony-arm64': 4.53.3
+      '@rollup/rollup-win32-arm64-msvc': 4.53.3
+      '@rollup/rollup-win32-ia32-msvc': 4.53.3
+      '@rollup/rollup-win32-x64-gnu': 4.53.3
+      '@rollup/rollup-win32-x64-msvc': 4.53.3
+      fsevents: 2.3.3
+
+  scheduler@0.27.0: {}
+
+  semver@6.3.1: {}
+
+  semver@7.7.3: {}
+
+  set-cookie-parser@2.7.2: {}
+
+  shebang-command@2.0.0:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  shebang-regex@3.0.0: {}
+
+  source-map-js@1.2.1: {}
+
+  source-map@0.5.7: {}
+
+  strip-json-comments@3.1.1: {}
+
+  stylis@4.2.0: {}
+
+  supports-color@7.2.0:
+    dependencies:
+      has-flag: 4.0.0
+
+  supports-preserve-symlinks-flag@1.0.0: {}
+
+  tinyglobby@0.2.15:
+    dependencies:
+      fdir: 6.5.0(picomatch@4.0.3)
+      picomatch: 4.0.3
+
+  truncate@3.0.0: {}
+
+  ts-api-utils@2.1.0(typescript@5.9.3):
+    dependencies:
+      typescript: 5.9.3
+
+  tslib@2.8.1: {}
+
+  type-check@0.4.0:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  typescript-eslint@8.48.0(eslint@9.39.1)(typescript@5.9.3):
+    dependencies:
+      '@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1)(typescript@5.9.3))(eslint@9.39.1)(typescript@5.9.3)
+      '@typescript-eslint/parser': 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      '@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
+      '@typescript-eslint/utils': 8.48.0(eslint@9.39.1)(typescript@5.9.3)
+      eslint: 9.39.1
+      typescript: 5.9.3
+    transitivePeerDependencies:
+      - supports-color
+
+  typescript@5.9.3: {}
+
+  undici-types@7.16.0: {}
+
+  update-browserslist-db@1.1.4(browserslist@4.28.0):
+    dependencies:
+      browserslist: 4.28.0
+      escalade: 3.2.0
+      picocolors: 1.1.1
+
+  uri-js@4.4.1:
+    dependencies:
+      punycode: 2.3.1
+
+  use-callback-ref@1.3.3(@types/react@19.2.7)(react@19.2.0):
+    dependencies:
+      react: 19.2.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  use-sidecar@1.1.3(@types/react@19.2.7)(react@19.2.0):
+    dependencies:
+      detect-node-es: 1.1.0
+      react: 19.2.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.2.7
+
+  vite@7.2.6(@types/node@24.10.1):
+    dependencies:
+      esbuild: 0.25.12
+      fdir: 6.5.0(picomatch@4.0.3)
+      picomatch: 4.0.3
+      postcss: 8.5.6
+      rollup: 4.53.3
+      tinyglobby: 0.2.15
+    optionalDependencies:
+      '@types/node': 24.10.1
+      fsevents: 2.3.3
+
+  warning@4.0.3:
+    dependencies:
+      loose-envify: 1.4.0
+
+  which@2.0.2:
+    dependencies:
+      isexe: 2.0.0
+
+  word-wrap@1.2.5: {}
+
+  yallist@3.1.1: {}
+
+  yaml@1.10.2: {}
+
+  yocto-queue@0.1.0: {}
+
+  zod-validation-error@4.0.2(zod@4.1.13):
+    dependencies:
+      zod: 4.1.13
+
+  zod@4.1.13: {}

+ 1 - 0
public/vite.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

+ 42 - 0
src/App.css

@@ -0,0 +1,42 @@
+#root {
+  width: 100%;
+  height: 100vh;
+  margin: 0;
+  padding: 0;
+}
+
+.logo {
+  height: 6em;
+  padding: 1.5em;
+  will-change: filter;
+  transition: filter 300ms;
+}
+.logo:hover {
+  filter: drop-shadow(0 0 2em #646cffaa);
+}
+.logo.react:hover {
+  filter: drop-shadow(0 0 2em #61dafbaa);
+}
+
+@keyframes logo-spin {
+  from {
+    transform: rotate(0deg);
+  }
+  to {
+    transform: rotate(360deg);
+  }
+}
+
+@media (prefers-reduced-motion: no-preference) {
+  a:nth-of-type(2) .logo {
+    animation: logo-spin infinite 20s linear;
+  }
+}
+
+.card {
+  padding: 2em;
+}
+
+.read-the-docs {
+  color: #888;
+}

+ 58 - 0
src/App.tsx

@@ -0,0 +1,58 @@
+import { AuthProvider, useAuth } from "./contexts/AuthContext";
+import { MenuProvider } from "./contexts/MenuContext";
+import { RouterProvider } from "react-router-dom";
+import { createAppRouter } from "./router";
+import { useMemo } from "react";
+
+function AppRouter() {
+  const { menus, loading } = useAuth();
+
+  const router = useMemo(() => {
+    if (menus.length === 0) {
+      return null;
+    }
+    return createAppRouter(menus);
+  }, [menus]);
+
+  if (loading) {
+    return (
+      <div
+        style={{
+          display: "flex",
+          justifyContent: "center",
+          alignItems: "center",
+          height: "100vh",
+        }}
+      >
+        加载中...
+      </div>
+    );
+  }
+
+  if (!router) {
+    return (
+      <div
+        style={{
+          display: "flex",
+          justifyContent: "center",
+          alignItems: "center",
+          height: "100vh",
+        }}
+      >
+        无可用菜单
+      </div>
+    );
+  }
+
+  return <RouterProvider router={router} />;
+}
+
+export default function App() {
+  return (
+    <AuthProvider>
+      <MenuProvider>
+        <AppRouter />
+      </MenuProvider>
+    </AuthProvider>
+  );
+}

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
src/assets/react.svg


+ 28 - 0
src/components/common/PermissionButton.tsx

@@ -0,0 +1,28 @@
+import { Button } from "@contentful/f36-components";
+import { useAuth } from "../../contexts/AuthContext";
+import type { ComponentProps } from "react";
+
+interface PermissionButtonProps extends Omit<ComponentProps<typeof Button>, "children"> {
+  menuId: string;
+  action: string; // 'add' | 'edit' | 'delete' | 'view' | 'export' | 'import' | ...
+  children: React.ReactNode;
+}
+
+/**
+ * 带权限控制的按钮组件
+ * 如果用户没有权限,按钮将不会渲染
+ */
+export default function PermissionButton({
+  menuId,
+  action,
+  children,
+  ...buttonProps
+}: PermissionButtonProps) {
+  const { hasPermission } = useAuth();
+
+  if (!hasPermission(menuId, action)) {
+    return null;
+  }
+
+  return <Button {...buttonProps}>{children}</Button>;
+}

+ 34 - 0
src/components/common/PermissionWrapper.tsx

@@ -0,0 +1,34 @@
+import { useAuth } from "../../contexts/AuthContext";
+
+import type { ReactNode } from "react";
+
+interface PermissionWrapperProps {
+  menuId: string;
+  action: string | string[]; // 单个权限或权限数组
+  mode?: "all" | "any"; // all: 需要所有权限, any: 需要任一权限
+  fallback?: ReactNode; // 无权限时显示的内容
+  children: ReactNode;
+}
+
+/**
+ * 权限包装组件
+ * 根据权限控制子组件的显示
+ */
+export default function PermissionWrapper({
+  menuId,
+  action,
+  mode = "all",
+  fallback = null,
+  children,
+}: PermissionWrapperProps) {
+  const { hasPermission } = useAuth();
+
+  const actions = Array.isArray(action) ? action : [action];
+
+  const hasAccess =
+    mode === "all"
+      ? actions.every((act) => hasPermission(menuId, act))
+      : actions.some((act) => hasPermission(menuId, act));
+
+  return hasAccess ? <>{children}</> : <>{fallback}</>;
+}

+ 12 - 0
src/config/api.ts

@@ -0,0 +1,12 @@
+/**
+ * API 配置
+ */
+
+// API 前缀
+export const API_PREFIX = "/sohoyw-som";
+
+// API 基础路径
+export const API_BASE_URL = import.meta.env.DEV ? "" : import.meta.env.VITE_API_BASE_URL || "";
+
+// 超时时间
+export const TIMEOUT = 30000;

+ 23 - 0
src/config/menuIds.ts

@@ -0,0 +1,23 @@
+/**
+ * 菜单 ID 配置
+ * 统一管理所有菜单的 ID,便于维护和使用
+ */
+
+export const MENU_IDS = {
+  // 内容管理
+  CONTENT_MANAGEMENT: "content-management",
+  CONTENT_LIST: "content-list",
+  CONTENT_CREATE: "content-create",
+
+  // 用户管理
+  USER_MANAGEMENT: "user-management",
+  USER_LIST: "user-list",
+  ROLE_MANAGEMENT: "role-management",
+
+  // 系统管理
+  SYSTEM_MANAGEMENT: "system-management",
+  MENU_MANAGEMENT: "menu-management",
+  PERMISSION_MANAGEMENT: "permission-management",
+} as const;
+
+export type MenuId = (typeof MENU_IDS)[keyof typeof MENU_IDS];

+ 67 - 0
src/contexts/AuthContext.tsx

@@ -0,0 +1,67 @@
+import { createContext, useContext, useState, useEffect } from "react";
+import type { ReactNode } from "react";
+import type { JeecgMenu } from "../types/menu";
+import { getUserPermissions } from "../services/api";
+
+interface AuthContextType {
+  menus: JeecgMenu[];
+  permissions: Map<string, string[]>; // menuId -> actions[]
+  loading: boolean;
+  hasPermission: (menuId: string, action: string) => boolean;
+  refreshAuth: () => Promise<void>;
+}
+
+const AuthContext = createContext<AuthContextType | undefined>(undefined);
+
+export function AuthProvider({ children }: { children: ReactNode }) {
+  const [menus, setMenus] = useState<JeecgMenu[]>([]);
+  const [permissions] = useState<Map<string, string[]>>(
+    new Map()
+  );
+  const [loading, setLoading] = useState(true);
+
+  const loadAuthData = async () => {
+    try {
+      setLoading(true);
+      const [menusData] = await Promise.all([
+        getUserPermissions(),
+      ]);
+      setMenus(menusData.menu);
+    } catch (error) {
+      console.error("加载权限数据失败:", error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  useEffect(() => {
+    loadAuthData();
+  }, []);
+
+  const hasPermission = (menuId: string, action: string): boolean => {
+    const actions = permissions.get(menuId);
+    return actions ? actions.includes(action) : false;
+  };
+
+  return (
+    <AuthContext.Provider
+      value={{
+        menus,
+        permissions,
+        loading,
+        hasPermission,
+        refreshAuth: loadAuthData,
+      }}
+    >
+      {children}
+    </AuthContext.Provider>
+  );
+}
+
+export function useAuth() {
+  const context = useContext(AuthContext);
+  if (!context) {
+    throw new Error("useAuth must be used within AuthProvider");
+  }
+  return context;
+}

+ 46 - 0
src/contexts/MenuContext.tsx

@@ -0,0 +1,46 @@
+/**
+ * 菜单上下文
+ * 管理当前选中的一级菜单和二级菜单
+ */
+
+import { createContext, useContext, useState, type ReactNode } from "react";
+import type { JeecgMenu } from "../types/menu";
+
+interface MenuContextType {
+  activeFirstMenu: JeecgMenu | null;
+  activeSecondMenu: JeecgMenu | null;
+  setActiveFirstMenu: (menu: JeecgMenu | null) => void;
+  setActiveSecondMenu: (menu: JeecgMenu | null) => void;
+  hasSecondMenu: boolean;
+}
+
+const MenuContext = createContext<MenuContextType | undefined>(undefined);
+
+export function MenuProvider({ children }: { children: ReactNode }) {
+  const [activeFirstMenu, setActiveFirstMenu] = useState<JeecgMenu | null>(null);
+  const [activeSecondMenu, setActiveSecondMenu] = useState<JeecgMenu | null>(null);
+
+  const hasSecondMenu = activeFirstMenu?.children && activeFirstMenu.children.length > 0;
+
+  return (
+    <MenuContext.Provider
+      value={{
+        activeFirstMenu,
+        activeSecondMenu,
+        setActiveFirstMenu,
+        setActiveSecondMenu,
+        hasSecondMenu: !!hasSecondMenu,
+      }}
+    >
+      {children}
+    </MenuContext.Provider>
+  );
+}
+
+export function useMenu() {
+  const context = useContext(MenuContext);
+  if (!context) {
+    throw new Error("useMenu must be used within MenuProvider");
+  }
+  return context;
+}

+ 20 - 0
src/hooks/usePermission.ts

@@ -0,0 +1,20 @@
+import { useAuth } from "../contexts/AuthContext";
+
+/**
+ * 权限检查 Hook
+ * @param menuId 菜单ID
+ * @returns 权限检查函数
+ */
+export function usePermission(menuId: string) {
+  const { hasPermission } = useAuth();
+
+  return {
+    canView: hasPermission(menuId, "view"),
+    canAdd: hasPermission(menuId, "add"),
+    canEdit: hasPermission(menuId, "edit"),
+    canDelete: hasPermission(menuId, "delete"),
+    canExport: hasPermission(menuId, "export"),
+    canImport: hasPermission(menuId, "import"),
+    hasAction: (action: string) => hasPermission(menuId, action),
+  };
+}

+ 220 - 0
src/http/Axios.ts

@@ -0,0 +1,220 @@
+/**
+ * Axios 封装类
+ * 参考 JeecgBoot 的 VAxios
+ */
+
+import type { AxiosRequestConfig, AxiosInstance, AxiosResponse, AxiosError } from "axios";
+import axios from "axios";
+import type { RequestOptions, Result } from "../types/axios";
+import type { CreateAxiosOptions } from "./axiosTransform";
+import { AxiosTransform } from "./axiosTransform";
+
+export class VAxios {
+  private axiosInstance: AxiosInstance;
+  private readonly options: CreateAxiosOptions;
+
+  constructor(options: CreateAxiosOptions) {
+    this.options = options;
+    this.axiosInstance = axios.create(options);
+    this.setupInterceptors();
+  }
+
+  /**
+   * 获取 axios 实例
+   */
+  getAxios(): AxiosInstance {
+    return this.axiosInstance;
+  }
+
+  /**
+   * 重新配置 axios
+   */
+  configAxios(config: CreateAxiosOptions) {
+    if (!this.axiosInstance) {
+      return;
+    }
+    this.createAxios(config);
+  }
+
+  /**
+   * 设置通用 header
+   */
+  setHeader(headers: Record<string, string>): void {
+    if (!this.axiosInstance) {
+      return;
+    }
+    Object.assign(this.axiosInstance.defaults.headers, headers);
+  }
+
+  /**
+   * 创建 axios 实例
+   */
+  private createAxios(config: CreateAxiosOptions): void {
+    this.axiosInstance = axios.create(config);
+  }
+
+  private getTransform() {
+    const { transform } = this.options;
+    return transform;
+  }
+
+  /**
+   * 设置拦截器
+   */
+  private setupInterceptors() {
+    const transform = this.getTransform();
+    if (!transform) {
+      return;
+    }
+
+    const {
+      requestInterceptors,
+      requestInterceptorsCatch,
+      responseInterceptors,
+      responseInterceptorsCatch,
+    } = transform;
+
+    // 请求拦截器
+    this.axiosInstance.interceptors.request.use((config: any) => {
+      if (requestInterceptors && typeof requestInterceptors === "function") {
+        config = requestInterceptors(config, this.options);
+      }
+      return config;
+    }, undefined);
+
+    // 请求拦截器错误捕获
+    requestInterceptorsCatch &&
+      typeof requestInterceptorsCatch === "function" &&
+      this.axiosInstance.interceptors.request.use(undefined, requestInterceptorsCatch);
+
+    // 响应拦截器
+    this.axiosInstance.interceptors.response.use((res: AxiosResponse<any>) => {
+      if (responseInterceptors && typeof responseInterceptors === "function") {
+        res = responseInterceptors(res);
+      }
+      return res;
+    }, undefined);
+
+    // 响应拦截器错误捕获
+    responseInterceptorsCatch &&
+      typeof responseInterceptorsCatch === "function" &&
+      this.axiosInstance.interceptors.response.use(undefined, responseInterceptorsCatch);
+  }
+
+  /**
+   * 文件上传
+   */
+  uploadFile<T = any>(config: AxiosRequestConfig, params: Record<string, any>) {
+    const formData = new FormData();
+    const customFilename = params.name || "file";
+
+    if (params.file) {
+      formData.append(customFilename, params.file);
+    }
+
+    if (params.data) {
+      Object.keys(params.data).forEach((key) => {
+        const value = params.data[key];
+        if (Array.isArray(value)) {
+          value.forEach((item) => {
+            formData.append(`${key}[]`, item);
+          });
+          return;
+        }
+        formData.append(key, params.data[key]);
+      });
+    }
+
+    return this.axiosInstance.request<T>({
+      ...config,
+      method: "POST",
+      data: formData,
+      headers: {
+        "Content-Type": "multipart/form-data",
+      },
+    });
+  }
+
+  /**
+   * 支持 form-data
+   */
+  supportFormData(config: AxiosRequestConfig) {
+    const headers = config.headers || this.options.headers;
+    const contentType = headers?.["Content-Type"] || headers?.["content-type"];
+
+    if (
+      contentType !== "application/x-www-form-urlencoded;charset=UTF-8" ||
+      !Reflect.has(config, "data") ||
+      config.method?.toUpperCase() === "GET"
+    ) {
+      return config;
+    }
+
+    return {
+      ...config,
+      data: new URLSearchParams(config.data).toString(),
+    };
+  }
+
+  get<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: "GET" }, options);
+  }
+
+  post<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: "POST" }, options);
+  }
+
+  put<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: "PUT" }, options);
+  }
+
+  delete<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    return this.request({ ...config, method: "DELETE" }, options);
+  }
+
+  request<T = any>(config: AxiosRequestConfig, options?: RequestOptions): Promise<T> {
+    let conf: CreateAxiosOptions = Object.assign({}, config);
+    const transform = this.getTransform();
+
+    const { requestOptions } = this.options;
+
+    const opt: RequestOptions = Object.assign({}, requestOptions, options);
+
+    const { beforeRequestHook, requestCatchHook, transformRequestHook } = transform || {};
+
+    if (beforeRequestHook && typeof beforeRequestHook === "function") {
+      conf = beforeRequestHook(conf, opt);
+    }
+
+    conf.requestOptions = opt;
+
+    conf = this.supportFormData(conf);
+
+    return new Promise((resolve, reject) => {
+      this.axiosInstance
+        .request<any, AxiosResponse<Result>>(conf)
+        .then((res: AxiosResponse<Result>) => {
+          if (transformRequestHook && typeof transformRequestHook === "function") {
+            try {
+              const ret = transformRequestHook(res, opt);
+              resolve(ret);
+            } catch (err) {
+              reject(err || new Error("request error!"));
+            }
+            return;
+          }
+          resolve(res as unknown as Promise<T>);
+        })
+        .catch((e: Error | AxiosError) => {
+          if (requestCatchHook && typeof requestCatchHook === "function") {
+            reject(requestCatchHook(e, opt));
+            return;
+          }
+          if (axios.isAxiosError(e)) {
+            // 在这里重写 axios 的错误信息
+          }
+          reject(e);
+        });
+    });
+  }
+}

+ 49 - 0
src/http/axiosTransform.ts

@@ -0,0 +1,49 @@
+/**
+ * Axios 数据处理类
+ */
+
+import type { AxiosRequestConfig, AxiosResponse } from "axios";
+import type { RequestOptions, Result } from "../types/axios";
+
+export interface CreateAxiosOptions extends AxiosRequestConfig {
+  authenticationScheme?: string;
+  transform?: AxiosTransform;
+  requestOptions?: RequestOptions;
+}
+
+export abstract class AxiosTransform {
+  /**
+   * 请求之前的钩子
+   */
+  beforeRequestHook?: (config: AxiosRequestConfig, options: RequestOptions) => AxiosRequestConfig;
+
+  /**
+   * 请求成功处理
+   */
+  transformRequestHook?: (res: AxiosResponse<Result>, options: RequestOptions) => any;
+
+  /**
+   * 请求失败处理
+   */
+  requestCatchHook?: (e: Error, options: RequestOptions) => Promise<any>;
+
+  /**
+   * 请求之前的拦截器
+   */
+  requestInterceptors?: (config: AxiosRequestConfig, options: CreateAxiosOptions) => AxiosRequestConfig;
+
+  /**
+   * 请求之后的拦截器
+   */
+  responseInterceptors?: (res: AxiosResponse<any>) => AxiosResponse<any>;
+
+  /**
+   * 请求之前的拦截器错误处理
+   */
+  requestInterceptorsCatch?: (error: Error) => void;
+
+  /**
+   * 请求之后的拦截器错误处理
+   */
+  responseInterceptorsCatch?: (error: any) => void;
+}

+ 60 - 0
src/http/checkStatus.ts

@@ -0,0 +1,60 @@
+/**
+ * HTTP 状态码检查
+ */
+
+import type { ErrorMessageMode } from "../types/axios";
+import message from "../utils/message";
+
+export function checkStatus(status: number, msg: string, errorMessageMode: ErrorMessageMode = "message"): void {
+  let errMessage = "";
+
+  switch (status) {
+    case 400:
+      errMessage = msg || "请求参数错误";
+      break;
+    case 401:
+      errMessage = msg || "登录已过期,请重新登录";
+      break;
+    case 403:
+      errMessage = msg || "没有权限访问该资源";
+      break;
+    case 404:
+      errMessage = msg || "请求的资源不存在";
+      break;
+    case 405:
+      errMessage = msg || "请求方法不允许";
+      break;
+    case 408:
+      errMessage = msg || "请求超时";
+      break;
+    case 500:
+      errMessage = msg || "服务器内部错误";
+      break;
+    case 501:
+      errMessage = msg || "服务未实现";
+      break;
+    case 502:
+      errMessage = msg || "网关错误";
+      break;
+    case 503:
+      errMessage = msg || "服务不可用";
+      break;
+    case 504:
+      errMessage = msg || "网关超时";
+      break;
+    case 505:
+      errMessage = msg || "HTTP版本不受支持";
+      break;
+    default:
+      errMessage = msg || "请求失败";
+  }
+
+  if (errMessage) {
+    if (errorMessageMode === "modal") {
+      // 可以使用 Modal 组件
+      message.error(errMessage);
+    } else if (errorMessageMode === "message") {
+      message.error(errMessage);
+    }
+  }
+}

+ 57 - 0
src/http/helper.ts

@@ -0,0 +1,57 @@
+/**
+ * 辅助函数
+ */
+
+const DATE_TIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
+
+export function joinTimestamp<T extends boolean>(join: boolean, restful: T): T extends true ? string : object;
+
+export function joinTimestamp(join: boolean, restful = false): string | object {
+  if (!join) {
+    return restful ? "" : {};
+  }
+  const now = new Date().getTime();
+  if (restful) {
+    return `?_t=${now}`;
+  }
+  return { _t: now };
+}
+
+/**
+ * 格式化请求参数时间
+ */
+export function formatRequestDate(params: Record<string, any>) {
+  if (Object.prototype.toString.call(params) !== "[object Object]") {
+    return;
+  }
+
+  for (const key in params) {
+    const value = params[key];
+    if (value) {
+      try {
+        // 如果是字符串,trim 处理
+        if (typeof value === "string") {
+          params[key] = value.trim();
+        }
+        // 如果是对象,递归处理
+        if (typeof value === "object" && value !== null) {
+          formatRequestDate(value);
+        }
+      } catch (error: any) {
+        throw new Error(error);
+      }
+    }
+  }
+}
+
+/**
+ * 将对象转换为 URL 参数
+ */
+export function setObjToUrlParams(baseUrl: string, obj: Record<string, any>): string {
+  let parameters = "";
+  for (const key in obj) {
+    parameters += key + "=" + encodeURIComponent(obj[key]) + "&";
+  }
+  parameters = parameters.replace(/&$/, "");
+  return /\?$/.test(baseUrl) ? baseUrl + parameters : baseUrl.replace(/\/?$/, "?") + parameters;
+}

+ 235 - 0
src/http/index.ts

@@ -0,0 +1,235 @@
+/**
+ * Axios 配置
+ * 参考 JeecgBoot 前端封装,适配 React
+ */
+
+import type { AxiosResponse } from "axios";
+import type { RequestOptions, Result } from "../types/axios";
+import type { AxiosTransform, CreateAxiosOptions } from "./axiosTransform";
+import { VAxios } from "./Axios";
+import { checkStatus } from "./checkStatus";
+import { API_BASE_URL, API_PREFIX, TIMEOUT } from "../config/api";
+import message from "../utils/message";
+import { joinTimestamp, formatRequestDate, setObjToUrlParams } from "./helper";
+import { generateSign, getTimestamp } from "../utils/sign";
+import { getCurrentTenantId } from "../utils/tenant";
+
+/**
+ * 数据处理
+ */
+const transform: AxiosTransform = {
+  /**
+   * 处理请求数据
+   */
+  transformRequestHook: (res: AxiosResponse<Result>, options: RequestOptions) => {
+    const { isTransformResponse, isReturnNativeResponse } = options;
+
+    // 返回原生响应头
+    if (isReturnNativeResponse) {
+      return res;
+    }
+
+    // 不进行任何处理,直接返回
+    if (!isTransformResponse) {
+      return res.data;
+    }
+
+    const { data } = res;
+    if (!data) {
+      throw new Error("请求失败,无返回数据");
+    }
+
+    // JeecgBoot 标准响应格式
+    const { code, result, message: msg, success } = data;
+
+    // 成功
+    const hasSuccess = data && Reflect.has(data, "code") && (code === 200 || code === 0);
+    if (hasSuccess) {
+      if (success && msg && options.successMessageMode === "success") {
+        message.success(msg);
+      }
+      return result;
+    }
+
+    // 错误处理
+    let errorMsg = msg || "请求失败";
+
+    // 根据不同的 code 处理
+    switch (code) {
+      case 401:
+        errorMsg = msg || "登录已过期";
+        // 跳转登录页
+        setTimeout(() => {
+          window.location.href = "/login";
+        }, 1500);
+        break;
+      default:
+        if (msg) {
+          errorMsg = msg;
+        }
+    }
+
+    // 错误提示
+    if (options.errorMessageMode === "modal") {
+      message.error(errorMsg);
+    } else if (options.errorMessageMode === "message") {
+      message.error(errorMsg);
+    }
+
+    throw new Error(errorMsg);
+  },
+
+  /**
+   * 请求之前处理 config
+   */
+  beforeRequestHook: (config, options) => {
+    const { apiUrl, joinPrefix, joinParamsToUrl, formatDate, joinTime = true, urlPrefix } = options;
+
+    // 添加前缀
+    if (joinPrefix && urlPrefix && !config.url?.startsWith("http")) {
+      config.url = `${urlPrefix}${config.url}`;
+    }
+
+    if (apiUrl && !config.url?.startsWith("http")) {
+      config.url = `${apiUrl}${config.url}`;
+    }
+
+    const params = config.params || {};
+    const data = config.data || false;
+
+    formatDate && data && typeof data === "object" && formatRequestDate(data);
+
+    if (config.method?.toUpperCase() === "GET") {
+      if (typeof params === "object") {
+        // GET 请求加上时间戳参数,避免从缓存中拿数据
+        config.params = Object.assign(params || {}, joinTimestamp(joinTime, false));
+      } else {
+        // 兼容 restful 风格
+        config.url = config.url + params + `${joinTimestamp(joinTime, true)}`;
+        config.params = undefined;
+      }
+    } else {
+      if (typeof params === "object") {
+        formatDate && formatRequestDate(params);
+        if (Reflect.has(config, "data") && config.data && Object.keys(config.data).length > 0) {
+          config.data = data;
+          config.params = params;
+        } else {
+          // 非 GET 请求如果没有提供 data,则将 params 视为 data
+          config.data = params;
+          config.params = undefined;
+        }
+        if (joinParamsToUrl) {
+          config.url = setObjToUrlParams(config.url as string, Object.assign({}, config.params, config.data));
+        }
+      } else {
+        // 兼容 restful 风格
+        config.url = config.url + params;
+        config.params = undefined;
+      }
+    }
+
+    return config;
+  },
+
+  /**
+   * 请求拦截器处理
+   */
+  requestInterceptors: (config, options) => {
+    // 添加签名和时间戳
+    const timestamp = getTimestamp();
+    const sign = generateSign(config.url!, config.params, config.data);
+
+    config.headers = config.headers || {};
+    config.headers["X-Timestamp"] = timestamp;
+    config.headers["X-Sign"] = sign;
+    config.headers["X-Version"] = "react";
+
+    // 添加租户ID
+    const tenantId = getCurrentTenantId();
+    config.headers["X-Tenant-ID"] = String(tenantId);
+
+    // 如果需要 token,这里可以添加
+    const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuWGheWuueS4reW_g-euoeeQhuWRmCIsImV4cCI6MTc2NDk2MzkwN30.m9qVPagPzMpSIdeaYdw_yWmno64A2ntLnFDi7GUB3Lg';
+    if (token && options.requestOptions?.withToken !== false) {
+      config.headers.Authorization = token;
+      config.headers['X-Access-Token'] = token;
+    }
+
+    return config;
+  },
+
+  /**
+   * 响应拦截器处理
+   */
+  responseInterceptors: (res: AxiosResponse<any>) => {
+    return res;
+  },
+
+  /**
+   * 响应错误处理
+   */
+  responseInterceptorsCatch: (error: any) => {
+    const { response, code, message: msg, config } = error || {};
+    const errorMessageMode = config?.requestOptions?.errorMessageMode || "none";
+    const responseMsg: string = response?.data?.message ?? "";
+    const err: string = error?.toString?.() ?? "";
+    let errMessage = "";
+
+    try {
+      if (code === "ECONNABORTED" && msg.indexOf("timeout") !== -1) {
+        errMessage = "请求超时";
+      }
+      if (err?.includes("Network Error")) {
+        errMessage = "网络异常,请检查您的网络连接";
+      }
+
+      if (errMessage) {
+        if (errorMessageMode === "modal") {
+          message.error(errMessage);
+        } else if (errorMessageMode === "message") {
+          message.error(errMessage);
+        }
+        return Promise.reject(error);
+      }
+    } catch (error) {
+      throw new Error(error as any);
+    }
+
+    checkStatus(error?.response?.status, responseMsg, errorMessageMode);
+    return Promise.reject(error);
+  },
+};
+
+/**
+ * 创建 axios 实例
+ */
+function createAxios(opt?: Partial<CreateAxiosOptions>) {
+  return new VAxios({
+    authenticationScheme: "",
+    timeout: TIMEOUT,
+    baseURL: API_BASE_URL,
+    headers: { "Content-Type": "application/json;charset=UTF-8" },
+    transform,
+    requestOptions: {
+      joinPrefix: true,
+      isReturnNativeResponse: false,
+      isTransformResponse: true,
+      joinParamsToUrl: false,
+      formatDate: true,
+      errorMessageMode: "message",
+      successMessageMode: "none",
+      apiUrl: API_BASE_URL,
+      urlPrefix: API_PREFIX,
+      joinTime: true,
+      ignoreCancelToken: true,
+      withToken: true,
+    },
+    ...opt,
+  });
+}
+
+export const defHttp = createAxios();
+
+// 导出类型
+export type { RequestOptions, Result };

+ 69 - 0
src/index.css

@@ -0,0 +1,69 @@
+:root {
+  font-family: system-ui, Avenir, Helvetica, Arial, sans-serif;
+  line-height: 1.5;
+  font-weight: 400;
+
+  color-scheme: light dark;
+  color: rgba(255, 255, 255, 0.87);
+  background-color: #242424;
+
+  font-synthesis: none;
+  text-rendering: optimizeLegibility;
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
+}
+
+a {
+  font-weight: 500;
+  color: #646cff;
+  text-decoration: inherit;
+}
+a:hover {
+  color: #535bf2;
+}
+
+body {
+  margin: 0;
+  min-width: 320px;
+  min-height: 100vh;
+  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
+    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
+    sans-serif;
+}
+
+h1 {
+  font-size: 3.2em;
+  line-height: 1.1;
+}
+
+button {
+  border-radius: 8px;
+  border: 1px solid transparent;
+  padding: 0.6em 1.2em;
+  font-size: 1em;
+  font-weight: 500;
+  font-family: inherit;
+  background-color: #1a1a1a;
+  cursor: pointer;
+  transition: border-color 0.25s;
+}
+button:hover {
+  border-color: #646cff;
+}
+button:focus,
+button:focus-visible {
+  outline: 4px auto -webkit-focus-ring-color;
+}
+
+@media (prefers-color-scheme: light) {
+  :root {
+    color: #213547;
+    background-color: #ffffff;
+  }
+  a:hover {
+    color: #747bff;
+  }
+  button {
+    background-color: #f9f9f9;
+  }
+}

+ 143 - 0
src/layouts/Header/index.module.css

@@ -0,0 +1,143 @@
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 16px;
+  height: 48px;
+  border-bottom: 1px solid #e5e8eb;
+  background-color: #f7f9fa;
+}
+
+.left {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  flex: 1;
+}
+
+.logo {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  cursor: pointer;
+}
+
+.logo svg {
+  width: 24px;
+  height: 24px;
+}
+
+.loading {
+  font-size: 13px;
+  color: #999;
+  padding: 0 12px;
+}
+
+/* Card Navigation */
+.menuCards {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  height: 100%;
+}
+
+.menuCard {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 6px 14px;
+  height: 36px;
+  background: white;
+  border: 1px solid #d3dce0;
+  border-radius: 6px;
+  cursor: pointer;
+  font-size: 13px;
+  color: #536171;
+  transition: all 0.2s;
+  font-weight: 400;
+}
+
+.menuCard:hover {
+  border-color: #b8c4ce;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
+}
+
+.menuCardActive {
+  background: #0066ff;
+  border-color: #0066ff;
+  color: white;
+  box-shadow: 0 2px 4px rgba(0, 102, 255, 0.2);
+}
+
+.menuCardActive:hover {
+  background: #0052cc;
+  border-color: #0052cc;
+}
+
+.menuIcon {
+  font-size: 16px;
+  display: flex;
+  align-items: center;
+  line-height: 1;
+}
+
+.menuText {
+  font-weight: 500;
+  white-space: nowrap;
+}
+
+/* Right Side */
+.right {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
+
+.envSelector {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.branchButton {
+  min-width: 80px;
+}
+
+.actions {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  padding-left: 12px;
+  border-left: 1px solid #e5e8eb;
+}
+
+.iconButton {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
+  background: transparent;
+  border: none;
+  border-radius: 4px;
+  cursor: pointer;
+  font-size: 16px;
+  transition: background-color 0.2s;
+}
+
+.iconButton:hover {
+  background-color: rgba(0, 0, 0, 0.05);
+}
+
+.avatar {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  background-color: #e5e8eb;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+}

+ 122 - 0
src/layouts/Header/index.tsx

@@ -0,0 +1,122 @@
+import { Button } from "@contentful/f36-components";
+import { useAuth } from "../../contexts/AuthContext";
+import { useMenu } from "../../contexts/MenuContext";
+import { useNavigate, useLocation } from "react-router-dom";
+import { useEffect } from "react";
+import styles from "./index.module.css";
+
+export default function Header() {
+  const { menus, loading } = useAuth();
+  const { activeFirstMenu, setActiveFirstMenu, setActiveSecondMenu } = useMenu();
+  const navigate = useNavigate();
+  const location = useLocation();
+
+  // 获取顶级菜单(一级菜单)
+  const topMenus = menus.filter((menu) => !menu.hidden && !menu.meta?.hideMenu);
+
+  // 根据当前路由自动设置活动菜单
+  useEffect(() => {
+    const path = location.pathname;
+    const segments = path.split("/").filter(Boolean);
+    const firstPath = segments[0];
+    
+    // 尝试匹配一级菜单
+    const matchedMenu = topMenus.find((menu) => {
+      const menuPath = menu.path || "";
+      const menuSegments = menuPath.split("/").filter(Boolean);
+      return menuSegments[0] === firstPath;
+    });
+
+    if (matchedMenu && matchedMenu.id !== activeFirstMenu?.id) {
+      setActiveFirstMenu(matchedMenu);
+      setActiveSecondMenu(null);
+    }
+  }, [location.pathname, topMenus, activeFirstMenu, setActiveFirstMenu, setActiveSecondMenu]);
+
+  const handleMenuClick = (menu: typeof topMenus[0]) => {
+    console.log("Header: Clicked menu", menu.name, menu.path);
+    setActiveFirstMenu(menu);
+    setActiveSecondMenu(null);
+
+    // 如果有子菜单,导航到第一个可见子菜单
+    if (menu.children && menu.children.length > 0) {
+      const firstChild = menu.children.find((child) => !child.hidden && !child.meta?.hideMenu);
+      if (firstChild) {
+        const childPath = firstChild.path || "";
+        console.log("Header: Navigating to first child", childPath);
+        navigate(childPath);
+      }
+    } else {
+      // 没有子菜单,直接导航到该菜单
+      const menuPath = menu.path || "";
+      console.log("Header: Navigating to menu", menuPath);
+      navigate(menuPath);
+    }
+  };
+
+  return (
+    <header className={styles.header}>
+      <div className={styles.left}>
+        {/* Logo */}
+        <div className={styles.logo}>
+          <svg width="24" height="24" viewBox="0 0 24 24" fill="none">
+            <circle cx="12" cy="12" r="10" fill="#0066ff"/>
+            <path d="M8 12L11 15L16 9" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
+          </svg>
+        </div>
+        
+        {/* Card Navigation */}
+        {loading ? (
+          <div className={styles.loading}>加载中...</div>
+        ) : (
+          <nav className={styles.menuCards}>
+            {topMenus.map((menu) => {
+              const isActive = activeFirstMenu?.id === menu.id;
+              
+              return (
+                <button
+                  key={menu.id}
+                  className={`${styles.menuCard} ${isActive ? styles.menuCardActive : ""}`}
+                  onClick={() => handleMenuClick(menu)}
+                >
+                  <span className={styles.menuText}>{menu.meta?.title || menu.name}</span>
+                </button>
+              );
+            })}
+          </nav>
+        )}
+      </div>
+
+      <div className={styles.right}>
+        {/* Environment Selector */}
+        <div className={styles.envSelector}>
+          <Button variant="positive" size="small">
+            Blank
+          </Button>
+          <Button variant="secondary" size="small" className={styles.branchButton}>
+            master 🔽
+          </Button>
+        </div>
+
+        {/* Action Icons */}
+        <div className={styles.actions}>
+          <button className={styles.iconButton} title="Search">
+            🔍
+          </button>
+          <button className={styles.iconButton} title="Help">
+            ❓
+          </button>
+          <button className={styles.iconButton} title="Notifications">
+            🔔
+          </button>
+          <button className={styles.iconButton} title="Settings">
+            ⚙️
+          </button>
+          <button className={styles.iconButton} title="User">
+            <div className={styles.avatar}>👤</div>
+          </button>
+        </div>
+      </div>
+    </header>
+  );
+}

+ 18 - 0
src/layouts/MainLayout/index.module.css

@@ -0,0 +1,18 @@
+.layout {
+  display: flex;
+  flex-direction: column;
+  height: 100vh;
+  overflow: hidden;
+}
+
+.content {
+  display: flex;
+  flex: 1;
+  overflow: hidden;
+}
+
+.main {
+  flex: 1;
+  overflow: auto;
+  background-color: #f7f9fa;
+}

+ 21 - 0
src/layouts/MainLayout/index.tsx

@@ -0,0 +1,21 @@
+import { Outlet } from "react-router-dom";
+import Header from "../Header/index";
+import Sidebar from "../Sidebar/index";
+import { useMenu } from "../../contexts/MenuContext";
+import styles from "./index.module.css";
+
+export default function MainLayout() {
+  const { hasSecondMenu } = useMenu();
+
+  return (
+    <div className={styles.layout}>
+      <Header />
+      <div className={styles.content}>
+        {hasSecondMenu && <Sidebar />}
+        <main className={styles.main}>
+          <Outlet />
+        </main>
+      </div>
+    </div>
+  );
+}

+ 37 - 0
src/layouts/Sidebar/index.module.css

@@ -0,0 +1,37 @@
+.sidebar {
+  display: flex;
+  flex-direction: column;
+  width: 220px;
+  padding: 16px;
+  border-right: 1px solid #e5e8eb;
+  background-color: white;
+  height: calc(100vh - 60px);
+  overflow-y: auto;
+  gap: 4px;
+}
+
+.menuItem {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding: 12px;
+  cursor: pointer;
+  background-color: transparent;
+  color: #333;
+  border-radius: 4px;
+  font-size: 14px;
+  transition: all 0.2s;
+}
+
+.menuItem:hover {
+  background-color: #f0f0f0;
+}
+
+.menuItemActive {
+  background-color: #0066ff;
+  color: white;
+}
+
+.menuItemActive:hover {
+  background-color: #0052cc;
+}

+ 59 - 0
src/layouts/Sidebar/index.tsx

@@ -0,0 +1,59 @@
+import { Text } from "@contentful/f36-components";
+import { useMenu } from "../../contexts/MenuContext";
+import { useNavigate, useLocation } from "react-router-dom";
+import type { JeecgMenu } from "../../types/menu";
+import styles from "./index.module.css";
+
+interface SidebarItemProps {
+  menu: JeecgMenu;
+  active?: boolean;
+  onClick?: () => void;
+}
+
+function SidebarItem({ menu, active, onClick }: SidebarItemProps) {
+  return (
+    <div
+      className={`${styles.menuItem} ${active ? styles.menuItemActive : ""}`}
+      onClick={onClick}
+    >
+      <Text>{menu.meta?.title || menu.name}</Text>
+    </div>
+  );
+}
+
+export default function Sidebar() {
+  const { activeFirstMenu, activeSecondMenu, setActiveSecondMenu } = useMenu();
+  const navigate = useNavigate();
+  const location = useLocation();
+
+  if (!activeFirstMenu || !activeFirstMenu.children || activeFirstMenu.children.length === 0) {
+    return null;
+  }
+
+  const secondMenus = activeFirstMenu.children.filter((menu) => !menu.hidden && !menu.meta?.hideMenu);
+
+  const handleMenuClick = (menu: JeecgMenu) => {
+    console.log("Sidebar: Clicked menu", menu.name, menu.path);
+    setActiveSecondMenu(menu);
+    const menuPath = menu.path || "";
+    navigate(menuPath);
+  };
+
+  return (
+    <aside className={styles.sidebar}>
+      {secondMenus.map((menu) => {
+        const menuPath = menu.path || "";
+        const isActive = location.pathname === menuPath || activeSecondMenu?.id === menu.id;
+        
+        return (
+          <SidebarItem
+            key={menu.id}
+            menu={menu}
+            active={isActive}
+            onClick={() => handleMenuClick(menu)}
+          />
+        );
+      })}
+    </aside>
+  );
+}

+ 10 - 0
src/main.tsx

@@ -0,0 +1,10 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App";
+import "@contentful/f36-tokens/dist/css/index.css";
+
+ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
+  <React.StrictMode>
+    <App />
+  </React.StrictMode>
+);

+ 178 - 0
src/pages/content-model/index.module.css

@@ -0,0 +1,178 @@
+/* 内容模型页面样式 */
+
+.page {
+  padding: 32px 40px;
+  background-color: #fff;
+  min-height: 100%;
+}
+
+/* 页面头部 */
+.pageHeader {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 24px;
+}
+
+.pageHeaderLeft {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.pageTitle {
+  font-size: 28px;
+  font-weight: 400;
+  color: #1a1a1a;
+  margin: 0;
+}
+
+.helpIcon {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  border: 1px solid #ccc;
+  font-size: 12px;
+  color: #666;
+  cursor: help;
+}
+
+.pageHeaderRight {
+  display: flex;
+  gap: 12px;
+}
+
+/* 搜索栏 */
+.searchBar {
+  margin-bottom: 24px;
+}
+
+.searchBar input {
+  max-width: 500px;
+}
+
+/* 表格容器 */
+.tableContainer {
+  background: white;
+  border: 1px solid #e5e8eb;
+  border-radius: 4px;
+  overflow: hidden;
+}
+
+.loadingContainer {
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  padding: 60px;
+}
+
+/* 表格样式 */
+.table {
+  width: 100%;
+  border-collapse: collapse;
+  font-size: 14px;
+}
+
+.table thead {
+  background-color: #f7f9fa;
+  border-bottom: 1px solid #e5e8eb;
+}
+
+.table th {
+  text-align: left;
+  padding: 12px 16px;
+  font-weight: 500;
+  color: #666;
+  font-size: 13px;
+}
+
+.thName {
+  width: 35%;
+}
+
+.thFields {
+  width: 15%;
+}
+
+.thUpdatedBy {
+  width: 25%;
+}
+
+.thUpdated {
+  width: 25%;
+}
+
+.sortIcon {
+  margin-left: 4px;
+  color: #999;
+  font-size: 12px;
+}
+
+.table tbody tr {
+  border-bottom: 1px solid #f0f0f0;
+  transition: background-color 0.15s;
+}
+
+.table tbody tr:hover {
+  background-color: #f7f9fa;
+}
+
+.table tbody tr:last-child {
+  border-bottom: none;
+}
+
+.table td {
+  padding: 16px;
+  color: #333;
+}
+
+.tdName {
+  font-weight: 500;
+}
+
+.modelName {
+  font-size: 14px;
+  color: #1a1a1a;
+  margin-bottom: 4px;
+}
+
+.modelId {
+  font-size: 12px;
+  color: #999;
+  font-weight: 400;
+}
+
+.tdFields {
+  color: #666;
+}
+
+.userInfo {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.avatar {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  background-color: #e5e8eb;
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+}
+
+.tdUpdated {
+  color: #666;
+  font-size: 13px;
+}
+
+.emptyState {
+  text-align: center;
+  padding: 60px 20px;
+  color: #999;
+}

+ 128 - 0
src/pages/content-model/index.tsx

@@ -0,0 +1,128 @@
+import { useState, useEffect } from "react";
+import { Button, TextInput, Spinner } from "@contentful/f36-components";
+import { contentModelApi, type ContentModel } from "../../services/modules/contentModel";
+import styles from "./index.module.css";
+
+export default function ContentModelPage() {
+  const [loading, setLoading] = useState(false);
+  const [models, setModels] = useState<ContentModel[]>([]);
+  const [searchText, setSearchText] = useState("");
+
+  // 加载内容模型列表
+  const loadModels = async () => {
+    try {
+      setLoading(true);
+      const result = await contentModelApi.getList({
+        name: searchText,
+      });
+      setModels(result.records);
+    } catch (error) {
+      console.error("加载内容模型失败:", error);
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  // 创建内容模型
+  const handleCreate = () => {
+    console.log("创建内容模型");
+    // 这里可以打开创建对话框
+  };
+
+  // 组件挂载时加载数据
+  useEffect(() => {
+    loadModels();
+  }, []);
+
+  // 搜索
+  const handleSearch = (value: string) => {
+    setSearchText(value);
+  };
+
+  return (
+    <div className={styles.page}>
+      {/* 页面头部 */}
+      <div className={styles.pageHeader}>
+        <div className={styles.pageHeaderLeft}>
+          <h1 className={styles.pageTitle}>Content model</h1>
+          <span className={styles.helpIcon}>?</span>
+        </div>
+        <div className={styles.pageHeaderRight}>
+          <Button variant="secondary" size="small">
+            Visual Modeler
+          </Button>
+          <Button variant="primary" size="small" onClick={handleCreate}>
+            + Create content type
+          </Button>
+        </div>
+      </div>
+
+      {/* 搜索栏 */}
+      <div className={styles.searchBar}>
+        <TextInput
+          placeholder="Search for a content type"
+          value={searchText}
+          onChange={(e) => handleSearch(e.target.value)}
+        />
+      </div>
+
+      {/* 表格 */}
+      <div className={styles.tableContainer}>
+        {loading ? (
+          <div className={styles.loadingContainer}>
+            <Spinner />
+          </div>
+        ) : (
+          <table className={styles.table}>
+            <thead>
+              <tr>
+                <th className={styles.thName}>
+                  Name <span className={styles.sortIcon}>⇅</span>
+                </th>
+                <th className={styles.thFields}>Fields</th>
+                <th className={styles.thUpdatedBy}>Last Updated By</th>
+                <th className={styles.thUpdated}>Updated</th>
+              </tr>
+            </thead>
+            <tbody>
+              {models.length > 0 ? (
+                models.map((model) => (
+                  <tr key={model.id} className={styles.tableRow}>
+                    <td className={styles.tdName}>
+                      <div className={styles.modelName}>{model.name}</div>
+                      <div className={styles.modelId}>{model.id}</div>
+                    </td>
+                    <td className={styles.tdFields}>
+                      {model.fields?.length || 0}
+                    </td>
+                    <td className={styles.tdUpdatedBy}>
+                      <div className={styles.userInfo}>
+                        <span className={styles.avatar}>👤</span>
+                        <span>管理员</span>
+                      </div>
+                    </td>
+                    <td className={styles.tdUpdated}>
+                      {new Date(model.updatedAt).toLocaleString("zh-CN", {
+                        year: "numeric",
+                        month: "2-digit",
+                        day: "2-digit",
+                        hour: "2-digit",
+                        minute: "2-digit",
+                      })}
+                    </td>
+                  </tr>
+                ))
+              ) : (
+                <tr>
+                  <td colSpan={4} className={styles.emptyState}>
+                    暂无数据
+                  </td>
+                </tr>
+              )}
+            </tbody>
+          </table>
+        )}
+      </div>
+    </div>
+  );
+}

+ 0 - 0
src/pages/content/index.module.css


+ 0 - 0
src/pages/content/index.tsx


+ 197 - 0
src/router/index.tsx

@@ -0,0 +1,197 @@
+/**
+ * 动态路由配置
+ */
+
+import { lazy, Suspense } from "react";
+import { createBrowserRouter, Navigate, type RouteObject } from "react-router-dom";
+import MainLayout from "../layouts/MainLayout/index";
+import type { JeecgMenu } from "../types/menu";
+
+// 默认加载组件
+function LoadingPage() {
+  return <div style={{ padding: "20px" }}>加载中...</div>;
+}
+
+// 默认页面(当组件路径不存在时)
+function DefaultPage({ title }: { title: string }) {
+  return (
+    <div style={{ padding: "20px" }}>
+      <h2>{title}</h2>
+      <p>页面开发中...</p>
+    </div>
+  );
+}
+
+// 使用 Vite 的 import.meta.glob 预加载所有页面组件
+// 这会在构建时扫描 src/pages 目录下的所有 .tsx 文件
+const pageModules = import.meta.glob("../pages/**/*.tsx");
+
+console.log("[Router] Available page modules:", Object.keys(pageModules));
+
+// 动态加载组件
+// 根据 component 配置动态导入 src/pages 下的组件
+// 例如: "content-model/index" -> src/pages/content-model/index.tsx
+function loadComponent(componentPath: string, title: string) {
+  if (!componentPath) {
+    return <DefaultPage title={title} />;
+  }
+
+  // 清理路径:移除开头的 /,移除文件后缀
+  const cleanPath = componentPath
+    .replace(/^\//, "")
+    .replace(/\.(tsx|vue|jsx|js)$/, "");
+  
+  // 构建完整的模块路径
+  const modulePath = `../pages/${cleanPath}.tsx`;
+  
+  console.log(`[Router] Loading: "${cleanPath}" (${title})`);
+  console.log(`[Router] Module path: ${modulePath}`);
+  
+  // 检查模块是否存在
+  const moduleLoader = pageModules[modulePath];
+  
+  if (!moduleLoader) {
+    console.warn(`[Router] ✗ Module not found: ${modulePath}`);
+    console.warn(`[Router] Available modules:`, Object.keys(pageModules));
+    return <DefaultPage title={title} />;
+  }
+  
+  // 使用 lazy 加载组件
+  const Component = lazy(() => 
+    moduleLoader()
+      .then((module: any) => {
+        console.log(`[Router] ✓ Loaded: ${cleanPath}`);
+        return module;
+      })
+      .catch((error) => {
+        console.error(`[Router] ✗ Failed to load: ${cleanPath}`, error);
+        return {
+          default: () => <DefaultPage title={title} />,
+        };
+      })
+  );
+
+  return (
+    <Suspense fallback={<LoadingPage />}>
+      <Component />
+    </Suspense>
+  );
+}
+
+// 将菜单转换为路由配置
+function menuToRoute(menu: JeecgMenu, isChild = false): RouteObject | null {
+  // 跳过隐藏的菜单
+  if (menu.hidden || menu.meta?.hideMenu) {
+    return null;
+  }
+
+  // 处理路径
+  let routePath = menu.path || "";
+  
+  // 如果是子路由且路径是绝对路径,只取最后一段作为相对路径
+  if (isChild && routePath.startsWith("/")) {
+    const segments = routePath.split("/").filter(Boolean);
+    routePath = segments[segments.length - 1] || "";
+  }
+
+  const route: RouteObject = {
+    path: routePath,
+  };
+
+  // 如果有子菜单
+  if (menu.children && menu.children.length > 0) {
+    const childRoutes = menu.children
+      .map((child) => menuToRoute(child, true))
+      .filter((r): r is RouteObject => r !== null);
+
+    if (childRoutes.length > 0) {
+      // 有子菜单,父路由作为容器,使用 Outlet
+      // 获取第一个可见子菜单
+      const firstChild = menu.children.find((child) => !child.hidden && !child.meta?.hideMenu);
+      
+      if (firstChild) {
+        // 获取第一个子路由的相对路径
+        const firstChildPath = firstChild.path || "";
+        const segments = firstChildPath.split("/").filter(Boolean);
+        const redirectPath = segments[segments.length - 1] || "";
+
+        route.children = [
+          // 默认重定向到第一个子菜单
+          {
+            index: true,
+            element: <Navigate to={redirectPath} replace />,
+          },
+          ...childRoutes,
+        ];
+      } else {
+        route.children = childRoutes;
+      }
+    } else {
+      // 有 children 但都被隐藏了,当作叶子节点处理
+      const title = menu.meta?.title || menu.name;
+      const component = menu.component || "";
+      // 跳过 layouts 组件
+      if (component && !component.includes("layouts/")) {
+        route.element = loadComponent(component, title);
+      } else {
+        route.element = <DefaultPage title={title} />;
+      }
+    }
+  } else {
+    // 没有子菜单,加载组件
+    const title = menu.meta?.title || menu.name;
+    const component = menu.component || "";
+    // 跳过 layouts 组件
+    if (component && !component.includes("layouts/")) {
+      route.element = loadComponent(component, title);
+    } else {
+      route.element = <DefaultPage title={title} />;
+    }
+  }
+
+  return route;
+}
+
+// 根据菜单数据生成路由
+export function generateRoutes(menus: JeecgMenu[]): RouteObject[] {
+  const routes = menus
+    .map((menu) => menuToRoute(menu))
+    .filter((r): r is RouteObject => r !== null);
+
+  return [
+    {
+      path: "/",
+      element: <MainLayout />,
+      children: [
+        {
+          index: true,
+          element: <Navigate to={menus[0]?.path || "/content-model"} replace />,
+        },
+        ...routes,
+      ],
+    },
+  ];
+}
+
+// 创建路由器(初始为空路由)
+export function createAppRouter(menus: JeecgMenu[]) {
+  const routes = generateRoutes(menus);
+  
+  // 调试:打印路由路径结构
+  console.log("=== Generated Routes ===");
+  routes.forEach((route) => {
+    console.log(`Route: ${route.path}`);
+    if (route.children) {
+      route.children.forEach((child) => {
+        console.log(`  - Child: ${child.path || "index"}`);
+        if (child.children) {
+          child.children.forEach((grandChild) => {
+            console.log(`    - GrandChild: ${grandChild.path || "index"}`);
+          });
+        }
+      });
+    }
+  });
+  
+  return createBrowserRouter(routes);
+}

+ 103 - 0
src/services/BaseService.ts

@@ -0,0 +1,103 @@
+/**
+ * 基础服务类
+ * 提供通用的 CRUD 操作,参考 JeecgBoot 的 CommonAPI
+ */
+
+import { defHttp as http } from "../http";
+
+export interface PageParams {
+  pageNo: number;
+  pageSize: number;
+  column?: string; // 排序字段
+  order?: "asc" | "desc"; // 排序方式
+  [key: string]: any; // 其他查询参数
+}
+
+export interface PageResult<T> {
+  records: T[];
+  total: number;
+  size: number;
+  current: number;
+  pages: number;
+}
+
+export class BaseService<T = any> {
+  protected baseUrl: string;
+
+  constructor(baseUrl: string) {
+    // 直接使用传入的路径,前缀由 http 工具自动添加
+    this.baseUrl = baseUrl;
+  }
+
+  /**
+   * 分页查询列表
+   */
+  list(params: Partial<PageParams>) {
+    return http.get<PageResult<T>>({ url: `${this.baseUrl}/list`, params });
+  }
+
+  /**
+   * 查询所有数据(不分页)
+   */
+  queryAll(params?: Record<string, any>) {
+    return http.get<T[]>({ url: `${this.baseUrl}/queryAll`, params });
+  }
+
+  /**
+   * 根据ID查询
+   */
+  getById(id: string) {
+    return http.get<T>({ url: `${this.baseUrl}/queryById`, params: { id } });
+  }
+
+  /**
+   * 新增
+   */
+  add(data: Partial<T>) {
+    return http.post({ url: `${this.baseUrl}/add`, data });
+  }
+
+  /**
+   * 编辑
+   */
+  edit(data: Partial<T>) {
+    return http.put({ url: `${this.baseUrl}/edit`, data });
+  }
+
+  /**
+   * 删除
+   */
+  delete(id: string) {
+    return http.delete({ url: `${this.baseUrl}/delete`, params: { id } });
+  }
+
+  /**
+   * 批量删除
+   */
+  deleteBatch(ids: string[]) {
+    return http.delete({ url: `${this.baseUrl}/deleteBatch`, params: { ids: ids.join(",") } });
+  }
+
+  /**
+   * 导出Excel
+   */
+  exportXls(params?: Record<string, any>, filename = "导出数据.xls") {
+    return http.download(`${this.baseUrl}/exportXls`, filename, params);
+  }
+
+  /**
+   * 导入Excel
+   */
+  importExcel(file: File) {
+    const formData = new FormData();
+    formData.append("file", file);
+    return http.upload(`${this.baseUrl}/importExcel`, formData);
+  }
+}
+
+/**
+ * 创建服务实例的工厂函数
+ */
+export function createService<T = any>(baseUrl: string) {
+  return new BaseService<T>(baseUrl);
+}

+ 23 - 0
src/services/api.ts

@@ -0,0 +1,23 @@
+/**
+ * API 统一导出
+ * 从各个模块导入并重新导出
+ */
+
+import { defHttp } from "../http";
+
+// 导出系统管理相关 API
+export { authApi, permissionApi, dictApi, uploadApi, sysApi } from "./modules/system";
+
+// 兼容旧的导出方式
+export { permissionApi as permissionService } from "./modules/system";
+
+// 便捷方法
+import { authApi, permissionApi } from "./modules/system";
+
+export const getUserPermissions = permissionApi.getUserPermissions;
+export const login = (username: string, password: string) => authApi.login({ username, password });
+export const logout = authApi.logout;
+
+// 导出 http 实例
+export const http = defHttp;
+export default defHttp;

+ 21 - 0
src/services/index.ts

@@ -0,0 +1,21 @@
+/**
+ * 服务统一导出
+ */
+
+export * from "./api";
+export * from "./BaseService";
+
+// 系统管理模块
+export {
+  authApi,
+  permissionApi,
+  dictApi,
+  uploadApi,
+  sysApi,
+} from "./modules/system";
+
+// 业务模块服务
+export { default as userService } from "./modules/user";
+
+// 导出类型
+export type { User } from "./modules/user";

+ 67 - 0
src/services/modules/contentModel.ts

@@ -0,0 +1,67 @@
+/**
+ * 内容模型服务
+ */
+
+import { defHttp } from "../../http";
+
+export interface ContentModel {
+  id: string;
+  name: string;
+  description: string;
+  fields: Array<{
+    name: string;
+    type: string;
+    required: boolean;
+  }>;
+  createdAt: string;
+  updatedAt: string;
+}
+
+export interface ContentModelListParams {
+  pageNo?: number;
+  pageSize?: number;
+  name?: string;
+}
+
+export interface ContentModelListResult {
+  records: ContentModel[];
+  total: number;
+}
+
+/**
+ * 内容模型 API
+ */
+export const contentModelApi = {
+  // 获取内容模型列表
+  getList: (params?: ContentModelListParams) =>
+    defHttp.get<ContentModelListResult>({
+      url: "/api/content-model/list",
+      params,
+    }),
+
+  // 获取内容模型详情
+  getDetail: (id: string) =>
+    defHttp.get<ContentModel>({
+      url: `/api/content-model/${id}`,
+    }),
+
+  // 创建内容模型
+  create: (data: Partial<ContentModel>) =>
+    defHttp.post<ContentModel>({
+      url: "/api/content-model",
+      data,
+    }),
+
+  // 更新内容模型
+  update: (id: string, data: Partial<ContentModel>) =>
+    defHttp.put<ContentModel>({
+      url: `/api/content-model/${id}`,
+      data,
+    }),
+
+  // 删除内容模型
+  delete: (id: string) =>
+    defHttp.delete({
+      url: `/api/content-model/${id}`,
+    }),
+};

+ 92 - 0
src/services/modules/system.ts

@@ -0,0 +1,92 @@
+/**
+ * 系统管理服务
+ */
+
+import { defHttp } from "../../http";
+import type { JeecgUserPermissions } from "../../types/menu";
+
+const http = defHttp;
+
+/**
+ * 登录登出相关
+ */
+export const authApi = {
+  // 登录
+  login: (data: { username: string; password: string; captcha?: string }) =>
+    http.post<{ token: string; userInfo: any }>({ url: "/sys/login", data }),
+
+  // 登出
+  logout: () => http.post({ url: "/sys/logout" }),
+
+  // 获取用户信息
+  getUserInfo: () => http.get({ url: "/sys/user/getUserInfo" }),
+
+  // 修改密码
+  changePassword: (data: { oldPassword: string; newPassword: string; confirmPassword: string }) =>
+    http.put({ url: "/sys/user/changePassword", data }),
+};
+
+/**
+ * 权限管理相关
+ */
+export const permissionApi = {
+  // 获取用户菜单权限
+  getUserPermissions: () => http.get<JeecgUserPermissions>({ url: "/sys/permission/getUserPermissionByToken" }),
+
+  // 查询角色权限
+  queryRolePermission: (roleId: string) =>
+    http.get({ url: "/sys/permission/queryRolePermission", params: { roleId } }),
+
+  // 保存角色权限
+  saveRolePermission: (roleId: string, permissionIds: string[], lastPermissionIds?: string[]) =>
+    http.post({
+      url: "/sys/permission/saveRolePermission",
+      data: {
+        roleId,
+        permissionIds: permissionIds.join(","),
+        lastpermissionIds: lastPermissionIds?.join(","),
+      },
+    }),
+};
+
+/**
+ * 字典管理相关
+ */
+export const dictApi = {
+  // 获取字典数据
+  getDictItems: (dictCode: string) =>
+    http.get<Array<{ text: string; value: string }>>({ url: `/sys/dict/getDictItems/${dictCode}` }),
+
+  // 批量获取字典数据
+  queryAllDictItems: () => http.get({ url: "/sys/dict/queryAllDictItems" }),
+};
+
+/**
+ * 文件上传相关
+ */
+export const uploadApi = {
+  // 上传文件
+  upload: (file: File) => {
+    return http.uploadFile<{ message: string; url: string }>(
+      { url: "/sys/common/upload" },
+      { file, name: "file" }
+    );
+  },
+
+  // 批量上传
+  uploadBatch: (files: File[]) => {
+    const formData = new FormData();
+    files.forEach((file) => formData.append("files", file));
+    return http.post<{ message: string; urls: string[] }>({
+      url: "/sys/common/uploadBatch",
+      data: formData,
+      headers: { "Content-Type": "multipart/form-data" },
+    });
+  },
+};
+
+// 导出系统 API(兼容旧版本)
+export const sysApi = {
+  ...authApi,
+  ...permissionApi,
+};

+ 78 - 0
src/services/modules/user.ts

@@ -0,0 +1,78 @@
+/**
+ * 用户管理服务
+ */
+
+import { BaseService } from "../BaseService";
+import { defHttp as http } from "../../http";
+
+export interface User {
+  id: string;
+  username: string;
+  realname: string;
+  avatar?: string;
+  birthday?: string;
+  sex?: number;
+  email?: string;
+  phone?: string;
+  orgCode?: string;
+  status?: number;
+  delFlag?: number;
+  workNo?: string;
+  post?: string;
+  telephone?: string;
+  createBy?: string;
+  createTime?: string;
+  updateBy?: string;
+  updateTime?: string;
+}
+
+class UserService extends BaseService<User> {
+  constructor() {
+    super("/sys/user");
+  }
+
+  /**
+   * 冻结/解冻用户
+   */
+  frozenBatch(ids: string[], status: 1 | 2) {
+    return http.put({ url: "/sys/user/frozenBatch", data: { ids: ids.join(","), status } });
+  }
+
+  /**
+   * 修改密码
+   */
+  changePassword(userId: string, password: string) {
+    return http.put({ url: "/sys/user/changePassword", data: { userId, password } });
+  }
+
+  /**
+   * 查询用户角色
+   */
+  getUserRoles(userId: string) {
+    return http.get<string[]>({ url: "/sys/user/queryUserRole", params: { userid: userId } });
+  }
+
+  /**
+   * 根据部门查询用户
+   */
+  queryByDepId(depId: string) {
+    return http.get<User[]>({ url: "/sys/user/queryByDepId", params: { depId } });
+  }
+
+  /**
+   * 用户注册
+   */
+  register(data: Partial<User> & { password: string }) {
+    return http.post({ url: "/sys/user/register", data });
+  }
+
+  /**
+   * 校验用户是否存在
+   */
+  checkOnlyUser(username: string, id?: string) {
+    return http.get<boolean>({ url: "/sys/user/checkOnlyUser", params: { username, id } });
+  }
+}
+
+export const userService = new UserService();
+export default userService;

+ 43 - 0
src/types/axios.d.ts

@@ -0,0 +1,43 @@
+/**
+ * Axios 相关类型定义
+ */
+
+export interface RequestOptions {
+  // 是否返回原生响应头
+  isReturnNativeResponse?: boolean;
+  // 需要对返回数据进行处理
+  isTransformResponse?: boolean;
+  // post请求的时候添加参数到url
+  joinParamsToUrl?: boolean;
+  // 格式化提交参数时间
+  formatDate?: boolean;
+  // 消息提示类型
+  errorMessageMode?: ErrorMessageMode;
+  // 成功消息提示类型
+  successMessageMode?: SuccessMessageMode;
+  // 接口地址
+  apiUrl?: string;
+  // 接口拼接地址
+  urlPrefix?: string;
+  // 是否添加前缀
+  joinPrefix?: boolean;
+  // 是否加入时间戳
+  joinTime?: boolean;
+  // 忽略重复请求
+  ignoreCancelToken?: boolean;
+  // 是否携带token
+  withToken?: boolean;
+  // 跳过错误处理
+  skipErrorHandler?: boolean;
+}
+
+export type ErrorMessageMode = "none" | "modal" | "message" | undefined;
+export type SuccessMessageMode = "none" | "success" | undefined;
+
+export interface Result<T = any> {
+  code: number;
+  message: string;
+  result: T;
+  success: boolean;
+  timestamp?: number;
+}

+ 39 - 0
src/types/menu.ts

@@ -0,0 +1,39 @@
+// JeecgBoot 菜单数据结构
+export interface JeecgMenu {
+  id: string;
+  name: string;
+  path?: string;
+  component?: string;
+  redirect?: string | null;
+  route?: string;
+  hidden?: boolean;
+  meta?: {
+    title: string;
+    icon?: string;
+    hideMenu?: boolean;
+    hideChildrenInMenu?: boolean;
+    keepAlive?: boolean;
+    internalOrExternal?: boolean;
+    componentName?: string;
+  };
+  children?: JeecgMenu[];
+}
+
+// jeecgBoot 用户权限数据
+export interface JeecgUserPermissions {
+  menu: JeecgMenu[]
+}
+
+// 权限按钮配置
+export interface PermissionButton {
+  action: string; // 权限标识,如 'add', 'edit', 'delete', 'view'
+  describe: string; // 描述
+  type?: string; // 按钮类型
+}
+
+// 权限配置
+export interface Permission {
+  id: string;
+  menuId: string;
+  actions: string[]; // 用户拥有的权限列表,如 ['add', 'edit', 'delete', 'view']
+}

+ 46 - 0
src/utils/message.ts

@@ -0,0 +1,46 @@
+/**
+ * 消息提示工具
+ * 使用 Contentful F36 的 Notification 组件
+ */
+
+import { Notification } from "@contentful/f36-components";
+
+export const message = {
+  /**
+   * 成功提示
+   */
+  success: (content: string, title?: string) => {
+    Notification.success(title ? `${title}: ${content}` : content, {
+      duration: 3000,
+    });
+  },
+
+  /**
+   * 错误提示
+   */
+  error: (content: string, title?: string) => {
+    Notification.error(title ? `${title}: ${content}` : content, {
+      duration: 4000,
+    });
+  },
+
+  /**
+   * 警告提示
+   */
+  warning: (content: string, title?: string) => {
+    Notification.warning(title ? `${title}: ${content}` : content, {
+      duration: 3000,
+    });
+  },
+
+  /**
+   * 信息提示
+   */
+  info: (content: string, title?: string) => {
+    Notification.info(title ? `${title}: ${content}` : content, {
+      duration: 3000,
+    });
+  },
+};
+
+export default message;

+ 102 - 0
src/utils/sign.ts

@@ -0,0 +1,102 @@
+/**
+ * 请求签名工具
+ * 参考 JeecgBoot 的 signMd5Utils
+ */
+
+import CryptoJS from "crypto-js";
+
+// 签名密钥(需要与后端约定)
+const SIGN_KEY = "DD05F1C54D63749EEEB10B4F8B6830B1";
+
+/**
+ * 生成时间戳
+ */
+export function getTimestamp(): string {
+  return Date.now().toString();
+}
+
+/**
+ * 对象排序
+ */
+function sortObject(obj: Record<string, any>): Record<string, any> {
+  const sorted: Record<string, any> = {};
+  Object.keys(obj)
+    .sort()
+    .forEach((key) => {
+      sorted[key] = obj[key];
+    });
+  return sorted;
+}
+
+/**
+ * 将对象转换为查询字符串
+ */
+function objectToQueryString(obj: Record<string, any>): string {
+  if (!obj || Object.keys(obj).length === 0) {
+    return "";
+  }
+
+  const sorted = sortObject(obj);
+  return Object.entries(sorted)
+    .filter(([, value]) => value !== undefined && value !== null && value !== "")
+    .map(([key, value]) => {
+      if (typeof value === "object") {
+        return `${key}=${JSON.stringify(value)}`;
+      }
+      return `${key}=${value}`;
+    })
+    .join("&");
+}
+
+/**
+ * 生成签名
+ * @param url 请求URL
+ * @param params GET参数或POST参数
+ * @param data POST请求体数据
+ */
+export function generateSign(url: string, params?: Record<string, any>, data?: any): string {
+  try {
+    // 提取URL路径(去除域名和查询参数)
+    let path = url;
+    if (url.includes("?")) {
+      path = url.split("?")[0];
+    }
+    if (url.startsWith("http")) {
+      const urlObj = new URL(url);
+      path = urlObj.pathname;
+    }
+
+    // 合并所有参数
+    let allParams: Record<string, any> = {};
+
+    if (params && typeof params === "object") {
+      allParams = { ...allParams, ...params };
+    }
+
+    if (data && typeof data === "object" && !(data instanceof FormData)) {
+      allParams = { ...allParams, ...data };
+    }
+
+    // 生成查询字符串
+    const queryString = objectToQueryString(allParams);
+
+    // 拼接签名字符串:路径 + 查询字符串 + 密钥
+    const signStr = queryString ? `${path}${queryString}${SIGN_KEY}` : `${path}${SIGN_KEY}`;
+
+    // MD5 加密
+    const sign = CryptoJS.MD5(signStr).toString();
+
+    return sign;
+  } catch (error) {
+    console.error("生成签名失败:", error);
+    return "";
+  }
+}
+
+/**
+ * 设置签名密钥(可选,用于动态配置)
+ */
+export function setSignKey(key: string): void {
+  // 这里可以实现动态设置密钥的逻辑
+  console.warn("动态设置签名密钥功能待实现");
+}

+ 68 - 0
src/utils/tenant.ts

@@ -0,0 +1,68 @@
+/**
+ * 多租户管理工具
+ */
+
+const TENANT_ID_KEY = "TENANT_ID";
+const SHARE_TENANT_ID_KEY = "SHARE_TENANT_ID";
+
+/**
+ * 获取租户ID
+ */
+export function getTenantId(): string | number {
+  const tenantId = localStorage.getItem(TENANT_ID_KEY);
+  return tenantId ? (isNaN(Number(tenantId)) ? tenantId : Number(tenantId)) : 0;
+}
+
+/**
+ * 设置租户ID
+ */
+export function setTenantId(tenantId: string | number): void {
+  localStorage.setItem(TENANT_ID_KEY, String(tenantId));
+}
+
+/**
+ * 移除租户ID
+ */
+export function removeTenantId(): void {
+  localStorage.removeItem(TENANT_ID_KEY);
+}
+
+/**
+ * 获取分享租户ID(临时租户ID)
+ */
+export function getShareTenantId(): string | number {
+  const shareTenantId = localStorage.getItem(SHARE_TENANT_ID_KEY);
+  return shareTenantId ? (isNaN(Number(shareTenantId)) ? shareTenantId : Number(shareTenantId)) : 0;
+}
+
+/**
+ * 设置分享租户ID
+ */
+export function setShareTenantId(tenantId: string | number): void {
+  localStorage.setItem(SHARE_TENANT_ID_KEY, String(tenantId));
+}
+
+/**
+ * 移除分享租户ID
+ */
+export function removeShareTenantId(): void {
+  localStorage.removeItem(SHARE_TENANT_ID_KEY);
+}
+
+/**
+ * 是否有分享租户ID
+ */
+export function hasShareTenantId(): boolean {
+  const shareTenantId = getShareTenantId();
+  return shareTenantId !== 0 && shareTenantId !== "0";
+}
+
+/**
+ * 获取当前有效的租户ID(优先使用分享租户ID)
+ */
+export function getCurrentTenantId(): string | number {
+  if (hasShareTenantId()) {
+    return getShareTenantId();
+  }
+  return getTenantId();
+}

+ 28 - 0
tsconfig.app.json

@@ -0,0 +1,28 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+    "target": "ES2022",
+    "useDefineForClassFields": true,
+    "lib": ["ES2022", "DOM", "DOM.Iterable"],
+    "module": "ESNext",
+    "types": ["vite/client"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+    "jsx": "react-jsx",
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["src"]
+}

+ 7 - 0
tsconfig.json

@@ -0,0 +1,7 @@
+{
+  "files": [],
+  "references": [
+    { "path": "./tsconfig.app.json" },
+    { "path": "./tsconfig.node.json" }
+  ]
+}

+ 26 - 0
tsconfig.node.json

@@ -0,0 +1,26 @@
+{
+  "compilerOptions": {
+    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+    "target": "ES2023",
+    "lib": ["ES2023"],
+    "module": "ESNext",
+    "types": ["node"],
+    "skipLibCheck": true,
+
+    /* Bundler mode */
+    "moduleResolution": "bundler",
+    "allowImportingTsExtensions": true,
+    "verbatimModuleSyntax": true,
+    "moduleDetection": "force",
+    "noEmit": true,
+
+    /* Linting */
+    "strict": true,
+    "noUnusedLocals": true,
+    "noUnusedParameters": true,
+    "erasableSyntaxOnly": true,
+    "noFallthroughCasesInSwitch": true,
+    "noUncheckedSideEffectImports": true
+  },
+  "include": ["vite.config.ts"]
+}

+ 63 - 0
vite.config.ts

@@ -0,0 +1,63 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+import path from "path";
+
+export default defineConfig(() => {
+  // 加载环境变量
+  return {
+    plugins: [react()],
+
+    // 路径别名
+    resolve: {
+      alias: {
+        "@": path.resolve(__dirname, "./src"),
+      },
+    },
+
+    // 开发服务器配置
+    server: {
+      port: 3000,
+      host: true, // 监听所有地址
+      open: false, // 自动打开浏览器
+
+      // 代理配置
+      proxy: {
+        // 代理所有 /sohoyw-som 开头的请求
+        "/sohoyw-som": {
+          target: "http://52.83.132.95:8080",
+          changeOrigin: true,
+          configure: (proxy) => {
+            proxy.on("proxyReq", (proxyReq) => {
+              // 添加模拟登录的 Cookie
+              proxyReq.setHeader(
+                "Authorization",
+                "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzY0NTMxMjM3fQ.15IfYua8M_cSE4i3kT86fanqO3OTG39fl2EJYXSQ1z8"
+              );
+            });
+          },
+        },
+      },
+    },
+
+    // 构建配置
+    build: {
+      outDir: "dist",
+      sourcemap: false,
+      // 消除打包大小超过500kb警告
+      chunkSizeWarningLimit: 2000,
+      rollupOptions: {
+        output: {
+          // 分包策略
+          manualChunks: {
+            "react-vendor": ["react", "react-dom"],
+            "contentful-vendor": [
+              "@contentful/f36-components",
+              "@contentful/f36-icons",
+              "@contentful/f36-tokens",
+            ],
+          },
+        },
+      },
+    },
+  };
+});

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff