Переглянути джерело

添加初始项目结构,包括配置文件、环境示例、数据库配置、UI组件、API路由及用户管理功能。引入必要的依赖和类型定义,设置基本的样式和布局。

硕 张 4 місяців тому
коміт
47f3261fca
100 змінених файлів з 14094 додано та 0 видалено
  1. 12 0
      .editorconfig
  2. 7 0
      .env.example
  3. 50 0
      .gitignore
  4. 275 0
      README.md
  5. 21 0
      components.json
  6. 16 0
      deploy/.env.example
  7. 40 0
      deploy/Dockerfile
  8. 142 0
      deploy/README.md
  9. 54 0
      deploy/docker-compose.yaml
  10. 47 0
      deploy/entrypoint.sh
  11. 14 0
      drizzle.config.ts
  12. 61 0
      drizzle/0000_needy_spectrum.sql
  13. 0 0
      drizzle/0001_mute_wong.sql
  14. 19 0
      drizzle/0002_fine_exiles.sql
  15. 469 0
      drizzle/meta/0000_snapshot.json
  16. 387 0
      drizzle/meta/0001_snapshot.json
  17. 723 0
      drizzle/meta/0002_snapshot.json
  18. 27 0
      drizzle/meta/_journal.json
  19. 23 0
      eslint.config.mjs
  20. 5 0
      next.config.ts
  21. 59 0
      package.json
  22. 5841 0
      pnpm-lock.yaml
  23. 5 0
      postcss.config.mjs
  24. 1 0
      public/file.svg
  25. 1 0
      public/globe.svg
  26. 1 0
      public/next.svg
  27. BIN
      public/readme/价格表.webp
  28. BIN
      public/readme/供应商.webp
  29. BIN
      public/readme/客户端.webp
  30. BIN
      public/readme/文档.webp
  31. BIN
      public/readme/统计.webp
  32. 1 0
      public/vercel.svg
  33. 1 0
      public/window.svg
  34. 125 0
      src/actions/keys.ts
  35. 142 0
      src/actions/model-prices.ts
  36. 125 0
      src/actions/providers.ts
  37. 112 0
      src/actions/statistics.ts
  38. 5 0
      src/actions/types.ts
  39. 203 0
      src/actions/users.ts
  40. 42 0
      src/app/api/auth/login/route.ts
  41. 7 0
      src/app/api/auth/logout/route.ts
  42. 38 0
      src/app/dashboard/_components/dashboard-header.tsx
  43. 51 0
      src/app/dashboard/_components/dashboard-nav.tsx
  44. 447 0
      src/app/dashboard/_components/statistics/chart.tsx
  45. 3 0
      src/app/dashboard/_components/statistics/index.ts
  46. 40 0
      src/app/dashboard/_components/statistics/time-range-selector.tsx
  47. 89 0
      src/app/dashboard/_components/statistics/wrapper.tsx
  48. 58 0
      src/app/dashboard/_components/user-menu.tsx
  49. 33 0
      src/app/dashboard/_components/user/add-user-dialog.tsx
  50. 92 0
      src/app/dashboard/_components/user/forms/add-key-form.tsx
  51. 68 0
      src/app/dashboard/_components/user/forms/delete-key-confirm.tsx
  52. 66 0
      src/app/dashboard/_components/user/forms/delete-user-confirm.tsx
  53. 94 0
      src/app/dashboard/_components/user/forms/edit-key-form.tsx
  54. 126 0
      src/app/dashboard/_components/user/forms/user-form.tsx
  55. 75 0
      src/app/dashboard/_components/user/key-actions.tsx
  56. 136 0
      src/app/dashboard/_components/user/key-list-header.tsx
  57. 105 0
      src/app/dashboard/_components/user/key-list.tsx
  58. 68 0
      src/app/dashboard/_components/user/user-actions.tsx
  59. 72 0
      src/app/dashboard/_components/user/user-key-manager.tsx
  60. 70 0
      src/app/dashboard/_components/user/user-list.tsx
  61. 22 0
      src/app/dashboard/layout.tsx
  62. 45 0
      src/app/dashboard/page.tsx
  63. BIN
      src/app/favicon.ico
  64. 123 0
      src/app/globals.css
  65. 23 0
      src/app/layout.tsx
  66. 134 0
      src/app/login/page.tsx
  67. 5 0
      src/app/page.tsx
  68. 46 0
      src/app/settings/_components/settings-nav.tsx
  69. 15 0
      src/app/settings/_components/settings-page-header.tsx
  70. 10 0
      src/app/settings/_lib/nav-items.ts
  71. 19 0
      src/app/settings/config/page.tsx
  72. 37 0
      src/app/settings/layout.tsx
  73. 8 0
      src/app/settings/page.tsx
  74. 170 0
      src/app/settings/prices/_components/price-list.tsx
  75. 288 0
      src/app/settings/prices/_components/upload-price-dialog.tsx
  76. 37 0
      src/app/settings/prices/page.tsx
  77. 25 0
      src/app/settings/providers/_components/add-provider-dialog.tsx
  78. 274 0
      src/app/settings/providers/_components/forms/provider-form.tsx
  79. 227 0
      src/app/settings/providers/_components/hooks/use-provider-edit.ts
  80. 306 0
      src/app/settings/providers/_components/provider-list-item.tsx
  81. 34 0
      src/app/settings/providers/_components/provider-list.tsx
  82. 19 0
      src/app/settings/providers/_components/provider-manager.tsx
  83. 32 0
      src/app/settings/providers/page.tsx
  84. 33 0
      src/app/usage-doc/layout.tsx
  85. 338 0
      src/app/usage-doc/page.tsx
  86. 18 0
      src/app/v1/[...route]/route.ts
  87. 96 0
      src/app/v1/_lib/headers.ts
  88. 32 0
      src/app/v1/_lib/proxy-handler.ts
  89. 79 0
      src/app/v1/_lib/proxy/auth-guard.ts
  90. 18 0
      src/app/v1/_lib/proxy/error-handler.ts
  91. 46 0
      src/app/v1/_lib/proxy/forwarder.ts
  92. 86 0
      src/app/v1/_lib/proxy/logger.ts
  93. 27 0
      src/app/v1/_lib/proxy/message-service.ts
  94. 81 0
      src/app/v1/_lib/proxy/provider-selector.ts
  95. 184 0
      src/app/v1/_lib/proxy/response-handler.ts
  96. 17 0
      src/app/v1/_lib/proxy/responses.ts
  97. 163 0
      src/app/v1/_lib/proxy/session.ts
  98. 30 0
      src/app/v1/_lib/url.ts
  99. 119 0
      src/components/error-boundary.tsx
  100. 34 0
      src/components/form-error-boundary.tsx

+ 12 - 0
.editorconfig

@@ -0,0 +1,12 @@
+# EditorConfig is awesome: https://EditorConfig.org
+
+# top-most EditorConfig file
+root = true
+
+[*]
+indent_style = space
+indent_size = 2
+end_of_line = lf
+charset = utf-8
+trim_trailing_whitespace = false
+insert_final_newline = false

+ 7 - 0
.env.example

@@ -0,0 +1,7 @@
+
+ADMIN_TOKEN=change-me
+
+AUTO_MIGRATE=true
+
+DSN="postgres://user:password@host:port/db_name"
+

+ 50 - 0
.gitignore

@@ -0,0 +1,50 @@
+# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
+
+# dependencies
+/node_modules
+/.pnp
+.pnp.*
+.yarn/*
+!.yarn/patches
+!.yarn/plugins
+!.yarn/releases
+!.yarn/versions
+
+# testing
+/coverage
+
+# next.js
+/.next/
+/out/
+
+
+# production
+/build
+
+# misc
+.DS_Store
+*.pem
+
+# debug
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+.pnpm-debug.log*
+
+# env files (can opt-in for committing if needed)
+.env*
+!.env.example
+
+# vercel
+.vercel
+
+# typescript
+*.tsbuildinfo
+next-env.d.ts
+
+# tooling & logs
+.cursor/
+.claude/
+.serena/
+logs/
+.idea/

+ 275 - 0
README.md

@@ -0,0 +1,275 @@
+# Claude Code Hub
+
+> 一个现代化的 AI API 代理服务,提供智能负载均衡、用户管理和使用统计功能。
+
+[![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)
+[![Next.js](https://img.shields.io/badge/Next.js-15.4-black)](https://nextjs.org/)
+[![React](https://img.shields.io/badge/React-19.1-blue)](https://reactjs.org/)
+[![Tailwind CSS](https://img.shields.io/badge/Tailwind%20CSS-4.1-38bdf8)](https://tailwindcss.com/)
+[![pnpm](https://img.shields.io/badge/pnpm-9.15-ffbf00)](https://pnpm.io/)
+
+## ✨ 核心特性
+
+- 🔄 **智能代理** - 基于 Hono 的高性能 API 代理,支持流式响应 (SSE)
+- ⚖️ **负载均衡** - 智能分发请求到多个上游提供商,支持权重配置
+- 👥 **用户管理** - 多用户支持,细粒度的权限和配额控制
+- 🔑 **密钥管理** - 用户密钥生成、管理和权限控制
+- 📊 **使用统计** - 详细的请求统计、成本分析和可视化图表
+- 🎨 **现代界面** - 基于 shadcn/ui 的响应式管理面板
+- 💾 **数据持久化** - PostgreSQL + Drizzle ORM 的可靠数据存储
+
+## 🏗️ 技术栈
+
+### 前端
+- **Next.js 15** - 使用 App Router 的全栈框架
+- **React 19** - 最新的 React 特性支持
+- **TypeScript** - 类型安全的开发体验
+- **Tailwind CSS v4** - 实用优先的 CSS 框架
+- **shadcn/ui** - 高质量的 React 组件库
+
+### 后端
+- **Hono** - 轻量级、高性能的 Web 框架
+- **Drizzle ORM** - 类型安全的 SQL ORM
+- **PostgreSQL** - 生产级关系型数据库
+- **Node.js** - 稳定的 JavaScript 运行时
+
+## 🚀 快速开始
+
+### 环境要求
+
+- **pnpm** ≥ 9.15.0
+- **Node.js** ≥ 18
+- **PostgreSQL** ≥ 12
+
+### 安装与运行
+
+1. **克隆仓库**
+   ```bash
+   git clone https://github.com/your-username/claude-code-hub.git
+   cd claude-code-hub
+   ```
+
+2. **安装依赖**
+   ```bash
+   pnpm install
+   ```
+
+3. **配置环境变量**
+
+   复制环境变量模板:
+   ```bash
+   cp .env.example .env.local
+   ```
+
+   编辑 `.env.local` 文件:
+   ```bash
+   # 应用配置
+   NODE_ENV=development
+
+   # 管理员令牌
+   ADMIN_TOKEN=your-secure-admin-token
+
+   # 数据库连接
+   DATABASE_URL=postgres://user:password@localhost:5432/claude_code_hub
+   ```
+
+4. **初始化数据库**
+   ```bash
+   # 生成迁移文件
+   pnpm run db:generate
+
+   # 执行数据库迁移
+   pnpm run db:migrate
+   ```
+
+5. **启动开发服务器**
+   ```bash
+   pnpm dev
+   ```
+
+   应用将在 http://localhost:13500 启动
+
+### 生产部署
+
+1. **构建应用**
+   ```bash
+   pnpm run build
+   ```
+
+2. **启动生产服务器**
+   ```bash
+   pnpm run start
+   ```
+
+## 📁 项目结构
+
+```
+src/
+├── app/                          # Next.js App Router
+│   ├── v1/[...route]/           # API 代理路由 (Hono)
+│   ├── dashboard/               # 管理面板页面
+│   ├── login/                   # 登录页面
+│   └── layout.tsx               # 根布局
+├── actions/                     # Server Actions
+│   ├── users.ts                 # 用户相关操作
+│   ├── keys.ts                  # 密钥管理
+│   ├── providers.ts             # 供应商管理
+│   └── statistics.ts            # 统计数据
+├── repository/                  # 数据访问层
+│   ├── user.ts                  # 用户数据访问
+│   ├── key.ts                   # 密钥数据访问
+│   ├── provider.ts              # 供应商数据访问
+│   └── message.ts               # 消息记录
+├── types/                       # TypeScript 类型定义
+│   ├── user.ts                  # 用户类型
+│   ├── key.ts                   # 密钥类型
+│   ├── provider.ts              # 供应商类型
+│   └── statistics.ts            # 统计类型
+├── components/                  # 共享 UI 组件 (shadcn/ui)
+├── lib/                         # 工具函数和配置
+│   ├── config/                  # 环境配置
+│   ├── constants/               # 常量定义
+│   ├── validation/              # 数据验证
+│   └── utils.ts                 # 通用工具
+└── drizzle/                     # 数据库 schema 和迁移
+```
+
+## 🎯 主要功能
+
+### 1. API 代理服务
+
+智能代理 AI API 请求,支持:
+- 多上游提供商负载均衡
+- 基于权重的请求分发
+- 流式响应 (Server-Sent Events)
+- 请求认证和速率限制
+- 自动故障转移
+
+### 2. 用户管理系统
+
+- **多用户支持** - 支持管理员和普通用户角色
+- **配额控制** - 每日消费限额和请求频率限制
+- **权限管理** - 细粒度的功能权限控制
+
+### 3. 密钥管理
+
+- **密钥生成** - 自动生成安全的 API 密钥
+- **生命周期管理** - 支持密钥过期和禁用
+- **使用统计** - 实时监控密钥使用情况
+
+### 4. 供应商管理
+
+- **多供应商支持** - 管理多个 AI 服务提供商
+- **负载均衡配置** - 支持权重、速率限制等参数
+- **健康检查** - 自动检测和切换不可用的供应商
+
+### 5. 数据分析
+
+- **实时统计** - 请求量、成本、响应时间等指标
+- **可视化图表** - 基于 Recharts 的交互式图表
+- **历史数据** - 支持不同时间范围的数据查看
+
+## 🔧 开发命令
+
+```bash
+# 开发
+pnpm dev                    # 启动开发服务器 (Turbopack)
+
+# 构建
+pnpm run build             # 构建生产版本
+pnpm run start                 # 启动生产服务器
+
+# 代码质量
+pnpm run lint              # ESLint 检查
+pnpm run typecheck         # TypeScript 类型检查
+
+# 数据库
+pnpm run db:generate       # 生成迁移文件
+pnpm run db:migrate        # 执行数据库迁移
+pnpm run db:push           # 推送 schema 到数据库
+pnpm run db:studio         # 启动 Drizzle Studio
+```
+
+## 🎨 UI 组件
+
+项目使用 [shadcn/ui](https://ui.shadcn.com/) 组件库,提供:
+
+- 一致的设计语言
+- 高度可定制的组件
+- 无障碍访问支持
+- 深色模式支持
+
+### 添加新组件
+
+```bash
+pnpm dlx shadcn@latest add [component-name]
+```
+
+## 📊 数据库设计
+
+核心数据表:
+
+- **users** - 用户信息和配额设置
+- **keys** - API 密钥管理
+- **providers** - 上游服务提供商配置
+- **message_request** - 请求记录和统计
+- **model_prices** - 模型定价信息
+
+## 🔒 安全特性
+
+- **密钥加密存储** - 敏感信息加密保存
+- **请求认证** - 基于 JWT 的身份验证
+- **速率限制** - 防止 API 滥用
+- **权限控制** - 基于角色的访问控制
+- **输入验证** - 使用 Zod 进行严格的数据验证
+
+## 🚀 部署指南
+
+### Docker 部署
+
+```bash
+# 构建镜像
+docker build -t claude-code-hub .
+
+# 运行容器
+docker run -p 3000:3000 -e DATABASE_URL=your-db-url claude-code-hub
+```
+
+### 生产环境建议
+
+- 使用反向代理 (Nginx/Caddy)
+- 配置 HTTPS
+- 设置数据库连接池
+- 启用日志收集
+- 配置监控和告警
+
+## 🤝 贡献指南
+
+1. Fork 本仓库
+2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
+3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
+4. 推送到分支 (`git push origin feature/AmazingFeature`)
+5. 开启 Pull Request
+
+### 代码规范
+
+- 使用 TypeScript 严格模式
+- 遵循 ESLint 规则
+- 提交前运行类型检查和 lint
+- 使用有意义的提交消息
+
+## 📝 许可证
+
+本项目采用 MIT 许可证 - 查看 [LICENSE](LICENSE) 文件了解详情。
+
+## 🆘 支持与反馈
+
+- 🐛 [报告问题](https://github.com/your-username/claude-code-hub/issues)
+- 💡 [功能建议](https://github.com/your-username/claude-code-hub/discussions)
+- 📧 邮件: [email protected]
+
+---
+
+<p align="center">
+  用 ❤️ 构建,为 AI 应用开发者服务
+</p>

+ 21 - 0
components.json

@@ -0,0 +1,21 @@
+{
+  "$schema": "https://ui.shadcn.com/schema.json",
+  "style": "new-york",
+  "rsc": true,
+  "tsx": true,
+  "tailwind": {
+    "config": "",
+    "css": "src/app/globals.css",
+    "baseColor": "neutral",
+    "cssVariables": true,
+    "prefix": ""
+  },
+  "aliases": {
+    "components": "@/components",
+    "utils": "@/lib/utils",
+    "ui": "@/components/ui",
+    "lib": "@/lib",
+    "hooks": "@/hooks"
+  },
+  "iconLibrary": "lucide"
+}

+ 16 - 0
deploy/.env.example

@@ -0,0 +1,16 @@
+NODE_ENV=production
+
+ADMIN_TOKEN=change-me
+
+# 数据库配置
+DB_USER=postgres
+DB_PASSWORD=your-secure-password_change-me
+DB_NAME=claude_code_hub
+
+# 自动迁移控制(生产环境默认开启)
+# 设置为 false 可禁用自动迁移
+AUTO_MIGRATE=true
+
+# 应用配置
+PORT=23000
+

+ 40 - 0
deploy/Dockerfile

@@ -0,0 +1,40 @@
+
+# 使用 Node.js 22 slim 版本
+FROM docker.m.daocloud.io/library/node:22-slim
+
+# 启用 corepack 以使用 pnpm
+ENV PNPM_HOME=/pnpm
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable
+
+# 设置工作目录
+WORKDIR /app
+
+# 复制 package.json 和 pnpm-lock.yaml
+COPY package.json pnpm-lock.yaml ./
+
+# 安装依赖(使用 --no-frozen-lockfile 避免锁文件不同步问题)
+RUN pnpm install --no-frozen-lockfile
+
+# 复制项目文件
+COPY . .
+
+# 构建 Next.js 应用
+RUN pnpm run build
+
+# 设置环境变量
+ENV NODE_ENV=production
+ENV PORT=3000
+ENV HOST=0.0.0.0
+
+# 创建非 root 用户运行应用
+RUN chown -R node:node /app
+
+# 切换到 node 用户
+USER node
+
+# 暴露端口
+EXPOSE 3000
+
+# 启动应用(迁移将在应用启动时自动执行)
+CMD ["pnpm", "run", "start"]

+ 142 - 0
deploy/README.md

@@ -0,0 +1,142 @@
+# Docker 部署指南
+
+## 快速开始
+
+### 1. 准备环境变量
+```bash
+cd deploy
+cp .env.example ../.env.local
+# 编辑 ../.env.local 设置你的数据库密码
+```
+
+### 2. 启动服务
+```bash
+# 构建并启动所有服务(仅两个容器:数据库 + 应用)
+docker-compose up -d
+
+# 查看日志
+docker-compose logs -f app
+```
+
+### 3. 验证部署
+- 应用地址:http://localhost:23000
+- 数据库端口:35432(可用于外部连接)
+
+## 架构说明
+
+### 容器结构
+1. **postgres 容器**:PostgreSQL 16 数据库
+2. **app 容器**:Next.js 应用(包含自动迁移)
+
+### 自动迁移机制
+
+**方案选择:**
+
+#### 当前方案:代码集成(推荐)
+- 在 `src/instrumentation.ts` 中实现
+- Next.js 启动时自动执行
+- 无需额外脚本文件
+- 通过 `AUTO_MIGRATE=false` 可禁用
+
+#### 备选方案:启动脚本
+- 使用 `deploy/entrypoint.sh`
+- 更传统的 Docker 方式
+- 适合需要更多控制的场景
+
+### 迁移流程
+
+1. **应用启动时**:
+   - 检查数据库连接(最多重试 30 次)
+   - 自动运行 Drizzle 迁移
+   - 迁移成功后启动 Next.js
+
+2. **失败处理**:
+   - 数据库不可用:容器退出,Docker 自动重启
+   - 迁移失败:记录错误并退出
+
+## 常用命令
+
+```bash
+# 查看服务状态
+docker-compose ps
+
+# 重新构建并启动
+docker-compose up -d --build
+
+# 停止所有服务
+docker-compose down
+
+# 清理所有数据(危险!)
+docker-compose down -v
+
+# 手动执行迁移(开发环境)
+docker-compose exec app pnpm run db:migrate
+
+# 进入应用容器
+docker-compose exec app sh
+
+# 查看数据库
+docker-compose exec postgres psql -U postgres -d claude_code_hub
+```
+
+## 环境变量
+
+| 变量 | 说明 | 默认值 |
+|------|------|--------|
+| `DB_USER` | 数据库用户名 | postgres |
+| `DB_PASSWORD` | 数据库密码 | postgres |
+| `DB_NAME` | 数据库名称 | claude_code_hub |
+| `DSN` | 数据库连接字符串 | 自动生成 |
+| `AUTO_MIGRATE` | 是否自动迁移 | true |
+| `PORT` | 应用端口 | 23000 |
+
+## 故障排查
+
+### 数据库连接失败
+```bash
+# 检查数据库容器状态
+docker-compose ps postgres
+docker-compose logs postgres
+
+# 测试数据库连接
+docker-compose exec postgres pg_isready
+```
+
+### 迁移失败
+```bash
+# 查看迁移日志
+docker-compose logs app | grep -i migration
+
+# 手动运行迁移查看详细错误
+docker-compose exec app pnpm run db:migrate
+```
+
+### 应用无法启动
+```bash
+# 查看完整日志
+docker-compose logs -f app
+
+# 检查环境变量
+docker-compose exec app env | grep -E "(DSN|DB_|NODE_ENV)"
+```
+
+## 生产部署建议
+
+1. **安全性**:
+   - 使用强密码
+   - 限制数据库端口访问
+   - 使用 Docker secrets 管理敏感信息
+
+2. **性能**:
+   - 挂载 SSD 作为数据库存储
+   - 适当调整 PostgreSQL 配置
+   - 使用 Docker Swarm 或 K8s 实现高可用
+
+3. **备份**:
+   ```bash
+   # 备份数据库
+   docker-compose exec postgres pg_dump -U postgres claude_code_hub > backup.sql
+
+   # 恢复数据库
+   docker-compose exec -T postgres psql -U postgres claude_code_hub < backup.sql
+   ```

+ 54 - 0
deploy/docker-compose.yaml

@@ -0,0 +1,54 @@
+version: '3.9'
+
+services:
+  # PostgreSQL 数据库服务
+  postgres:
+    image: postgres:16-alpine
+    container_name: claude-code-hub-db
+    restart: unless-stopped
+    ports:
+      - "35432:5432"  # 映射到主机端口,避免冲突
+    environment:
+      POSTGRES_USER: ${DB_USER:-postgres}
+      POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
+      POSTGRES_DB: ${DB_NAME:-claude_code_hub}
+    volumes:
+      - postgres_data:/var/lib/postgresql/data
+    healthcheck:
+      test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-claude_code_hub}"]
+      interval: 5s
+      timeout: 5s
+      retries: 10
+      start_period: 10s
+    networks:
+      - app-network
+
+  # 主应用服务(包含自动迁移)
+  app:
+    build:
+      context: ..
+      dockerfile: deploy/Dockerfile
+    container_name: claude-code-hub-app
+    depends_on:
+      postgres:
+        condition: service_healthy
+    ports:
+      - "23000:23000"
+    env_file:
+      - ../.env
+    environment:
+      NODE_ENV: production
+      PORT: 23000
+      # 使用容器网络连接数据库
+      DSN: postgresql://${DB_USER:-postgres}:${DB_PASSWORD:-postgres}@postgres:5432/${DB_NAME:-claude_code_hub}
+    restart: unless-stopped
+    networks:
+      - app-network
+
+networks:
+  app-network:
+    driver: bridge
+
+volumes:
+  postgres_data:
+    driver: local

+ 47 - 0
deploy/entrypoint.sh

@@ -0,0 +1,47 @@
+#!/bin/sh
+set -e
+
+echo "🚀 Starting Claude Code Hub..."
+echo ""
+
+# 等待数据库就绪(可选,因为 docker-compose 已有健康检查)
+echo "⏳ Checking database connection..."
+MAX_RETRIES=30
+RETRY_COUNT=0
+
+while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
+    if pnpm run db:migrate 2>&1 | grep -q "Error"; then
+        echo "   Database not ready, waiting... ($((RETRY_COUNT + 1))/$MAX_RETRIES)"
+        sleep 2
+        RETRY_COUNT=$((RETRY_COUNT + 1))
+    else
+        echo "✅ Database is ready!"
+        break
+    fi
+done
+
+if [ $RETRY_COUNT -eq $MAX_RETRIES ]; then
+    echo "❌ Database connection timeout after $MAX_RETRIES attempts"
+    exit 1
+fi
+
+# 执行数据库迁移
+echo ""
+echo "🔄 Running database migrations..."
+pnpm run db:migrate
+
+if [ $? -eq 0 ]; then
+    echo "✅ Database migrations completed successfully!"
+else
+    echo "❌ Database migration failed!"
+    exit 1
+fi
+
+# 启动应用
+echo ""
+echo "🎯 Starting Next.js application on port ${PORT:-3000}..."
+echo "================================"
+echo ""
+
+# 使用 exec 替换当前进程,确保信号正确传递
+exec pnpm run start

+ 14 - 0
drizzle.config.ts

@@ -0,0 +1,14 @@
+import { config } from 'dotenv';
+import { defineConfig } from 'drizzle-kit';
+
+// Load environment variables from .env.local
+config({ path: '.env.local' });
+
+export default defineConfig({
+  out: './drizzle',
+  schema: './src/drizzle/schema.ts',
+  dialect: 'postgresql',
+  dbCredentials: {
+    url: process.env.DSN!,
+  },
+});

+ 61 - 0
drizzle/0000_needy_spectrum.sql

@@ -0,0 +1,61 @@
+CREATE TABLE "keys" (
+	"id" serial PRIMARY KEY NOT NULL,
+	"user_id" integer NOT NULL,
+	"key" varchar NOT NULL,
+	"name" varchar NOT NULL,
+	"is_enabled" boolean DEFAULT true,
+	"expires_at" timestamp,
+	"created_at" timestamp with time zone DEFAULT now(),
+	"updated_at" timestamp with time zone DEFAULT now(),
+	"deleted_at" timestamp with time zone
+);
+--> statement-breakpoint
+CREATE TABLE "message_request" (
+	"id" serial PRIMARY KEY NOT NULL,
+	"provider_id" integer NOT NULL,
+	"user_id" integer NOT NULL,
+	"key" varchar NOT NULL,
+	"message" jsonb NOT NULL,
+	"duration_ms" integer,
+	"cost_usd" numeric(10, 8) DEFAULT '0',
+	"created_at" timestamp with time zone DEFAULT now(),
+	"updated_at" timestamp with time zone DEFAULT now(),
+	"deleted_at" timestamp with time zone
+);
+--> statement-breakpoint
+CREATE TABLE "model_prices" (
+	"id" serial PRIMARY KEY NOT NULL,
+	"model_name" varchar NOT NULL,
+	"price_data" jsonb NOT NULL,
+	"created_at" timestamp with time zone DEFAULT now(),
+	"updated_at" timestamp with time zone DEFAULT now()
+);
+--> statement-breakpoint
+CREATE TABLE "providers" (
+	"id" serial PRIMARY KEY NOT NULL,
+	"name" varchar NOT NULL,
+	"description" text,
+	"url" varchar NOT NULL,
+	"key" varchar NOT NULL,
+	"is_enabled" boolean DEFAULT true NOT NULL,
+	"weight" integer DEFAULT 1 NOT NULL,
+	"tpm" integer DEFAULT 0,
+	"rpm" integer DEFAULT 0,
+	"rpd" integer DEFAULT 0,
+	"cc" integer DEFAULT 0,
+	"created_at" timestamp with time zone DEFAULT now(),
+	"updated_at" timestamp with time zone DEFAULT now(),
+	"deleted_at" timestamp with time zone
+);
+--> statement-breakpoint
+CREATE TABLE "users" (
+	"id" serial PRIMARY KEY NOT NULL,
+	"name" varchar NOT NULL,
+	"description" text,
+	"role" varchar DEFAULT 'user',
+	"rpm_limit" integer DEFAULT 60,
+	"daily_limit_usd" numeric(10, 2) DEFAULT '100.00',
+	"created_at" timestamp with time zone DEFAULT now(),
+	"updated_at" timestamp with time zone DEFAULT now(),
+	"deleted_at" timestamp with time zone
+);

+ 0 - 0
drizzle/0001_mute_wong.sql


+ 19 - 0
drizzle/0002_fine_exiles.sql

@@ -0,0 +1,19 @@
+CREATE INDEX "idx_keys_user_id" ON "keys" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "idx_keys_created_at" ON "keys" USING btree ("created_at");--> statement-breakpoint
+CREATE INDEX "idx_keys_deleted_at" ON "keys" USING btree ("deleted_at");--> statement-breakpoint
+CREATE INDEX "idx_message_request_user_date_cost" ON "message_request" USING btree ("user_id","created_at","cost_usd") WHERE "message_request"."deleted_at" IS NULL;--> statement-breakpoint
+CREATE INDEX "idx_message_request_user_query" ON "message_request" USING btree ("user_id","created_at") WHERE "message_request"."deleted_at" IS NULL;--> statement-breakpoint
+CREATE INDEX "idx_message_request_provider_id" ON "message_request" USING btree ("provider_id");--> statement-breakpoint
+CREATE INDEX "idx_message_request_user_id" ON "message_request" USING btree ("user_id");--> statement-breakpoint
+CREATE INDEX "idx_message_request_key" ON "message_request" USING btree ("key");--> statement-breakpoint
+CREATE INDEX "idx_message_request_created_at" ON "message_request" USING btree ("created_at");--> statement-breakpoint
+CREATE INDEX "idx_message_request_deleted_at" ON "message_request" USING btree ("deleted_at");--> statement-breakpoint
+CREATE INDEX "idx_model_prices_latest" ON "model_prices" USING btree ("model_name","created_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "idx_model_prices_model_name" ON "model_prices" USING btree ("model_name");--> statement-breakpoint
+CREATE INDEX "idx_model_prices_created_at" ON "model_prices" USING btree ("created_at" DESC NULLS LAST);--> statement-breakpoint
+CREATE INDEX "idx_providers_enabled_weight" ON "providers" USING btree ("is_enabled","weight") WHERE "providers"."deleted_at" IS NULL;--> statement-breakpoint
+CREATE INDEX "idx_providers_created_at" ON "providers" USING btree ("created_at");--> statement-breakpoint
+CREATE INDEX "idx_providers_deleted_at" ON "providers" USING btree ("deleted_at");--> statement-breakpoint
+CREATE INDEX "idx_users_active_role_sort" ON "users" USING btree ("deleted_at","role","id") WHERE "users"."deleted_at" IS NULL;--> statement-breakpoint
+CREATE INDEX "idx_users_created_at" ON "users" USING btree ("created_at");--> statement-breakpoint
+CREATE INDEX "idx_users_deleted_at" ON "users" USING btree ("deleted_at");

+ 469 - 0
drizzle/meta/0000_snapshot.json

@@ -0,0 +1,469 @@
+{
+  "id": "bf77b884-1979-4c4f-b6b9-205f7e0e21c2",
+  "prevId": "00000000-0000-0000-0000-000000000000",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "message": {
+          "name": "message",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(10, 8)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_response": {
+      "name": "message_response",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "message_request_id": {
+          "name": "message_request_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "event": {
+          "name": "event",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "data": {
+          "name": "data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "sequence_order": {
+          "name": "sequence_order",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 387 - 0
drizzle/meta/0001_snapshot.json

@@ -0,0 +1,387 @@
+{
+  "id": "3359cde7-0f6d-452a-a8af-3c1d97660f8e",
+  "prevId": "bf77b884-1979-4c4f-b6b9-205f7e0e21c2",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "message": {
+          "name": "message",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(10, 8)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {},
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 723 - 0
drizzle/meta/0002_snapshot.json

@@ -0,0 +1,723 @@
+{
+  "id": "f640b3c5-723e-44c5-9d20-645ea2ad0125",
+  "prevId": "3359cde7-0f6d-452a-a8af-3c1d97660f8e",
+  "version": "7",
+  "dialect": "postgresql",
+  "tables": {
+    "public.keys": {
+      "name": "keys",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": false,
+          "default": true
+        },
+        "expires_at": {
+          "name": "expires_at",
+          "type": "timestamp",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_keys_user_id": {
+          "name": "idx_keys_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_created_at": {
+          "name": "idx_keys_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_keys_deleted_at": {
+          "name": "idx_keys_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.message_request": {
+      "name": "message_request",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "provider_id": {
+          "name": "provider_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "user_id": {
+          "name": "user_id",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "message": {
+          "name": "message",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "duration_ms": {
+          "name": "duration_ms",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "cost_usd": {
+          "name": "cost_usd",
+          "type": "numeric(10, 8)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'0'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_message_request_user_date_cost": {
+          "name": "idx_message_request_user_date_cost",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "cost_usd",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_query": {
+          "name": "idx_message_request_user_query",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"message_request\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_provider_id": {
+          "name": "idx_message_request_provider_id",
+          "columns": [
+            {
+              "expression": "provider_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_user_id": {
+          "name": "idx_message_request_user_id",
+          "columns": [
+            {
+              "expression": "user_id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_key": {
+          "name": "idx_message_request_key",
+          "columns": [
+            {
+              "expression": "key",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_created_at": {
+          "name": "idx_message_request_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_message_request_deleted_at": {
+          "name": "idx_message_request_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.model_prices": {
+      "name": "model_prices",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "model_name": {
+          "name": "model_name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "price_data": {
+          "name": "price_data",
+          "type": "jsonb",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        }
+      },
+      "indexes": {
+        "idx_model_prices_latest": {
+          "name": "idx_model_prices_latest",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_model_name": {
+          "name": "idx_model_prices_model_name",
+          "columns": [
+            {
+              "expression": "model_name",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_model_prices_created_at": {
+          "name": "idx_model_prices_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": false,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.providers": {
+      "name": "providers",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "url": {
+          "name": "url",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "key": {
+          "name": "key",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "is_enabled": {
+          "name": "is_enabled",
+          "type": "boolean",
+          "primaryKey": false,
+          "notNull": true,
+          "default": true
+        },
+        "weight": {
+          "name": "weight",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": true,
+          "default": 1
+        },
+        "tpm": {
+          "name": "tpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpm": {
+          "name": "rpm",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "rpd": {
+          "name": "rpd",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "cc": {
+          "name": "cc",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 0
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_providers_enabled_weight": {
+          "name": "idx_providers_enabled_weight",
+          "columns": [
+            {
+              "expression": "is_enabled",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "weight",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"providers\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_created_at": {
+          "name": "idx_providers_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_providers_deleted_at": {
+          "name": "idx_providers_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    },
+    "public.users": {
+      "name": "users",
+      "schema": "",
+      "columns": {
+        "id": {
+          "name": "id",
+          "type": "serial",
+          "primaryKey": true,
+          "notNull": true
+        },
+        "name": {
+          "name": "name",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": true
+        },
+        "description": {
+          "name": "description",
+          "type": "text",
+          "primaryKey": false,
+          "notNull": false
+        },
+        "role": {
+          "name": "role",
+          "type": "varchar",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'user'"
+        },
+        "rpm_limit": {
+          "name": "rpm_limit",
+          "type": "integer",
+          "primaryKey": false,
+          "notNull": false,
+          "default": 60
+        },
+        "daily_limit_usd": {
+          "name": "daily_limit_usd",
+          "type": "numeric(10, 2)",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "'100.00'"
+        },
+        "created_at": {
+          "name": "created_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "updated_at": {
+          "name": "updated_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false,
+          "default": "now()"
+        },
+        "deleted_at": {
+          "name": "deleted_at",
+          "type": "timestamp with time zone",
+          "primaryKey": false,
+          "notNull": false
+        }
+      },
+      "indexes": {
+        "idx_users_active_role_sort": {
+          "name": "idx_users_active_role_sort",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "role",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            },
+            {
+              "expression": "id",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "where": "\"users\".\"deleted_at\" IS NULL",
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_created_at": {
+          "name": "idx_users_created_at",
+          "columns": [
+            {
+              "expression": "created_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        },
+        "idx_users_deleted_at": {
+          "name": "idx_users_deleted_at",
+          "columns": [
+            {
+              "expression": "deleted_at",
+              "isExpression": false,
+              "asc": true,
+              "nulls": "last"
+            }
+          ],
+          "isUnique": false,
+          "concurrently": false,
+          "method": "btree",
+          "with": {}
+        }
+      },
+      "foreignKeys": {},
+      "compositePrimaryKeys": {},
+      "uniqueConstraints": {},
+      "policies": {},
+      "checkConstraints": {},
+      "isRLSEnabled": false
+    }
+  },
+  "enums": {},
+  "schemas": {},
+  "sequences": {},
+  "roles": {},
+  "policies": {},
+  "views": {},
+  "_meta": {
+    "columns": {},
+    "schemas": {},
+    "tables": {}
+  }
+}

+ 27 - 0
drizzle/meta/_journal.json

@@ -0,0 +1,27 @@
+{
+  "version": "7",
+  "dialect": "postgresql",
+  "entries": [
+    {
+      "idx": 0,
+      "version": "7",
+      "when": 1758539474419,
+      "tag": "0000_needy_spectrum",
+      "breakpoints": true
+    },
+    {
+      "idx": 1,
+      "version": "7",
+      "when": 1758725866971,
+      "tag": "0001_mute_wong",
+      "breakpoints": true
+    },
+    {
+      "idx": 2,
+      "version": "7",
+      "when": 1758728327172,
+      "tag": "0002_fine_exiles",
+      "breakpoints": true
+    }
+  ]
+}

+ 23 - 0
eslint.config.mjs

@@ -0,0 +1,23 @@
+import { dirname } from "path";
+import { fileURLToPath } from "url";
+import { FlatCompat } from "@eslint/eslintrc";
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+const compat = new FlatCompat({
+  baseDirectory: __dirname,
+});
+
+const eslintConfig = [
+  ...compat.extends("next/core-web-vitals", "next/typescript"),
+  {
+    files: ["src/components/ui/**"],
+    rules: {
+      "@typescript-eslint/no-explicit-any": "off",
+      "@typescript-eslint/no-unused-vars": "off",
+    },
+  },
+];
+
+export default eslintConfig;

+ 5 - 0
next.config.ts

@@ -0,0 +1,5 @@
+import type { NextConfig } from 'next'
+
+const nextConfig: NextConfig = {}
+
+export default nextConfig

+ 59 - 0
package.json

@@ -0,0 +1,59 @@
+{
+  "name": "claude-code-hub",
+  "version": "0.1.0",
+  "private": true,
+  "scripts": {
+    "dev": "next dev --port 13500 --turbo",
+    "build": "next build",
+    "start": "next start",
+    "lint": "next lint",
+    "typecheck": "tsc -p tsconfig.json --noEmit",
+    "cui": "npx cui-server --host 0.0.0.0 --port 30000 --token a7564bc8882aa9a2d25d8b4ea6ea1e2e",
+    "db:generate": "drizzle-kit generate",
+    "db:migrate": "drizzle-kit migrate",
+    "db:push": "drizzle-kit push",
+    "db:studio": "drizzle-kit studio"
+  },
+  "dependencies": {
+    "@radix-ui/react-alert-dialog": "^1.1.15",
+    "@radix-ui/react-avatar": "^1.1.10",
+    "@radix-ui/react-dialog": "^1.1.15",
+    "@radix-ui/react-dropdown-menu": "^2.1.16",
+    "@radix-ui/react-label": "^2.1.7",
+    "@radix-ui/react-popover": "^1.1.15",
+    "@radix-ui/react-slider": "^1.3.6",
+    "@radix-ui/react-slot": "^1.2.3",
+    "@radix-ui/react-switch": "^1.2.6",
+    "class-variance-authority": "^0.7.1",
+    "clsx": "^2.1.1",
+    "dotenv": "^17.2.2",
+    "drizzle-orm": "^0.44.5",
+    "hono": "^4.9.6",
+    "lucide-react": "^0.544.0",
+    "next": "15.4.6",
+    "next-themes": "^0.4.6",
+    "postgres": "^3.4.7",
+    "react": "19.1.0",
+    "react-dom": "19.1.0",
+    "recharts": "2.15.4",
+    "sonner": "^2.0.7",
+    "tailwind-merge": "^3.3.1",
+    "timeago.js": "^4.0.2",
+    "tw-animate-css": "^1.3.8",
+    "zod": "^4.1.8"
+  },
+  "devDependencies": {
+    "@eslint/eslintrc": "^3.3.1",
+    "@tailwindcss/postcss": "^4.1.13",
+    "@types/node": "^20.19.13",
+    "@types/pg": "^8.15.5",
+    "@types/react": "^19.1.12",
+    "@types/react-dom": "^19.1.9",
+    "drizzle-kit": "^0.31.4",
+    "eslint": "^9.35.0",
+    "eslint-config-next": "15.4.6",
+    "tailwindcss": "^4.1.13",
+    "typescript": "^5.9.2"
+  },
+  "packageManager": "[email protected]"
+}

+ 5841 - 0
pnpm-lock.yaml

@@ -0,0 +1,5841 @@
+lockfileVersion: '9.0'
+
+settings:
+  autoInstallPeers: true
+  excludeLinksFromLockfile: false
+
+importers:
+
+  .:
+    dependencies:
+      '@radix-ui/react-alert-dialog':
+        specifier: ^1.1.15
+        version: 1.1.15(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-avatar':
+        specifier: ^1.1.10
+        version: 1.1.10(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-dialog':
+        specifier: ^1.1.15
+        version: 1.1.15(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-dropdown-menu':
+        specifier: ^2.1.16
+        version: 2.1.16(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-label':
+        specifier: ^2.1.7
+        version: 2.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-popover':
+        specifier: ^1.1.15
+        version: 1.1.15(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slider':
+        specifier: ^1.3.6
+        version: 1.3.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot':
+        specifier: ^1.2.3
+        version: 1.2.3(@types/[email protected])([email protected])
+      '@radix-ui/react-switch':
+        specifier: ^1.2.6
+        version: 1.2.6(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      class-variance-authority:
+        specifier: ^0.7.1
+        version: 0.7.1
+      clsx:
+        specifier: ^2.1.1
+        version: 2.1.1
+      dotenv:
+        specifier: ^17.2.2
+        version: 17.2.2
+      drizzle-orm:
+        specifier: ^0.44.5
+        version: 0.44.5(@types/[email protected])([email protected](@types/[email protected]))([email protected])
+      hono:
+        specifier: ^4.9.6
+        version: 4.9.8
+      lucide-react:
+        specifier: ^0.544.0
+        version: 0.544.0([email protected])
+      next:
+        specifier: 15.4.6
+        version: 15.4.6([email protected]([email protected]))([email protected])
+      next-themes:
+        specifier: ^0.4.6
+        version: 0.4.6([email protected]([email protected]))([email protected])
+      postgres:
+        specifier: ^3.4.7
+        version: 3.4.7
+      react:
+        specifier: 19.1.0
+        version: 19.1.0
+      react-dom:
+        specifier: 19.1.0
+        version: 19.1.0([email protected])
+      recharts:
+        specifier: 2.15.4
+        version: 2.15.4([email protected]([email protected]))([email protected])
+      sonner:
+        specifier: ^2.0.7
+        version: 2.0.7([email protected]([email protected]))([email protected])
+      tailwind-merge:
+        specifier: ^3.3.1
+        version: 3.3.1
+      timeago.js:
+        specifier: ^4.0.2
+        version: 4.0.2
+      tw-animate-css:
+        specifier: ^1.3.8
+        version: 1.4.0
+      zod:
+        specifier: ^4.1.8
+        version: 4.1.11
+    devDependencies:
+      '@eslint/eslintrc':
+        specifier: ^3.3.1
+        version: 3.3.1
+      '@tailwindcss/postcss':
+        specifier: ^4.1.13
+        version: 4.1.13
+      '@types/node':
+        specifier: ^20.19.13
+        version: 20.19.17
+      '@types/pg':
+        specifier: ^8.15.5
+        version: 8.15.5
+      '@types/react':
+        specifier: ^19.1.12
+        version: 19.1.13
+      '@types/react-dom':
+        specifier: ^19.1.9
+        version: 19.1.9(@types/[email protected])
+      drizzle-kit:
+        specifier: ^0.31.4
+        version: 0.31.4
+      eslint:
+        specifier: ^9.35.0
+        version: 9.36.0([email protected])
+      eslint-config-next:
+        specifier: 15.4.6
+        version: 15.4.6([email protected]([email protected]))([email protected])
+      tailwindcss:
+        specifier: ^4.1.13
+        version: 4.1.13
+      typescript:
+        specifier: ^5.9.2
+        version: 5.9.2
+
+packages:
+
+  '@alloc/[email protected]':
+    resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
+    engines: {node: '>=10'}
+
+  '@babel/[email protected]':
+    resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
+    engines: {node: '>=6.9.0'}
+
+  '@drizzle-team/[email protected]':
+    resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==}
+
+  '@emnapi/[email protected]':
+    resolution: {integrity: sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==}
+
+  '@emnapi/[email protected]':
+    resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==}
+
+  '@emnapi/[email protected]':
+    resolution: {integrity: sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==}
+
+  '@esbuild-kit/[email protected]':
+    resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==}
+    deprecated: 'Merged into tsx: https://tsx.is'
+
+  '@esbuild-kit/[email protected]':
+    resolution: {integrity: sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==}
+    deprecated: 'Merged into tsx: https://tsx.is'
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [aix]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [android]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [freebsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==}
+    engines: {node: '>=12'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==}
+    engines: {node: '>=18'}
+    cpu: [arm]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==}
+    engines: {node: '>=12'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==}
+    engines: {node: '>=18'}
+    cpu: [loong64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==}
+    engines: {node: '>=12'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==}
+    engines: {node: '>=18'}
+    cpu: [mips64el]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==}
+    engines: {node: '>=12'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==}
+    engines: {node: '>=18'}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==}
+    engines: {node: '>=12'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==}
+    engines: {node: '>=18'}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==}
+    engines: {node: '>=12'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==}
+    engines: {node: '>=18'}
+    cpu: [s390x]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [linux]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [netbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [netbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [openbsd]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [openharmony]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [sunos]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==}
+    engines: {node: '>=12'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==}
+    engines: {node: '>=18'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==}
+    engines: {node: '>=12'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==}
+    engines: {node: '>=18'}
+    cpu: [ia32]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==}
+    engines: {node: '>=12'}
+    cpu: [x64]
+    os: [win32]
+
+  '@esbuild/[email protected]':
+    resolution: {integrity: sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==}
+    engines: {node: '>=18'}
+    cpu: [x64]
+    os: [win32]
+
+  '@eslint-community/[email protected]':
+    resolution: {integrity: sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+    peerDependencies:
+      eslint: ^6.0.0 || ^7.0.0 || >=8.0.0
+
+  '@eslint-community/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==}
+    engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-uhCbYtYynH30iZErszX78U+nR3pJU3RHGQ57NXy5QupD4SBVwDeU8TNBy+MjMngc1UyIW9noKqsRqfjQTBU2dw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@eslint/[email protected]':
+    resolution: {integrity: sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==}
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==}
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+
+  '@floating-ui/[email protected]':
+    resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
+
+  '@humanfs/[email protected]':
+    resolution: {integrity: sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanfs/[email protected]':
+    resolution: {integrity: sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==}
+    engines: {node: '>=18.18.0'}
+
+  '@humanwhocodes/[email protected]':
+    resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==}
+    engines: {node: '>=12.22'}
+
+  '@humanwhocodes/[email protected]':
+    resolution: {integrity: sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==}
+    engines: {node: '>=18.18'}
+
+  '@humanwhocodes/[email protected]':
+    resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==}
+    engines: {node: '>=18.18'}
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [darwin]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==}
+    cpu: [arm]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==}
+    cpu: [s390x]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [s390x]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [linux]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [wasm32]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [arm64]
+    os: [win32]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [ia32]
+    os: [win32]
+
+  '@img/[email protected]':
+    resolution: {integrity: sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+    cpu: [x64]
+    os: [win32]
+
+  '@isaacs/[email protected]':
+    resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
+    engines: {node: '>=18.0.0'}
+
+  '@jridgewell/[email protected]':
+    resolution: {integrity: sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==}
+
+  '@jridgewell/[email protected]':
+    resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
+
+  '@jridgewell/[email protected]':
+    resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
+    engines: {node: '>=6.0.0'}
+
+  '@jridgewell/[email protected]':
+    resolution: {integrity: sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==}
+
+  '@jridgewell/[email protected]':
+    resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
+
+  '@jridgewell/[email protected]':
+    resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==}
+
+  '@napi-rs/[email protected]':
+    resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-yHDKVTcHrZy/8TWhj0B23ylKv5ypocuCwey9ZqPyv4rPdUdRzpGCkSi03t04KBPyU96kxVtUqx6O3nE1kpxASQ==}
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-2NOu3ln+BTcpnbIDuxx6MNq+pRrCyey4WSXGaJIyt0D2TYicHeO9QrUENNjcf673n3B1s7hsiV5xBYRCK1Q8kA==}
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-667R0RTP4DwxzmrqTs4Lr5dcEda9OxuZsVFsjVtxVMVhzSpo6nLclXejJVfQo2/g7/Z9qF3ETDmN3h65mTjpTQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-KMSFoistFkaiQYVQQnaU9MPWtp/3m0kn2Xed1Ces5ll+ag1+rlac20sxG+MqhH2qYWX1O2GFOATQXEyxKiIscg==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-PnOx1YdO0W7m/HWFeYd2A6JtBO8O8Eb9h6nfJia2Dw1sRHoHpNf6lN1U4GKFRzRDBi9Nq2GrHk9PF3Vmwf7XVw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-XBbuQddtY1p5FGPc2naMO0kqs4YYtLYK/8aPausI5lyOjr4J77KTG9mtlU4P3NwkLI1+OjsPzKVvSJdMs3cFaw==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-+WTeK7Qdw82ez3U9JgD+igBAP75gqZ1vbK6R8PlEEuY0OIe5FuYXA4aTjL811kWPf7hNeslD4hHK2WoM9W0IgA==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-XP824mCbgQsK20jlXKrUpZoh/iO3vUWhMpxCz8oYeagoiZ4V0TQiKy0ASji1KK6IAe3DYGfj5RfKP6+L2020OQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-FxrsenhUz0LbgRkNWx6FRRJIPe/MI1JRA4W4EPd5leXO00AZ6YU8v5vfx4MDXTvN77lM/EqsE3+6d2CIeF5NYg==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@next/[email protected]':
+    resolution: {integrity: sha512-T4ufqnZ4u88ZheczkBTtOF+eKaM14V8kbjud/XrAakoM5DKQWjW09vD6B9fsdsWS2T7D5EY31hRHdta7QKWOng==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@nodelib/[email protected]':
+    resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/[email protected]':
+    resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==}
+    engines: {node: '>= 8'}
+
+  '@nodelib/[email protected]':
+    resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
+    engines: {node: '>= 8'}
+
+  '@nolyfill/[email protected]':
+    resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==}
+    engines: {node: '>=12.4.0'}
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==}
+    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/[email protected]':
+    resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==}
+    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/[email protected]':
+    resolution: {integrity: sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog==}
+    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/[email protected]':
+    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/[email protected]':
+    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/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==}
+    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/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
+    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/[email protected]':
+    resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==}
+    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/[email protected]':
+    resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==}
+    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/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-YT1GqPSL8kJn20djelMX7/cTRp/Y9w5IZHvfxQTVHrOqa2yMl7i/UfMqKRU5V7mEyKTrUVgJXhNQPVCG8PBLoQ==}
+    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/[email protected]':
+    resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==}
+    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/[email protected]':
+    resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==}
+    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/[email protected]':
+    resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==}
+    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/[email protected]':
+    resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==}
+    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/[email protected]':
+    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/[email protected]':
+    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/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==}
+    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/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==}
+    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/[email protected]':
+    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/[email protected]':
+    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/[email protected]':
+    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/[email protected]':
+    resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    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
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  '@radix-ui/[email protected]':
+    resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==}
+
+  '@rtsao/[email protected]':
+    resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
+
+  '@rushstack/[email protected]':
+    resolution: {integrity: sha512-5EwMtOqvJMMa3HbmxLlF74e+3/HhwBTMcvt3nqVJgGCozO6hzIPOBlwm8mGVNR9SN2IJpxSnlxczyDjcn7qIyw==}
+
+  '@swc/[email protected]':
+    resolution: {integrity: sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==}
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==}
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [android]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [darwin]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw==}
+    engines: {node: '>= 10'}
+    cpu: [arm]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [linux]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA==}
+    engines: {node: '>=14.0.0'}
+    cpu: [wasm32]
+    bundledDependencies:
+      - '@napi-rs/wasm-runtime'
+      - '@emnapi/core'
+      - '@emnapi/runtime'
+      - '@tybys/wasm-util'
+      - '@emnapi/wasi-threads'
+      - tslib
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg==}
+    engines: {node: '>= 10'}
+    cpu: [arm64]
+    os: [win32]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw==}
+    engines: {node: '>= 10'}
+    cpu: [x64]
+    os: [win32]
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA==}
+    engines: {node: '>= 10'}
+
+  '@tailwindcss/[email protected]':
+    resolution: {integrity: sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ==}
+
+  '@tybys/[email protected]':
+    resolution: {integrity: sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-gfehUI8N1z92kygssiuWvLiwcbOB3IRktR6hTDgJlXMYh5OvkPSRmgfoBUmfZt+vhwJtX7v1Yw4KvvAf7c5QKQ==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-LF7lF6zWEKxuT3/OR8wAZGzkg4ENGXFNyiV/JeOt9z5B+0ZVwbql9McqX5c/WStFq1GaGso7H1AzP/qSzmlCKQ==}
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ==}
+    peerDependencies:
+      '@types/react': ^19.0.0
+
+  '@types/[email protected]':
+    resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==}
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      '@typescript-eslint/parser': ^8.39.0
+      eslint: ^8.57.0 || ^9.0.0
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==}
+    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/[email protected]':
+    resolution: {integrity: sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==}
+    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/[email protected]':
+    resolution: {integrity: sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    peerDependencies:
+      typescript: '>=4.8.4 <6.0.0'
+
+  '@typescript-eslint/[email protected]':
+    resolution: {integrity: sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==}
+    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/[email protected]':
+    resolution: {integrity: sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==}
+    cpu: [arm]
+    os: [android]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==}
+    cpu: [arm64]
+    os: [android]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==}
+    cpu: [arm64]
+    os: [darwin]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==}
+    cpu: [x64]
+    os: [darwin]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==}
+    cpu: [x64]
+    os: [freebsd]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==}
+    cpu: [arm]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==}
+    cpu: [arm]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==}
+    cpu: [arm64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==}
+    cpu: [ppc64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==}
+    cpu: [riscv64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==}
+    cpu: [s390x]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==}
+    cpu: [x64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==}
+    cpu: [x64]
+    os: [linux]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==}
+    engines: {node: '>=14.0.0'}
+    cpu: [wasm32]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==}
+    cpu: [arm64]
+    os: [win32]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==}
+    cpu: [ia32]
+    os: [win32]
+
+  '@unrs/[email protected]':
+    resolution: {integrity: sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==}
+    cpu: [x64]
+    os: [win32]
+
+  [email protected]:
+    resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
+    peerDependencies:
+      acorn: ^6.0.0 || ^7.0.0 || ^8.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==}
+    engines: {node: '>=0.4.0'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==}
+    engines: {node: '>=4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-hwaAu8tct/Zn6Zft4U9BsZcXkYomzpHJX28ofvx7k0Zz2HNz54n1n+tDgxoWFGB4PcFvJXJQloPhaV2eP3Q6EA==}
+    peerDependencies:
+      '@types/react': ^19
+
+  [email protected]:
+    resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
+    engines: {node: '>=7.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
+    engines: {node: '>=12.5.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
+    engines: {node: '>= 8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Sf2LSQP+bOlhKWWyhFsn0UsfdK/kCWRv1iuA2gXAwt3dyNabr6QSj00I2V10pidqz69soatm9ZwZvpQMTIOd5Q==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-tCPWVZWZqWVx2XUsVpJRnH9Mx0ClVOf5YUHerZ5so1OKSlqww4zy1R5ksEdGRcO3tM3zj0PYN6V48TbQCL1RfA==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-jBe37K7d8ZSKptdKfakQFdeljtu3P2Cbo7tJoJSVZADzIKOBo9IAJPOmMsH2bZl90bZgh8FQlD8BjxXA/zuBkQ==}
+    peerDependencies:
+      '@aws-sdk/client-rds-data': '>=3'
+      '@cloudflare/workers-types': '>=4'
+      '@electric-sql/pglite': '>=0.2.0'
+      '@libsql/client': '>=0.10.0'
+      '@libsql/client-wasm': '>=0.10.0'
+      '@neondatabase/serverless': '>=0.10.0'
+      '@op-engineering/op-sqlite': '>=2'
+      '@opentelemetry/api': ^1.4.1
+      '@planetscale/database': '>=1.13'
+      '@prisma/client': '*'
+      '@tidbcloud/serverless': '*'
+      '@types/better-sqlite3': '*'
+      '@types/pg': '*'
+      '@types/sql.js': '*'
+      '@upstash/redis': '>=1.34.7'
+      '@vercel/postgres': '>=0.8.0'
+      '@xata.io/client': '*'
+      better-sqlite3: '>=7'
+      bun-types: '*'
+      expo-sqlite: '>=14.0.0'
+      gel: '>=2'
+      knex: '*'
+      kysely: '*'
+      mysql2: '>=2'
+      pg: '>=8'
+      postgres: '>=3'
+      prisma: '*'
+      sql.js: '>=1'
+      sqlite3: '>=5'
+    peerDependenciesMeta:
+      '@aws-sdk/client-rds-data':
+        optional: true
+      '@cloudflare/workers-types':
+        optional: true
+      '@electric-sql/pglite':
+        optional: true
+      '@libsql/client':
+        optional: true
+      '@libsql/client-wasm':
+        optional: true
+      '@neondatabase/serverless':
+        optional: true
+      '@op-engineering/op-sqlite':
+        optional: true
+      '@opentelemetry/api':
+        optional: true
+      '@planetscale/database':
+        optional: true
+      '@prisma/client':
+        optional: true
+      '@tidbcloud/serverless':
+        optional: true
+      '@types/better-sqlite3':
+        optional: true
+      '@types/pg':
+        optional: true
+      '@types/sql.js':
+        optional: true
+      '@upstash/redis':
+        optional: true
+      '@vercel/postgres':
+        optional: true
+      '@xata.io/client':
+        optional: true
+      better-sqlite3:
+        optional: true
+      bun-types:
+        optional: true
+      expo-sqlite:
+        optional: true
+      gel:
+        optional: true
+      knex:
+        optional: true
+      kysely:
+        optional: true
+      mysql2:
+        optional: true
+      pg:
+        optional: true
+      postgres:
+        optional: true
+      prisma:
+        optional: true
+      sql.js:
+        optional: true
+      sqlite3:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
+    engines: {node: '>=10.13.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-WSzPgsdLtTcQwm4CROfS5ju2Wa1QQcVeT37jFjYzdFz1r9ahadC8B8/a4qxJxM+09F18iumCdRmlr96ZYkQvEg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==}
+    peerDependencies:
+      esbuild: '>=0.12 <1'
+
+  [email protected]:
+    resolution: {integrity: sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==}
+    engines: {node: '>=12'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==}
+    engines: {node: '>=18'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-4uznvw5DlTTjrZgYZjMciSdDDMO2SWIuQgUNaFyC2O3Zw3Z91XeIejeVa439yRq2CnJb/KEvE4U2AeN/66FpUA==}
+    peerDependencies:
+      eslint: ^7.23.0 || ^8.0.0 || ^9.0.0
+      typescript: '>=3.3.1'
+    peerDependenciesMeta:
+      typescript:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==}
+    engines: {node: ^14.18.0 || >=16.0.0}
+    peerDependencies:
+      eslint: '*'
+      eslint-plugin-import: '*'
+      eslint-plugin-import-x: '*'
+    peerDependenciesMeta:
+      eslint-plugin-import:
+        optional: true
+      eslint-plugin-import-x:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: '*'
+      eslint-import-resolver-node: '*'
+      eslint-import-resolver-typescript: '*'
+      eslint-import-resolver-webpack: '*'
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+      eslint:
+        optional: true
+      eslint-import-resolver-node:
+        optional: true
+      eslint-import-resolver-typescript:
+        optional: true
+      eslint-import-resolver-webpack:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      '@typescript-eslint/parser': '*'
+      eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+    peerDependenciesMeta:
+      '@typescript-eslint/parser':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9
+
+  [email protected]:
+    resolution: {integrity: sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==}
+    engines: {node: '>=4'}
+    peerDependencies:
+      eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7
+
+  [email protected]:
+    resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==}
+    engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-hB4FIzXovouYzwzECDcUkJ4OcfOEkXTv2zRY6B9bkwjx/cprAq0uvm1nl7zvQ0/TsUk0zQiN4uPfJpB9m+rPMQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+    hasBin: true
+    peerDependencies:
+      jiti: '*'
+    peerDependenciesMeta:
+      jiti:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==}
+    engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==}
+    engines: {node: '>=0.10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==}
+    engines: {node: '>=4.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==}
+    engines: {node: '>=4.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==}
+
+  [email protected]:
+    resolution: {integrity: sha512-xwP+dG/in/nJelMOUEQBiIYeOoHKihWPB2sNZ8ZeDbZFoGb1OwTGMggGRgg6CRitNx7kmHgtIz2dOHDQ8Ap7Bw==}
+    engines: {node: '>=6.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==}
+    engines: {node: '>=8.6.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==}
+    engines: {node: '>=8.6.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==}
+    peerDependencies:
+      picomatch: ^3 || ^4
+    peerDependenciesMeta:
+      picomatch:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
+    engines: {node: '>=16.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
+    engines: {node: '>=16'}
+
+  [email protected]:
+    resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==}
+    engines: {node: '>= 6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==}
+    engines: {node: '>=10.13.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
+
+  [email protected]:
+    resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-JW8Bb4RFWD9iOKxg5PbUarBYGM99IcxFl2FPBo2gSJO11jjUDqlP1Bmfyqt8Z/dGhIQ63PMA9LdcLefXyIasyg==}
+    engines: {node: '>=16.9.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
+    engines: {node: '>= 4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==}
+    engines: {node: '>= 4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
+    engines: {node: '>=0.8.19'}
+
+  [email protected]:
+    resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
+    engines: {node: '>=0.12.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
+    engines: {node: '>=4.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==}
+    engines: {node: '>=0.10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [darwin]
+
+  [email protected]:
+    resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [darwin]
+
+  [email protected]:
+    resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [freebsd]
+
+  [email protected]:
+    resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [linux]
+
+  [email protected]:
+    resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [arm64]
+    os: [win32]
+
+  [email protected]:
+    resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==}
+    engines: {node: '>= 12.0.0'}
+    cpu: [x64]
+    os: [win32]
+
+  [email protected]:
+    resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==}
+    engines: {node: '>= 12.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-t5tS44bqd825zAW45UQxpG2CvcC4urOwn2TrwSH8u+MjeE+1NnWl6QqeQ/6NdjMqdOygyiT9p3Ev0p1NJykxjw==}
+    peerDependencies:
+      react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
+    engines: {node: '>= 8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
+    engines: {node: '>=8.6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  [email protected]:
+    resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
+    engines: {node: '>=16 || 14 >=14.17'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==}
+    engines: {node: '>= 18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
+    engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==}
+    engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
+    peerDependencies:
+      react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+      react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
+
+  [email protected]:
+    resolution: {integrity: sha512-us++E/Q80/8+UekzB3SAGs71AlLDsadpFMXVNM/uQ0BMwsh9m3mr0UNQIfjKed8vpWXsASe+Qifrnu1oLIcKEQ==}
+    engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
+    hasBin: true
+    peerDependencies:
+      '@opentelemetry/api': ^1.1.0
+      '@playwright/test': ^1.51.1
+      babel-plugin-react-compiler: '*'
+      react: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+      react-dom: ^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0
+      sass: ^1.3.0
+    peerDependenciesMeta:
+      '@opentelemetry/api':
+        optional: true
+      '@playwright/test':
+        optional: true
+      babel-plugin-react-compiler:
+        optional: true
+      sass:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==}
+    engines: {node: '>=4.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==}
+    engines: {node: '>=4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
+    engines: {node: '>=8.6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  [email protected]:
+    resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
+    engines: {node: ^10 || ^12 || >=14}
+
+  [email protected]:
+    resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==}
+    engines: {node: '>=4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==}
+    engines: {node: '>=12'}
+
+  [email protected]:
+    resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==}
+    peerDependencies:
+      react: ^19.1.0
+
+  [email protected]:
+    resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA==}
+    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
+
+  [email protected]:
+    resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==}
+    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
+
+  [email protected]:
+    resolution: {integrity: sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==}
+    peerDependencies:
+      react: '>=16.6.0'
+      react-dom: '>=16.6.0'
+
+  [email protected]:
+    resolution: {integrity: sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-UT/q6fwS3c1dHbXv2uFgYJ9BMFHu3fwnd7AYZaEQhXuYQ4hgsxLvsUXzGdKeZrW5xopzDCvuA2N41WJ88I7zIw==}
+    engines: {node: '>=14'}
+    peerDependencies:
+      react: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+      react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
+    engines: {node: '>=4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==}
+
+  [email protected]:
+    resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
+    engines: {node: '>= 0.4'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==}
+    engines: {iojs: '>=1.0.0', node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==}
+    engines: {node: '>=0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==}
+    engines: {node: '>=10'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==}
+    engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
+
+  [email protected]:
+    resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
+    peerDependencies:
+      react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+      react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
+
+  [email protected]:
+    resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==}
+
+  [email protected]:
+    resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==}
+    engines: {node: '>=4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==}
+    engines: {node: '>= 12.0.0'}
+    peerDependencies:
+      '@babel/core': '*'
+      babel-plugin-macros: '*'
+      react: '>= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0'
+    peerDependenciesMeta:
+      '@babel/core':
+        optional: true
+      babel-plugin-macros:
+        optional: true
+
+  [email protected]:
+    resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
+    engines: {node: '>=8'}
+
+  [email protected]:
+    resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==}
+
+  [email protected]:
+    resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==}
+    engines: {node: '>=6'}
+
+  [email protected]:
+    resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==}
+    engines: {node: '>=12.0.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
+    engines: {node: '>=8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==}
+    engines: {node: '>=18.12'}
+    peerDependencies:
+      typescript: '>=4.8.4'
+
+  [email protected]:
+    resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
+
+  [email protected]:
+    resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
+    engines: {node: '>= 0.8.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==}
+    engines: {node: '>=14.17'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==}
+
+  [email protected]:
+    resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
+
+  [email protected]:
+    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
+
+  [email protected]:
+    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
+
+  [email protected]:
+    resolution: {integrity: sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==}
+    peerDependencies:
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
+
+  [email protected]:
+    resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==}
+
+  [email protected]:
+    resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==}
+    engines: {node: '>= 0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
+    engines: {node: '>= 8'}
+    hasBin: true
+
+  [email protected]:
+    resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==}
+    engines: {node: '>=0.10.0'}
+
+  [email protected]:
+    resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
+    engines: {node: '>=0.4'}
+
+  [email protected]:
+    resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==}
+    engines: {node: '>=18'}
+
+  [email protected]:
+    resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
+    engines: {node: '>=10'}
+
+  [email protected]:
+    resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==}
+
+snapshots:
+
+  '@alloc/[email protected]': {}
+
+  '@babel/[email protected]': {}
+
+  '@drizzle-team/[email protected]': {}
+
+  '@emnapi/[email protected]':
+    dependencies:
+      '@emnapi/wasi-threads': 1.0.4
+      tslib: 2.8.1
+    optional: true
+
+  '@emnapi/[email protected]':
+    dependencies:
+      tslib: 2.8.1
+    optional: true
+
+  '@emnapi/[email protected]':
+    dependencies:
+      tslib: 2.8.1
+    optional: true
+
+  '@esbuild-kit/[email protected]':
+    dependencies:
+      esbuild: 0.18.20
+      source-map-support: 0.5.21
+
+  '@esbuild-kit/[email protected]':
+    dependencies:
+      '@esbuild-kit/core-utils': 3.3.2
+      get-tsconfig: 4.10.1
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@esbuild/[email protected]':
+    optional: true
+
+  '@eslint-community/[email protected]([email protected]([email protected]))':
+    dependencies:
+      eslint: 9.36.0([email protected])
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/[email protected]([email protected]([email protected]))':
+    dependencies:
+      eslint: 9.36.0([email protected])
+      eslint-visitor-keys: 3.4.3
+
+  '@eslint-community/[email protected]': {}
+
+  '@eslint/[email protected]':
+    dependencies:
+      '@eslint/object-schema': 2.1.6
+      debug: 4.4.1
+      minimatch: 3.1.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/[email protected]': {}
+
+  '@eslint/[email protected]':
+    dependencies:
+      '@types/json-schema': 7.0.15
+
+  '@eslint/[email protected]':
+    dependencies:
+      ajv: 6.12.6
+      debug: 4.4.1
+      espree: 10.4.0
+      globals: 14.0.0
+      ignore: 5.3.2
+      import-fresh: 3.3.1
+      js-yaml: 4.1.0
+      minimatch: 3.1.2
+      strip-json-comments: 3.1.1
+    transitivePeerDependencies:
+      - supports-color
+
+  '@eslint/[email protected]': {}
+
+  '@eslint/[email protected]': {}
+
+  '@eslint/[email protected]':
+    dependencies:
+      '@eslint/core': 0.15.2
+      levn: 0.4.1
+
+  '@floating-ui/[email protected]':
+    dependencies:
+      '@floating-ui/utils': 0.2.10
+
+  '@floating-ui/[email protected]':
+    dependencies:
+      '@floating-ui/core': 1.7.3
+      '@floating-ui/utils': 0.2.10
+
+  '@floating-ui/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@floating-ui/dom': 1.7.4
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  '@floating-ui/[email protected]': {}
+
+  '@humanfs/[email protected]': {}
+
+  '@humanfs/[email protected]':
+    dependencies:
+      '@humanfs/core': 0.19.1
+      '@humanwhocodes/retry': 0.3.1
+
+  '@humanwhocodes/[email protected]': {}
+
+  '@humanwhocodes/[email protected]': {}
+
+  '@humanwhocodes/[email protected]': {}
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-arm64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-darwin-x64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-arm': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-ppc64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-s390x': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linux-x64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    optionalDependencies:
+      '@img/sharp-libvips-linuxmusl-x64': 1.2.0
+    optional: true
+
+  '@img/[email protected]':
+    dependencies:
+      '@emnapi/runtime': 1.4.5
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@img/[email protected]':
+    optional: true
+
+  '@isaacs/[email protected]':
+    dependencies:
+      minipass: 7.1.2
+
+  '@jridgewell/[email protected]':
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.4
+      '@jridgewell/trace-mapping': 0.3.29
+
+  '@jridgewell/[email protected]':
+    dependencies:
+      '@jridgewell/gen-mapping': 0.3.12
+      '@jridgewell/trace-mapping': 0.3.29
+
+  '@jridgewell/[email protected]': {}
+
+  '@jridgewell/[email protected]': {}
+
+  '@jridgewell/[email protected]': {}
+
+  '@jridgewell/[email protected]':
+    dependencies:
+      '@jridgewell/resolve-uri': 3.1.2
+      '@jridgewell/sourcemap-codec': 1.5.4
+
+  '@napi-rs/[email protected]':
+    dependencies:
+      '@emnapi/core': 1.4.5
+      '@emnapi/runtime': 1.4.5
+      '@tybys/wasm-util': 0.10.0
+    optional: true
+
+  '@next/[email protected]': {}
+
+  '@next/[email protected]':
+    dependencies:
+      fast-glob: 3.3.1
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@next/[email protected]':
+    optional: true
+
+  '@nodelib/[email protected]':
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      run-parallel: 1.2.0
+
+  '@nodelib/[email protected]': {}
+
+  '@nodelib/[email protected]':
+    dependencies:
+      '@nodelib/fs.scandir': 2.1.5
+      fastq: 1.19.1
+
+  '@nolyfill/[email protected]': {}
+
+  '@radix-ui/[email protected]': {}
+
+  '@radix-ui/[email protected]': {}
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dialog': 1.1.15(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-is-hydrated': 0.1.0(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.3(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-portal': 1.1.9(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      aria-hidden: 1.2.6
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.7.1(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-escape-keydown': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-menu': 2.1.16(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-collection': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.3(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.9(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-roving-focus': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      aria-hidden: 1.2.6
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.7.1(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-dismissable-layer': 1.1.11(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-focus-guards': 1.1.3(@types/[email protected])([email protected])
+      '@radix-ui/react-focus-scope': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-popper': 1.2.8(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-portal': 1.1.9(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-presence': 1.1.5(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      aria-hidden: 1.2.6
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-remove-scroll: 2.7.1(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@floating-ui/react-dom': 2.1.6([email protected]([email protected]))([email protected])
+      '@radix-ui/react-arrow': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-rect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/rect': 1.1.1
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/react-slot': 1.2.3(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-collection': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-id': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/number': 1.1.1
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-collection': 1.1.7(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-direction': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@radix-ui/primitive': 1.1.3
+      '@radix-ui/react-compose-refs': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-context': 1.1.2(@types/[email protected])([email protected])
+      '@radix-ui/react-primitive': 2.1.3(@types/[email protected](@types/[email protected]))(@types/[email protected])([email protected]([email protected]))([email protected])
+      '@radix-ui/react-use-controllable-state': 1.2.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-previous': 1.1.1(@types/[email protected])([email protected])
+      '@radix-ui/react-use-size': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+      '@types/react-dom': 19.1.9(@types/[email protected])
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-effect-event': 0.0.2(@types/[email protected])([email protected])
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-callback-ref': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+      use-sync-external-store: 1.5.0([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/rect': 1.1.1
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected](@types/[email protected])([email protected])':
+    dependencies:
+      '@radix-ui/react-use-layout-effect': 1.1.1(@types/[email protected])([email protected])
+      react: 19.1.0
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  '@radix-ui/[email protected]': {}
+
+  '@rtsao/[email protected]': {}
+
+  '@rushstack/[email protected]': {}
+
+  '@swc/[email protected]':
+    dependencies:
+      tslib: 2.8.1
+
+  '@tailwindcss/[email protected]':
+    dependencies:
+      '@jridgewell/remapping': 2.3.5
+      enhanced-resolve: 5.18.3
+      jiti: 2.5.1
+      lightningcss: 1.30.1
+      magic-string: 0.30.19
+      source-map-js: 1.2.1
+      tailwindcss: 4.1.13
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    optional: true
+
+  '@tailwindcss/[email protected]':
+    dependencies:
+      detect-libc: 2.0.4
+      tar: 7.4.3
+    optionalDependencies:
+      '@tailwindcss/oxide-android-arm64': 4.1.13
+      '@tailwindcss/oxide-darwin-arm64': 4.1.13
+      '@tailwindcss/oxide-darwin-x64': 4.1.13
+      '@tailwindcss/oxide-freebsd-x64': 4.1.13
+      '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.13
+      '@tailwindcss/oxide-linux-arm64-gnu': 4.1.13
+      '@tailwindcss/oxide-linux-arm64-musl': 4.1.13
+      '@tailwindcss/oxide-linux-x64-gnu': 4.1.13
+      '@tailwindcss/oxide-linux-x64-musl': 4.1.13
+      '@tailwindcss/oxide-wasm32-wasi': 4.1.13
+      '@tailwindcss/oxide-win32-arm64-msvc': 4.1.13
+      '@tailwindcss/oxide-win32-x64-msvc': 4.1.13
+
+  '@tailwindcss/[email protected]':
+    dependencies:
+      '@alloc/quick-lru': 5.2.0
+      '@tailwindcss/node': 4.1.13
+      '@tailwindcss/oxide': 4.1.13
+      postcss: 8.5.6
+      tailwindcss: 4.1.13
+
+  '@tybys/[email protected]':
+    dependencies:
+      tslib: 2.8.1
+    optional: true
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]':
+    dependencies:
+      '@types/d3-color': 3.1.3
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]':
+    dependencies:
+      '@types/d3-time': 3.0.4
+
+  '@types/[email protected]':
+    dependencies:
+      '@types/d3-path': 3.1.1
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]': {}
+
+  '@types/[email protected]':
+    dependencies:
+      undici-types: 6.21.0
+
+  '@types/[email protected]':
+    dependencies:
+      '@types/node': 20.19.17
+      pg-protocol: 1.10.3
+      pg-types: 2.2.0
+
+  '@types/[email protected](@types/[email protected])':
+    dependencies:
+      '@types/react': 19.1.13
+
+  '@types/[email protected]':
+    dependencies:
+      csstype: 3.1.3
+
+  '@typescript-eslint/[email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@eslint-community/regexpp': 4.12.1
+      '@typescript-eslint/parser': 8.39.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/scope-manager': 8.39.0
+      '@typescript-eslint/type-utils': 8.39.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/utils': 8.39.0([email protected]([email protected]))([email protected])
+      '@typescript-eslint/visitor-keys': 8.39.0
+      eslint: 9.36.0([email protected])
+      graphemer: 1.4.0
+      ignore: 7.0.5
+      natural-compare: 1.4.0
+      ts-api-utils: 2.1.0([email protected])
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@typescript-eslint/scope-manager': 8.39.0
+      '@typescript-eslint/types': 8.39.0
+      '@typescript-eslint/typescript-estree': 8.39.0([email protected])
+      '@typescript-eslint/visitor-keys': 8.39.0
+      debug: 4.4.1
+      eslint: 9.36.0([email protected])
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]([email protected])':
+    dependencies:
+      '@typescript-eslint/tsconfig-utils': 8.39.0([email protected])
+      '@typescript-eslint/types': 8.39.0
+      debug: 4.4.1
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]':
+    dependencies:
+      '@typescript-eslint/types': 8.39.0
+      '@typescript-eslint/visitor-keys': 8.39.0
+
+  '@typescript-eslint/[email protected]([email protected])':
+    dependencies:
+      typescript: 5.9.2
+
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@typescript-eslint/types': 8.39.0
+      '@typescript-eslint/typescript-estree': 8.39.0([email protected])
+      '@typescript-eslint/utils': 8.39.0([email protected]([email protected]))([email protected])
+      debug: 4.4.1
+      eslint: 9.36.0([email protected])
+      ts-api-utils: 2.1.0([email protected])
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]': {}
+
+  '@typescript-eslint/[email protected]([email protected])':
+    dependencies:
+      '@typescript-eslint/project-service': 8.39.0([email protected])
+      '@typescript-eslint/tsconfig-utils': 8.39.0([email protected])
+      '@typescript-eslint/types': 8.39.0
+      '@typescript-eslint/visitor-keys': 8.39.0
+      debug: 4.4.1
+      fast-glob: 3.3.3
+      is-glob: 4.0.3
+      minimatch: 9.0.5
+      semver: 7.7.2
+      ts-api-utils: 2.1.0([email protected])
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])':
+    dependencies:
+      '@eslint-community/eslint-utils': 4.7.0([email protected]([email protected]))
+      '@typescript-eslint/scope-manager': 8.39.0
+      '@typescript-eslint/types': 8.39.0
+      '@typescript-eslint/typescript-estree': 8.39.0([email protected])
+      eslint: 9.36.0([email protected])
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - supports-color
+
+  '@typescript-eslint/[email protected]':
+    dependencies:
+      '@typescript-eslint/types': 8.39.0
+      eslint-visitor-keys: 4.2.1
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    dependencies:
+      '@napi-rs/wasm-runtime': 0.2.12
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  '@unrs/[email protected]':
+    optional: true
+
+  [email protected]([email protected]):
+    dependencies:
+      acorn: 8.15.0
+
+  [email protected]: {}
+
+  [email protected]:
+    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
+
+  [email protected]:
+    dependencies:
+      color-convert: 2.0.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      tslib: 2.8.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      is-array-buffer: 3.0.5
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-object-atoms: 1.1.1
+      get-intrinsic: 1.3.0
+      is-string: 1.1.1
+      math-intrinsics: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      es-shim-unscopables: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      es-shim-unscopables: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-shim-unscopables: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-shim-unscopables: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      es-shim-unscopables: 1.1.0
+
+  [email protected]:
+    dependencies:
+      array-buffer-byte-length: 1.0.2
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      is-array-buffer: 3.0.5
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      possible-typed-array-names: 1.1.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      balanced-match: 1.0.2
+      concat-map: 0.0.1
+
+  [email protected]:
+    dependencies:
+      balanced-match: 1.0.2
+
+  [email protected]:
+    dependencies:
+      fill-range: 7.1.1
+
+  [email protected]: {}
+
+  [email protected](@types/[email protected]):
+    dependencies:
+      '@types/node': 20.19.17
+      '@types/react': 19.1.13
+    optional: true
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+
+  [email protected]:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-define-property: 1.0.1
+      get-intrinsic: 1.3.0
+      set-function-length: 1.2.2
+
+  [email protected]:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      get-intrinsic: 1.3.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      ansi-styles: 4.3.0
+      supports-color: 7.2.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      clsx: 2.1.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      color-name: 1.1.4
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      color-name: 1.1.4
+      simple-swizzle: 0.2.2
+    optional: true
+
+  [email protected]:
+    dependencies:
+      color-convert: 2.0.1
+      color-string: 1.9.1
+    optional: true
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      path-key: 3.1.1
+      shebang-command: 2.0.0
+      which: 2.0.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      internmap: 2.0.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      d3-color: 3.1.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      d3-array: 3.2.4
+      d3-format: 3.1.0
+      d3-interpolate: 3.0.1
+      d3-time: 3.1.0
+      d3-time-format: 4.1.0
+
+  [email protected]:
+    dependencies:
+      d3-path: 3.1.0
+
+  [email protected]:
+    dependencies:
+      d3-time: 3.1.0
+
+  [email protected]:
+    dependencies:
+      d3-array: 3.2.4
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      is-data-view: 1.0.2
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      is-data-view: 1.0.2
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      is-data-view: 1.0.2
+
+  [email protected]:
+    dependencies:
+      ms: 2.1.3
+
+  [email protected]:
+    dependencies:
+      ms: 2.1.3
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  [email protected]:
+    dependencies:
+      define-data-property: 1.1.4
+      has-property-descriptors: 1.0.2
+      object-keys: 1.1.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      esutils: 2.0.3
+
+  [email protected]:
+    dependencies:
+      '@babel/runtime': 7.28.4
+      csstype: 3.1.3
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@drizzle-team/brocli': 0.10.2
+      '@esbuild-kit/esm-loader': 2.6.5
+      esbuild: 0.25.10
+      esbuild-register: 3.6.0([email protected])
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected](@types/[email protected])([email protected](@types/[email protected]))([email protected]):
+    optionalDependencies:
+      '@types/pg': 8.15.5
+      bun-types: 1.2.22(@types/[email protected])
+      postgres: 3.4.7
+
+  [email protected]:
+    dependencies:
+      call-bind-apply-helpers: 1.0.2
+      es-errors: 1.3.0
+      gopd: 1.2.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      graceful-fs: 4.2.11
+      tapable: 2.2.2
+
+  [email protected]:
+    dependencies:
+      array-buffer-byte-length: 1.0.2
+      arraybuffer.prototype.slice: 1.0.4
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      data-view-buffer: 1.0.2
+      data-view-byte-length: 1.0.2
+      data-view-byte-offset: 1.0.1
+      es-define-property: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      es-set-tostringtag: 2.1.0
+      es-to-primitive: 1.3.0
+      function.prototype.name: 1.1.8
+      get-intrinsic: 1.3.0
+      get-proto: 1.0.1
+      get-symbol-description: 1.1.0
+      globalthis: 1.0.4
+      gopd: 1.2.0
+      has-property-descriptors: 1.0.2
+      has-proto: 1.2.0
+      has-symbols: 1.1.0
+      hasown: 2.0.2
+      internal-slot: 1.1.0
+      is-array-buffer: 3.0.5
+      is-callable: 1.2.7
+      is-data-view: 1.0.2
+      is-negative-zero: 2.0.3
+      is-regex: 1.2.1
+      is-set: 2.0.3
+      is-shared-array-buffer: 1.0.4
+      is-string: 1.1.1
+      is-typed-array: 1.1.15
+      is-weakref: 1.1.1
+      math-intrinsics: 1.1.0
+      object-inspect: 1.13.4
+      object-keys: 1.1.1
+      object.assign: 4.1.7
+      own-keys: 1.0.1
+      regexp.prototype.flags: 1.5.4
+      safe-array-concat: 1.1.3
+      safe-push-apply: 1.0.0
+      safe-regex-test: 1.1.0
+      set-proto: 1.0.0
+      stop-iteration-iterator: 1.1.0
+      string.prototype.trim: 1.2.10
+      string.prototype.trimend: 1.0.9
+      string.prototype.trimstart: 1.0.8
+      typed-array-buffer: 1.0.3
+      typed-array-byte-length: 1.0.3
+      typed-array-byte-offset: 1.0.4
+      typed-array-length: 1.0.7
+      unbox-primitive: 1.1.0
+      which-typed-array: 1.1.19
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      es-set-tostringtag: 2.1.0
+      function-bind: 1.1.2
+      get-intrinsic: 1.3.0
+      globalthis: 1.0.4
+      gopd: 1.2.0
+      has-property-descriptors: 1.0.2
+      has-proto: 1.2.0
+      has-symbols: 1.1.0
+      internal-slot: 1.1.0
+      iterator.prototype: 1.1.5
+      safe-array-concat: 1.1.3
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  [email protected]:
+    dependencies:
+      hasown: 2.0.2
+
+  [email protected]:
+    dependencies:
+      is-callable: 1.2.7
+      is-date-object: 1.1.0
+      is-symbol: 1.1.1
+
+  [email protected]([email protected]):
+    dependencies:
+      debug: 4.4.1
+      esbuild: 0.25.10
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected]:
+    optionalDependencies:
+      '@esbuild/android-arm': 0.18.20
+      '@esbuild/android-arm64': 0.18.20
+      '@esbuild/android-x64': 0.18.20
+      '@esbuild/darwin-arm64': 0.18.20
+      '@esbuild/darwin-x64': 0.18.20
+      '@esbuild/freebsd-arm64': 0.18.20
+      '@esbuild/freebsd-x64': 0.18.20
+      '@esbuild/linux-arm': 0.18.20
+      '@esbuild/linux-arm64': 0.18.20
+      '@esbuild/linux-ia32': 0.18.20
+      '@esbuild/linux-loong64': 0.18.20
+      '@esbuild/linux-mips64el': 0.18.20
+      '@esbuild/linux-ppc64': 0.18.20
+      '@esbuild/linux-riscv64': 0.18.20
+      '@esbuild/linux-s390x': 0.18.20
+      '@esbuild/linux-x64': 0.18.20
+      '@esbuild/netbsd-x64': 0.18.20
+      '@esbuild/openbsd-x64': 0.18.20
+      '@esbuild/sunos-x64': 0.18.20
+      '@esbuild/win32-arm64': 0.18.20
+      '@esbuild/win32-ia32': 0.18.20
+      '@esbuild/win32-x64': 0.18.20
+
+  [email protected]:
+    optionalDependencies:
+      '@esbuild/aix-ppc64': 0.25.10
+      '@esbuild/android-arm': 0.25.10
+      '@esbuild/android-arm64': 0.25.10
+      '@esbuild/android-x64': 0.25.10
+      '@esbuild/darwin-arm64': 0.25.10
+      '@esbuild/darwin-x64': 0.25.10
+      '@esbuild/freebsd-arm64': 0.25.10
+      '@esbuild/freebsd-x64': 0.25.10
+      '@esbuild/linux-arm': 0.25.10
+      '@esbuild/linux-arm64': 0.25.10
+      '@esbuild/linux-ia32': 0.25.10
+      '@esbuild/linux-loong64': 0.25.10
+      '@esbuild/linux-mips64el': 0.25.10
+      '@esbuild/linux-ppc64': 0.25.10
+      '@esbuild/linux-riscv64': 0.25.10
+      '@esbuild/linux-s390x': 0.25.10
+      '@esbuild/linux-x64': 0.25.10
+      '@esbuild/netbsd-arm64': 0.25.10
+      '@esbuild/netbsd-x64': 0.25.10
+      '@esbuild/openbsd-arm64': 0.25.10
+      '@esbuild/openbsd-x64': 0.25.10
+      '@esbuild/openharmony-arm64': 0.25.10
+      '@esbuild/sunos-x64': 0.25.10
+      '@esbuild/win32-arm64': 0.25.10
+      '@esbuild/win32-ia32': 0.25.10
+      '@esbuild/win32-x64': 0.25.10
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      '@next/eslint-plugin-next': 15.4.6
+      '@rushstack/eslint-patch': 1.12.0
+      '@typescript-eslint/eslint-plugin': 8.39.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected]([email protected]))([email protected])
+      '@typescript-eslint/parser': 8.39.0([email protected]([email protected]))([email protected])
+      eslint: 9.36.0([email protected])
+      eslint-import-resolver-node: 0.3.9
+      eslint-import-resolver-typescript: 3.10.1([email protected])([email protected]([email protected]))
+      eslint-plugin-import: 2.32.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected]))
+      eslint-plugin-jsx-a11y: 6.10.2([email protected]([email protected]))
+      eslint-plugin-react: 7.37.5([email protected]([email protected]))
+      eslint-plugin-react-hooks: 5.2.0([email protected]([email protected]))
+    optionalDependencies:
+      typescript: 5.9.2
+    transitivePeerDependencies:
+      - eslint-import-resolver-webpack
+      - eslint-plugin-import-x
+      - supports-color
+
+  [email protected]:
+    dependencies:
+      debug: 3.2.7
+      is-core-module: 2.16.1
+      resolve: 1.22.10
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected]([email protected])([email protected]([email protected])):
+    dependencies:
+      '@nolyfill/is-core-module': 1.0.39
+      debug: 4.4.1
+      eslint: 9.36.0([email protected])
+      get-tsconfig: 4.10.1
+      is-bun-module: 2.0.0
+      stable-hash: 0.0.5
+      tinyglobby: 0.2.14
+      unrs-resolver: 1.11.1
+    optionalDependencies:
+      eslint-plugin-import: 2.32.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected]))
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected]([email protected])):
+    dependencies:
+      debug: 3.2.7
+    optionalDependencies:
+      '@typescript-eslint/parser': 8.39.0([email protected]([email protected]))([email protected])
+      eslint: 9.36.0([email protected])
+      eslint-import-resolver-node: 0.3.9
+      eslint-import-resolver-typescript: 3.10.1([email protected])([email protected]([email protected]))
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected](@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected]([email protected])):
+    dependencies:
+      '@rtsao/scc': 1.1.0
+      array-includes: 3.1.9
+      array.prototype.findlastindex: 1.2.6
+      array.prototype.flat: 1.3.3
+      array.prototype.flatmap: 1.3.3
+      debug: 3.2.7
+      doctrine: 2.1.0
+      eslint: 9.36.0([email protected])
+      eslint-import-resolver-node: 0.3.9
+      eslint-module-utils: 2.12.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected]))([email protected])([email protected])([email protected]([email protected]))
+      hasown: 2.0.2
+      is-core-module: 2.16.1
+      is-glob: 4.0.3
+      minimatch: 3.1.2
+      object.fromentries: 2.0.8
+      object.groupby: 1.0.3
+      object.values: 1.2.1
+      semver: 6.3.1
+      string.prototype.trimend: 1.0.9
+      tsconfig-paths: 3.15.0
+    optionalDependencies:
+      '@typescript-eslint/parser': 8.39.0([email protected]([email protected]))([email protected])
+    transitivePeerDependencies:
+      - eslint-import-resolver-typescript
+      - eslint-import-resolver-webpack
+      - supports-color
+
+  [email protected]([email protected]([email protected])):
+    dependencies:
+      aria-query: 5.3.2
+      array-includes: 3.1.9
+      array.prototype.flatmap: 1.3.3
+      ast-types-flow: 0.0.8
+      axe-core: 4.10.3
+      axobject-query: 4.1.0
+      damerau-levenshtein: 1.0.8
+      emoji-regex: 9.2.2
+      eslint: 9.36.0([email protected])
+      hasown: 2.0.2
+      jsx-ast-utils: 3.3.5
+      language-tags: 1.0.9
+      minimatch: 3.1.2
+      object.fromentries: 2.0.8
+      safe-regex-test: 1.1.0
+      string.prototype.includes: 2.0.1
+
+  [email protected]([email protected]([email protected])):
+    dependencies:
+      eslint: 9.36.0([email protected])
+
+  [email protected]([email protected]([email protected])):
+    dependencies:
+      array-includes: 3.1.9
+      array.prototype.findlast: 1.2.5
+      array.prototype.flatmap: 1.3.3
+      array.prototype.tosorted: 1.1.4
+      doctrine: 2.1.0
+      es-iterator-helpers: 1.2.1
+      eslint: 9.36.0([email protected])
+      estraverse: 5.3.0
+      hasown: 2.0.2
+      jsx-ast-utils: 3.3.5
+      minimatch: 3.1.2
+      object.entries: 1.1.9
+      object.fromentries: 2.0.8
+      object.values: 1.2.1
+      prop-types: 15.8.1
+      resolve: 2.0.0-next.5
+      semver: 6.3.1
+      string.prototype.matchall: 4.0.12
+      string.prototype.repeat: 1.0.0
+
+  [email protected]:
+    dependencies:
+      esrecurse: 4.3.0
+      estraverse: 5.3.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      '@eslint-community/eslint-utils': 4.9.0([email protected]([email protected]))
+      '@eslint-community/regexpp': 4.12.1
+      '@eslint/config-array': 0.21.0
+      '@eslint/config-helpers': 0.3.1
+      '@eslint/core': 0.15.2
+      '@eslint/eslintrc': 3.3.1
+      '@eslint/js': 9.36.0
+      '@eslint/plugin-kit': 0.3.5
+      '@humanfs/node': 0.16.6
+      '@humanwhocodes/module-importer': 1.0.1
+      '@humanwhocodes/retry': 0.4.3
+      '@types/estree': 1.0.8
+      '@types/json-schema': 7.0.15
+      ajv: 6.12.6
+      chalk: 4.1.2
+      cross-spawn: 7.0.6
+      debug: 4.4.1
+      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
+    optionalDependencies:
+      jiti: 2.5.1
+    transitivePeerDependencies:
+      - supports-color
+
+  [email protected]:
+    dependencies:
+      acorn: 8.15.0
+      acorn-jsx: 5.3.2([email protected])
+      eslint-visitor-keys: 4.2.1
+
+  [email protected]:
+    dependencies:
+      estraverse: 5.3.0
+
+  [email protected]:
+    dependencies:
+      estraverse: 5.3.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.8
+
+  [email protected]:
+    dependencies:
+      '@nodelib/fs.stat': 2.0.5
+      '@nodelib/fs.walk': 1.2.8
+      glob-parent: 5.1.2
+      merge2: 1.4.1
+      micromatch: 4.0.8
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      reusify: 1.1.0
+
+  [email protected]([email protected]):
+    optionalDependencies:
+      picomatch: 4.0.3
+
+  [email protected]:
+    dependencies:
+      flat-cache: 4.0.1
+
+  [email protected]:
+    dependencies:
+      to-regex-range: 5.0.1
+
+  [email protected]:
+    dependencies:
+      locate-path: 6.0.0
+      path-exists: 4.0.0
+
+  [email protected]:
+    dependencies:
+      flatted: 3.3.3
+      keyv: 4.5.4
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      is-callable: 1.2.7
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      functions-have-names: 1.2.3
+      hasown: 2.0.2
+      is-callable: 1.2.7
+
+  [email protected]: {}
+
+  [email protected]:
+    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
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+
+  [email protected]:
+    dependencies:
+      resolve-pkg-maps: 1.0.0
+
+  [email protected]:
+    dependencies:
+      is-glob: 4.0.3
+
+  [email protected]:
+    dependencies:
+      is-glob: 4.0.3
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      define-properties: 1.2.1
+      gopd: 1.2.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      es-define-property: 1.0.1
+
+  [email protected]:
+    dependencies:
+      dunder-proto: 1.0.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      has-symbols: 1.1.0
+
+  [email protected]:
+    dependencies:
+      function-bind: 1.1.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      parent-module: 1.0.1
+      resolve-from: 4.0.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      hasown: 2.0.2
+      side-channel: 1.1.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      get-intrinsic: 1.3.0
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    dependencies:
+      async-function: 1.0.0
+      call-bound: 1.0.4
+      get-proto: 1.0.1
+      has-tostringtag: 1.0.2
+      safe-regex-test: 1.1.0
+
+  [email protected]:
+    dependencies:
+      has-bigints: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      has-tostringtag: 1.0.2
+
+  [email protected]:
+    dependencies:
+      semver: 7.7.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      hasown: 2.0.2
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      get-intrinsic: 1.3.0
+      is-typed-array: 1.1.15
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      has-tostringtag: 1.0.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      get-proto: 1.0.1
+      has-tostringtag: 1.0.2
+      safe-regex-test: 1.1.0
+
+  [email protected]:
+    dependencies:
+      is-extglob: 2.1.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      has-tostringtag: 1.0.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      gopd: 1.2.0
+      has-tostringtag: 1.0.2
+      hasown: 2.0.2
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      has-tostringtag: 1.0.2
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      has-symbols: 1.1.0
+      safe-regex-test: 1.1.0
+
+  [email protected]:
+    dependencies:
+      which-typed-array: 1.1.19
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      get-intrinsic: 1.3.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      define-data-property: 1.1.4
+      es-object-atoms: 1.1.1
+      get-intrinsic: 1.3.0
+      get-proto: 1.0.1
+      has-symbols: 1.1.0
+      set-function-name: 2.0.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      argparse: 2.0.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      minimist: 1.2.8
+
+  [email protected]:
+    dependencies:
+      array-includes: 3.1.9
+      array.prototype.flat: 1.3.3
+      object.assign: 4.1.7
+      object.values: 1.2.1
+
+  [email protected]:
+    dependencies:
+      json-buffer: 3.0.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      language-subtag-registry: 0.3.23
+
+  [email protected]:
+    dependencies:
+      prelude-ls: 1.2.1
+      type-check: 0.4.0
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    optional: true
+
+  [email protected]:
+    dependencies:
+      detect-libc: 2.0.4
+    optionalDependencies:
+      lightningcss-darwin-arm64: 1.30.1
+      lightningcss-darwin-x64: 1.30.1
+      lightningcss-freebsd-x64: 1.30.1
+      lightningcss-linux-arm-gnueabihf: 1.30.1
+      lightningcss-linux-arm64-gnu: 1.30.1
+      lightningcss-linux-arm64-musl: 1.30.1
+      lightningcss-linux-x64-gnu: 1.30.1
+      lightningcss-linux-x64-musl: 1.30.1
+      lightningcss-win32-arm64-msvc: 1.30.1
+      lightningcss-win32-x64-msvc: 1.30.1
+
+  [email protected]:
+    dependencies:
+      p-locate: 5.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      js-tokens: 4.0.0
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+
+  [email protected]:
+    dependencies:
+      '@jridgewell/sourcemap-codec': 1.5.5
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      braces: 3.0.3
+      picomatch: 2.3.1
+
+  [email protected]:
+    dependencies:
+      brace-expansion: 1.1.12
+
+  [email protected]:
+    dependencies:
+      brace-expansion: 2.0.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      minipass: 7.1.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      '@next/env': 15.4.6
+      '@swc/helpers': 0.5.15
+      caniuse-lite: 1.0.30001733
+      postcss: 8.4.31
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      styled-jsx: 5.1.6([email protected])
+    optionalDependencies:
+      '@next/swc-darwin-arm64': 15.4.6
+      '@next/swc-darwin-x64': 15.4.6
+      '@next/swc-linux-arm64-gnu': 15.4.6
+      '@next/swc-linux-arm64-musl': 15.4.6
+      '@next/swc-linux-x64-gnu': 15.4.6
+      '@next/swc-linux-x64-musl': 15.4.6
+      '@next/swc-win32-arm64-msvc': 15.4.6
+      '@next/swc-win32-x64-msvc': 15.4.6
+      sharp: 0.34.3
+    transitivePeerDependencies:
+      - '@babel/core'
+      - babel-plugin-macros
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-object-atoms: 1.1.1
+      has-symbols: 1.1.0
+      object-keys: 1.1.1
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    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
+
+  [email protected]:
+    dependencies:
+      get-intrinsic: 1.3.0
+      object-keys: 1.1.1
+      safe-push-apply: 1.0.0
+
+  [email protected]:
+    dependencies:
+      yocto-queue: 0.1.0
+
+  [email protected]:
+    dependencies:
+      p-limit: 3.1.0
+
+  [email protected]:
+    dependencies:
+      callsites: 3.1.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      pg-int8: 1.0.1
+      postgres-array: 2.0.0
+      postgres-bytea: 1.0.0
+      postgres-date: 1.0.7
+      postgres-interval: 1.2.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  [email protected]:
+    dependencies:
+      nanoid: 3.3.11
+      picocolors: 1.1.1
+      source-map-js: 1.2.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      xtend: 4.0.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      loose-envify: 1.4.0
+      object-assign: 4.1.1
+      react-is: 16.13.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+      scheduler: 0.26.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-style-singleton: 2.2.3(@types/[email protected])([email protected])
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-remove-scroll-bar: 2.3.8(@types/[email protected])([email protected])
+      react-style-singleton: 2.2.3(@types/[email protected])([email protected])
+      tslib: 2.8.1
+      use-callback-ref: 1.3.3(@types/[email protected])([email protected])
+      use-sidecar: 1.1.3(@types/[email protected])([email protected])
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      fast-equals: 5.3.0
+      prop-types: 15.8.1
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-transition-group: 4.4.5([email protected]([email protected]))([email protected])
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      get-nonce: 1.0.1
+      react: 19.1.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      '@babel/runtime': 7.28.4
+      dom-helpers: 5.2.1
+      loose-envify: 1.4.0
+      prop-types: 15.8.1
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      decimal.js-light: 2.5.1
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      clsx: 2.1.1
+      eventemitter3: 4.0.7
+      lodash: 4.17.21
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+      react-is: 18.3.1
+      react-smooth: 4.0.4([email protected]([email protected]))([email protected])
+      recharts-scale: 0.4.5
+      tiny-invariant: 1.3.3
+      victory-vendor: 36.9.2
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      get-intrinsic: 1.3.0
+      get-proto: 1.0.1
+      which-builtin-type: 1.2.1
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-errors: 1.3.0
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      set-function-name: 2.0.2
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      is-core-module: 2.16.1
+      path-parse: 1.0.7
+      supports-preserve-symlinks-flag: 1.0.0
+
+  [email protected]:
+    dependencies:
+      is-core-module: 2.16.1
+      path-parse: 1.0.7
+      supports-preserve-symlinks-flag: 1.0.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      queue-microtask: 1.2.3
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      get-intrinsic: 1.3.0
+      has-symbols: 1.1.0
+      isarray: 2.0.5
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      isarray: 2.0.5
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      is-regex: 1.2.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      define-data-property: 1.1.4
+      es-errors: 1.3.0
+      function-bind: 1.1.2
+      get-intrinsic: 1.3.0
+      gopd: 1.2.0
+      has-property-descriptors: 1.0.2
+
+  [email protected]:
+    dependencies:
+      define-data-property: 1.1.4
+      es-errors: 1.3.0
+      functions-have-names: 1.2.3
+      has-property-descriptors: 1.0.2
+
+  [email protected]:
+    dependencies:
+      dunder-proto: 1.0.1
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    dependencies:
+      color: 4.2.3
+      detect-libc: 2.0.4
+      semver: 7.7.2
+    optionalDependencies:
+      '@img/sharp-darwin-arm64': 0.34.3
+      '@img/sharp-darwin-x64': 0.34.3
+      '@img/sharp-libvips-darwin-arm64': 1.2.0
+      '@img/sharp-libvips-darwin-x64': 1.2.0
+      '@img/sharp-libvips-linux-arm': 1.2.0
+      '@img/sharp-libvips-linux-arm64': 1.2.0
+      '@img/sharp-libvips-linux-ppc64': 1.2.0
+      '@img/sharp-libvips-linux-s390x': 1.2.0
+      '@img/sharp-libvips-linux-x64': 1.2.0
+      '@img/sharp-libvips-linuxmusl-arm64': 1.2.0
+      '@img/sharp-libvips-linuxmusl-x64': 1.2.0
+      '@img/sharp-linux-arm': 0.34.3
+      '@img/sharp-linux-arm64': 0.34.3
+      '@img/sharp-linux-ppc64': 0.34.3
+      '@img/sharp-linux-s390x': 0.34.3
+      '@img/sharp-linux-x64': 0.34.3
+      '@img/sharp-linuxmusl-arm64': 0.34.3
+      '@img/sharp-linuxmusl-x64': 0.34.3
+      '@img/sharp-wasm32': 0.34.3
+      '@img/sharp-win32-arm64': 0.34.3
+      '@img/sharp-win32-ia32': 0.34.3
+      '@img/sharp-win32-x64': 0.34.3
+    optional: true
+
+  [email protected]:
+    dependencies:
+      shebang-regex: 3.0.0
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      object-inspect: 1.13.4
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      object-inspect: 1.13.4
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      get-intrinsic: 1.3.0
+      object-inspect: 1.13.4
+      side-channel-map: 1.0.1
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      object-inspect: 1.13.4
+      side-channel-list: 1.0.0
+      side-channel-map: 1.0.1
+      side-channel-weakmap: 1.0.2
+
+  [email protected]:
+    dependencies:
+      is-arrayish: 0.3.2
+    optional: true
+
+  [email protected]([email protected]([email protected]))([email protected]):
+    dependencies:
+      react: 19.1.0
+      react-dom: 19.1.0([email protected])
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      buffer-from: 1.1.2
+      source-map: 0.6.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      es-errors: 1.3.0
+      internal-slot: 1.1.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-errors: 1.3.0
+      es-object-atoms: 1.1.1
+      get-intrinsic: 1.3.0
+      gopd: 1.2.0
+      has-symbols: 1.1.0
+      internal-slot: 1.1.0
+      regexp.prototype.flags: 1.5.4
+      set-function-name: 2.0.2
+      side-channel: 1.1.0
+
+  [email protected]:
+    dependencies:
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-data-property: 1.1.4
+      define-properties: 1.2.1
+      es-abstract: 1.24.0
+      es-object-atoms: 1.1.1
+      has-property-descriptors: 1.0.2
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      define-properties: 1.2.1
+      es-object-atoms: 1.1.1
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      define-properties: 1.2.1
+      es-object-atoms: 1.1.1
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]([email protected]):
+    dependencies:
+      client-only: 0.0.1
+      react: 19.1.0
+
+  [email protected]:
+    dependencies:
+      has-flag: 4.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      '@isaacs/fs-minipass': 4.0.1
+      chownr: 3.0.0
+      minipass: 7.1.2
+      minizlib: 3.0.2
+      mkdirp: 3.0.1
+      yallist: 5.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      fdir: 6.4.6([email protected])
+      picomatch: 4.0.3
+
+  [email protected]:
+    dependencies:
+      is-number: 7.0.0
+
+  [email protected]([email protected]):
+    dependencies:
+      typescript: 5.9.2
+
+  [email protected]:
+    dependencies:
+      '@types/json5': 0.0.29
+      json5: 1.0.2
+      minimist: 1.2.8
+      strip-bom: 3.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      prelude-ls: 1.2.1
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      es-errors: 1.3.0
+      is-typed-array: 1.1.15
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      for-each: 0.3.5
+      gopd: 1.2.0
+      has-proto: 1.2.0
+      is-typed-array: 1.1.15
+
+  [email protected]:
+    dependencies:
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.8
+      for-each: 0.3.5
+      gopd: 1.2.0
+      has-proto: 1.2.0
+      is-typed-array: 1.1.15
+      reflect.getprototypeof: 1.0.10
+
+  [email protected]:
+    dependencies:
+      call-bind: 1.0.8
+      for-each: 0.3.5
+      gopd: 1.2.0
+      is-typed-array: 1.1.15
+      possible-typed-array-names: 1.1.0
+      reflect.getprototypeof: 1.0.10
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      has-bigints: 1.1.0
+      has-symbols: 1.1.0
+      which-boxed-primitive: 1.1.1
+
+  [email protected]: {}
+
+  [email protected]:
+    dependencies:
+      napi-postinstall: 0.3.2
+    optionalDependencies:
+      '@unrs/resolver-binding-android-arm-eabi': 1.11.1
+      '@unrs/resolver-binding-android-arm64': 1.11.1
+      '@unrs/resolver-binding-darwin-arm64': 1.11.1
+      '@unrs/resolver-binding-darwin-x64': 1.11.1
+      '@unrs/resolver-binding-freebsd-x64': 1.11.1
+      '@unrs/resolver-binding-linux-arm-gnueabihf': 1.11.1
+      '@unrs/resolver-binding-linux-arm-musleabihf': 1.11.1
+      '@unrs/resolver-binding-linux-arm64-gnu': 1.11.1
+      '@unrs/resolver-binding-linux-arm64-musl': 1.11.1
+      '@unrs/resolver-binding-linux-ppc64-gnu': 1.11.1
+      '@unrs/resolver-binding-linux-riscv64-gnu': 1.11.1
+      '@unrs/resolver-binding-linux-riscv64-musl': 1.11.1
+      '@unrs/resolver-binding-linux-s390x-gnu': 1.11.1
+      '@unrs/resolver-binding-linux-x64-gnu': 1.11.1
+      '@unrs/resolver-binding-linux-x64-musl': 1.11.1
+      '@unrs/resolver-binding-wasm32-wasi': 1.11.1
+      '@unrs/resolver-binding-win32-arm64-msvc': 1.11.1
+      '@unrs/resolver-binding-win32-ia32-msvc': 1.11.1
+      '@unrs/resolver-binding-win32-x64-msvc': 1.11.1
+
+  [email protected]:
+    dependencies:
+      punycode: 2.3.1
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      react: 19.1.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  [email protected](@types/[email protected])([email protected]):
+    dependencies:
+      detect-node-es: 1.1.0
+      react: 19.1.0
+      tslib: 2.8.1
+    optionalDependencies:
+      '@types/react': 19.1.13
+
+  [email protected]([email protected]):
+    dependencies:
+      react: 19.1.0
+
+  [email protected]:
+    dependencies:
+      '@types/d3-array': 3.2.2
+      '@types/d3-ease': 3.0.2
+      '@types/d3-interpolate': 3.0.4
+      '@types/d3-scale': 4.0.9
+      '@types/d3-shape': 3.1.7
+      '@types/d3-time': 3.0.4
+      '@types/d3-timer': 3.0.2
+      d3-array: 3.2.4
+      d3-ease: 3.0.1
+      d3-interpolate: 3.0.1
+      d3-scale: 4.0.2
+      d3-shape: 3.2.0
+      d3-time: 3.1.0
+      d3-timer: 3.0.1
+
+  [email protected]:
+    dependencies:
+      is-bigint: 1.1.0
+      is-boolean-object: 1.2.2
+      is-number-object: 1.1.1
+      is-string: 1.1.1
+      is-symbol: 1.1.1
+
+  [email protected]:
+    dependencies:
+      call-bound: 1.0.4
+      function.prototype.name: 1.1.8
+      has-tostringtag: 1.0.2
+      is-async-function: 2.1.1
+      is-date-object: 1.1.0
+      is-finalizationregistry: 1.1.1
+      is-generator-function: 1.1.0
+      is-regex: 1.2.1
+      is-weakref: 1.1.1
+      isarray: 2.0.5
+      which-boxed-primitive: 1.1.1
+      which-collection: 1.0.2
+      which-typed-array: 1.1.19
+
+  [email protected]:
+    dependencies:
+      is-map: 2.0.3
+      is-set: 2.0.3
+      is-weakmap: 2.0.2
+      is-weakset: 2.0.4
+
+  [email protected]:
+    dependencies:
+      available-typed-arrays: 1.0.7
+      call-bind: 1.0.8
+      call-bound: 1.0.4
+      for-each: 0.3.5
+      get-proto: 1.0.1
+      gopd: 1.2.0
+      has-tostringtag: 1.0.2
+
+  [email protected]:
+    dependencies:
+      isexe: 2.0.0
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}
+
+  [email protected]: {}

+ 5 - 0
postcss.config.mjs

@@ -0,0 +1,5 @@
+const config = {
+  plugins: ["@tailwindcss/postcss"],
+};
+
+export default config;

+ 1 - 0
public/file.svg

@@ -0,0 +1 @@
+<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>

+ 1 - 0
public/globe.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>

+ 1 - 0
public/next.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

BIN
public/readme/价格表.webp


BIN
public/readme/供应商.webp


BIN
public/readme/客户端.webp


BIN
public/readme/文档.webp


BIN
public/readme/统计.webp


+ 1 - 0
public/vercel.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>

+ 1 - 0
public/window.svg

@@ -0,0 +1 @@
+<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>

+ 125 - 0
src/actions/keys.ts

@@ -0,0 +1,125 @@
+'use server';
+
+import { revalidatePath } from "next/cache";
+import { randomBytes } from "node:crypto";
+import { KeyFormSchema } from "@/lib/validation/schemas";
+import { createKey, updateKey, deleteKey, findActiveKeyByUserIdAndName, findKeyById, countActiveKeysByUser } from "@/repository/key";
+import { getSession } from "@/lib/auth";
+import type { ActionResult } from "./types";
+
+// 添加密钥
+// 说明:为提升前端可控性,避免直接抛错,返回判别式结果。
+export async function addKey(
+  data: { userId: number; name: string; expiresAt?: string }
+): Promise<ActionResult<{ generatedKey: string; name: string }>> {
+  try {
+    // 权限检查:用户只能给自己添加Key,管理员可以给所有人添加Key
+    const session = await getSession();
+    if (!session) {
+      return { ok: false, error: '未登录' };
+    }
+    if (session.user.role !== 'admin' && session.user.id !== data.userId) {
+      return { ok: false, error: '无权限执行此操作' };
+    }
+
+    const validatedData = KeyFormSchema.parse({
+      name: data.name,
+      expiresAt: data.expiresAt
+    });
+
+    // 检查是否存在同名的生效key
+    const existingKey = await findActiveKeyByUserIdAndName(data.userId, validatedData.name);
+    if (existingKey) {
+      return { ok: false, error: `名为"${validatedData.name}"的密钥已存在且正在生效中,请使用不同的名称` };
+    }
+
+    const generatedKey = 'sk-' + randomBytes(16).toString('hex');
+
+    await createKey({
+      user_id: data.userId,
+      name: validatedData.name,
+      key: generatedKey,
+      is_enabled: true,
+      expires_at: validatedData.expiresAt ? new Date(validatedData.expiresAt) : undefined
+    });
+
+    revalidatePath('/dashboard');
+
+    // 返回生成的key供前端显示
+    return { ok: true, data: { generatedKey, name: validatedData.name } };
+  } catch (error) {
+    console.error('添加密钥失败:', error);
+    const message = error instanceof Error ? error.message : '添加密钥失败,请稍后重试';
+    return { ok: false, error: message };
+  }
+}
+
+// 更新密钥
+export async function editKey(
+  keyId: number,
+  data: { name: string; expiresAt?: string }
+): Promise<ActionResult> {
+  try {
+    // 权限检查:用户只能编辑自己的Key,管理员可以编辑所有Key
+    const session = await getSession();
+    if (!session) {
+      return { ok: false, error: '未登录' };
+    }
+
+    const key = await findKeyById(keyId);
+    if (!key) {
+      return { ok: false, error: '密钥不存在' };
+    }
+
+    if (session.user.role !== 'admin' && session.user.id !== key.userId) {
+      return { ok: false, error: '无权限执行此操作' };
+    }
+
+    const validatedData = KeyFormSchema.parse(data);
+    
+    await updateKey(keyId, {
+      name: validatedData.name,
+      expires_at: validatedData.expiresAt ? new Date(validatedData.expiresAt) : undefined
+    });
+    
+    revalidatePath('/dashboard');
+    return { ok: true };
+  } catch (error) {
+    console.error('更新密钥失败:', error);
+    const message = error instanceof Error ? error.message : '更新密钥失败,请稍后重试';
+    return { ok: false, error: message };
+  }
+}
+
+// 删除密钥
+export async function removeKey(keyId: number): Promise<ActionResult> {
+  try {
+    // 权限检查:用户只能删除自己的Key,管理员可以删除所有Key
+    const session = await getSession();
+    if (!session) {
+      return { ok: false, error: '未登录' };
+    }
+
+    const key = await findKeyById(keyId);
+    if (!key) {
+      return { ok: false, error: '密钥不存在' };
+    }
+
+    if (session.user.role !== 'admin' && session.user.id !== key.userId) {
+      return { ok: false, error: '无权限执行此操作' };
+    }
+
+    const activeKeyCount = await countActiveKeysByUser(key.userId);
+    if (activeKeyCount <= 1) {
+      return { ok: false, error: '该用户至少需要保留一个可用的密钥,无法删除最后一个密钥' };
+    }
+
+    await deleteKey(keyId);
+    revalidatePath('/dashboard');
+    return { ok: true };
+  } catch (error) {
+    console.error('删除密钥失败:', error);
+    const message = error instanceof Error ? error.message : '删除密钥失败,请稍后重试';
+    return { ok: false, error: message };
+  }
+}

+ 142 - 0
src/actions/model-prices.ts

@@ -0,0 +1,142 @@
+"use server";
+
+import { revalidatePath } from "next/cache";
+import { getSession } from "@/lib/auth";
+import {
+  findLatestPriceByModel,
+  createModelPrice,
+  findAllLatestPrices,
+} from "@/repository/model-price";
+import type {
+  PriceTableJson,
+  PriceUpdateResult,
+  ModelPrice,
+  ModelPriceData,
+} from "@/types/model-price";
+import type { ActionResult } from "./types";
+
+/**
+ * 检查价格数据是否相同
+ */
+function isPriceDataEqual(
+  data1: ModelPriceData,
+  data2: ModelPriceData
+): boolean {
+  // 深度比较两个价格对象
+  return JSON.stringify(data1) === JSON.stringify(data2);
+}
+
+/**
+ * 上传并更新模型价格表
+ */
+export async function uploadPriceTable(
+  jsonContent: string
+): Promise<ActionResult<PriceUpdateResult>> {
+  try {
+    // 权限检查:只有管理员可以上传价格表
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
+    }
+
+    // 解析JSON内容
+    let priceTable: PriceTableJson;
+    try {
+      priceTable = JSON.parse(jsonContent);
+    } catch {
+      return { ok: false, error: "JSON格式不正确,请检查文件内容" };
+    }
+
+    // 验证是否为对象
+    if (typeof priceTable !== "object" || priceTable === null) {
+      return { ok: false, error: "价格表必须是一个JSON对象" };
+    }
+
+    const entries = Object.entries(priceTable).filter(([modelName]) =>
+      typeof modelName === "string" && modelName.toLowerCase().startsWith("claude-")
+    );
+
+    const result: PriceUpdateResult = {
+      added: [],
+      updated: [],
+      unchanged: [],
+      failed: [],
+      total: entries.length,
+    };
+
+    // 处理每个模型的价格
+    for (const [modelName, priceData] of entries) {
+      try {
+        // 验证价格数据
+        if (typeof priceData !== "object" || priceData === null) {
+          result.failed.push(modelName);
+          continue;
+        }
+
+        // 查找该模型的最新价格
+        const existingPrice = await findLatestPriceByModel(modelName);
+
+        if (!existingPrice) {
+          // 模型不存在,新增记录
+          await createModelPrice(modelName, priceData);
+          result.added.push(modelName);
+        } else if (!isPriceDataEqual(existingPrice.priceData, priceData)) {
+          // 模型存在但价格发生变化,新增记录
+          await createModelPrice(modelName, priceData);
+          result.updated.push(modelName);
+        } else {
+          // 价格未发生变化,不需要更新
+          result.unchanged.push(modelName);
+        }
+      } catch (error) {
+        console.error(`处理模型 ${modelName} 失败:`, error);
+        result.failed.push(modelName);
+      }
+    }
+
+    // 刷新页面数据
+    revalidatePath("/settings/prices");
+
+    return { ok: true, data: result };
+  } catch (error) {
+    console.error("上传价格表失败:", error);
+    const message =
+      error instanceof Error ? error.message : "上传失败,请稍后重试";
+    return { ok: false, error: message };
+  }
+}
+
+/**
+ * 获取所有模型的最新价格,仅包含 claude 系列
+ */
+export async function getModelPrices(): Promise<ModelPrice[]> {
+  try {
+    // 权限检查:只有管理员可以查看价格表
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return [];
+    }
+
+    return await findAllLatestPrices();
+  } catch (error) {
+    console.error("获取模型价格失败:", error);
+    return [];
+  }
+}
+
+/**
+ * 检查是否存在价格表数据
+ */
+export async function hasPriceTable(): Promise<boolean> {
+  try {
+    const prices = await getModelPrices();
+    return prices.length > 0;
+  } catch (error) {
+    console.error("检查价格表失败:", error);
+    return false;
+  }
+}
+
+/**
+ * 获取指定模型的最新价格
+ */

+ 125 - 0
src/actions/providers.ts

@@ -0,0 +1,125 @@
+'use server';
+
+import { findProviderList, createProvider, updateProvider, deleteProvider } from "@/repository/provider";
+import { revalidatePath } from "next/cache";
+import { type ProviderDisplay } from "@/types/provider";
+import { maskKey } from "@/lib/utils/validation";
+import { getSession } from "@/lib/auth";
+import { CreateProviderSchema, UpdateProviderSchema } from "@/lib/validation/schemas";
+import type { ActionResult } from "./types";
+
+// 获取服务商数据
+export async function getProviders(): Promise<ProviderDisplay[]> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== 'admin') {
+      return [];
+    }
+
+    const providers = await findProviderList();
+    
+    return providers.map(provider => ({
+      id: provider.id,
+      name: provider.name,
+      url: provider.url,
+      maskedKey: maskKey(provider.key),
+      isEnabled: provider.isEnabled,
+      weight: provider.weight,
+      tpm: provider.tpm,
+      rpm: provider.rpm,
+      rpd: provider.rpd,
+      cc: provider.cc,
+      createdAt: provider.createdAt.toISOString().split('T')[0],
+      updatedAt: provider.updatedAt.toISOString().split('T')[0],
+    }));
+  } catch (error) {
+    console.error("获取服务商数据失败:", error);
+    return [];
+  }
+}
+
+// 添加服务商
+export async function addProvider(data: {
+  name: string;
+  url: string;
+  key: string;
+  is_enabled?: boolean;
+  weight?: number;
+  tpm: number | null;
+  rpm: number | null;
+  rpd: number | null;
+  cc: number | null;
+}): Promise<ActionResult> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== 'admin') {
+      return { ok: false, error: '无权限执行此操作' };
+    }
+
+    const validated = CreateProviderSchema.parse(data);
+    const payload = {
+      ...validated,
+      tpm: validated.tpm ?? null,
+      rpm: validated.rpm ?? null,
+      rpd: validated.rpd ?? null,
+      cc: validated.cc ?? null,
+    };
+    await createProvider(payload);
+    revalidatePath('/settings/providers');
+    return { ok: true };
+  } catch (error) {
+    console.error('创建服务商失败:', error);
+    const message = error instanceof Error ? error.message : '创建服务商失败';
+    return { ok: false, error: message };
+  }
+}
+
+// 更新服务商
+export async function editProvider(
+  providerId: number,
+  data: {
+    name?: string;
+    url?: string;
+    key?: string;
+    is_enabled?: boolean;
+    weight?: number;
+    tpm?: number | null;
+    rpm?: number | null;
+    rpd?: number | null;
+    cc?: number | null;
+  }
+): Promise<ActionResult> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== 'admin') {
+      return { ok: false, error: '无权限执行此操作' };
+    }
+
+    const validated = UpdateProviderSchema.parse(data);
+    await updateProvider(providerId, validated);
+    revalidatePath('/settings/providers');
+    return { ok: true };
+  } catch (error) {
+    console.error('更新服务商失败:', error);
+    const message = error instanceof Error ? error.message : '更新服务商失败';
+    return { ok: false, error: message };
+  }
+}
+
+// 删除服务商
+export async function removeProvider(providerId: number): Promise<ActionResult> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== 'admin') {
+      return { ok: false, error: '无权限执行此操作' };
+    }
+
+    await deleteProvider(providerId);
+    revalidatePath('/settings/providers');
+    return { ok: true };
+  } catch (error) {
+    console.error('删除服务商失败:', error);
+    const message = error instanceof Error ? error.message : '删除服务商失败';
+    return { ok: false, error: message };
+  }
+}

+ 112 - 0
src/actions/statistics.ts

@@ -0,0 +1,112 @@
+"use server";
+
+import { getSession } from "@/lib/auth";
+import { getUserStatisticsFromDB, getActiveUsersFromDB } from "@/repository/statistics";
+import type {
+  TimeRange,
+  UserStatisticsData,
+  DatabaseStatRow,
+  DatabaseUser,
+  ChartDataItem,
+  StatisticsUser,
+} from "@/types/statistics";
+import { TIME_RANGE_OPTIONS, DEFAULT_TIME_RANGE } from "@/types/statistics";
+import type { ActionResult } from "./types";
+
+/**
+ * 生成图表数据使用的用户键,避免名称碰撞
+ */
+const createUserDataKey = (userId: number): string => `user-${userId}`;
+
+/**
+ * 获取用户统计数据,用于图表展示
+ */
+export async function getUserStatistics(
+  timeRange: TimeRange = DEFAULT_TIME_RANGE
+): Promise<ActionResult<UserStatisticsData>> {
+  try {
+    const session = await getSession();
+    if (!session) {
+      return {
+        ok: false,
+        error: "未登录",
+      };
+    }
+
+    // 获取时间范围配置
+    const rangeConfig = TIME_RANGE_OPTIONS.find(option => option.key === timeRange);
+    if (!rangeConfig) {
+      throw new Error(`Invalid time range: ${timeRange}`);
+    }
+
+    const [statsData, users] = await Promise.all([
+      getUserStatisticsFromDB(timeRange),
+      getActiveUsersFromDB()
+    ]);
+
+    // 将数据转换为适合图表的格式
+    const dataByDate = new Map<string, ChartDataItem>();
+
+    statsData.forEach((row: DatabaseStatRow) => {
+      // 根据分辨率格式化日期
+      let dateStr: string;
+      if (rangeConfig.resolution === 'hour') {
+        // 小时分辨率:显示为 "HH:mm" 格式
+        const hour = new Date(row.date);
+        dateStr = hour.toISOString();
+      } else {
+        // 天分辨率:显示为 "YYYY-MM-DD" 格式
+        dateStr = new Date(row.date).toISOString().split('T')[0];
+      }
+
+      if (!dataByDate.has(dateStr)) {
+        dataByDate.set(dateStr, {
+          date: dateStr,
+        });
+      }
+
+      const dateData = dataByDate.get(dateStr)!;
+      const userKey = createUserDataKey(row.user_id);
+
+      // 安全地处理大数值,防止精度问题
+      const cost = row.total_cost ? parseFloat(row.total_cost.toString()) : 0;
+      const calls = row.api_calls || 0;
+
+      // 为每个用户创建消费和调用次数的键
+      dateData[`${userKey}_cost`] = cost;
+      dateData[`${userKey}_calls`] = calls;
+    });
+
+    const result: UserStatisticsData = {
+      chartData: Array.from(dataByDate.values()),
+      users: users.map((u: DatabaseUser): StatisticsUser => ({
+        id: u.id,
+        name: u.name || `User${u.id}`,
+        dataKey: createUserDataKey(u.id),
+      })),
+      timeRange,
+      resolution: rangeConfig.resolution
+    };
+
+    return {
+      ok: true,
+      data: result
+    };
+  } catch (error) {
+    console.error('Failed to get user statistics:', error);
+
+    // 提供更具体的错误信息
+    const errorMessage = error instanceof Error ? error.message : '未知错误';
+    if (errorMessage.includes('numeric field overflow')) {
+      return {
+        ok: false,
+        error: '数据金额过大,请检查数据库中的费用记录'
+      };
+    }
+
+    return {
+      ok: false,
+      error: '获取统计数据失败:' + errorMessage
+    };
+  }
+}

+ 5 - 0
src/actions/types.ts

@@ -0,0 +1,5 @@
+export type SuccessResult<T = undefined> = T extends undefined
+  ? { ok: true; data?: undefined }
+  : { ok: true; data: T };
+
+export type ActionResult<T = undefined> = SuccessResult<T> | { ok: false; error: string };

+ 203 - 0
src/actions/users.ts

@@ -0,0 +1,203 @@
+"use server";
+
+import {
+  findUserList,
+  createUser,
+  updateUser,
+  deleteUser,
+} from "@/repository/user";
+import { findKeyList, findKeyUsageToday } from "@/repository/key";
+import { revalidatePath } from "next/cache";
+import { randomBytes } from "node:crypto";
+import { type UserDisplay } from "@/types/user";
+import { maskKey } from "@/lib/utils/validation";
+import { CreateUserSchema, UpdateUserSchema } from "@/lib/validation/schemas";
+import { USER_DEFAULTS } from "@/lib/constants/user.constants";
+import { createKey } from "@/repository/key";
+import { getSession } from "@/lib/auth";
+import type { ActionResult } from "./types";
+
+// 获取用户数据
+export async function getUsers(): Promise<UserDisplay[]> {
+  try {
+    const session = await getSession();
+    if (!session) {
+      return [];
+    }
+
+    // 普通用户只能看到自己的数据
+    let users;
+    if (session.user.role === "user") {
+      users = [session.user]; // 只返回当前用户
+    } else {
+      users = await findUserList(); // 管理员可以看到所有用户
+    }
+
+    if (users.length === 0) {
+      return [];
+    }
+
+    // 管理员可以看到完整Key,普通用户只能看到掩码
+    const isAdmin = session.user.role === "admin";
+
+    const userDisplays: UserDisplay[] = await Promise.all(
+      users.map(async (user) => {
+        try {
+          const [keys, usageRecords] = await Promise.all([
+            findKeyList(user.id),
+            findKeyUsageToday(user.id)
+          ]);
+
+          const usageMap = new Map(
+            usageRecords.map((item) => [item.keyId, item.totalCost ?? 0])
+          );
+
+          return {
+            id: user.id,
+            name: user.name,
+            note: user.description || undefined,
+            role: user.role,
+            rpm: user.rpm,
+            dailyQuota: user.dailyQuota,
+            keys: keys.map((key) => ({
+              id: key.id,
+              name: key.name,
+              maskedKey: maskKey(key.key),
+              fullKey: isAdmin ? key.key : undefined, // 仅管理员可见
+              canCopy: isAdmin, // 仅管理员可复制
+              expiresAt: key.expiresAt
+                ? key.expiresAt.toISOString().split("T")[0]
+                : "永不过期",
+              status: key.isEnabled ? "enabled" : ("disabled" as const),
+              createdAt: key.createdAt,
+              createdAtFormatted: key.createdAt.toLocaleString('zh-CN', {
+                year: 'numeric',
+                month: '2-digit',
+                day: '2-digit',
+                hour: '2-digit',
+                minute: '2-digit',
+                second: '2-digit'
+              }),
+              todayUsage: usageMap.get(key.id) ?? 0
+            })),
+          };
+        } catch (error) {
+          console.error(`获取用户 ${user.id} 的密钥失败:`, error);
+          return {
+            id: user.id,
+            name: user.name,
+            note: user.description || undefined,
+            role: user.role,
+            rpm: user.rpm,
+            dailyQuota: user.dailyQuota,
+            keys: [],
+          };
+        }
+      }),
+    );
+
+    return userDisplays;
+  } catch (error) {
+    console.error("获取用户数据失败:", error);
+    return [];
+  }
+}
+
+// 添加用户
+export async function addUser(data: {
+  name: string;
+  note?: string;
+  rpm?: number;
+  dailyQuota?: number;
+}): Promise<ActionResult> {
+  try {
+    // 权限检查:只有管理员可以添加用户
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" } as const;
+    }
+
+    const validatedData = CreateUserSchema.parse({
+      name: data.name,
+      note: data.note || "",
+      rpm: data.rpm || USER_DEFAULTS.RPM,
+      dailyQuota: data.dailyQuota || USER_DEFAULTS.DAILY_QUOTA,
+    });
+
+    const newUser = await createUser({
+      name: validatedData.name,
+      description: validatedData.note || "",
+      rpm: validatedData.rpm,
+      dailyQuota: validatedData.dailyQuota,
+    });
+
+    // 为新用户创建默认密钥
+    const generatedKey = "sk-" + randomBytes(16).toString("hex");
+    await createKey({
+      user_id: newUser.id,
+      name: "default",
+      key: generatedKey,
+      is_enabled: true,
+      expires_at: undefined,
+    });
+
+    revalidatePath("/dashboard");
+    return { ok: true };
+  } catch (error) {
+    console.error("添加用户失败:", error);
+    const message =
+      error instanceof Error ? error.message : "添加用户失败,请稍后重试";
+    return { ok: false, error: message };
+  }
+}
+
+// 更新用户
+export async function editUser(
+  userId: number,
+  data: { name?: string; note?: string; rpm?: number; dailyQuota?: number },
+): Promise<ActionResult> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
+    }
+
+    const validatedData = UpdateUserSchema.parse(data);
+
+    await updateUser(userId, {
+      name: validatedData.name,
+      description: validatedData.note,
+      rpm: validatedData.rpm,
+      dailyQuota: validatedData.dailyQuota,
+    });
+
+    revalidatePath("/dashboard");
+    return { ok: true };
+  } catch (error) {
+    console.error("更新用户失败:", error);
+    const message =
+      error instanceof Error ? error.message : "更新用户失败,请稍后重试";
+    return { ok: false, error: message };
+  }
+}
+
+// 删除用户
+export async function removeUser(
+  userId: number,
+): Promise<ActionResult> {
+  try {
+    const session = await getSession();
+    if (!session || session.user.role !== "admin") {
+      return { ok: false, error: "无权限执行此操作" };
+    }
+
+    await deleteUser(userId);
+    revalidatePath("/dashboard");
+    return { ok: true };
+  } catch (error) {
+    console.error("删除用户失败:", error);
+    const message =
+      error instanceof Error ? error.message : "删除用户失败,请稍后重试";
+    return { ok: false, error: message };
+  }
+}

+ 42 - 0
src/app/api/auth/login/route.ts

@@ -0,0 +1,42 @@
+import { NextRequest, NextResponse } from 'next/server';
+import { validateKey, setAuthCookie } from '@/lib/auth';
+
+export async function POST(request: NextRequest) {
+  try {
+    const { key } = await request.json();
+
+    if (!key) {
+      return NextResponse.json(
+        { error: '请输入 API Key' },
+        { status: 400 }
+      );
+    }
+
+    const session = await validateKey(key);
+    if (!session) {
+      return NextResponse.json(
+        { error: 'API Key 无效或已过期' },
+        { status: 401 }
+      );
+    }
+
+    // 设置认证 cookie
+    await setAuthCookie(key);
+
+    return NextResponse.json({
+      ok: true,
+      user: {
+        id: session.user.id,
+        name: session.user.name,
+        description: session.user.description,
+        role: session.user.role,
+      },
+    });
+  } catch (error) {
+    console.error('Login error:', error);
+    return NextResponse.json(
+      { error: '登录失败,请稍后重试' },
+      { status: 500 }
+    );
+  }
+}

+ 7 - 0
src/app/api/auth/logout/route.ts

@@ -0,0 +1,7 @@
+import { NextResponse } from 'next/server';
+import { clearAuthCookie } from '@/lib/auth';
+
+export async function POST() {
+  await clearAuthCookie();
+  return NextResponse.json({ ok: true });
+}

+ 38 - 0
src/app/dashboard/_components/dashboard-header.tsx

@@ -0,0 +1,38 @@
+import Link from "next/link";
+
+import type { AuthSession } from "@/lib/auth";
+import { Button } from "@/components/ui/button";
+import { DashboardNav, type DashboardNavItem } from "./dashboard-nav";
+import { UserMenu } from "./user-menu";
+
+interface DashboardHeaderProps {
+  session: AuthSession | null;
+}
+
+const NAV_ITEMS: (DashboardNavItem & { adminOnly?: boolean })[] = [
+  { href: "/dashboard", label: "仪表盘" },
+  { href: "/usage-doc", label: "文档" },
+  { href: "/settings", label: "系统设置", adminOnly: true },
+];
+
+export function DashboardHeader({ session }: DashboardHeaderProps) {
+  const isAdmin = session?.user.role === "admin";
+  const items = NAV_ITEMS.filter((item) => !item.adminOnly || isAdmin);
+
+  return (
+    <header className="sticky top-0 z-40 border-b border-border/80 bg-card/80 backdrop-blur supports-[backdrop-filter]:bg-card/60">
+      <div className="mx-auto flex h-16 w-full max-w-7xl items-center justify-between px-6">
+        <DashboardNav items={items} />
+        <div className="flex items-center gap-3">
+          {session ? (
+            <UserMenu user={session.user} />
+          ) : (
+            <Button asChild size="sm" variant="outline">
+              <Link href="/login">登录</Link>
+            </Button>
+          )}
+        </div>
+      </div>
+    </header>
+  );
+}

+ 51 - 0
src/app/dashboard/_components/dashboard-nav.tsx

@@ -0,0 +1,51 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { cn } from "@/lib/utils";
+
+export interface DashboardNavItem {
+  href: string;
+  label: string;
+}
+
+interface DashboardNavProps {
+  items: DashboardNavItem[];
+}
+
+export function DashboardNav({ items }: DashboardNavProps) {
+  const pathname = usePathname();
+
+  if (items.length === 0) {
+    return null;
+  }
+
+  const getIsActive = (href: string) => {
+    if (href === "/dashboard") {
+      return pathname === "/dashboard";
+    }
+
+    return pathname.startsWith(href);
+  };
+
+  return (
+    <nav className="flex items-center gap-1 rounded-full border border-border/80 bg-background/80 px-1 py-1 backdrop-blur supports-[backdrop-filter]:bg-background/60">
+      {items.map((item) => {
+        const isActive = getIsActive(item.href);
+        return (
+          <Link
+            key={item.href}
+            href={item.href}
+            className={cn(
+              "rounded-full px-3 py-1.5 text-sm font-medium text-muted-foreground transition-all hover:text-foreground",
+              isActive && "bg-primary/5 text-foreground shadow-[0_1px_0_0_rgba(0,0,0,0.03)]"
+            )}
+          >
+            {item.label}
+          </Link>
+        );
+      })}
+    </nav>
+  );
+}

+ 447 - 0
src/app/dashboard/_components/statistics/chart.tsx

@@ -0,0 +1,447 @@
+"use client"
+
+import * as React from "react";
+import { Area, AreaChart, CartesianGrid, XAxis, YAxis } from "recharts";
+
+import {
+  Card,
+  CardContent,
+  CardDescription,
+  CardHeader,
+  CardTitle,
+} from "@/components/ui/card";
+import { cn } from "@/lib/utils";
+import {
+  ChartConfig,
+  ChartContainer,
+  ChartLegend,
+  ChartTooltip,
+} from "@/components/ui/chart";
+
+import type { UserStatisticsData, TimeRange } from "@/types/statistics";
+import { TimeRangeSelector } from "./time-range-selector";
+
+// 固定的调色盘,确保新增用户也能获得可辨识的颜色
+const USER_COLOR_PALETTE = [
+  "var(--chart-1)",
+  "var(--chart-2)",
+  "var(--chart-3)",
+  "var(--chart-4)",
+  "var(--chart-5)",
+  "hsl(15, 85%, 60%)",
+  "hsl(195, 85%, 60%)",
+  "hsl(285, 85%, 60%)",
+  "hsl(135, 85%, 50%)",
+  "hsl(45, 85%, 55%)",
+  "hsl(315, 85%, 65%)",
+  "hsl(165, 85%, 55%)",
+  "hsl(35, 85%, 65%)",
+  "hsl(255, 85%, 65%)",
+  "hsl(75, 85%, 50%)",
+  "hsl(345, 85%, 65%)",
+  "hsl(105, 85%, 55%)",
+  "hsl(225, 85%, 65%)",
+  "hsl(55, 85%, 60%)",
+  "hsl(275, 85%, 60%)",
+  "hsl(25, 85%, 65%)",
+  "hsl(185, 85%, 60%)",
+  "hsl(125, 85%, 55%)",
+  "hsl(295, 85%, 70%)",
+] as const;
+
+// 根据索引循环分配颜色,避免重复定义数组
+const getUserColor = (index: number) =>
+  USER_COLOR_PALETTE[index % USER_COLOR_PALETTE.length];
+
+export interface UserStatisticsChartProps {
+  data: UserStatisticsData;
+  onTimeRangeChange?: (timeRange: TimeRange) => void;
+  loading?: boolean;
+}
+
+/**
+ * 用户统计图表组件
+ * 展示用户的消费金额和API调用次数
+ */
+export function UserStatisticsChart({ data, onTimeRangeChange, loading = false }: UserStatisticsChartProps) {
+  const [activeChart, setActiveChart] = React.useState<"cost" | "calls">("cost")
+
+  // 动态生成图表配置
+  const chartConfig = React.useMemo(() => {
+    const config: ChartConfig = {
+      cost: {
+        label: "消费金额",
+      },
+      calls: {
+        label: "API调用次数",
+      },
+    }
+
+    data.users.forEach((user, index) => {
+      config[user.dataKey] = {
+        label: user.name,
+        color: getUserColor(index),
+      }
+    })
+
+    return config
+  }, [data.users])
+
+  const userMap = React.useMemo(() => {
+    return new Map(data.users.map((user) => [user.dataKey, user]))
+  }, [data.users])
+
+  // 计算每个用户的总数据
+  const userTotals = React.useMemo(() => {
+    const totals: Record<string, { cost: number; calls: number }> = {}
+
+    data.users.forEach(user => {
+      totals[user.dataKey] = { cost: 0, calls: 0 }
+    })
+
+    data.chartData.forEach(day => {
+      data.users.forEach(user => {
+        const costValue = day[`${user.dataKey}_cost`]
+        const callsValue = day[`${user.dataKey}_calls`]
+
+        totals[user.dataKey].cost += typeof costValue === 'number' ? costValue : 0
+        totals[user.dataKey].calls += typeof callsValue === 'number' ? callsValue : 0
+      })
+    })
+
+    return totals
+  }, [data.chartData, data.users])
+
+  const sortedLegendUsers = React.useMemo(() => {
+    return data.users
+      .map((user, index) => ({ user, index }))
+      .sort((a, b) => {
+        const totalsA = userTotals[a.user.dataKey]
+        const totalsB = userTotals[b.user.dataKey]
+        const valueA = totalsA ? totalsA[activeChart] : 0
+        const valueB = totalsB ? totalsB[activeChart] : 0
+
+        if (valueA === valueB) {
+          return a.index - b.index
+        }
+
+        return valueB - valueA
+      })
+  }, [data.users, userTotals, activeChart])
+
+  // 计算总计
+  const totals = React.useMemo(() => {
+    const costTotal = data.chartData.reduce((sum, day) => {
+      const dayTotal = data.users.reduce((daySum, user) => {
+        const costValue = day[`${user.dataKey}_cost`]
+        return daySum + (typeof costValue === 'number' ? costValue : 0);
+      }, 0)
+      return sum + dayTotal
+    }, 0)
+
+    const callsTotal = data.chartData.reduce((sum, day) => {
+      const dayTotal = data.users.reduce((daySum, user) => {
+        const callsValue = day[`${user.dataKey}_calls`]
+        return daySum + (typeof callsValue === 'number' ? callsValue : 0);
+      }, 0)
+      return sum + dayTotal
+    }, 0)
+
+    return { cost: costTotal, calls: callsTotal }
+  }, [data.chartData, data.users])
+
+  // 格式化日期显示(根据分辨率)
+  const formatDate = (dateStr: string) => {
+    const date = new Date(dateStr)
+    if (data.resolution === 'hour') {
+      return date.toLocaleTimeString("zh-CN", {
+        hour: "2-digit",
+        minute: "2-digit",
+      })
+    } else {
+      return date.toLocaleDateString("zh-CN", {
+        month: "numeric",
+        day: "numeric",
+      })
+    }
+  }
+
+  // 格式化tooltip日期
+  const formatTooltipDate = (dateStr: string) => {
+    const date = new Date(dateStr)
+    if (data.resolution === 'hour') {
+      return date.toLocaleString("zh-CN", {
+        month: "long",
+        day: "numeric",
+        hour: "2-digit",
+        minute: "2-digit",
+      })
+    } else {
+      return date.toLocaleDateString("zh-CN", {
+        year: "numeric",
+        month: "long",
+        day: "numeric",
+      })
+    }
+  }
+
+  // 获取时间范围的描述文本
+  const getTimeRangeDescription = () => {
+    switch (data.timeRange) {
+      case 'today':
+        return '今天的使用情况'
+      case '7days':
+        return '过去 7 天的使用情况'
+      case '30days':
+        return '过去 30 天的使用情况'
+      default:
+        return '使用情况'
+    }
+  }
+
+  return (
+    <Card className="gap-0 py-0">
+      <CardHeader
+        className={cn(
+          "flex flex-col items-stretch lg:flex-row",
+          onTimeRangeChange && "border-b !pb-0 !px-0"
+        )}
+      >
+        <div className="flex flex-1 flex-col justify-center gap-1 px-6 pt-4 pb-3 lg:!py-0">
+          <div className="flex items-center gap-2">
+            <CardTitle>使用统计</CardTitle>
+            {loading && (
+              <div className="h-4 w-4 animate-spin rounded-full border-2 border-orange-600 border-t-transparent" />
+            )}
+          </div>
+          <CardDescription>
+            {getTimeRangeDescription()}
+          </CardDescription>
+        </div>
+        {/* 时间范围选择器 */}
+        {onTimeRangeChange && (
+          <TimeRangeSelector
+            value={data.timeRange}
+            onChange={onTimeRangeChange}
+            disabled={loading}
+            className="border-t lg:border-t-0"
+          />
+        )}
+        {/* 如果没有时间范围选择回调,显示原有的指标切换按钮 */}
+        {!onTimeRangeChange && (
+          <div className="flex">
+            <button
+              data-active={activeChart === "cost"}
+              className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
+              onClick={() => setActiveChart("cost")}
+            >
+              <span className="text-muted-foreground text-xs">
+                总消费金额
+              </span>
+              <span className="text-lg leading-none font-bold sm:text-3xl">
+                ${totals.cost.toFixed(2)}
+              </span>
+            </button>
+            <button
+              data-active={activeChart === "calls"}
+              className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 border-t px-6 py-4 text-left even:border-l lg:border-t-0 lg:border-l lg:px-8 lg:py-6"
+              onClick={() => setActiveChart("calls")}
+            >
+              <span className="text-muted-foreground text-xs">
+                总API调用次数
+              </span>
+              <span className="text-lg leading-none font-bold sm:text-3xl">
+                {totals.calls.toLocaleString()}
+              </span>
+            </button>
+          </div>
+        )}
+      </CardHeader>
+
+      {onTimeRangeChange && (
+        <div className="flex border-b">
+          <button
+            data-active={activeChart === "cost"}
+            className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
+            onClick={() => setActiveChart("cost")}
+          >
+            <span className="text-muted-foreground text-xs">
+              总消费金额
+            </span>
+            <span className="text-lg leading-none font-bold sm:text-xl">
+              ${totals.cost.toFixed(2)}
+            </span>
+          </button>
+          <button
+            data-active={activeChart === "calls"}
+            className="data-[active=true]:bg-muted/50 relative z-30 flex flex-1 flex-col justify-center gap-1 px-6 py-3 text-left even:border-l transition-colors hover:bg-muted/30"
+            onClick={() => setActiveChart("calls")}
+          >
+            <span className="text-muted-foreground text-xs">
+              总API调用次数
+            </span>
+            <span className="text-lg leading-none font-bold sm:text-xl">
+              {totals.calls.toLocaleString()}
+            </span>
+          </button>
+        </div>
+      )}
+      <CardContent className="px-1 sm:p-6">
+        <ChartContainer
+          config={chartConfig}
+          className="aspect-auto h-[280px] w-full"
+        >
+          <AreaChart
+            data={data.chartData}
+            margin={{
+              left: 12,
+              right: 12,
+            }}
+          >
+            <defs>
+              {data.users.map((user, index) => {
+                const color = getUserColor(index)
+                return (
+                  <linearGradient
+                    key={user.dataKey}
+                    id={`fill-${user.dataKey}`}
+                    x1="0"
+                    y1="0"
+                    x2="0"
+                    y2="1"
+                  >
+                    <stop
+                      offset="5%"
+                      stopColor={color}
+                      stopOpacity={0.8}
+                    />
+                    <stop
+                      offset="95%"
+                      stopColor={color}
+                      stopOpacity={0.1}
+                    />
+                  </linearGradient>
+                )
+              })}
+            </defs>
+            <CartesianGrid vertical={false} />
+            <XAxis
+              dataKey="date"
+              tickLine={false}
+              axisLine={false}
+              tickMargin={2}
+              tickFormatter={formatDate}
+            />
+            <YAxis
+              tickLine={false}
+              axisLine={false}
+              tickMargin={8}
+              tickFormatter={(value) => {
+                if (activeChart === "cost") {
+                  return `$${value}`
+                }
+                return value.toString()
+              }}
+            />
+            <ChartTooltip
+              cursor={false}
+              content={({ active, payload, label }) => {
+                if (!active || !payload || !payload.length) return null
+
+                return (
+                  <div className="rounded-lg border bg-background p-3 shadow-sm min-w-[200px]">
+                    <div className="grid gap-2">
+                      <div className="font-medium text-center">
+                        {formatTooltipDate(label)}
+                      </div>
+                      <div className="grid gap-1.5">
+                        {payload
+                          .sort((a, b) => (b.value as number) - (a.value as number))
+                          .map((entry, index) => {
+                          const baseKey = entry.dataKey?.toString().replace(`_${activeChart}`, '') || ''
+                          const displayUser = userMap.get(baseKey)
+                          const value = entry.value as number
+                          const color = entry.color
+
+                          return (
+                            <div key={index} className="flex items-center justify-between gap-3 text-sm">
+                              <div className="flex items-center gap-2 min-w-0">
+                                <div
+                                  className="h-2 w-2 rounded-full flex-shrink-0"
+                                  style={{ backgroundColor: color }}
+                                />
+                                <span className="font-medium truncate">{displayUser?.name || baseKey}:</span>
+                              </div>
+                              <span className="ml-auto font-mono flex-shrink-0">
+                                {activeChart === "cost"
+                                  ? `$${value.toFixed(2)}`
+                                  : value.toLocaleString()
+                                }
+                              </span>
+                            </div>
+                          )
+                        })}
+                      </div>
+                    </div>
+                  </div>
+                )
+              }}
+            />
+            {data.users.map((user, index) => {
+              const color = getUserColor(index)
+              return (
+                <Area
+                  key={user.dataKey}
+                  dataKey={`${user.dataKey}_${activeChart}`}
+                  name={user.name}
+                  type="monotone"
+                  fill={`url(#fill-${user.dataKey})`}
+                  stroke={color}
+                  stackId="a"
+                />
+              )
+            })}
+            <ChartLegend
+              content={() => (
+                <div className="px-1">
+                  <div className="flex flex-wrap justify-center gap-1">
+                    {sortedLegendUsers.map(({ user, index }) => {
+                      const color = getUserColor(index)
+                      const userTotal = userTotals[user.dataKey] ?? { cost: 0, calls: 0 }
+
+                      return (
+                        <div
+                          key={user.dataKey}
+                          className="bg-muted/30 rounded-md px-3 py-2 text-center transition-all hover:bg-muted/50 min-w-16"
+                        >
+                          {/* 上方:颜色点 + 用户名 */}
+                          <div className="flex items-center justify-center gap-1 mb-1">
+                            <div
+                              className="h-2 w-2 rounded-full flex-shrink-0"
+                              style={{ backgroundColor: color }}
+                            />
+                            <span className="text-xs font-medium text-foreground truncate max-w-12">
+                              {user.name}
+                            </span>
+                          </div>
+
+                          {/* 下方:数据值 */}
+                          <div className="text-xs font-bold text-foreground">
+                            {activeChart === "cost"
+                              ? `$${userTotal.cost.toFixed(2)}`
+                              : userTotal.calls.toLocaleString()
+                            }
+                          </div>
+                        </div>
+                      )
+                    })}
+                  </div>
+                </div>
+              )}
+            />
+          </AreaChart>
+        </ChartContainer>
+      </CardContent>
+    </Card>
+  )
+}

+ 3 - 0
src/app/dashboard/_components/statistics/index.ts

@@ -0,0 +1,3 @@
+export { StatisticsWrapper } from "./wrapper";
+export { UserStatisticsChart } from "./chart";
+export { TimeRangeSelector } from "./time-range-selector";

+ 40 - 0
src/app/dashboard/_components/statistics/time-range-selector.tsx

@@ -0,0 +1,40 @@
+"use client";
+
+import * as React from "react";
+import { TIME_RANGE_OPTIONS, type TimeRange } from "@/types/statistics";
+import { cn } from "@/lib/utils";
+
+interface TimeRangeSelectorProps {
+  value: TimeRange;
+  onChange: (timeRange: TimeRange) => void;
+  className?: string;
+  disabled?: boolean;
+}
+
+/**
+ * 时间范围选择器组件
+ * 提供今天、7天、30天的选择
+ */
+export function TimeRangeSelector({ value, onChange, className, disabled = false }: TimeRangeSelectorProps) {
+  return (
+    <div className={cn("flex flex-wrap ", className)}>
+      {TIME_RANGE_OPTIONS.map((option) => (
+        <button
+          key={option.key}
+          data-active={value === option.key}
+          disabled={disabled}
+          className="data-[active=true]:bg-muted/50 disabled:opacity-50 disabled:cursor-not-allowed relative z-30 flex flex-none flex-col items-start justify-center gap-1 border-t px-6 py-4 text-left lg:border-t-0 lg:px-8 lg:py-6 lg:[&:not(:first-child)]:border-l transition-all duration-200 hover:bg-muted/30 disabled:hover:bg-transparent"
+          onClick={() => !disabled && onChange(option.key)}
+          title={option.description}
+        >
+          <span className="text-muted-foreground text-xs transition-colors whitespace-nowrap">
+            {option.description}
+          </span>
+          <span className="text-lg leading-none font-bold sm:text-xl transition-colors whitespace-nowrap">
+            {option.label}
+          </span>
+        </button>
+      ))}
+    </div>
+  );
+}

+ 89 - 0
src/app/dashboard/_components/statistics/wrapper.tsx

@@ -0,0 +1,89 @@
+"use client";
+
+import * as React from "react";
+import { UserStatisticsChart } from "./chart";
+import { getUserStatistics } from "@/actions/statistics";
+import type { TimeRange, UserStatisticsData } from "@/types/statistics";
+import { DEFAULT_TIME_RANGE } from "@/types/statistics";
+import { toast } from "sonner";
+
+interface StatisticsWrapperProps {
+  initialData?: UserStatisticsData;
+}
+
+/**
+ * 统计组件包装器
+ * 处理时间范围状态管理和数据获取
+ */
+export function StatisticsWrapper({ initialData }: StatisticsWrapperProps) {
+  const [timeRange, setTimeRange] = React.useState<TimeRange>(
+    initialData?.timeRange ?? DEFAULT_TIME_RANGE
+  );
+  const [data, setData] = React.useState<UserStatisticsData | null>(initialData ?? null);
+  const [loading, setLoading] = React.useState(false);
+
+  // 防抖获取统计数据
+  const fetchStatistics = React.useCallback(async (newTimeRange: TimeRange) => {
+    if (loading) return; // 防止重复请求
+
+    setLoading(true);
+    try {
+      const result = await getUserStatistics(newTimeRange);
+      if (!result.ok) {
+        toast.error(result.error || '获取统计数据失败');
+        return;
+      }
+
+      if (!result.data) {
+        toast.error('统计数据为空');
+        return;
+      }
+
+      setData(result.data);
+      setTimeRange(newTimeRange);
+    } catch (error) {
+      console.error('Failed to fetch statistics:', error);
+      toast.error('获取统计数据失败');
+    } finally {
+      // 添加一个小延迟让用户看到加载状态,避免闪烁
+      setTimeout(() => setLoading(false), 150);
+    }
+  }, [loading]);
+
+  // 处理时间范围变化
+  const handleTimeRangeChange = React.useCallback((newTimeRange: TimeRange) => {
+    if (newTimeRange !== timeRange && !loading) {
+      fetchStatistics(newTimeRange);
+    }
+  }, [timeRange, loading, fetchStatistics]);
+
+  // 如果没有数据且不在加载中,显示空状态
+  if (!data && !loading) {
+    return (
+      <div className="text-center py-8 text-muted-foreground">
+        暂无统计数据
+      </div>
+    );
+  }
+
+  // 如果有数据,显示图表
+  if (data) {
+    return (
+      <UserStatisticsChart
+        data={data}
+        onTimeRangeChange={handleTimeRangeChange}
+        loading={loading}
+      />
+    );
+  }
+
+  // 纯加载状态
+  return (
+    <div className="text-center py-8">
+      <div className="flex items-center justify-center gap-2 text-sm text-muted-foreground">
+        <div className="h-4 w-4 animate-spin rounded-full border-2 border-orange-600 border-t-transparent" />
+        加载统计数据...
+      </div>
+    </div>
+  );
+}

+ 58 - 0
src/app/dashboard/_components/user-menu.tsx

@@ -0,0 +1,58 @@
+'use client';
+
+import { useRouter } from 'next/navigation';
+import { Avatar, AvatarFallback } from '@/components/ui/avatar';
+import { Button } from '@/components/ui/button';
+import { LogOut } from 'lucide-react';
+
+interface UserMenuProps {
+  user: {
+    id: number;
+    name: string;
+    description?: string | null;
+  };
+}
+
+export function UserMenu({ user }: UserMenuProps) {
+  const router = useRouter();
+
+  const handleLogout = () => {
+    // 立即跳转到登录页面,避免延迟
+    router.push('/login');
+    // 异步调用登出接口,不等待响应
+    fetch('/api/auth/logout', { method: 'POST' }).then(() => {
+      router.refresh();
+    });
+  };
+
+  const getInitials = (name: string) => {
+    return name
+      .split(' ')
+      .map(word => word[0])
+      .join('')
+      .toUpperCase()
+      .slice(0, 2);
+  };
+
+  return (
+    <div className="flex items-center gap-2">
+      <div className="flex items-center gap-2.5 px-3 py-1.5 rounded-full bg-muted/50 border border-border/50">
+        <Avatar className="h-7 w-7">
+          <AvatarFallback className="bg-gradient-to-br from-primary/20 to-primary/10 text-primary font-semibold text-xs">
+            {getInitials(user.name)}
+          </AvatarFallback>
+        </Avatar>
+        <span className="text-sm font-medium text-foreground/90">{user.name}</span>
+      </div>
+      <Button
+        variant="ghost"
+        size="icon"
+        onClick={handleLogout}
+        className="h-9 w-9 rounded-full hover:bg-destructive/10 hover:text-destructive transition-all duration-200"
+        title="退出登录"
+      >
+        <LogOut className="h-4 w-4" />
+      </Button>
+    </div>
+  );
+}

+ 33 - 0
src/app/dashboard/_components/user/add-user-dialog.tsx

@@ -0,0 +1,33 @@
+"use client";
+import { useState, type ComponentProps } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { ListPlus } from "lucide-react";
+import { UserForm } from "./forms/user-form";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
+
+type ButtonProps = ComponentProps<typeof Button>;
+
+interface AddUserDialogProps {
+  variant?: ButtonProps["variant"];
+  size?: ButtonProps["size"];
+  className?: string;
+}
+
+export function AddUserDialog({ variant = "default", size = "default", className }: AddUserDialogProps) {
+  const [open, setOpen] = useState(false);
+  return (
+    <Dialog open={open} onOpenChange={setOpen}>
+      <DialogTrigger asChild>
+        <Button variant={variant} size={size} className={className}>
+          <ListPlus className="h-4 w-4" /> 新增用户
+        </Button>
+      </DialogTrigger>
+      <DialogContent>
+        <FormErrorBoundary>
+          <UserForm onSuccess={() => setOpen(false)} />
+        </FormErrorBoundary>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 92 - 0
src/app/dashboard/_components/user/forms/add-key-form.tsx

@@ -0,0 +1,92 @@
+"use client";
+import { useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { toast } from "sonner";
+import { addKey } from "@/actions/keys";
+import { DialogFormLayout } from "@/components/form/form-layout";
+import { TextField, DateField } from "@/components/form/form-field";
+import { useZodForm } from "@/lib/hooks/use-zod-form";
+import { KeyFormSchema } from "@/lib/validation/schemas";
+
+interface AddKeyFormProps {
+  userId?: number;
+  onSuccess?: (result: { generatedKey: string; name: string }) => void;
+}
+
+export function AddKeyForm({ userId, onSuccess }: AddKeyFormProps) {
+  const [isPending, startTransition] = useTransition();
+  const router = useRouter();
+
+  const form = useZodForm({
+    schema: KeyFormSchema,
+    defaultValues: {
+      name: '',
+      expiresAt: ''
+    },
+    onSubmit: async (data) => {
+      if (!userId) {
+        throw new Error("用户ID不存在");
+      }
+
+      try {
+        const result = await addKey({
+          userId: userId!,
+          name: data.name,
+          expiresAt: data.expiresAt || undefined
+        });
+
+        if (!result.ok) {
+          toast.error(result.error || "创建失败,请稍后重试");
+          return;
+        }
+
+        const payload = result.data;
+        if (!payload) {
+          toast.error("创建成功但未返回密钥");
+          return;
+        }
+
+        startTransition(() => {
+          onSuccess?.({ generatedKey: payload.generatedKey, name: payload.name });
+          router.refresh();
+        });
+      } catch (err) {
+        console.error("添加Key失败:", err);
+        // 使用toast显示具体的错误信息
+        const errorMessage = err instanceof Error ? err.message : "创建失败,请稍后重试";
+        toast.error(errorMessage);
+      }
+    }
+  });
+
+  return (
+    <DialogFormLayout
+      config={{
+        title: "新增 Key",
+        description: "为当前用户创建新的API密钥,Key值将自动生成。",
+        submitText: "确认创建",
+        loadingText: "创建中..."
+      }}
+      onSubmit={form.handleSubmit}
+      isSubmitting={isPending}
+      canSubmit={form.canSubmit && !!userId}
+      error={form.errors._form}
+    >
+      <TextField
+        label="Key名称"
+        required
+        maxLength={64}
+        autoFocus
+        placeholder="请输入Key名称"
+        {...form.getFieldProps('name')}
+      />
+
+      <DateField
+        label="过期时间"
+        placeholder="选择过期时间"
+        description="留空表示永不过期"
+        {...form.getFieldProps('expiresAt')}
+      />
+    </DialogFormLayout>
+  );
+}

+ 68 - 0
src/app/dashboard/_components/user/forms/delete-key-confirm.tsx

@@ -0,0 +1,68 @@
+"use client";
+import { useTransition } from "react";
+import { DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { useRouter } from "next/navigation";
+import { removeKey } from "@/actions/keys";
+import { toast } from "sonner";
+
+interface DeleteKeyConfirmProps {
+  keyData?: {
+    id: number;
+    name: string;
+    maskedKey: string;
+  };
+}
+
+export function DeleteKeyConfirm({ keyData, onSuccess }: DeleteKeyConfirmProps & { onSuccess?: () => void }) {
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+
+  const handleConfirm = () => {
+    if (!keyData) return;
+    startTransition(async () => {
+      try {
+        const res = await removeKey(keyData.id);
+        if (!res.ok) {
+          toast.error(res.error || '删除失败');
+          return;
+        }
+        onSuccess?.();
+        router.refresh();
+      } catch (error) {
+        console.error("删除Key失败:", error);
+        toast.error('删除失败,请稍后重试');
+      }
+    });
+  };
+
+  return (
+    <>
+      <DialogHeader>
+        <DialogTitle>确认删除密钥</DialogTitle>
+        <DialogDescription>
+          您确定要删除密钥 &ldquo;<strong>{keyData?.name}</strong>&rdquo; 吗?
+          <br />
+          <code className="bg-muted px-2 py-1 rounded text-xs">{keyData?.maskedKey}</code>
+          <br />
+          此操作无法撤销,删除后所有使用此密钥的应用将无法访问。
+        </DialogDescription>
+      </DialogHeader>
+      
+      <DialogFooter>
+        <DialogClose asChild>
+          <Button type="button" variant="outline" disabled={isPending}>
+            取消
+          </Button>
+        </DialogClose>
+        <Button 
+          variant="destructive" 
+          onClick={handleConfirm}
+          disabled={isPending}
+        >
+          {isPending ? "删除中..." : "确认删除"}
+        </Button>
+      </DialogFooter>
+    </>
+  );
+}

+ 66 - 0
src/app/dashboard/_components/user/forms/delete-user-confirm.tsx

@@ -0,0 +1,66 @@
+"use client";
+import { useTransition } from "react";
+import { DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from "@/components/ui/dialog";
+import { Button } from "@/components/ui/button";
+import { useRouter } from "next/navigation";
+import { removeUser } from "@/actions/users";
+import { toast } from "sonner";
+
+interface DeleteUserConfirmProps {
+  user?: {
+    id: number;
+    name: string;
+    keys: { id: number; name: string }[];
+  };
+}
+
+export function DeleteUserConfirm({ user, onSuccess }: DeleteUserConfirmProps & { onSuccess?: () => void }) {
+  const router = useRouter();
+  const [isPending, startTransition] = useTransition();
+
+  const handleConfirm = () => {
+    if (!user) return;
+    startTransition(async () => {
+      try {
+        const res = await removeUser(user.id);
+        if (!res.ok) {
+          toast.error(res.error || '删除失败');
+          return;
+        }
+        onSuccess?.();
+        router.refresh();
+      } catch (error) {
+        console.error("删除用户失败:", error);
+        toast.error('删除失败,请稍后重试');
+      }
+    });
+  };
+
+  return (
+    <>
+      <DialogHeader>
+        <DialogTitle>确认删除用户</DialogTitle>
+        <DialogDescription>
+          您确定要删除用户 &ldquo;<strong>{user?.name}</strong>&rdquo; 吗?
+          <br />
+          此操作将同时删除该用户的 {user?.keys.length || 0} 个密钥,且无法撤销。
+        </DialogDescription>
+      </DialogHeader>
+      
+      <DialogFooter>
+        <DialogClose asChild>
+          <Button type="button" variant="outline" disabled={isPending}>
+            取消
+          </Button>
+        </DialogClose>
+        <Button 
+          variant="destructive" 
+          onClick={handleConfirm}
+          disabled={isPending}
+        >
+          {isPending ? "删除中..." : "确认删除"}
+        </Button>
+      </DialogFooter>
+    </>
+  );
+}

+ 94 - 0
src/app/dashboard/_components/user/forms/edit-key-form.tsx

@@ -0,0 +1,94 @@
+"use client";
+import { useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { editKey } from "@/actions/keys";
+import { DialogFormLayout } from "@/components/form/form-layout";
+import { TextField, DateField } from "@/components/form/form-field";
+import { useZodForm } from "@/lib/hooks/use-zod-form";
+import { KeyFormSchema } from "@/lib/validation/schemas";
+import { toast } from "sonner";
+
+interface EditKeyFormProps {
+  keyData?: {
+    id: number;
+    name: string;
+    expiresAt: string;
+  };
+  onSuccess?: () => void;
+}
+
+export function EditKeyForm({ keyData, onSuccess }: EditKeyFormProps) {
+  const [isPending, startTransition] = useTransition();
+  const router = useRouter();
+
+  const formatExpiresAt = (expiresAt: string) => {
+    if (!expiresAt || expiresAt === "永不过期") return "";
+    try {
+      return new Date(expiresAt).toISOString().split('T')[0];
+    } catch {
+      return "";
+    }
+  };
+
+  const form = useZodForm({
+    schema: KeyFormSchema,
+    defaultValues: {
+      name: keyData?.name || '',
+      expiresAt: formatExpiresAt(keyData?.expiresAt || "")
+    },
+    onSubmit: async (data) => {
+      if (!keyData) {
+        throw new Error("密钥信息不存在");
+      }
+      
+      startTransition(async () => {
+        try {
+          const res = await editKey(keyData.id, {
+            name: data.name,
+            expiresAt: data.expiresAt || undefined,
+          });
+          if (!res.ok) {
+            toast.error(res.error || '保存失败');
+            return;
+          }
+          onSuccess?.();
+          router.refresh();
+        } catch (err) {
+          console.error("编辑Key失败:", err);
+          toast.error("保存失败,请稍后重试");
+        }
+      });
+    }
+  });
+
+  return (
+    <DialogFormLayout
+      config={{
+        title: "编辑 Key",
+        description: "修改密钥的名称和过期时间。",
+        submitText: "保存修改",
+        loadingText: "保存中..."
+      }}
+      onSubmit={form.handleSubmit}
+      isSubmitting={isPending}
+      canSubmit={form.canSubmit}
+      error={form.errors._form}
+    >
+      <TextField
+        label="Key名称"
+        required
+        maxLength={64}
+        autoFocus
+        placeholder="请输入Key名称"
+        {...form.getFieldProps('name')}
+      />
+      
+      <DateField
+        label="过期时间"
+        placeholder="选择过期时间"
+        description="留空表示永不过期"
+        {...form.getFieldProps('expiresAt')}
+      />
+    </DialogFormLayout>
+  );
+}

+ 126 - 0
src/app/dashboard/_components/user/forms/user-form.tsx

@@ -0,0 +1,126 @@
+"use client";
+import { useTransition } from "react";
+import { useRouter } from "next/navigation";
+import { addUser, editUser } from "@/actions/users";
+import { DialogFormLayout } from "@/components/form/form-layout";
+import { TextField } from "@/components/form/form-field";
+import { useZodForm } from "@/lib/hooks/use-zod-form";
+import { CreateUserSchema } from "@/lib/validation/schemas";
+import { USER_DEFAULTS } from "@/lib/constants/user.constants";
+import { toast } from "sonner";
+
+interface UserFormProps {
+  user?: {
+    id: number;
+    name: string;
+    note?: string;
+    rpm: number;
+    dailyQuota: number;
+  };
+  onSuccess?: () => void;
+}
+
+export function UserForm({ user, onSuccess }: UserFormProps) {
+  const [isPending, startTransition] = useTransition();
+  const router = useRouter();
+  const isEdit = Boolean(user?.id);
+
+  const form = useZodForm({
+    schema: CreateUserSchema, // Use CreateUserSchema for both, it has all fields with defaults
+    defaultValues: {
+      name: user?.name || '',
+      note: user?.note || '',
+      rpm: user?.rpm || USER_DEFAULTS.RPM,
+      dailyQuota: user?.dailyQuota || USER_DEFAULTS.DAILY_QUOTA,
+    },
+    onSubmit: async (data) => {
+      startTransition(async () => {
+        try {
+          let res;
+          if (isEdit) {
+            res = await editUser(user!.id, {
+              name: data.name,
+              note: data.note,
+              rpm: data.rpm,
+              dailyQuota: data.dailyQuota,
+            });
+          } else {
+            res = await addUser({
+              name: data.name,
+              note: data.note,
+              rpm: data.rpm,
+              dailyQuota: data.dailyQuota,
+            });
+          }
+
+          if (!res.ok) {
+            const msg = res.error || (isEdit ? "保存失败" : "创建失败,请稍后重试");
+            toast.error(msg);
+            return;
+          }
+
+          onSuccess?.();
+          router.refresh();
+        } catch (err) {
+          console.error(`${isEdit ? '编辑' : '添加'}用户失败:`, err);
+          toast.error(isEdit ? "保存失败,请稍后重试" : "创建失败,请稍后重试");
+        }
+      });
+    },
+  });
+
+  return (
+    <DialogFormLayout
+      config={{
+        title: isEdit ? "编辑用户" : "新增用户",
+        description: isEdit ? "修改用户的基本信息。" : "创建新用户,系统将自动为其生成默认密钥。",
+        submitText: isEdit ? "保存修改" : "确认创建",
+        loadingText: isEdit ? "保存中..." : "创建中...",
+      }}
+      onSubmit={form.handleSubmit}
+      isSubmitting={isPending}
+      canSubmit={form.canSubmit}
+      error={form.errors._form}
+    >
+      <TextField
+        label="用户名"
+        required
+        maxLength={64}
+        autoFocus
+        placeholder="请输入用户名"
+        {...form.getFieldProps("name")}
+      />
+
+      <TextField
+        label="备注"
+        maxLength={200}
+        placeholder="请输入备注(可选)"
+        description="用于描述用户的用途或备注信息"
+        {...form.getFieldProps("note")}
+      />
+
+      <TextField
+        label="RPM限制"
+        type="number"
+        required
+        min={1}
+        max={10000}
+        placeholder="每分钟请求数限制"
+        description={`默认值: ${USER_DEFAULTS.RPM},范围: 1-10000`}
+        {...form.getFieldProps("rpm")}
+      />
+
+      <TextField
+        label="每日额度"
+        type="number"
+        required
+        min={0.01}
+        max={1000}
+        step={0.01}
+        placeholder="每日消费额度限制"
+        description={`默认值: $${USER_DEFAULTS.DAILY_QUOTA},范围: $0.01-$1000`}
+        {...form.getFieldProps("dailyQuota")}
+      />
+    </DialogFormLayout>
+  );
+}

+ 75 - 0
src/app/dashboard/_components/user/key-actions.tsx

@@ -0,0 +1,75 @@
+"use client";
+import { useState } from "react";
+import { SquarePen, Trash2 } from "lucide-react";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { EditKeyForm } from "./forms/edit-key-form";
+import { DeleteKeyConfirm } from "./forms/delete-key-confirm";
+import type { UserKeyDisplay } from "@/types/user";
+import type { User } from "@/types/user";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
+
+interface KeyActionsProps {
+  keyData: UserKeyDisplay;
+  currentUser?: User;
+  keyOwnerUserId: number; // 这个Key所属的用户ID
+  canDelete: boolean;
+}
+
+export function KeyActions({ keyData, currentUser, keyOwnerUserId, canDelete }: KeyActionsProps) {
+  const [openEdit, setOpenEdit] = useState(false);
+  const [openDelete, setOpenDelete] = useState(false);
+
+  // 权限检查:只有管理员或Key的拥有者才能编辑/删除
+  const canManageKey = currentUser && (
+    currentUser.role === 'admin' || currentUser.id === keyOwnerUserId
+  );
+
+  // 如果没有权限,不显示任何操作按钮
+  if (!canManageKey) {
+    return null;
+  }
+
+  return (
+    <div className="flex items-center gap-1">
+      {/* 编辑Key */}
+      <Dialog open={openEdit} onOpenChange={setOpenEdit}>
+        <DialogTrigger asChild>
+          <button
+            type="button"
+            aria-label="编辑密钥"
+            className="inline-flex items-center justify-center p-1.5 text-muted-foreground hover:text-foreground transition-colors"
+            title="编辑"
+          >
+            <SquarePen className="h-4 w-4" />
+          </button>
+        </DialogTrigger>
+        <DialogContent>
+          <FormErrorBoundary>
+            <EditKeyForm keyData={keyData} onSuccess={() => setOpenEdit(false)} />
+          </FormErrorBoundary>
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除Key */}
+      {canDelete && (
+        <Dialog open={openDelete} onOpenChange={setOpenDelete}>
+          <DialogTrigger asChild>
+            <button
+              type="button"
+              aria-label="删除密钥"
+              className="inline-flex items-center justify-center p-1.5 text-muted-foreground hover:text-red-600"
+              title="删除"
+            >
+              <Trash2 className="h-4 w-4" />
+            </button>
+          </DialogTrigger>
+          <DialogContent>
+            <FormErrorBoundary>
+              <DeleteKeyConfirm keyData={keyData} onSuccess={() => setOpenDelete(false)} />
+            </FormErrorBoundary>
+          </DialogContent>
+        </Dialog>
+      )}
+    </div>
+  );
+}

+ 136 - 0
src/app/dashboard/_components/user/key-list-header.tsx

@@ -0,0 +1,136 @@
+"use client";
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
+import { ListPlus, Copy, CheckCircle } from "lucide-react";
+import { AddKeyForm } from "./forms/add-key-form";
+import { UserActions } from "./user-actions";
+import type { UserDisplay } from "@/types/user";
+import type { User } from "@/types/user";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
+import { formatCurrency } from "@/lib/utils/currency";
+
+interface KeyListHeaderProps {
+  activeUser: UserDisplay | null;
+  currentUser?: User;
+}
+
+export function KeyListHeader({ activeUser, currentUser }: KeyListHeaderProps) {
+  const [openAdd, setOpenAdd] = useState(false);
+  const [keyResult, setKeyResult] = useState<{ generatedKey: string; name: string } | null>(null);
+  const [copied, setCopied] = useState(false);
+
+  const totalTodayUsage = activeUser?.keys.reduce((sum, key) => sum + (key.todayUsage ?? 0), 0) ?? 0;
+
+  const handleKeyCreated = (result: { generatedKey: string; name: string }) => {
+    setOpenAdd(false); // 关闭表单dialog
+    setKeyResult(result); // 显示成功dialog
+  };
+
+  const handleCopy = async () => {
+    if (!keyResult) return;
+    try {
+      await navigator.clipboard.writeText(keyResult.generatedKey);
+      setCopied(true);
+      setTimeout(() => setCopied(false), 2000);
+    } catch (err) {
+      console.error('复制失败:', err);
+    }
+  };
+
+  const handleCloseSuccess = () => {
+    setKeyResult(null);
+    setCopied(false);
+  };
+
+  // 权限检查:管理员可以给所有人添加Key,普通用户只能给自己添加Key
+  const canAddKey = currentUser && activeUser && (
+    currentUser.role === 'admin' || currentUser.id === activeUser.id
+  );
+
+  return (
+    <>
+      <div className="mb-3 flex items-center justify-between">
+        <div>
+          <div className="flex items-center gap-2 text-base font-semibold tracking-tight">
+            <span>{activeUser ? activeUser.name : "-"}</span>
+            {activeUser && <UserActions user={activeUser} currentUser={currentUser} />}
+          </div>
+          <div className="mt-1 space-y-1">
+            <div className="text-xs text-muted-foreground">
+              今日用量 {activeUser ? formatCurrency(totalTodayUsage) : "-"} /  {activeUser ? formatCurrency(activeUser.dailyQuota) : "-"}
+            </div>
+          </div>
+        </div>
+        {canAddKey && (
+          <Dialog open={openAdd} onOpenChange={setOpenAdd}>
+            <DialogTrigger asChild>
+              <Button
+                variant="secondary"
+                size="sm"
+                className="hover:bg-primary hover:text-primary-foreground cursor-pointer transition-colors"
+                disabled={!activeUser}
+              >
+                <ListPlus className="h-3.5 w-3.5" /> 新增 Key
+              </Button>
+            </DialogTrigger>
+            <DialogContent>
+              <FormErrorBoundary>
+                <AddKeyForm userId={activeUser?.id} onSuccess={handleKeyCreated} />
+              </FormErrorBoundary>
+            </DialogContent>
+          </Dialog>
+        )}
+      </div>
+
+      {/* Key创建成功弹窗 */}
+      <Dialog open={!!keyResult} onOpenChange={(open) => !open && handleCloseSuccess()}>
+        <DialogContent className="max-w-lg">
+          <DialogHeader>
+            <DialogTitle className="flex items-center gap-2">
+              <CheckCircle className="h-5 w-5 text-green-600" />
+              Key 创建成功
+            </DialogTitle>
+            <DialogDescription>
+              你的 API Key 已成功创建。请务必复制并妥善保存,此密钥仅显示一次。
+            </DialogDescription>
+          </DialogHeader>
+
+          {keyResult && (
+            <div className="space-y-4">
+              <div>
+                <label className="text-sm font-medium mb-2 block">API Key</label>
+                <div className="relative">
+                  <div className="p-3 bg-muted/50 rounded-md font-mono text-sm break-all border-2 border-dashed border-orange-300 pr-12">
+                    {keyResult.generatedKey}
+                  </div>
+                  <Button
+                    variant="ghost"
+                    size="sm"
+                    onClick={handleCopy}
+                    className="absolute top-1/2 right-2 -translate-y-1/2 h-7 w-7 p-0"
+                  >
+                    {copied ? (
+                      <CheckCircle className="h-3 w-3 text-orange-600" />
+                    ) : (
+                      <Copy className="h-3 w-3" />
+                    )}
+                  </Button>
+                </div>
+                <p className="text-xs text-muted-foreground mt-2">
+                  请在关闭前复制并保存,关闭后将无法再次查看此密钥
+                </p>
+              </div>
+            </div>
+          )}
+
+          <DialogFooter>
+            <Button onClick={handleCloseSuccess} variant="secondary">
+              关闭
+            </Button>
+          </DialogFooter>
+        </DialogContent>
+      </Dialog>
+    </>
+  );
+}

+ 105 - 0
src/app/dashboard/_components/user/key-list.tsx

@@ -0,0 +1,105 @@
+"use client";
+import { useState } from "react";
+import { DataTable, TableColumnTypes } from "@/components/ui/data-table";
+import { Button } from "@/components/ui/button";
+import { Copy, Check } from "lucide-react";
+import { KeyActions } from "./key-actions";
+import type { UserKeyDisplay } from "@/types/user";
+import type { User } from "@/types/user";
+import { format } from "timeago.js";
+import { formatCurrency } from "@/lib/utils/currency";
+
+interface KeyListProps {
+  keys: UserKeyDisplay[];
+  currentUser?: User;
+  keyOwnerUserId: number; // 这些Key所属的用户ID
+}
+
+export function KeyList({ keys, currentUser, keyOwnerUserId }: KeyListProps) {
+  const [copiedKeyId, setCopiedKeyId] = useState<number | null>(null);
+  const canDeleteKeys = keys.length > 1;
+
+  const handleCopyKey = async (key: UserKeyDisplay) => {
+    if (!key.fullKey || !key.canCopy) return;
+
+    try {
+      await navigator.clipboard.writeText(key.fullKey);
+      setCopiedKeyId(key.id);
+      setTimeout(() => setCopiedKeyId(null), 2000);
+    } catch (err) {
+      console.error('复制失败:', err);
+    }
+  };
+
+  const columns = [
+    TableColumnTypes.text<UserKeyDisplay>('name', '名称', {
+      render: (value) => (
+        <div className="truncate">{value}</div>
+      )
+    }),
+    TableColumnTypes.text<UserKeyDisplay>('maskedKey', 'Key', {
+      render: (_, record: UserKeyDisplay) => (
+        <div className="group inline-flex items-center gap-1">
+          <div className="font-mono truncate">
+            {record.maskedKey || "-"}
+          </div>
+          {record.canCopy && record.fullKey && (
+            <Button
+              variant="ghost"
+              size="sm"
+              onClick={() => handleCopyKey(record)}
+              className="h-5 w-5 p-0 hover:bg-muted opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0"
+              title="复制完整密钥"
+            >
+              {copiedKeyId === record.id ? (
+                <Check className="h-3 w-3 text-green-600" />
+              ) : (
+                <Copy className="h-3 w-3" />
+              )}
+            </Button>
+          )}
+        </div>
+      )
+    }),
+    TableColumnTypes.text<UserKeyDisplay>('expiresAt', '过期时间'),
+    TableColumnTypes.number<UserKeyDisplay>('todayUsage', '今日用量', {
+      render: (value) => {
+        const amount = typeof value === 'number' ? value : 0;
+        return formatCurrency(amount);
+      }
+    }),
+    TableColumnTypes.text<UserKeyDisplay>('createdAt', '创建时间', {
+      render: (_, record: UserKeyDisplay) => (
+        <div className="space-y-0.5">
+          <div className="text-sm">
+            {format(record.createdAt)}
+          </div>
+          <div className="text-xs text-muted-foreground">
+            {record.createdAtFormatted}
+          </div>
+        </div>
+      )
+    }),
+    TableColumnTypes.actions<UserKeyDisplay>('操作', (value, record) => (
+      <KeyActions
+        keyData={record}
+        currentUser={currentUser}
+        keyOwnerUserId={keyOwnerUserId}
+        canDelete={canDeleteKeys}
+      />
+    ))
+  ];
+
+  return (
+    <DataTable
+      columns={columns}
+      data={keys}
+      emptyState={{
+        title: "暂无 Key",
+        description: '可点击右上角 "新增 Key" 按钮添加密钥'
+      }}
+      maxHeight="460px"
+      stickyHeader
+    />
+  );
+}

+ 68 - 0
src/app/dashboard/_components/user/user-actions.tsx

@@ -0,0 +1,68 @@
+"use client";
+import { useState } from "react";
+import { SquarePen, Trash } from "lucide-react";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { UserForm } from "./forms/user-form";
+import { DeleteUserConfirm } from "./forms/delete-user-confirm";
+import type { UserDisplay, User } from "@/types/user";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
+
+interface UserActionsProps {
+  user: UserDisplay;
+  currentUser?: User;
+}
+
+export function UserActions({ user, currentUser }: UserActionsProps) {
+  const [openEdit, setOpenEdit] = useState(false);
+  const [openDelete, setOpenDelete] = useState(false);
+
+  // 权限检查:只有管理员才能编辑用户信息
+  const canEditUser = currentUser?.role === "admin";
+
+  // 如果没有权限,不显示任何按钮
+  if (!canEditUser) {
+    return null;
+  }
+
+  return (
+    <div className="flex items-center gap-1">
+      {/* 编辑用户 */}
+      <Dialog open={openEdit} onOpenChange={setOpenEdit}>
+        <DialogTrigger asChild>
+          <button
+            type="button"
+            aria-label="编辑用户"
+            className="inline-flex items-center justify-center p-1 text-muted-foreground hover:text-foreground transition-colors"
+            title="编辑用户"
+          >
+            <SquarePen className="h-3.5 w-3.5" />
+          </button>
+        </DialogTrigger>
+        <DialogContent>
+          <FormErrorBoundary>
+            <UserForm user={user} onSuccess={() => setOpenEdit(false)} />
+          </FormErrorBoundary>
+        </DialogContent>
+      </Dialog>
+
+      {/* 删除用户 */}
+      <Dialog open={openDelete} onOpenChange={setOpenDelete}>
+        <DialogTrigger asChild>
+          <button
+            type="button"
+            aria-label="删除用户"
+            className="inline-flex items-center justify-center p-1 text-muted-foreground hover:text-red-600 transition-colors"
+            title="删除用户"
+          >
+            <Trash className="h-3.5 w-3.5" />
+          </button>
+        </DialogTrigger>
+        <DialogContent>
+          <FormErrorBoundary>
+            <DeleteUserConfirm user={user} onSuccess={() => setOpenDelete(false)} />
+          </FormErrorBoundary>
+        </DialogContent>
+      </Dialog>
+    </div>
+  );
+}

+ 72 - 0
src/app/dashboard/_components/user/user-key-manager.tsx

@@ -0,0 +1,72 @@
+"use client";
+import { useState } from "react";
+import { UserList } from "./user-list";
+import { KeyListHeader } from "./key-list-header";
+import { KeyList } from "./key-list";
+import type { UserDisplay } from "@/types/user";
+import type { User } from "@/types/user";
+
+interface UserKeyManagerProps {
+  users: UserDisplay[];
+  currentUser?: User;
+}
+
+export function UserKeyManager({ users, currentUser }: UserKeyManagerProps) {
+  // 普通用户默认选择自己,管理员选择第一个用户
+  const getInitialUser = () => {
+    if (currentUser?.role === 'user') {
+      // 普通用户只能看到自己
+      return users.find(u => u.id === currentUser.id) || users[0];
+    }
+    // 管理员看到第一个用户
+    return users[0];
+  };
+
+  const [activeUserId, setActiveUserId] = useState<number | null>(getInitialUser()?.id ?? null);
+  const activeUser = users.find((u) => u.id === activeUserId) ?? getInitialUser();
+
+  // 普通用户只显示Key列表,不显示用户列表
+  if (currentUser?.role === 'user') {
+    return (
+      <div className="space-y-3">
+        <div className="bg-card text-card-foreground border border-border rounded-xl p-4">
+          <KeyListHeader activeUser={activeUser} currentUser={currentUser} />
+          <KeyList
+            keys={activeUser?.keys || []}
+            currentUser={currentUser}
+            keyOwnerUserId={activeUser?.id || 0}
+          />
+        </div>
+      </div>
+    );
+  }
+
+  // 管理员显示完整布局(用户列表 + Key列表)
+  return (
+    <div className="space-y-3">
+      {/* 主从布局 */}
+      <div className="grid grid-cols-1 gap-4 md:grid-cols-3">
+        {/* 左侧用户列表 */}
+        <UserList
+          users={users}
+          activeUserId={activeUser?.id}
+          onUserSelect={setActiveUserId}
+          currentUser={currentUser}
+        />
+
+        {/* 右侧:当前用户的 Key 列表 */}
+        <div className="md:col-span-2 bg-card text-card-foreground border border-border rounded-xl p-4">
+          <KeyListHeader activeUser={activeUser} currentUser={currentUser} />
+          <KeyList
+            keys={activeUser?.keys || []}
+            currentUser={currentUser}
+            keyOwnerUserId={activeUser?.id || 0}
+          />
+        </div>
+      </div>
+    </div>
+  );
+}
+
+// 导出新的统一类型
+export type { UserDisplay, UserKeyDisplay } from "@/types/user";

+ 70 - 0
src/app/dashboard/_components/user/user-list.tsx

@@ -0,0 +1,70 @@
+"use client";
+import type { UserDisplay } from "@/types/user";
+import type { User } from "@/types/user";
+import { ListContainer, ListItem, ListItemData } from "@/components/ui/list";
+import { AddUserDialog } from "./add-user-dialog";
+
+interface UserListProps {
+  users: UserDisplay[];
+  activeUserId: number | null;
+  onUserSelect: (userId: number) => void;
+  currentUser?: User;
+}
+
+export function UserList({
+  users,
+  activeUserId,
+  onUserSelect,
+  currentUser,
+}: UserListProps) {
+  // 转换数据格式
+  const listItems: ListItemData[] = users.map((user) => ({
+    id: user.id,
+    title: user.name,
+    subtitle: user.note,
+    badge: {
+      text: `${user.keys.length} 个 Key`,
+      variant: "outline" as const,
+    },
+    metadata: [
+      {
+        label: "活跃密钥",
+        value: user.keys
+          .filter((k) => k.status === "enabled")
+          .length.toString(),
+      },
+      {
+        label: "总密钥",
+        value: user.keys.length.toString(),
+      },
+    ],
+  }));
+
+  return (
+    <div className="space-y-3">
+      <ListContainer
+        emptyState={{
+          title: "暂无用户",
+          description: "点击下方按钮创建第一个用户",
+        }}
+      >
+        <div className="space-y-2">
+          {listItems.map((item) => (
+            <ListItem
+              key={item.id}
+              data={item}
+              isActive={item.id === activeUserId}
+              onClick={() => onUserSelect(item.id as number)}
+              compact
+            />
+          ))}
+        </div>
+      </ListContainer>
+
+      {/* 新增用户按钮:列表下方、与列表同宽,中性配色 - 仅管理员可见 */}
+      {currentUser?.role === "admin" && (
+        <AddUserDialog variant="secondary" className="w-full" />
+      )}
+    </div>
+  );
+}

+ 22 - 0
src/app/dashboard/layout.tsx

@@ -0,0 +1,22 @@
+import type { ReactNode } from "react";
+import { redirect } from "next/navigation";
+
+import { getSession } from "@/lib/auth";
+import { DashboardHeader } from "./_components/dashboard-header";
+
+export default async function DashboardLayout({ children }: { children: ReactNode }) {
+  const session = await getSession();
+
+  if (!session) {
+    redirect("/login?from=/dashboard");
+  }
+
+  return (
+    <div className="min-h-screen bg-background">
+      <DashboardHeader session={session} />
+      <main className="mx-auto w-full max-w-7xl px-6 py-8">
+        {children}
+      </main>
+    </div>
+  );
+}

+ 45 - 0
src/app/dashboard/page.tsx

@@ -0,0 +1,45 @@
+import { redirect } from 'next/navigation';
+import { getSession } from '@/lib/auth';
+import { Section } from "@/components/section";
+import { UserKeyManager } from "./_components/user/user-key-manager";
+import { getUsers } from "@/actions/users";
+import { getUserStatistics } from "@/actions/statistics";
+import { hasPriceTable } from "@/actions/model-prices";
+import { ListErrorBoundary } from "@/components/error-boundary";
+import { StatisticsWrapper } from "./_components/statistics";
+import { DEFAULT_TIME_RANGE } from "@/types/statistics";
+
+export const dynamic = "force-dynamic";
+
+export default async function DashboardPage() {
+  // 检查价格表是否存在,如果不存在则跳转到价格上传页面
+  const hasPrices = await hasPriceTable();
+  if (!hasPrices) {
+    redirect('/settings/prices?required=true');
+  }
+
+  const [users, session, statistics] = await Promise.all([
+    getUsers(),
+    getSession(),
+    getUserStatistics(DEFAULT_TIME_RANGE),
+  ]);
+
+  return (
+    <div className="space-y-6">
+      <div>
+        <StatisticsWrapper
+          initialData={statistics.ok ? statistics.data : undefined}
+        />
+      </div>
+
+      <Section
+        title="客户端"
+        description="用户和密钥管理"
+      >
+        <ListErrorBoundary>
+          <UserKeyManager users={users} currentUser={session?.user} />
+        </ListErrorBoundary>
+      </Section>
+    </div>
+  );
+}

BIN
src/app/favicon.ico


+ 123 - 0
src/app/globals.css

@@ -0,0 +1,123 @@
+@import "tailwindcss";
+@import "tw-animate-css";
+
+@custom-variant dark (&:is(.dark *));
+
+@theme inline {
+  --color-background: var(--background);
+  --color-foreground: var(--foreground);
+  --font-sans: var(--font-geist-sans);
+  --font-mono: var(--font-geist-mono);
+  --color-sidebar-ring: var(--sidebar-ring);
+  --color-sidebar-border: var(--sidebar-border);
+  --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
+  --color-sidebar-accent: var(--sidebar-accent);
+  --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
+  --color-sidebar-primary: var(--sidebar-primary);
+  --color-sidebar-foreground: var(--sidebar-foreground);
+  --color-sidebar: var(--sidebar);
+  --color-chart-5: var(--chart-5);
+  --color-chart-4: var(--chart-4);
+  --color-chart-3: var(--chart-3);
+  --color-chart-2: var(--chart-2);
+  --color-chart-1: var(--chart-1);
+  --color-ring: var(--ring);
+  --color-input: var(--input);
+  --color-border: var(--border);
+  --color-destructive: var(--destructive);
+  --color-accent-foreground: var(--accent-foreground);
+  --color-accent: var(--accent);
+  --color-muted-foreground: var(--muted-foreground);
+  --color-muted: var(--muted);
+  --color-secondary-foreground: var(--secondary-foreground);
+  --color-secondary: var(--secondary);
+  --color-primary-foreground: var(--primary-foreground);
+  --color-primary: var(--primary);
+  --color-popover-foreground: var(--popover-foreground);
+  --color-popover: var(--popover);
+  --color-card-foreground: var(--card-foreground);
+  --color-card: var(--card);
+  --radius-sm: calc(var(--radius) - 4px);
+  --radius-md: calc(var(--radius) - 2px);
+  --radius-lg: var(--radius);
+  --radius-xl: calc(var(--radius) + 4px);
+}
+
+:root {
+  --radius: 0.65rem;
+  --background: oklch(1 0 0);
+  --foreground: oklch(0.141 0.005 285.823);
+  --card: oklch(1 0 0);
+  --card-foreground: oklch(0.141 0.005 285.823);
+  --popover: oklch(1 0 0);
+  --popover-foreground: oklch(0.141 0.005 285.823);
+  --primary: oklch(0.705 0.213 47.604);
+  --primary-foreground: oklch(0.98 0.016 73.684);
+  --secondary: oklch(0.967 0.001 286.375);
+  --secondary-foreground: oklch(0.21 0.006 285.885);
+  --muted: oklch(0.967 0.001 286.375);
+  --muted-foreground: oklch(0.552 0.016 285.938);
+  --accent: oklch(0.967 0.001 286.375);
+  --accent-foreground: oklch(0.21 0.006 285.885);
+  --destructive: oklch(0.577 0.245 27.325);
+  --border: oklch(0.92 0.004 286.32);
+  --input: oklch(0.92 0.004 286.32);
+  --ring: oklch(0.705 0.213 47.604);
+  --chart-1: oklch(0.646 0.222 41.116);
+  --chart-2: oklch(0.6 0.118 184.704);
+  --chart-3: oklch(0.398 0.07 227.392);
+  --chart-4: oklch(0.828 0.189 84.429);
+  --chart-5: oklch(0.769 0.188 70.08);
+  --sidebar: oklch(0.985 0 0);
+  --sidebar-foreground: oklch(0.141 0.005 285.823);
+  --sidebar-primary: oklch(0.705 0.213 47.604);
+  --sidebar-primary-foreground: oklch(0.98 0.016 73.684);
+  --sidebar-accent: oklch(0.967 0.001 286.375);
+  --sidebar-accent-foreground: oklch(0.21 0.006 285.885);
+  --sidebar-border: oklch(0.92 0.004 286.32);
+  --sidebar-ring: oklch(0.705 0.213 47.604);
+}
+
+.dark {
+  --background: oklch(0.141 0.005 285.823);
+  --foreground: oklch(0.985 0 0);
+  --card: oklch(0.21 0.006 285.885);
+  --card-foreground: oklch(0.985 0 0);
+  --popover: oklch(0.21 0.006 285.885);
+  --popover-foreground: oklch(0.985 0 0);
+  --primary: oklch(0.646 0.222 41.116);
+  --primary-foreground: oklch(0.98 0.016 73.684);
+  --secondary: oklch(0.274 0.006 286.033);
+  --secondary-foreground: oklch(0.985 0 0);
+  --muted: oklch(0.274 0.006 286.033);
+  --muted-foreground: oklch(0.705 0.015 286.067);
+  --accent: oklch(0.274 0.006 286.033);
+  --accent-foreground: oklch(0.985 0 0);
+  --destructive: oklch(0.704 0.191 22.216);
+  --border: oklch(1 0 0 / 10%);
+  --input: oklch(1 0 0 / 15%);
+  --ring: oklch(0.646 0.222 41.116);
+  --chart-1: oklch(0.488 0.243 264.376);
+  --chart-2: oklch(0.696 0.17 162.48);
+  --chart-3: oklch(0.769 0.188 70.08);
+  --chart-4: oklch(0.627 0.265 303.9);
+  --chart-5: oklch(0.645 0.246 16.439);
+  --sidebar: oklch(0.21 0.006 285.885);
+  --sidebar-foreground: oklch(0.985 0 0);
+  --sidebar-primary: oklch(0.646 0.222 41.116);
+  --sidebar-primary-foreground: oklch(0.98 0.016 73.684);
+  --sidebar-accent: oklch(0.274 0.006 286.033);
+  --sidebar-accent-foreground: oklch(0.985 0 0);
+  --sidebar-border: oklch(1 0 0 / 10%);
+  --sidebar-ring: oklch(0.646 0.222 41.116);
+}
+
+
+@layer base {
+  * {
+    @apply border-border outline-ring/50;
+  }
+  body {
+    @apply bg-background text-foreground;
+  }
+}

+ 23 - 0
src/app/layout.tsx

@@ -0,0 +1,23 @@
+import type { Metadata } from "next";
+import "./globals.css";
+import { Toaster } from "@/components/ui/sonner";
+
+export const metadata: Metadata = {
+  title: "Claude Code Hub",
+  description: "Claude Code Hub",
+};
+
+export default function RootLayout({
+  children,
+}: Readonly<{
+  children: React.ReactNode;
+}>) {
+  return (
+    <html lang="en">
+      <body className="antialiased">
+        {children}
+        <Toaster />
+      </body>
+    </html>
+  );
+}

+ 134 - 0
src/app/login/page.tsx

@@ -0,0 +1,134 @@
+'use client';
+
+import { Suspense, useState } from 'react';
+import { useRouter, useSearchParams } from 'next/navigation';
+import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
+import { Button } from '@/components/ui/button';
+import { Input } from '@/components/ui/input';
+import { Label } from '@/components/ui/label';
+import { Alert, AlertDescription } from '@/components/ui/alert';
+import { Key, Loader2 } from 'lucide-react';
+
+export default function LoginPage() {
+  return (
+    <Suspense fallback={<LoginPageFallback />}>
+      <LoginPageContent />
+    </Suspense>
+  );
+}
+
+function LoginPageContent() {
+  const router = useRouter();
+  const searchParams = useSearchParams();
+  const from = searchParams.get('from') || '/dashboard';
+
+  const [apiKey, setApiKey] = useState('');
+  const [loading, setLoading] = useState(false);
+  const [error, setError] = useState('');
+
+  const handleSubmit = async (e: React.FormEvent) => {
+    e.preventDefault();
+    setError('');
+    setLoading(true);
+
+    try {
+      const response = await fetch('/api/auth/login', {
+        method: 'POST',
+        headers: { 'Content-Type': 'application/json' },
+        body: JSON.stringify({ key: apiKey }),
+      });
+
+      const data = await response.json();
+
+      if (!response.ok) {
+        setError(data.error || '登录失败');
+        return;
+      }
+
+      // 登录成功,跳转到原页面
+      router.push(from);
+      router.refresh();
+    } catch {
+      setError('网络错误,请稍后重试');
+    } finally {
+      setLoading(false);
+    }
+  };
+
+  return (
+    <div className="relative min-h-screen overflow-hidden bg-gradient-to-br from-background via-background to-muted/40">
+      <div className="pointer-events-none absolute inset-0 -z-10">
+        <div className="absolute right-[10%] top-[-6rem] h-72 w-72 rounded-full bg-orange-500/10 blur-3xl" />
+        <div className="absolute bottom-[-4rem] left-[15%] h-80 w-80 rounded-full bg-orange-400/10 blur-3xl" />
+      </div>
+
+      <div className="mx-auto flex min-h-screen w-full items-center justify-center px-4 py-16">
+        <Card className="w-full max-w-lg border border-border/70 bg-card/95 shadow-xl backdrop-blur">
+          <CardHeader className="space-y-4">
+            <div className="flex items-center gap-3">
+              <div className="flex h-11 w-11 items-center justify-center rounded-full bg-orange-500/15 text-orange-500">
+                <Key className="h-5 w-5" />
+              </div>
+              <div>
+                <CardTitle className="text-2xl font-semibold">登录面板</CardTitle>
+                <CardDescription>使用您的 API Key 进入统一控制台</CardDescription>
+              </div>
+            </div>
+          </CardHeader>
+          <CardContent>
+            <form onSubmit={handleSubmit} className="space-y-6">
+              <div className="space-y-3">
+                <div className="space-y-2">
+                  <Label htmlFor="apiKey">API Key</Label>
+                  <div className="relative">
+                    <Key className="pointer-events-none absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
+                    <Input
+                      id="apiKey"
+                      type="password"
+                      placeholder="例如 sk-xxxxxxxx"
+                      value={apiKey}
+                      onChange={(e) => setApiKey(e.target.value)}
+                      className="pl-9"
+                      required
+                      disabled={loading}
+                    />
+                  </div>
+                </div>
+
+                {error ? (
+                  <Alert variant="destructive">
+                    <AlertDescription>{error}</AlertDescription>
+                  </Alert>
+                ) : null}
+              </div>
+
+              <div className="space-y-2">
+                <Button type="submit" className="w-full" disabled={loading || !apiKey.trim()}>
+                  {loading ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      登录中...
+                    </>
+                  ) : (
+                    '进入控制台'
+                  )}
+                </Button>
+                <p className="text-center text-xs text-muted-foreground">
+                  我们仅使用此 Key 作登录校验,绝不会保留原文。
+                </p>
+              </div>
+            </form>
+          </CardContent>
+        </Card>
+      </div>
+    </div>
+  );
+}
+
+function LoginPageFallback() {
+  return (
+    <div className="flex min-h-screen items-center justify-center bg-background">
+      <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
+    </div>
+  );
+}

+ 5 - 0
src/app/page.tsx

@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export default function Home() {
+  redirect("/dashboard");
+}

+ 46 - 0
src/app/settings/_components/settings-nav.tsx

@@ -0,0 +1,46 @@
+"use client";
+
+import Link from "next/link";
+import { usePathname } from "next/navigation";
+
+import { cn } from "@/lib/utils";
+import type { SettingsNavItem } from "../_lib/nav-items";
+
+interface SettingsNavProps {
+  items: SettingsNavItem[];
+}
+
+export function SettingsNav({ items }: SettingsNavProps) {
+  const pathname = usePathname();
+
+  if (items.length === 0) {
+    return null;
+  }
+
+  const getIsActive = (href: string) => {
+    return pathname === href || pathname.startsWith(`${href}/`);
+  };
+
+  return (
+    <nav className="rounded-xl border border-border/80 bg-card/70 p-1 backdrop-blur supports-[backdrop-filter]:bg-card/50">
+      <ul className="flex flex-col gap-1">
+        {items.map((item) => {
+          const isActive = getIsActive(item.href);
+          return (
+            <li key={item.href}>
+              <Link
+                href={item.href}
+                className={cn(
+                  "flex items-center justify-between rounded-lg px-3 py-2 text-sm font-medium text-muted-foreground transition-all hover:text-foreground",
+                  isActive && "bg-primary/5 text-foreground shadow-[0_1px_0_rgba(0,0,0,0.03)]"
+                )}
+              >
+                {item.label}
+              </Link>
+            </li>
+          );
+        })}
+      </ul>
+    </nav>
+  );
+}

+ 15 - 0
src/app/settings/_components/settings-page-header.tsx

@@ -0,0 +1,15 @@
+interface SettingsPageHeaderProps {
+  title: string;
+  description?: string;
+}
+
+export function SettingsPageHeader({ title, description }: SettingsPageHeaderProps) {
+  return (
+    <div className="space-y-1">
+      <h2 className="text-2xl font-semibold tracking-tight">{title}</h2>
+      {description ? (
+        <p className="text-sm text-muted-foreground">{description}</p>
+      ) : null}
+    </div>
+  );
+}

+ 10 - 0
src/app/settings/_lib/nav-items.ts

@@ -0,0 +1,10 @@
+export interface SettingsNavItem {
+  href: string;
+  label: string;
+}
+
+export const SETTINGS_NAV_ITEMS: SettingsNavItem[] = [
+  { href: "/settings/config", label: "配置" },
+  { href: "/settings/prices", label: "价格表" },
+  { href: "/settings/providers", label: "供应商" },
+];

+ 19 - 0
src/app/settings/config/page.tsx

@@ -0,0 +1,19 @@
+import { Section } from "@/components/section";
+import { SettingsPageHeader } from "../_components/settings-page-header";
+
+export default function SettingsConfigPage() {
+  return (
+    <>
+      <SettingsPageHeader
+        title="配置"
+        description="后续将在此管理基础系统参数。"
+      />
+
+      <Section title="系统配置" description="暂未开放配置项,敬请期待。">
+        <div className="text-sm text-muted-foreground">
+          当前没有可配置的项目。
+        </div>
+      </Section>
+    </>
+  );
+}

+ 37 - 0
src/app/settings/layout.tsx

@@ -0,0 +1,37 @@
+import type { ReactNode } from "react";
+import { redirect } from "next/navigation";
+
+import { getSession } from "@/lib/auth";
+import { DashboardHeader } from "@/app/dashboard/_components/dashboard-header";
+import { SettingsNav } from "./_components/settings-nav";
+import { SETTINGS_NAV_ITEMS } from "./_lib/nav-items";
+
+export default async function SettingsLayout({ children }: { children: ReactNode }) {
+  const session = await getSession();
+
+  if (!session) {
+    redirect("/login");
+  }
+
+  if (session.user.role !== "admin") {
+    redirect("/dashboard");
+  }
+
+  return (
+    <div className="min-h-screen bg-background">
+      <DashboardHeader session={session} />
+      <main className="mx-auto w-full max-w-7xl px-6 py-8">
+        <div className="space-y-6">
+          <div className="grid gap-6 lg:grid-cols-[220px_1fr]">
+            <aside className="lg:sticky lg:top-24 lg:self-start">
+              <SettingsNav items={SETTINGS_NAV_ITEMS} />
+            </aside>
+            <div className="space-y-6">
+              {children}
+            </div>
+          </div>
+        </div>
+      </main>
+    </div>
+  );
+}

+ 8 - 0
src/app/settings/page.tsx

@@ -0,0 +1,8 @@
+import { redirect } from "next/navigation";
+
+import { SETTINGS_NAV_ITEMS } from "./_lib/nav-items";
+
+export default function SettingsIndex() {
+  const firstItem = SETTINGS_NAV_ITEMS[0];
+  redirect(firstItem?.href ?? "/dashboard");
+}

+ 170 - 0
src/app/settings/prices/_components/price-list.tsx

@@ -0,0 +1,170 @@
+"use client";
+
+import { useState } from "react";
+import { Search, Package, DollarSign } from "lucide-react";
+import { Input } from "@/components/ui/input";
+import {
+  Table,
+  TableBody,
+  TableCell,
+  TableHead,
+  TableHeader,
+  TableRow,
+} from "@/components/ui/table";
+import { Badge } from "@/components/ui/badge";
+import type { ModelPrice } from "@/types/model-price";
+
+interface PriceListProps {
+  prices: ModelPrice[];
+}
+
+/**
+ * 价格列表组件
+ */
+export function PriceList({ prices }: PriceListProps) {
+  const [searchTerm, setSearchTerm] = useState("");
+
+  // 过滤价格数据
+  const filteredPrices = prices.filter((price) =>
+    price.modelName.toLowerCase().includes(searchTerm.toLowerCase())
+  );
+
+  /**
+   * 格式化价格显示为每百万token的价格
+   */
+  const formatPrice = (value?: number): string => {
+    if (!value) return "-";
+    // 将每token的价格转换为每百万token的价格
+    const pricePerMillion = value * 1000000;
+    // 格式化为合适的小数位数
+    if (pricePerMillion < 0.01) {
+      return pricePerMillion.toFixed(4);
+    } else if (pricePerMillion < 1) {
+      return pricePerMillion.toFixed(3);
+    } else if (pricePerMillion < 100) {
+      return pricePerMillion.toFixed(2);
+    } else {
+      return pricePerMillion.toFixed(0);
+    }
+  };
+
+  /**
+   * 获取模型类型标签
+   */
+  const getModeLabel = (mode?: string) => {
+    switch (mode) {
+      case "chat":
+        return <Badge variant="default">对话</Badge>;
+      case "image_generation":
+        return <Badge variant="secondary">图像生成</Badge>;
+      case "completion":
+        return <Badge variant="outline">补全</Badge>;
+      default:
+        return <Badge variant="outline">未知</Badge>;
+    }
+  };
+
+  return (
+    <div className="space-y-4">
+      {/* 搜索栏 */}
+      <div className="relative">
+        <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
+        <Input
+          placeholder="搜索模型名称..."
+          value={searchTerm}
+          onChange={(e) => setSearchTerm(e.target.value)}
+          className="pl-9"
+        />
+      </div>
+
+      {/* 价格表格 */}
+      <div className="border rounded-lg">
+        <Table className="table-fixed">
+          <TableHeader>
+            <TableRow>
+              <TableHead className="w-48 whitespace-normal">模型名称</TableHead>
+              <TableHead className="w-24">类型</TableHead>
+              <TableHead className="w-32 whitespace-normal">提供商</TableHead>
+              <TableHead className="w-32 text-right">输入价格 ($/M)</TableHead>
+              <TableHead className="w-32 text-right">输出价格 ($/M)</TableHead>
+              <TableHead className="w-32">更新时间</TableHead>
+            </TableRow>
+          </TableHeader>
+          <TableBody>
+            {filteredPrices.length > 0 ? (
+              filteredPrices.map((price) => (
+                <TableRow key={price.id}>
+                  <TableCell className="font-mono text-sm whitespace-normal break-words">
+                    {price.modelName}
+                  </TableCell>
+                  <TableCell>{getModeLabel(price.priceData.mode)}</TableCell>
+                  <TableCell className="whitespace-normal break-words">
+                    {price.priceData.litellm_provider || "-"}
+                  </TableCell>
+                  <TableCell className="font-mono text-sm text-right">
+                    {price.priceData.mode === "image_generation" ? (
+                      "-"
+                    ) : (
+                      <span className="text-muted-foreground">
+                        ${formatPrice(price.priceData.input_cost_per_token)}/M
+                      </span>
+                    )}
+                  </TableCell>
+                  <TableCell className="font-mono text-sm text-right">
+                    {price.priceData.mode === "image_generation" ? (
+                      <span className="text-muted-foreground">
+                        ${formatPrice(price.priceData.output_cost_per_image)}/img
+                      </span>
+                    ) : (
+                      <span className="text-muted-foreground">
+                        ${formatPrice(price.priceData.output_cost_per_token)}/M
+                      </span>
+                    )}
+                  </TableCell>
+                  <TableCell className="text-sm text-muted-foreground">
+                    {new Date(price.createdAt).toLocaleDateString("zh-CN")}
+                  </TableCell>
+                </TableRow>
+              ))
+            ) : (
+              <TableRow>
+                <TableCell colSpan={6} className="text-center py-8">
+                  <div className="flex flex-col items-center gap-2 text-muted-foreground">
+                    {searchTerm ? (
+                      <>
+                        <Search className="h-8 w-8 opacity-50" />
+                        <p>未找到匹配的模型</p>
+                      </>
+                    ) : (
+                      <>
+                        <Package className="h-8 w-8 opacity-50" />
+                        <p>暂无价格数据</p>
+                        <p className="text-sm">请上传价格表JSON文件</p>
+                      </>
+                    )}
+                  </div>
+                </TableCell>
+              </TableRow>
+            )}
+          </TableBody>
+        </Table>
+      </div>
+
+      {/* 统计信息 */}
+      <div className="flex items-center justify-between text-sm text-muted-foreground">
+        <div className="flex items-center gap-1">
+          <DollarSign className="h-4 w-4" />
+          <span>共 {filteredPrices.length} 个模型价格</span>
+        </div>
+        <div>
+          最后更新:
+          {prices.length > 0
+            ? new Date(
+                Math.max(...prices.map((p) => new Date(p.createdAt).getTime()))
+              ).toLocaleDateString("zh-CN")
+            : "-"}
+        </div>
+      </div>
+    </div>
+  );
+}

+ 288 - 0
src/app/settings/prices/_components/upload-price-dialog.tsx

@@ -0,0 +1,288 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import { useRouter } from "next/navigation";
+import { createPortal } from "react-dom";
+import { Upload, FileJson, CheckCircle, XCircle, AlertCircle, Loader2 } from "lucide-react";
+import { Button } from "@/components/ui/button";
+import {
+  Dialog,
+  DialogContent,
+  DialogDescription,
+  DialogHeader,
+  DialogTitle,
+  DialogTrigger,
+} from "@/components/ui/dialog";
+import { uploadPriceTable } from "@/actions/model-prices";
+import { toast } from "sonner";
+import type { PriceUpdateResult } from "@/types/model-price";
+
+interface PageLoadingOverlayProps {
+  active: boolean;
+}
+
+interface UploadPriceDialogProps {
+  defaultOpen?: boolean;
+  isRequired?: boolean;
+}
+
+function PageLoadingOverlay({ active }: PageLoadingOverlayProps) {
+  const [mounted, setMounted] = useState(false);
+
+  useEffect(() => {
+    setMounted(true);
+  }, []);
+
+  if (!mounted || !active) {
+    return null;
+  }
+
+  return createPortal(
+    <div className="fixed inset-0 z-[9999] flex items-center justify-center bg-background/80 backdrop-blur-sm">
+      <div className="flex items-center gap-3 rounded-lg bg-card/90 px-5 py-4 shadow-lg ring-1 ring-border/40">
+        <Loader2 className="h-5 w-5 animate-spin text-primary" />
+        <span className="text-sm text-muted-foreground">正在导入模型价格...</span>
+      </div>
+    </div>,
+    document.body
+  );
+}
+
+/**
+ * 价格表上传对话框组件
+ */
+export function UploadPriceDialog({
+  defaultOpen = false,
+  isRequired = false
+}: UploadPriceDialogProps) {
+  const router = useRouter();
+  const [open, setOpen] = useState(defaultOpen);
+  const [uploading, setUploading] = useState(false);
+  const [result, setResult] = useState<PriceUpdateResult | null>(null);
+
+  const handleOpenChange = (nextOpen: boolean) => {
+    if (!nextOpen && uploading) {
+      return;
+    }
+
+    if (!nextOpen) {
+      setResult(null);
+    }
+
+    setOpen(nextOpen);
+  };
+
+  /**
+   * 处理文件选择
+   */
+  const handleFileSelect = async (event: React.ChangeEvent<HTMLInputElement>) => {
+    const file = event.target.files?.[0];
+    if (!file) return;
+
+    // 验证文件类型
+    if (!file.name.endsWith(".json")) {
+      toast.error("请选择JSON文件");
+      return;
+    }
+
+    // 验证文件大小(限制10MB)
+    if (file.size > 10 * 1024 * 1024) {
+      toast.error("文件大小不能超过10MB");
+      return;
+    }
+
+    setUploading(true);
+    setResult(null);
+
+    try {
+      // 读取文件内容
+      const text = await file.text();
+
+      // 上传并处理
+      const response = await uploadPriceTable(text);
+
+      if (!response.ok) {
+        toast.error(response.error);
+        return;
+      }
+
+      if (!response.data) {
+        toast.error("价格表上传成功但未返回处理结果");
+        return;
+      }
+
+      setResult(response.data);
+      toast.success("价格表上传成功");
+    } catch (error) {
+      console.error("上传失败:", error);
+      toast.error("上传失败,请重试");
+    } finally {
+      setUploading(false);
+      // 清除文件输入
+      event.target.value = "";
+    }
+  };
+
+  /**
+   * 关闭对话框
+   */
+  const handleClose = () => {
+    if (uploading) {
+      return;
+    }
+
+    // 如果是必需上传且已成功上传,跳转到dashboard
+    if (isRequired && result && (result.added.length > 0 || result.updated.length > 0)) {
+      router.push('/dashboard');
+      return;
+    }
+
+    setOpen(false);
+    setResult(null);
+  };
+
+  return (
+    <>
+      <PageLoadingOverlay active={uploading} />
+      <Dialog open={open} onOpenChange={handleOpenChange}>
+        <DialogTrigger asChild>
+          <Button variant="outline" size="sm" disabled={uploading}>
+            <Upload className="h-4 w-4 mr-2" />
+            上传价格表
+          </Button>
+        </DialogTrigger>
+        <DialogContent
+          className="max-w-lg"
+          onEscapeKeyDown={(event) => {
+            if (uploading) {
+              event.preventDefault();
+            }
+          }}
+          onPointerDownOutside={(event) => {
+            if (uploading) {
+              event.preventDefault();
+            }
+          }}
+        >
+        <DialogHeader>
+          <DialogTitle>
+            {isRequired ? "请务必先上传价格表" : "上传模型价格表"}
+          </DialogTitle>
+          <DialogDescription>
+            {isRequired
+              ? "系统检测到尚未配置模型价格,请选择包含模型价格数据的JSON文件进行上传"
+              : "选择包含模型价格数据的JSON文件进行上传"
+            }
+          </DialogDescription>
+        </DialogHeader>
+
+        {!result ? (
+          <div className="space-y-4">
+            <div className="border-2 border-dashed border-muted-foreground/25 rounded-lg p-6">
+              <div className="flex flex-col items-center space-y-3">
+                <FileJson className="h-10 w-10 text-muted-foreground/50" />
+                <div className="text-center">
+                  <p className="text-sm text-muted-foreground">
+                    点击选择JSON文件或拖拽到此处
+                  </p>
+                  <p className="text-xs text-muted-foreground mt-1">
+                    文件大小不超过10MB
+                  </p>
+                </div>
+                <label htmlFor="price-file-input">
+                  <Button
+                    variant="secondary"
+                    size="sm"
+                    disabled={uploading}
+                    asChild
+                  >
+                    <span>
+                      {uploading ? "上传中..." : "选择文件"}
+                    </span>
+                  </Button>
+                </label>
+                <input
+                  id="price-file-input"
+                  type="file"
+                  accept=".json"
+                  className="hidden"
+                  onChange={handleFileSelect}
+                  disabled={uploading}
+                />
+              </div>
+            </div>
+
+            <div className="text-xs text-muted-foreground space-y-1">
+              <p>• 推荐使用 <a className="text-blue-500 underline" href="https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" target="_blank" rel="noopener noreferrer">LiteLLM</a> 的模型价格表</p>
+              <p>• 为避免网络问题,请您手动下载 json 文件并上传。</p>
+            </div>
+          </div>
+        ) : (
+          <div className="space-y-4">
+            <div className="text-sm space-y-2">
+              <div className="flex items-center justify-between p-2 bg-muted/50 rounded">
+                <span>处理总数</span>
+                <span className="font-mono">{result.total}</span>
+              </div>
+
+              {result.added.length > 0 && (
+                <div className="p-2 bg-green-50 dark:bg-green-950/20 rounded">
+                  <div className="flex items-center gap-2 mb-1">
+                    <CheckCircle className="h-4 w-4 text-green-600" />
+                    <span className="font-medium">新增模型 ({result.added.length})</span>
+                  </div>
+                  <div className="text-xs text-muted-foreground ml-6">
+                    {result.added.slice(0, 3).join(", ")}
+                    {result.added.length > 3 && ` 等${result.added.length}个`}
+                  </div>
+                </div>
+              )}
+
+              {result.updated.length > 0 && (
+                <div className="p-2 bg-blue-50 dark:bg-blue-950/20 rounded">
+                  <div className="flex items-center gap-2 mb-1">
+                    <AlertCircle className="h-4 w-4 text-blue-600" />
+                    <span className="font-medium">更新模型 ({result.updated.length})</span>
+                  </div>
+                  <div className="text-xs text-muted-foreground ml-6">
+                    {result.updated.slice(0, 3).join(", ")}
+                    {result.updated.length > 3 && ` 等${result.updated.length}个`}
+                  </div>
+                </div>
+              )}
+
+              {result.unchanged.length > 0 && (
+                <div className="p-2 bg-gray-50 dark:bg-gray-950/20 rounded">
+                  <div className="flex items-center gap-2">
+                    <span className="font-medium">未变化 ({result.unchanged.length})</span>
+                  </div>
+                </div>
+              )}
+
+              {result.failed.length > 0 && (
+                <div className="p-2 bg-red-50 dark:bg-red-950/20 rounded">
+                  <div className="flex items-center gap-2 mb-1">
+                    <XCircle className="h-4 w-4 text-red-600" />
+                    <span className="font-medium">处理失败 ({result.failed.length})</span>
+                  </div>
+                  <div className="text-xs text-muted-foreground ml-6">
+                    {result.failed.slice(0, 3).join(", ")}
+                    {result.failed.length > 3 && ` 等${result.failed.length}个`}
+                  </div>
+                </div>
+              )}
+            </div>
+
+            <Button onClick={handleClose} className="w-full">
+              {isRequired && result && (result.added.length > 0 || result.updated.length > 0)
+                ? "进入控制面板"
+                : "完成"
+              }
+            </Button>
+          </div>
+        )}
+      </DialogContent>
+    </Dialog>
+    </>
+  );
+}

+ 37 - 0
src/app/settings/prices/page.tsx

@@ -0,0 +1,37 @@
+import { getModelPrices } from "@/actions/model-prices";
+import { Section } from "@/components/section";
+import { PriceList } from "./_components/price-list";
+import { UploadPriceDialog } from "./_components/upload-price-dialog";
+import { SettingsPageHeader } from "../_components/settings-page-header";
+
+export const dynamic = "force-dynamic";
+
+interface SettingsPricesPageProps {
+  searchParams: Promise<{ required?: string }>;
+}
+
+export default async function SettingsPricesPage({
+  searchParams
+}: SettingsPricesPageProps) {
+  const params = await searchParams;
+  const prices = await getModelPrices();
+  const isRequired = params.required === 'true';
+  const isEmpty = prices.length === 0;
+
+  return (
+    <>
+      <SettingsPageHeader
+        title="价格表"
+        description="管理平台基础配置与模型价格"
+      />
+
+      <Section
+        title="模型价格"
+        description="管理 AI 模型的价格配置"
+        actions={<UploadPriceDialog defaultOpen={isRequired && isEmpty} isRequired={isRequired} />}
+      >
+        <PriceList prices={prices} />
+      </Section>
+    </>
+  );
+}

+ 25 - 0
src/app/settings/providers/_components/add-provider-dialog.tsx

@@ -0,0 +1,25 @@
+"use client";
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { ServerCog } from "lucide-react";
+import { ProviderForm } from "./forms/provider-form";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
+
+export function AddProviderDialog() {
+  const [open, setOpen] = useState(false);
+  return (
+    <Dialog open={open} onOpenChange={setOpen}>
+      <DialogTrigger asChild>
+        <Button>
+          <ServerCog className="h-4 w-4" /> 新增服务商
+        </Button>
+      </DialogTrigger>
+      <DialogContent className="max-w-2xl">
+        <FormErrorBoundary>
+          <ProviderForm mode="create" onSuccess={() => setOpen(false)} />
+        </FormErrorBoundary>
+      </DialogContent>
+    </Dialog>
+  );
+}

+ 274 - 0
src/app/settings/providers/_components/forms/provider-form.tsx

@@ -0,0 +1,274 @@
+"use client";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { DialogHeader, DialogTitle } from "@/components/ui/dialog";
+import { useState, useTransition } from "react";
+import { addProvider, editProvider, removeProvider } from "@/actions/providers";
+import {
+  AlertDialog,
+  AlertDialogAction,
+  AlertDialogCancel,
+  AlertDialogContent,
+  AlertDialogDescription,
+  AlertDialogHeader as AlertHeader,
+  AlertDialogTitle as AlertTitle,
+  AlertDialogTrigger,
+} from "@/components/ui/alert-dialog";
+import type { ProviderDisplay } from "@/types/provider";
+import { validateNumericField, isValidUrl } from "@/lib/utils/validation";
+import { PROVIDER_DEFAULTS } from "@/lib/constants/provider.constants";
+import { toast } from "sonner";
+
+type Mode = "create" | "edit";
+
+interface ProviderFormProps {
+  mode: Mode;
+  onSuccess?: () => void;
+  provider?: ProviderDisplay; // edit 模式需要,create 可空
+}
+
+export function ProviderForm({ mode, onSuccess, provider }: ProviderFormProps) {
+  const isEdit = mode === "edit";
+  const [isPending, startTransition] = useTransition();
+
+  const [name, setName] = useState(isEdit ? provider?.name ?? "" : "");
+  const [url, setUrl] = useState(isEdit ? provider?.url ?? "" : "");
+  const [key, setKey] = useState(""); // 编辑时留空代表不更新
+  const [tpm, setTpm] = useState<number | null>(isEdit ? provider?.tpm ?? null : null);
+  const [rpm, setRpm] = useState<number | null>(isEdit ? provider?.rpm ?? null : null);
+  const [rpd, setRpd] = useState<number | null>(isEdit ? provider?.rpd ?? null : null);
+  const [cc, setCc] = useState<number | null>(isEdit ? provider?.cc ?? null : null);
+
+  const handleSubmit = (e: React.FormEvent) => {
+    e.preventDefault();
+
+    if (!name.trim() || !url.trim() || (!isEdit && !key.trim())) {
+      return;
+    }
+
+    if (!isValidUrl(url.trim())) {
+      toast.error("请输入有效的URL地址");
+      return;
+    }
+
+    startTransition(async () => {
+      try {
+        if (isEdit && provider) {
+          const updateData: {
+            name?: string;
+            url?: string;
+            key?: string;
+            tpm?: number | null;
+            rpm?: number | null;
+            rpd?: number | null;
+            cc?: number | null;
+          } = {
+            name: name.trim(),
+            url: url.trim(),
+            tpm,
+            rpm,
+            rpd,
+            cc,
+          };
+          if (key.trim()) {
+            updateData.key = key.trim();
+          }
+          const res = await editProvider(provider.id, updateData);
+          if (!res.ok) {
+            toast.error(res.error || '更新服务商失败');
+            return;
+          }
+        } else {
+          const res = await addProvider({
+            name: name.trim(),
+            url: url.trim(),
+            key: key.trim(),
+            // 使用配置的默认值:默认不启用、权重=1
+            is_enabled: PROVIDER_DEFAULTS.IS_ENABLED,
+            weight: PROVIDER_DEFAULTS.WEIGHT,
+            tpm,
+            rpm,
+            rpd,
+            cc,
+          });
+          if (!res.ok) {
+            toast.error(res.error || '添加服务商失败');
+            return;
+          }
+          // 重置表单(仅新增)
+          setName("");
+          setUrl("");
+          setKey("");
+          setTpm(null);
+          setRpm(null);
+          setRpd(null);
+          setCc(null);
+        }
+        onSuccess?.();
+      } catch (error) {
+        console.error(isEdit ? "更新服务商失败:" : "添加服务商失败:", error);
+        toast.error(isEdit ? '更新服务商失败' : '添加服务商失败');
+      }
+    });
+  };
+
+  return (
+    <div className="space-y-4">
+      <DialogHeader>
+        <DialogTitle>{isEdit ? "编辑服务商" : "新增服务商"}</DialogTitle>
+      </DialogHeader>
+
+      <form onSubmit={handleSubmit} className="space-y-4">
+        <div className="space-y-2">
+          <Label htmlFor={isEdit ? "edit-name" : "name"}>服务商名称 *</Label>
+          <Input
+            id={isEdit ? "edit-name" : "name"}
+            value={name}
+            onChange={(e) => setName(e.target.value)}
+            placeholder="例如: 智谱"
+            disabled={isPending}
+            required
+          />
+        </div>
+
+        {/* 移除描述字段 */}
+
+        <div className="space-y-2">
+          <Label htmlFor={isEdit ? "edit-url" : "url"}>API 地址 *</Label>
+          <Input
+            id={isEdit ? "edit-url" : "url"}
+            value={url}
+            onChange={(e) => setUrl(e.target.value)}
+            placeholder="例如: https://open.bigmodel.cn/api/anthropic"
+            disabled={isPending}
+            required
+          />
+        </div>
+
+        <div className="space-y-2">
+          <Label htmlFor={isEdit ? "edit-key" : "key"}>API 密钥{isEdit ? "(留空不更改)" : " *"}</Label>
+          <Input
+            id={isEdit ? "edit-key" : "key"}
+            type="password"
+            value={key}
+            onChange={(e) => setKey(e.target.value)}
+            placeholder={isEdit ? "留空则不更改密钥" : "输入 API 密钥"}
+            disabled={isPending}
+            required={!isEdit}
+          />
+          {isEdit && provider ? (
+            <div className="text-xs text-muted-foreground">当前密钥: {provider.maskedKey}</div>
+          ) : null}
+        </div>
+
+        <div className="grid grid-cols-2 gap-4">
+          <div className="space-y-2">
+            <Label htmlFor={isEdit ? "edit-tpm" : "tpm"}>TPM (每分钟Token数)</Label>
+            <Input
+              id={isEdit ? "edit-tpm" : "tpm"}
+              type="number"
+              value={tpm?.toString() ?? ""}
+              onChange={(e) => setTpm(validateNumericField(e.target.value))}
+              placeholder="留空表示无限制"
+              disabled={isPending}
+              min="1"
+            />
+          </div>
+          <div className="space-y-2">
+            <Label htmlFor={isEdit ? "edit-rpm" : "rpm"}>RPM (每分钟请求数)</Label>
+            <Input
+              id={isEdit ? "edit-rpm" : "rpm"}
+              type="number"
+              value={rpm?.toString() ?? ""}
+              onChange={(e) => setRpm(validateNumericField(e.target.value))}
+              placeholder="留空表示无限制"
+              disabled={isPending}
+              min="1"
+            />
+          </div>
+        </div>
+
+        <div className="grid grid-cols-2 gap-4">
+          <div className="space-y-2">
+            <Label htmlFor={isEdit ? "edit-rpd" : "rpd"}>RPD (每日请求数)</Label>
+            <Input
+              id={isEdit ? "edit-rpd" : "rpd"}
+              type="number"
+              value={rpd?.toString() ?? ""}
+              onChange={(e) => setRpd(validateNumericField(e.target.value))}
+              placeholder="留空表示无限制"
+              disabled={isPending}
+              min="1"
+            />
+          </div>
+          <div className="space-y-2">
+            <Label htmlFor={isEdit ? "edit-cc" : "cc"}>并发连接数</Label>
+            <Input
+              id={isEdit ? "edit-cc" : "cc"}
+              type="number"
+              value={cc?.toString() ?? ""}
+              onChange={(e) => setCc(validateNumericField(e.target.value))}
+              placeholder="例如: 10,留空表示无限制"
+              disabled={isPending}
+              min="1"
+            />
+          </div>
+        </div>
+
+        {isEdit ? (
+          <div className="flex items-center justify-between pt-4">
+            <AlertDialog>
+              <AlertDialogTrigger asChild>
+                <Button type="button" variant="destructive" disabled={isPending}>
+                  删除
+                </Button>
+              </AlertDialogTrigger>
+              <AlertDialogContent>
+                <AlertHeader>
+                  <AlertTitle>删除服务商</AlertTitle>
+                  <AlertDialogDescription>
+                    确定要删除服务商&ldquo;{provider?.name}&rdquo;吗?此操作不可恢复。
+                  </AlertDialogDescription>
+                </AlertHeader>
+                <div className="flex gap-2 justify-end">
+                  <AlertDialogCancel>取消</AlertDialogCancel>
+                  <AlertDialogAction
+                    onClick={() => {
+                      if (!provider) return;
+                      startTransition(async () => {
+                        try {
+                          const res = await removeProvider(provider.id);
+                          if (!res.ok) {
+                            toast.error(res.error || '删除服务商失败');
+                            return;
+                          }
+                          onSuccess?.();
+                        } catch (e) {
+                          console.error("删除服务商失败", e);
+                          toast.error('删除服务商失败');
+                        }
+                      });
+                    }}
+                  >
+                    确认删除
+                  </AlertDialogAction>
+                </div>
+              </AlertDialogContent>
+            </AlertDialog>
+
+            <Button type="submit" disabled={isPending}>
+              {isPending ? "更新中..." : "确认更新"}
+            </Button>
+          </div>
+        ) : (
+          <div className="flex justify-end gap-2 pt-4">
+            <Button type="submit" disabled={isPending}>
+              {isPending ? "添加中..." : "确认添加"}
+            </Button>
+          </div>
+        )}
+      </form>
+    </div>
+  );
+}

+ 227 - 0
src/app/settings/providers/_components/hooks/use-provider-edit.ts

@@ -0,0 +1,227 @@
+import { useRef, useState } from "react";
+import { toast } from "sonner";
+import { editProvider } from "@/actions/providers";
+import type { ProviderDisplay } from "@/types/provider";
+import {
+  clampWeight,
+  clampIntInRange,
+  clampTpm,
+} from "@/lib/utils/validation";
+import { PROVIDER_LIMITS } from "@/lib/constants/provider.constants";
+
+export function useProviderEdit(item: ProviderDisplay, canEdit: boolean) {
+  // 基本状态
+  const [enabled, setEnabled] = useState<boolean>(item.isEnabled);
+  const [togglePending, setTogglePending] = useState(false);
+
+  // 权重编辑
+  const [showWeight, setShowWeight] = useState(false);
+  const [weight, setWeight] = useState<number>(clampWeight(item.weight));
+  const initialWeightRef = useRef<number>(item.weight);
+
+  // TPM 编辑
+  const [showTpm, setShowTpm] = useState(false);
+  const [tpmInfinite, setTpmInfinite] = useState<boolean>(item.tpm === null);
+  const [tpmValue, setTpmValue] = useState<number>(() => {
+    const base = item.tpm ?? PROVIDER_LIMITS.TPM.MIN;
+    return clampTpm(base);
+  });
+  const initialTpmRef = useRef<number | null>(item.tpm);
+
+  // RPM 编辑
+  const [showRpm, setShowRpm] = useState(false);
+  const [rpmInfinite, setRpmInfinite] = useState<boolean>(item.rpm === null);
+  const [rpmValue, setRpmValue] = useState<number>(() =>
+    clampIntInRange(item.rpm ?? PROVIDER_LIMITS.RPM.MIN, PROVIDER_LIMITS.RPM.MIN, PROVIDER_LIMITS.RPM.MAX)
+  );
+  const initialRpmRef = useRef<number | null>(item.rpm);
+
+  // RPD 编辑
+  const [showRpd, setShowRpd] = useState(false);
+  const [rpdInfinite, setRpdInfinite] = useState<boolean>(item.rpd === null);
+  const [rpdValue, setRpdValue] = useState<number>(() =>
+    clampIntInRange(item.rpd ?? PROVIDER_LIMITS.RPD.MIN, PROVIDER_LIMITS.RPD.MIN, PROVIDER_LIMITS.RPD.MAX)
+  );
+  const initialRpdRef = useRef<number | null>(item.rpd);
+
+  // CC 编辑
+  const [showCc, setShowCc] = useState(false);
+  const [ccInfinite, setCcInfinite] = useState<boolean>(item.cc === null);
+  const [ccValue, setCcValue] = useState<number>(() =>
+    clampIntInRange(item.cc ?? PROVIDER_LIMITS.CC.MIN, PROVIDER_LIMITS.CC.MIN, PROVIDER_LIMITS.CC.MAX)
+  );
+  const initialCcRef = useRef<number | null>(item.cc);
+
+  // 切换启用状态
+  const handleToggle = async (next: boolean) => {
+    if (!canEdit || togglePending) return;
+    setTogglePending(true);
+    const prev = enabled;
+    setEnabled(next);
+
+    try {
+      const res = await editProvider(item.id, { is_enabled: next });
+      if (!res.ok) {
+        throw new Error(res.error);
+      }
+    } catch (e) {
+      console.error("切换服务商启用状态失败", e);
+      setEnabled(prev);
+      const msg = e instanceof Error ? e.message : '切换失败';
+      toast.error(msg);
+    } finally {
+      setTogglePending(false);
+    }
+  };
+
+  // 权重编辑处理
+  const handleWeightPopover = (open: boolean) => {
+    if (!canEdit) return;
+    setShowWeight(open);
+    if (open) {
+      initialWeightRef.current = clampWeight(weight);
+      return;
+    }
+
+    const next = clampWeight(weight);
+    if (next !== clampWeight(initialWeightRef.current)) {
+      editProvider(item.id, { weight: next }).then(res => {
+        if (!res.ok) throw new Error(res.error);
+      }).catch((e) => {
+        console.error("更新权重失败", e);
+        const msg = e instanceof Error ? e.message : '更新权重失败';
+        toast.error(msg);
+        setWeight(clampWeight(initialWeightRef.current));
+      });
+    }
+  };
+
+  // TPM 编辑处理
+  const handleTpmPopover = (open: boolean) => {
+    if (!canEdit) return;
+    setShowTpm(open);
+    if (open) {
+      initialTpmRef.current = item.tpm;
+      return;
+    }
+
+    const nextValue = tpmInfinite ? null : clampTpm(tpmValue);
+    if (nextValue !== initialTpmRef.current) {
+      editProvider(item.id, { tpm: nextValue }).then(res => {
+        if (!res.ok) throw new Error(res.error);
+      }).catch((e) => {
+        console.error("更新TPM失败", e);
+        const msg = e instanceof Error ? e.message : '更新TPM失败';
+        toast.error(msg);
+        setTpmInfinite(initialTpmRef.current === null);
+        setTpmValue(clampTpm(initialTpmRef.current ?? PROVIDER_LIMITS.TPM.MIN));
+      });
+    }
+  };
+
+  // RPM 编辑处理
+  const handleRpmPopover = (open: boolean) => {
+    if (!canEdit) return;
+    setShowRpm(open);
+    if (open) {
+      initialRpmRef.current = item.rpm;
+      return;
+    }
+
+    const nextValue = rpmInfinite ? null : clampIntInRange(rpmValue, PROVIDER_LIMITS.RPM.MIN, PROVIDER_LIMITS.RPM.MAX);
+    if (nextValue !== initialRpmRef.current) {
+      editProvider(item.id, { rpm: nextValue }).then(res => {
+        if (!res.ok) throw new Error(res.error);
+      }).catch((e) => {
+        console.error("更新RPM失败", e);
+        const msg = e instanceof Error ? e.message : '更新RPM失败';
+        toast.error(msg);
+        setRpmInfinite(initialRpmRef.current === null);
+        setRpmValue(clampIntInRange(initialRpmRef.current ?? PROVIDER_LIMITS.RPM.MIN, PROVIDER_LIMITS.RPM.MIN, PROVIDER_LIMITS.RPM.MAX));
+      });
+    }
+  };
+
+  // RPD 编辑处理
+  const handleRpdPopover = (open: boolean) => {
+    if (!canEdit) return;
+    setShowRpd(open);
+    if (open) {
+      initialRpdRef.current = item.rpd;
+      return;
+    }
+
+    const nextValue = rpdInfinite ? null : clampIntInRange(rpdValue, PROVIDER_LIMITS.RPD.MIN, PROVIDER_LIMITS.RPD.MAX);
+    if (nextValue !== initialRpdRef.current) {
+      editProvider(item.id, { rpd: nextValue }).then(res => {
+        if (!res.ok) throw new Error(res.error);
+      }).catch((e) => {
+        console.error("更新RPD失败", e);
+        const msg = e instanceof Error ? e.message : '更新RPD失败';
+        toast.error(msg);
+        setRpdInfinite(initialRpdRef.current === null);
+        setRpdValue(clampIntInRange(initialRpdRef.current ?? PROVIDER_LIMITS.RPD.MIN, PROVIDER_LIMITS.RPD.MIN, PROVIDER_LIMITS.RPD.MAX));
+      });
+    }
+  };
+
+  // CC 编辑处理
+  const handleCcPopover = (open: boolean) => {
+    if (!canEdit) return;
+    setShowCc(open);
+    if (open) {
+      initialCcRef.current = item.cc;
+      return;
+    }
+
+    const nextValue = ccInfinite ? null : clampIntInRange(ccValue, PROVIDER_LIMITS.CC.MIN, PROVIDER_LIMITS.CC.MAX);
+    if (nextValue !== initialCcRef.current) {
+      editProvider(item.id, { cc: nextValue }).then(res => {
+        if (!res.ok) throw new Error(res.error);
+      }).catch((e) => {
+        console.error("更新CC失败", e);
+        const msg = e instanceof Error ? e.message : '更新CC失败';
+        toast.error(msg);
+        setCcInfinite(initialCcRef.current === null);
+        setCcValue(clampIntInRange(initialCcRef.current ?? PROVIDER_LIMITS.CC.MIN, PROVIDER_LIMITS.CC.MIN, PROVIDER_LIMITS.CC.MAX));
+      });
+    }
+  };
+
+  return {
+    // 状态
+    enabled,
+    togglePending,
+    weight,
+    setWeight,
+    showWeight,
+    tpmInfinite,
+    setTpmInfinite,
+    tpmValue,
+    setTpmValue,
+    showTpm,
+    rpmInfinite,
+    setRpmInfinite,
+    rpmValue,
+    setRpmValue,
+    showRpm,
+    rpdInfinite,
+    setRpdInfinite,
+    rpdValue,
+    setRpdValue,
+    showRpd,
+    ccInfinite,
+    setCcInfinite,
+    ccValue,
+    setCcValue,
+    showCc,
+
+    // 处理函数
+    handleToggle,
+    handleWeightPopover,
+    handleTpmPopover,
+    handleRpmPopover,
+    handleRpdPopover,
+    handleCcPopover,
+  };
+}

+ 306 - 0
src/app/settings/providers/_components/provider-list-item.tsx

@@ -0,0 +1,306 @@
+"use client";
+import { useState } from "react";
+import { Button } from "@/components/ui/button";
+import { Dialog, DialogContent, DialogTrigger } from "@/components/ui/dialog";
+import { Edit, Globe, Key } from "lucide-react";
+import type { ProviderDisplay } from "@/types/provider";
+import type { User } from "@/types/user";
+import { ProviderForm } from "./forms/provider-form";
+import { Switch } from "@/components/ui/switch";
+import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
+import { Slider } from "@/components/ui/slider";
+import { formatTpmDisplay } from "@/lib/utils/validation";
+import { PROVIDER_LIMITS } from "@/lib/constants/provider.constants";
+import { FormErrorBoundary } from "@/components/form-error-boundary";
+import { useProviderEdit } from "./hooks/use-provider-edit";
+
+interface ProviderListItemProps {
+  item: ProviderDisplay;
+  currentUser?: User;
+}
+
+export function ProviderListItem({ item, currentUser }: ProviderListItemProps) {
+  const [openEdit, setOpenEdit] = useState(false);
+  const canEdit = currentUser?.role === 'admin';
+
+  const {
+    enabled,
+    togglePending,
+    weight,
+    setWeight,
+    showWeight,
+    tpmInfinite,
+    setTpmInfinite,
+    tpmValue,
+    setTpmValue,
+    showTpm,
+    rpmInfinite,
+    setRpmInfinite,
+    rpmValue,
+    setRpmValue,
+    showRpm,
+    rpdInfinite,
+    setRpdInfinite,
+    rpdValue,
+    setRpdValue,
+    showRpd,
+    ccInfinite,
+    setCcInfinite,
+    ccValue,
+    setCcValue,
+    showCc,
+    handleToggle,
+    handleWeightPopover,
+    handleTpmPopover,
+    handleRpmPopover,
+    handleRpdPopover,
+    handleCcPopover,
+  } = useProviderEdit(item, canEdit);
+
+  return (
+    <div className="group relative h-full rounded-xl border border-border/70 bg-card p-4 shadow-sm transition-all duration-150 hover:shadow-md hover:border-border focus-within:ring-1 focus-within:ring-primary/20">
+      <div className="flex items-start justify-between gap-3 mb-3">
+        <div className="flex-1 min-w-0">
+          <div className="flex items-center gap-2 mb-1">
+            <span className={`inline-flex h-5 w-5 items-center justify-center rounded-md text-[10px] font-semibold ${enabled ? "bg-green-500/15 text-green-600" : "bg-muted text-muted-foreground"}`}>
+              ●
+            </span>
+            <h3 className="text-sm font-semibold text-foreground truncate tracking-tight">{item.name}</h3>
+            {/* 编辑按钮 - 仅管理员可见 */}
+            {canEdit && (
+              <Dialog open={openEdit} onOpenChange={setOpenEdit}>
+                <DialogTrigger asChild>
+                  <Button
+                    type="button"
+                    aria-label="编辑服务商"
+                    variant="ghost"
+                    size="icon"
+                    className="h-5 w-5 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
+                  >
+                    <Edit className="h-3.5 w-3.5" />
+                  </Button>
+                </DialogTrigger>
+                <DialogContent>
+                  <FormErrorBoundary>
+                    <ProviderForm mode="edit" provider={item} onSuccess={() => setOpenEdit(false)} />
+                  </FormErrorBoundary>
+                </DialogContent>
+              </Dialog>
+            )}
+          </div>
+        </div>
+        <div className="flex items-center gap-2">
+          <div className="flex items-center gap-2 text-xs text-muted-foreground">
+            <span>启用</span>
+            <Switch
+              aria-label="启用服务商"
+              checked={enabled}
+              disabled={!canEdit || togglePending}
+              onCheckedChange={handleToggle}
+            />
+          </div>
+        </div>
+      </div>
+
+      {/* 内容区改为上下结构 */}
+      <div className="space-y-3 mb-3">
+        {/* 上:URL 与密钥 */}
+        <div className="space-y-2">
+          <div className="flex items-center gap-2 text-xs">
+            <Globe className="h-3.5 w-3.5 text-blue-500 shrink-0" />
+            <span className="font-mono text-muted-foreground truncate">{item.url}</span>
+          </div>
+          <div className="flex items-center gap-2 text-xs">
+            <Key className="h-3.5 w-3.5 text-amber-500 shrink-0" />
+            <span className="font-mono text-muted-foreground">{item.maskedKey}</span>
+          </div>
+        </div>
+
+        {/* 下:5 个配置项(每个 20% 宽度)改为文本样式 */}
+        <div className="grid grid-cols-5 gap-2 text-[11px]">
+          <div className="min-w-0 text-center">
+            <div className="text-muted-foreground">TPM</div>
+            {canEdit ? (
+              <Popover open={showTpm} onOpenChange={handleTpmPopover}>
+                <PopoverTrigger asChild>
+                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                    <span>{formatTpmDisplay(tpmValue, tpmInfinite)}</span>
+                  </button>
+                </PopoverTrigger>
+                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
+                  <div className="mb-2 flex items-center justify-between text-[11px]">
+                    <span className="text-muted-foreground">TPM(令牌/分)</span>
+                    <div className="flex items-center gap-2 text-muted-foreground">
+                      <span>无限</span>
+                      <Switch checked={tpmInfinite} onCheckedChange={setTpmInfinite} aria-label="TPM无限" />
+                    </div>
+                  </div>
+                  <div className="flex items-center gap-3">
+                    <Slider
+                      min={PROVIDER_LIMITS.TPM.MIN / 1000}
+                      max={PROVIDER_LIMITS.TPM.MAX / 1000}
+                      step={1}
+                      value={[Math.round(tpmValue / 1000)]}
+                      onValueChange={(v) => !tpmInfinite && setTpmValue(Math.round((v?.[0] ?? PROVIDER_LIMITS.TPM.MIN / 1000) * 1000))}
+                      disabled={tpmInfinite}
+                    />
+                    <span className="w-12 text-right text-xs font-medium">{formatTpmDisplay(tpmValue, tpmInfinite)}</span>
+                  </div>
+                </PopoverContent>
+              </Popover>
+            ) : (
+              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
+                <span>{formatTpmDisplay(tpmValue, tpmInfinite)}</span>
+              </div>
+            )}
+          </div>
+
+          <div className="min-w-0 text-center">
+            <div className="text-muted-foreground">RPM</div>
+            {canEdit ? (
+              <Popover open={showRpm} onOpenChange={handleRpmPopover}>
+                <PopoverTrigger asChild>
+                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                    <span>{rpmInfinite ? "∞" : rpmValue.toLocaleString()}</span>
+                  </button>
+                </PopoverTrigger>
+                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
+                  <div className="mb-2 flex items-center justify-between text-[11px]">
+                    <span className="text-muted-foreground">RPM(请求/分)</span>
+                    <div className="flex items-center gap-2 text-muted-foreground">
+                      <span>无限</span>
+                      <Switch checked={rpmInfinite} onCheckedChange={setRpmInfinite} aria-label="RPM无限" />
+                    </div>
+                  </div>
+                  <div className="flex items-center gap-3">
+                    <Slider
+                      min={PROVIDER_LIMITS.RPM.MIN}
+                      max={PROVIDER_LIMITS.RPM.MAX}
+                      step={1}
+                      value={[rpmInfinite ? PROVIDER_LIMITS.RPM.MIN : rpmValue]}
+                      onValueChange={(v) => !rpmInfinite && setRpmValue(v?.[0] ?? PROVIDER_LIMITS.RPM.MIN)}
+                      disabled={rpmInfinite}
+                    />
+                    <span className="w-12 text-right text-xs font-medium">{rpmInfinite ? "∞" : rpmValue.toLocaleString()}</span>
+                  </div>
+                </PopoverContent>
+              </Popover>
+            ) : (
+              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
+                <span>{rpmInfinite ? "∞" : rpmValue.toLocaleString()}</span>
+              </div>
+            )}
+          </div>
+
+          <div className="min-w-0 text-center">
+            <div className="text-muted-foreground">RPD</div>
+            {canEdit ? (
+              <Popover open={showRpd} onOpenChange={handleRpdPopover}>
+                <PopoverTrigger asChild>
+                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                    <span>{rpdInfinite ? "∞" : rpdValue.toLocaleString()}</span>
+                  </button>
+                </PopoverTrigger>
+                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
+                  <div className="mb-2 flex items-center justify-between text-[11px]">
+                    <span className="text-muted-foreground">RPD(请求/日)</span>
+                    <div className="flex items-center gap-2 text-muted-foreground">
+                      <span>无限</span>
+                      <Switch checked={rpdInfinite} onCheckedChange={setRpdInfinite} aria-label="RPD无限" />
+                    </div>
+                  </div>
+                  <div className="flex items-center gap-3">
+                    <Slider
+                      min={PROVIDER_LIMITS.RPD.MIN}
+                      max={PROVIDER_LIMITS.RPD.MAX}
+                      step={1}
+                      value={[rpdInfinite ? PROVIDER_LIMITS.RPD.MIN : rpdValue]}
+                      onValueChange={(v) => !rpdInfinite && setRpdValue(v?.[0] ?? PROVIDER_LIMITS.RPD.MIN)}
+                      disabled={rpdInfinite}
+                    />
+                    <span className="w-12 text-right text-xs font-medium">{rpdInfinite ? "∞" : rpdValue.toLocaleString()}</span>
+                  </div>
+                </PopoverContent>
+              </Popover>
+            ) : (
+              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
+                <span>{rpdInfinite ? "∞" : rpdValue.toLocaleString()}</span>
+              </div>
+            )}
+          </div>
+
+          <div className="min-w-0 text-center">
+            <div className="text-muted-foreground">CC</div>
+            {canEdit ? (
+              <Popover open={showCc} onOpenChange={handleCcPopover}>
+                <PopoverTrigger asChild>
+                  <button type="button" className="w-full text-center font-medium tabular-nums truncate text-foreground hover:text-primary/80 transition-colors cursor-pointer">
+                    <span>{ccInfinite ? "∞" : ccValue.toLocaleString()}</span>
+                  </button>
+                </PopoverTrigger>
+                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-72 p-3">
+                  <div className="mb-2 flex items-center justify-between text-[11px]">
+                    <span className="text-muted-foreground">CC(并发)</span>
+                    <div className="flex items-center gap-2 text-muted-foreground">
+                      <span>无限</span>
+                      <Switch checked={ccInfinite} onCheckedChange={setCcInfinite} aria-label="无限" />
+                    </div>
+                  </div>
+                  <div className="flex items-center gap-3">
+                    <Slider
+                      min={PROVIDER_LIMITS.CC.MIN}
+                      max={PROVIDER_LIMITS.CC.MAX}
+                      step={1}
+                      value={[ccInfinite ? PROVIDER_LIMITS.CC.MIN : ccValue]}
+                      onValueChange={(v) => !ccInfinite && setCcValue(v?.[0] ?? PROVIDER_LIMITS.CC.MIN)}
+                      disabled={ccInfinite}
+                    />
+                    <span className="w-12 text-right text-xs font-medium">{ccInfinite ? "∞" : ccValue.toLocaleString()}</span>
+                  </div>
+                </PopoverContent>
+              </Popover>
+            ) : (
+              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
+                <span>{ccInfinite ? "∞" : ccValue.toLocaleString()}</span>
+              </div>
+            )}
+          </div>
+
+          <div className="min-w-0 text-center">
+            <div className="text-muted-foreground">权重</div>
+            {/* 权重编辑 - 仅管理员可编辑 */}
+            {canEdit ? (
+              <Popover open={showWeight} onOpenChange={handleWeightPopover}>
+                <PopoverTrigger asChild>
+                  <button
+                    type="button"
+                    aria-label="编辑权重"
+                    className="w-full text-center font-medium tabular-nums truncate text-foreground cursor-pointer hover:text-primary/80 transition-colors"
+                  >
+                    <span>{weight}</span>
+                  </button>
+                </PopoverTrigger>
+                <PopoverContent align="center" side="bottom" sideOffset={6} className="w-64 p-3">
+                  <div className="mb-2 flex items-center justify-between text-[11px] text-muted-foreground">
+                    <span>调整权重</span>
+                    <span className="font-medium text-foreground">{weight}</span>
+                  </div>
+                  <Slider min={PROVIDER_LIMITS.WEIGHT.MIN} max={PROVIDER_LIMITS.WEIGHT.MAX} step={1} value={[weight]} onValueChange={(v) => setWeight(v?.[0] ?? PROVIDER_LIMITS.WEIGHT.MIN)} />
+                </PopoverContent>
+              </Popover>
+            ) : (
+              <div className="w-full text-center font-medium tabular-nums truncate text-foreground">
+                <span>{weight}</span>
+              </div>
+            )}
+          </div>
+        </div>
+      </div>
+
+      <div className="flex items-center justify-between text-[11px] text-muted-foreground pt-2 border-t border-border/60">
+        <span>创建 {item.createdAt}</span>
+        <span>更新 {item.updatedAt}</span>
+      </div>
+    </div>
+  );
+}

+ 34 - 0
src/app/settings/providers/_components/provider-list.tsx

@@ -0,0 +1,34 @@
+"use client";
+import { Globe } from "lucide-react";
+import type { ProviderDisplay } from "@/types/provider";
+import type { User } from "@/types/user";
+import { ProviderListItem } from "./provider-list-item";
+
+interface ProviderListProps {
+  providers: ProviderDisplay[];
+  currentUser?: User;
+}
+
+export function ProviderList({ providers, currentUser }: ProviderListProps) {
+  if (providers.length === 0) {
+    return (
+      <div className="flex flex-col items-center justify-center py-12 px-4">
+        <div className="w-12 h-12 rounded-full bg-muted/50 flex items-center justify-center mb-3">
+          <Globe className="h-6 w-6 text-muted-foreground" />
+        </div>
+        <h3 className="font-medium text-foreground mb-1">暂无服务商配置</h3>
+        <p className="text-sm text-muted-foreground text-center">
+          添加你的第一个 API 服务商
+        </p>
+      </div>
+    );
+  }
+
+  return (
+    <div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-4">
+      {providers.map((provider) => (
+        <ProviderListItem key={provider.id} item={provider} currentUser={currentUser} />
+      ))}
+    </div>
+  );
+}

+ 19 - 0
src/app/settings/providers/_components/provider-manager.tsx

@@ -0,0 +1,19 @@
+"use client";
+import { ProviderList } from "./provider-list";
+import type { ProviderDisplay } from "@/types/provider";
+import type { User } from "@/types/user";
+
+interface ProviderManagerProps {
+  providers: ProviderDisplay[];
+  currentUser?: User;
+}
+
+export function ProviderManager({ providers, currentUser }: ProviderManagerProps) {
+  return (
+    <div className="space-y-4">
+      <ProviderList providers={providers} currentUser={currentUser} />
+    </div>
+  );
+}
+
+export type { ProviderDisplay } from "@/types/provider";

+ 32 - 0
src/app/settings/providers/page.tsx

@@ -0,0 +1,32 @@
+import { getProviders } from "@/actions/providers";
+import { Section } from "@/components/section";
+import { ProviderManager } from "./_components/provider-manager";
+import { AddProviderDialog } from "./_components/add-provider-dialog";
+import { SettingsPageHeader } from "../_components/settings-page-header";
+import { getSession } from "@/lib/auth";
+
+export const dynamic = "force-dynamic";
+
+export default async function SettingsProvidersPage() {
+  const [providers, session] = await Promise.all([
+    getProviders(),
+    getSession(),
+  ]);
+
+  return (
+    <>
+      <SettingsPageHeader
+        title="供应商管理"
+        description="配置 API 服务商并维护可用状态。"
+      />
+
+      <Section
+        title="服务商管理"
+        description="(TPM/RPM/RPD/CC 功能尚未实现,近期即将更新)"
+        actions={<AddProviderDialog />}
+      >
+        <ProviderManager providers={providers} currentUser={session?.user} />
+      </Section>
+    </>
+  );
+}

+ 33 - 0
src/app/usage-doc/layout.tsx

@@ -0,0 +1,33 @@
+import { Metadata } from 'next'
+import { getSession } from "@/lib/auth"
+import { DashboardHeader } from "../dashboard/_components/dashboard-header"
+
+export const metadata: Metadata = {
+  title: '使用文档 - Claude Code Hub',
+  description: 'Claude Code Hub API 代理服务使用文档和指南',
+}
+
+/**
+ * 文档页面布局
+ * 提供文档页面的容器、样式和共用头部
+ */
+export default async function UsageDocLayout({
+  children,
+}: {
+  children: React.ReactNode
+}) {
+  const session = await getSession()
+
+  return (
+    <div className="min-h-screen bg-background">
+      {/* 共用头部导航 */}
+      <DashboardHeader session={session} />
+
+      {/* 文档内容主体 */}
+      <main className="mx-auto w-full max-w-7xl px-6 py-8">
+        {/* 文档容器 */}
+        {children}
+      </main>
+    </div>
+  )
+}

+ 338 - 0
src/app/usage-doc/page.tsx

@@ -0,0 +1,338 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { cn } from '@/lib/utils'
+import { Skeleton } from '@/components/ui/skeleton'
+
+/**
+ * 文档目录项
+ */
+interface TocItem {
+  id: string
+  text: string
+  level: number
+}
+
+const headingClasses = {
+  h2: 'scroll-m-20 text-2xl font-semibold leading-snug text-foreground',
+  h3: 'scroll-m-20 mt-8 text-xl font-semibold leading-snug text-foreground',
+  h4: 'scroll-m-20 mt-6 text-lg font-semibold leading-snug text-foreground',
+} as const
+
+interface CodeBlockProps {
+  code: string
+  language: string
+}
+
+function CodeBlock({ code, language }: CodeBlockProps) {
+  return (
+    <pre
+      data-language={language}
+      className="group relative my-5 overflow-x-auto rounded-md bg-black px-4 py-5 font-mono text-[13px] text-white"
+    >
+      <code className="block whitespace-pre leading-relaxed">{code.trim()}</code>
+    </pre>
+  )
+}
+
+interface UsageDocContentProps {
+  origin: string
+}
+
+function UsageDocContent({ origin }: UsageDocContentProps) {
+  const resolvedOrigin = origin || '当前站点地址'
+
+  return (
+    <article className="space-y-12 text-[15px] leading-6 text-muted-foreground">
+
+      <section className="space-y-6">
+        <h2 id="quick-start" className={headingClasses.h2}>
+          🚀 快速开始
+        </h2>
+
+        <div className="space-y-4">
+          <h3 id="step-1-install" className={headingClasses.h3}>
+            第一步:安装 Claude Code
+          </h3>
+
+          <div className="space-y-3">
+            <h4 className={headingClasses.h4}>开发者(推荐)</h4>
+            <p>使用 npm 全局安装:</p>
+            <CodeBlock language="bash" code={`npm install -g @anthropic-ai/claude-code`} />
+          </div>
+
+          <div className="space-y-3">
+            <h4 className={headingClasses.h4}>非开发者</h4>
+            <p>使用一键安装脚本:</p>
+            <div className="space-y-2">
+              <p className="font-semibold text-foreground">macOS / Linux / WSL</p>
+              <CodeBlock language="bash" code={`curl -fsSL https://claude.ai/install.sh | bash`} />
+              <p className="font-semibold text-foreground">Windows PowerShell</p>
+              <CodeBlock language="powershell" code={`irm https://claude.ai/install.ps1 | iex`} />
+            </div>
+            <blockquote className="space-y-1 rounded-lg border-l-2 border-primary/50 bg-muted/40 px-4 py-3">
+              <p className="font-semibold text-foreground">提示</p>
+              <p>Windows 用户建议使用 WSL (Windows Subsystem for Linux) 以获得更好的体验</p>
+            </blockquote>
+          </div>
+        </div>
+
+        <div className="space-y-4">
+          <h3 id="step-2-config" className={headingClasses.h3}>
+            第二步:配置 API 密钥
+          </h3>
+          <div className="space-y-3">
+            <h4 className={headingClasses.h4}>1. 创建配置文件</h4>
+            <p>根据您的操作系统,在对应位置创建 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">settings.json</code> 文件:</p>
+            <div className="space-y-3">
+              <div>
+                <p className="font-semibold text-foreground">macOS / Linux</p>
+                <CodeBlock language="bash" code={`~/.claude/settings.json`} />
+              </div>
+              <div>
+                <p className="font-semibold text-foreground">Windows</p>
+                <CodeBlock language="powershell" code={`%USERPROFILE%\\.claude\\settings.json`} />
+              </div>
+            </div>
+          </div>
+
+          <div className="space-y-3">
+            <h4 className={headingClasses.h4}>2. 添加配置内容</h4>
+            <p>将以下配置复制到 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">settings.json</code> 文件中:</p>
+            <CodeBlock
+              language="json"
+              code={`{
+  "env": {
+    "ANTHROPIC_AUTH_TOKEN": "your-api-key-here",
+    "ANTHROPIC_BASE_URL": "${resolvedOrigin}",
+    "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": 1
+  },
+  "permissions": {
+    "allow": [],
+    "deny": []
+  },
+  "apiKeyHelper": "echo 'your-api-key-here'"
+}`}
+            />
+          </div>
+
+          <div className="space-y-3">
+            <h4 className={headingClasses.h4}>3. 替换 API 密钥</h4>
+            <blockquote className="space-y-2 rounded-lg border-l-2 border-primary/50 bg-muted/40 px-4 py-3">
+              <p className="font-semibold text-foreground">重要</p>
+              <p>请将配置中的 <code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">your-api-key-here</code> 替换为您的实际 API 密钥。</p>
+              <p>密钥获取方式:登录控制台 → API 密钥管理 → 创建 / 查看密钥。</p>
+            </blockquote>
+          </div>
+        </div>
+
+        <div className="space-y-4">
+          <h3 id="step-3-start" className={headingClasses.h3}>
+            第三步:开始使用
+          </h3>
+          <ol className="list-decimal space-y-2 pl-6">
+            <li>打开终端 / 命令行</li>
+            <li>进入您的项目目录</li>
+            <li>输入以下命令启动 Claude Code:</li>
+          </ol>
+          <CodeBlock language="bash" code={`claude`} />
+          <p>现在您可以开始使用 Claude Code 辅助开发了!</p>
+        </div>
+      </section>
+
+      <hr className="border-border/60" />
+
+      <section className="space-y-4">
+        <h2 id="common-commands" className={headingClasses.h2}>
+          📚 常用命令
+        </h2>
+        <p>启动 Claude Code 后,您可以使用以下常用命令:</p>
+        <ul className="list-disc space-y-2 pl-6">
+          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/help</code> - 查看帮助信息</li>
+          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/clear</code> - 清空对话历史,并开启新的对话</li>
+          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/compact</code> - 总结当前对话</li>
+          <li><code className="rounded bg-muted px-1 py-0.5 text-xs text-foreground">/cost</code> - 查看当前对话已经使用的金额</li>
+          <li>
+            ... 其他更多命令查看
+            <a
+              href="https://docs.claude.com/zh-CN/docs/claude-code/overview"
+              target="_blank"
+              rel="noopener noreferrer"
+              className="font-medium text-primary underline underline-offset-4 transition-colors hover:text-primary/80"
+            >
+              官方文档
+            </a>
+          </li>
+        </ul>
+      </section>
+
+      <section className="space-y-4">
+        <h2 id="troubleshooting" className={headingClasses.h2}>
+          🔍 故障排查
+        </h2>
+        <h3 className={headingClasses.h3}>常见问题</h3>
+
+        <div className="space-y-3">
+          <p className="font-semibold text-foreground">1. 安装失败</p>
+          <ul className="list-disc space-y-2 pl-6">
+            <li>检查网络连接是否正常</li>
+            <li>确保有管理员权限(Windows)或使用 sudo(macOS / Linux)</li>
+            <li>尝试使用代理或镜像源</li>
+          </ul>
+        </div>
+
+        <div className="space-y-3">
+          <p className="font-semibold text-foreground">2. API 密钥无效</p>
+          <ul className="list-disc space-y-2 pl-6">
+            <li>确认密钥已正确复制(无多余空格)</li>
+            <li>检查密钥是否在有效期内</li>
+            <li>验证账户权限是否正常</li>
+          </ul>
+        </div>
+      </section>
+    </article>
+  )
+}
+
+/**
+ * 文档页面
+ * 使用客户端组件渲染静态文档内容,并提供目录导航
+ */
+export default function UsageDocPage() {
+  const [activeId, setActiveId] = useState<string>('')
+  const [tocItems, setTocItems] = useState<TocItem[]>([])
+  const [tocReady, setTocReady] = useState(false)
+  const [serviceOrigin, setServiceOrigin] = useState(() =>
+    (typeof window !== 'undefined' && window.location.origin) || ''
+  )
+
+  useEffect(() => {
+    setServiceOrigin(window.location.origin)
+  }, [])
+
+  // 生成目录并监听滚动
+  useEffect(() => {
+    // 获取所有标题
+    const headings = document.querySelectorAll('h2, h3')
+    const items: TocItem[] = []
+
+    headings.forEach((heading) => {
+      // 为标题添加 id(如果没有的话)
+      if (!heading.id) {
+        heading.id = heading.textContent?.toLowerCase().replace(/\s+/g, '-') || ''
+      }
+
+      items.push({
+        id: heading.id,
+        text: heading.textContent || '',
+        level: parseInt(heading.tagName[1])
+      })
+    })
+
+    setTocItems(items)
+    setTocReady(true)
+
+    // 监听滚动,高亮当前章节
+    const handleScroll = () => {
+      const scrollPosition = window.scrollY + 100
+
+      for (const item of items) {
+        const element = document.getElementById(item.id)
+        if (element && element.offsetTop <= scrollPosition) {
+          setActiveId(item.id)
+        }
+      }
+    }
+
+    window.addEventListener('scroll', handleScroll)
+    handleScroll() // 初始化
+
+    return () => window.removeEventListener('scroll', handleScroll)
+  }, [])
+
+  // 点击目录项滚动到对应位置
+  const scrollToSection = (id: string) => {
+    const element = document.getElementById(id)
+    if (element) {
+      const offsetTop = element.offsetTop - 80
+      window.scrollTo({
+        top: offsetTop,
+        behavior: 'smooth'
+      })
+    }
+  }
+
+  return (
+    <div className="relative flex gap-8">
+      {/* 左侧主文档 */}
+      <div className="flex-1">
+        {/* 背景装饰 */}
+        <div className="absolute inset-0 -z-10 mx-auto max-w-7xl">
+          <div className="absolute left-1/2 top-0 -translate-x-1/2 w-[200%] h-48 bg-gradient-to-b from-primary/5 to-transparent rounded-[100%] blur-3xl" />
+        </div>
+
+        {/* 文档容器 */}
+        <div className="relative bg-card rounded-xl shadow-sm border p-8 md:p-12">
+          {/* 文档内容 */}
+          <UsageDocContent origin={serviceOrigin} />
+        </div>
+
+        {/* 页脚信息 */}
+        <div className="mt-12 text-center text-sm text-muted-foreground">
+          <p>文档持续更新中 • 最后更新: {new Date().toLocaleDateString('zh-CN')}</p>
+        </div>
+      </div>
+
+      {/* 右侧目录导航 */}
+      <aside className="hidden lg:block w-64 shrink-0">
+        <div className="sticky top-24 space-y-4">
+          <div className="bg-card rounded-lg border p-4">
+            <h4 className="font-semibold text-sm mb-3">本页导航</h4>
+            <nav className="space-y-1">
+              {!tocReady && (
+                <div className="space-y-2">
+                  {Array.from({ length: 5 }).map((_, index) => (
+                    <Skeleton key={index} className="h-5 w-full" />
+                  ))}
+                </div>
+              )}
+              {tocReady && tocItems.length === 0 && (
+                <p className="text-xs text-muted-foreground">本页暂无可用章节</p>
+              )}
+              {tocReady && tocItems.length > 0 &&
+                tocItems.map((item) => (
+                  <button
+                    key={item.id}
+                    onClick={() => scrollToSection(item.id)}
+                    className={cn(
+                      "block w-full text-left text-sm px-3 py-1.5 rounded-md transition-colors",
+                      item.level === 3 && "pl-6 text-xs",
+                      activeId === item.id
+                        ? "bg-primary/10 text-primary font-medium"
+                        : "text-muted-foreground hover:text-foreground hover:bg-muted"
+                    )}
+                  >
+                    {item.text}
+                  </button>
+                ))}
+            </nav>
+          </div>
+
+          {/* 快速操作 */}
+          <div className="bg-card rounded-lg border p-4">
+            <h4 className="font-semibold text-sm mb-3">快速链接</h4>
+            <div className="space-y-2">
+              <a href="/dashboard" className="block text-sm text-muted-foreground hover:text-primary transition-colors">
+                返回仪表盘
+              </a>
+              <a href="#" onClick={(e) => { e.preventDefault(); window.scrollTo({ top: 0, behavior: 'smooth' }) }}
+                 className="block text-sm text-muted-foreground hover:text-primary transition-colors">
+                回到顶部
+              </a>
+            </div>
+          </div>
+        </div>
+      </aside>
+    </div>
+  )
+}

+ 18 - 0
src/app/v1/[...route]/route.ts

@@ -0,0 +1,18 @@
+import { Hono } from "hono";
+import { handle } from "hono/vercel";
+import { handleProxyRequest } from "@/app/v1/_lib/proxy-handler";
+
+export const runtime = "nodejs";
+
+const app = new Hono().basePath("/v1");
+
+// 所有请求都通过代理处理器处理
+app.all("*", handleProxyRequest);
+
+export const GET = handle(app);
+export const POST = handle(app);
+export const PUT = handle(app);
+export const DELETE = handle(app);
+export const PATCH = handle(app);
+export const OPTIONS = handle(app);
+export const HEAD = handle(app);

+ 96 - 0
src/app/v1/_lib/headers.ts

@@ -0,0 +1,96 @@
+/**
+ * Header 处理器配置
+ */
+export interface HeaderProcessorConfig {
+  /** 需要删除的 header 黑名单 */
+  blacklist?: string[];
+  /** 需要设置/替换的 headers */
+  overrides?: Record<string, string>;
+  /** 是否保留原始 authorization(默认 false) */
+  preserveAuthorization?: boolean;
+}
+
+/**
+ * 代理请求 Header 处理器
+ */
+export class HeaderProcessor {
+  private blacklist: Set<string>;
+  private overrides: Map<string, string>;
+
+  constructor(config: HeaderProcessorConfig = {}) {
+    // 初始化黑名单(默认包含代理相关的 headers)
+    const defaultBlacklist = [
+      'x-forwarded-for',
+      'x-forwarded-host', 
+      'x-forwarded-port',
+      'x-forwarded-proto',
+    ];
+    
+    // 如果不保留 authorization,添加到黑名单
+    if (!config.preserveAuthorization) {
+      defaultBlacklist.push('authorization');
+    }
+    
+    this.blacklist = new Set(
+      [...defaultBlacklist, ...(config.blacklist || [])].map(h => h.toLowerCase())
+    );
+
+    // 初始化覆盖规则
+    this.overrides = new Map(
+      Object.entries(config.overrides || {}).map(([k, v]) => [k.toLowerCase(), v])
+    );
+  }
+
+  /**
+   * 处理 Headers 对象
+   */
+  process(headers: Headers): Headers {
+    const processed = new Headers();
+
+    // 第一步:根据黑名单过滤,默认全部透传
+    headers.forEach((value, key) => {
+      const lowerKey = key.toLowerCase();
+      
+      // 检查黑名单
+      if (this.blacklist.has(lowerKey)) {
+        return; // 跳过黑名单 header
+      }
+      
+      // 保留这个 header
+      processed.set(key, value);
+    });
+
+    // 第二步:应用覆盖规则
+    this.overrides.forEach((value, key) => {
+      processed.set(key, value);
+    });
+
+    return processed;
+  }
+
+  /**
+   * 从 baseUrl 提取 host
+   */
+  static extractHost(baseUrl: string): string {
+    try {
+      const url = new URL(baseUrl);
+      return url.host;
+    } catch (error) {
+      console.error("提取 host 失败:", error);
+      const match = baseUrl.match(/^https?:\/\/([^\/]+)/);
+      return match ? match[1] : 'localhost';
+    }
+  }
+
+  /**
+   * 创建预配置的代理处理器
+   */
+  static createForProxy(config?: HeaderProcessorConfig): HeaderProcessor {
+    // 默认的代理配置:删除常见的转发相关 headers
+    return new HeaderProcessor({
+      preserveAuthorization: false,
+      ...config
+    });
+  }
+}
+

+ 32 - 0
src/app/v1/_lib/proxy-handler.ts

@@ -0,0 +1,32 @@
+import type { Context } from "hono";
+import { ProxySession } from "./proxy/session";
+import { ProxyAuthenticator } from "./proxy/auth-guard";
+import { ProxyProviderResolver } from "./proxy/provider-selector";
+import { ProxyMessageService } from "./proxy/message-service";
+import { ProxyForwarder } from "./proxy/forwarder";
+import { ProxyResponseHandler } from "./proxy/response-handler";
+import { ProxyErrorHandler } from "./proxy/error-handler";
+
+export async function handleProxyRequest(c: Context): Promise<Response> {
+  const session = await ProxySession.fromContext(c);
+
+  try {
+    const unauthorized = await ProxyAuthenticator.ensure(session);
+    if (unauthorized) {
+      return unauthorized;
+    }
+
+    const providerUnavailable = await ProxyProviderResolver.ensure(session);
+    if (providerUnavailable) {
+      return providerUnavailable;
+    }
+
+    await ProxyMessageService.ensureContext(session);
+
+    const response = await ProxyForwarder.send(session);
+    return await ProxyResponseHandler.dispatch(session, response);
+  } catch (error) {
+    console.error("Proxy handler error:", error);
+    return await ProxyErrorHandler.handle(session, error);
+  }
+}

+ 79 - 0
src/app/v1/_lib/proxy/auth-guard.ts

@@ -0,0 +1,79 @@
+import { validateApiKeyAndGetUser } from "@/repository/key";
+import { ProxyLogger } from "./logger";
+import { ProxyResponses } from "./responses";
+import type { ProxySession, AuthState } from "./session";
+
+export class ProxyAuthenticator {
+  static async ensure(session: ProxySession): Promise<Response | null> {
+    const authHeader = session.headers.get("authorization") ?? undefined;
+    const apiKeyHeader = session.headers.get("x-api-key") ?? undefined;
+    const authState = await ProxyAuthenticator.validate({ authHeader, apiKeyHeader });
+    session.setAuthState(authState);
+
+    if (authState.success) {
+      return null;
+    }
+
+    await ProxyLogger.logFailure(session, new Error("Authorization failed"));
+    return ProxyResponses.buildError(401, "令牌已过期或验证不正确");
+  }
+
+  private static async validate(headers: {
+    authHeader?: string;
+    apiKeyHeader?: string;
+  }): Promise<AuthState> {
+    const bearerKey = ProxyAuthenticator.extractKeyFromAuthorization(headers.authHeader);
+    const apiKeyHeader = ProxyAuthenticator.normalizeKey(headers.apiKeyHeader);
+
+    const providedKeys = [bearerKey, apiKeyHeader].filter(
+      (value): value is string => typeof value === "string" && value.length > 0
+    );
+
+    if (providedKeys.length === 0) {
+      return { user: null, apiKey: null, success: false };
+    }
+
+    const [firstKey] = providedKeys;
+    const hasMismatch = providedKeys.some((key) => key !== firstKey);
+
+    if (hasMismatch) {
+      return { user: null, apiKey: null, success: false };
+    }
+
+    const apiKey = firstKey;
+    const authResult = await validateApiKeyAndGetUser(apiKey);
+
+    if (!authResult) {
+      return { user: null, apiKey, success: false };
+    }
+
+    return { user: authResult.user, apiKey, success: true };
+  }
+
+  private static extractKeyFromAuthorization(authHeader?: string): string | null {
+    if (!authHeader) {
+      return null;
+    }
+
+    const trimmed = authHeader.trim();
+    if (!trimmed) {
+      return null;
+    }
+
+    const match = /^Bearer\s+(.+)$/i.exec(trimmed);
+    if (!match) {
+      return null;
+    }
+
+    return match[1]?.trim() ?? null;
+  }
+
+  private static normalizeKey(value?: string): string | null {
+    if (!value) {
+      return null;
+    }
+
+    const trimmed = value.trim();
+    return trimmed.length > 0 ? trimmed : null;
+  }
+}

+ 18 - 0
src/app/v1/_lib/proxy/error-handler.ts

@@ -0,0 +1,18 @@
+import { updateMessageRequestDuration } from "@/repository/message";
+import { ProxyLogger } from "./logger";
+import { ProxyResponses } from "./responses";
+import type { ProxySession } from "./session";
+
+export class ProxyErrorHandler {
+  static async handle(session: ProxySession, error: unknown): Promise<Response> {
+    if (session.messageContext) {
+      const duration = Date.now() - session.startTime;
+      await updateMessageRequestDuration(session.messageContext.id, duration);
+    }
+
+    await ProxyLogger.logFailure(session, error);
+
+    const fallbackMessage = error instanceof Error ? error.message : "代理请求发生未知错误";
+    return ProxyResponses.buildError(500, fallbackMessage);
+  }
+}

+ 46 - 0
src/app/v1/_lib/proxy/forwarder.ts

@@ -0,0 +1,46 @@
+import { HeaderProcessor } from "../headers";
+import { buildProxyUrl } from "../url";
+import type { ProxySession } from "./session";
+
+export class ProxyForwarder {
+  static async send(session: ProxySession): Promise<Response> {
+    if (!session.provider || !session.authState) {
+      throw new Error("代理上下文缺少供应商或鉴权信息");
+    }
+
+    const processedHeaders = ProxyForwarder.buildHeaders(session);
+    const proxyUrl = buildProxyUrl(session.provider.url, session.requestUrl);
+
+    const hasBody = session.method !== "GET" && session.method !== "HEAD";
+    const init: RequestInit = {
+      method: session.method,
+      headers: processedHeaders,
+      ...(hasBody && session.request.buffer ? { body: session.request.buffer } : {})
+    };
+
+    (init as Record<string, unknown>).verbose = true;
+
+    return await fetch(proxyUrl, init);
+  }
+
+  private static buildHeaders(session: ProxySession): Headers {
+    const authState = session.authState;
+    const provider = session.provider;
+
+    if (!authState || !provider) {
+      return new Headers(session.headers);
+    }
+
+    const outboundKey = authState.success ? provider.key : "000";
+    const headerProcessor = HeaderProcessor.createForProxy({
+      blacklist: [],
+      overrides: {
+        "host": HeaderProcessor.extractHost(provider.url),
+        "authorization": `Bearer ${outboundKey}`,
+        "x-api-key": outboundKey
+      }
+    });
+
+    return headerProcessor.process(session.headers);
+  }
+}

+ 86 - 0
src/app/v1/_lib/proxy/logger.ts

@@ -0,0 +1,86 @@
+import { mkdir, writeFile } from "fs/promises";
+import { join } from "path";
+import { isDevelopment } from "@/lib/config/env.schema";
+import type { Provider } from "@/types/provider";
+import type { ProxySession } from "./session";
+
+export class ProxyLogger {
+  static async logNonStream(session: ProxySession, provider: Provider, responseBody: string): Promise<void> {
+    if (!isDevelopment()) {
+      return;
+    }
+
+    const timestamp = new Date();
+    const fileName = ProxyLogger.buildFileName(timestamp, "nonstream");
+    const outDir = join(process.cwd(), "out", "api");
+    const filePath = join(outDir, fileName);
+
+    const requestLogHeader = session.request.note ? `${session.request.note}\n` : "";
+    const requestLogBody = session.request.log || "(empty)";
+    const logContent = `=== Non-Stream API Call ${fileName} ===\n` +
+      `User: ${session.userName}\n` +
+      `Provider: ${provider.name} (${provider.url})\n` +
+      `Timestamp: ${timestamp.toISOString()}\n\n` +
+      "=== Request Headers ===\n" +
+      `${session.headerLog}\n\n` +
+      "=== Request Body ===\n" +
+      `${requestLogHeader}${requestLogBody}\n\n` +
+      "=== Response Body ===\n" +
+      `${responseBody}\n\n` +
+      "=== End ===\n";
+
+    try {
+      await mkdir(outDir, { recursive: true });
+      await writeFile(filePath, logContent, "utf-8");
+      console.log(`[${session.userName}] Non-stream API response saved to: out/api/${fileName}`);
+    } catch (error) {
+      console.error("Failed to save non-stream log file:", error);
+    }
+  }
+
+  static async logFailure(session: ProxySession, error: unknown): Promise<void> {
+    if (!isDevelopment()) {
+      return;
+    }
+
+    const timestamp = new Date();
+    const fileName = ProxyLogger.buildFileName(timestamp, "failure");
+    const outDir = join(process.cwd(), "out", "failures");
+    const filePath = join(outDir, fileName);
+
+    const errorMessage = error instanceof Error ? error.message : String(error);
+    const errorStack = error instanceof Error && error.stack ? error.stack : "(no stack available)";
+    const providerDisplay = session.provider
+      ? `${session.provider.name}${session.provider.url ? ` (${session.provider.url})` : ""}`
+      : "unavailable";
+    const requestLogHeader = session.request.note ? `${session.request.note}\n` : "";
+    const logContent = `=== Proxy Failure ${fileName} ===\n` +
+      `Timestamp: ${timestamp.toISOString()}\n` +
+      `Request: ${session.method} ${session.requestUrl.toString()}\n` +
+      `User: ${session.userName}\n` +
+      `Provider: ${providerDisplay}\n\n` +
+      `Error Message: ${errorMessage}\n` +
+      `Error Stack:\n${errorStack}\n\n` +
+      "=== Request Headers ===\n" +
+      `${session.headerLog}\n\n` +
+      "=== Request Body ===\n" +
+      `${requestLogHeader}${session.request.log || "(empty)"}\n\n` +
+      "=== End ===\n";
+
+    try {
+      await mkdir(outDir, { recursive: true });
+      await writeFile(filePath, logContent, "utf-8");
+      console.log(`[${session.userName}] Proxy failure saved to: out/failures/${fileName}`);
+    } catch (loggingError) {
+      console.error("Failed to save failure log file:", loggingError);
+    }
+  }
+
+  private static buildFileName(timestamp: Date, suffix: "nonstream" | "failure"): string {
+    const dateStr = String(timestamp.getMonth() + 1).padStart(2, "0") + "-" + String(timestamp.getDate()).padStart(2, "0");
+    const timeStr = [timestamp.getHours(), timestamp.getMinutes(), timestamp.getSeconds()]
+      .map((value) => String(value).padStart(2, "0"))
+      .join("-");
+    return `${dateStr}_${timeStr}_${suffix}.txt`;
+  }
+}

+ 27 - 0
src/app/v1/_lib/proxy/message-service.ts

@@ -0,0 +1,27 @@
+import { createMessageRequest } from "@/repository/message";
+import type { ProxySession } from "./session";
+
+export class ProxyMessageService {
+  static async ensureContext(session: ProxySession): Promise<void> {
+    const authState = session.authState;
+    const provider = session.provider;
+
+    if (!authState || !authState.success || !authState.user || !authState.apiKey || !provider) {
+      session.setMessageContext(null);
+      return;
+    }
+
+    const messageRequest = await createMessageRequest({
+      provider_id: provider.id,
+      user_id: authState.user.id,
+      key: authState.apiKey,
+      message: session.request.message
+    });
+
+    session.setMessageContext({
+      id: messageRequest.id,
+      user: authState.user,
+      apiKey: authState.apiKey
+    });
+  }
+}

+ 81 - 0
src/app/v1/_lib/proxy/provider-selector.ts

@@ -0,0 +1,81 @@
+import type { Provider } from "@/types/provider";
+import { findProviderList, findProviderById } from "@/repository/provider";
+import { findLatestMessageRequestByKey } from "@/repository/message";
+import { ProxyLogger } from "./logger";
+import { ProxyResponses } from "./responses";
+import type { ProxySession } from "./session";
+
+export class ProxyProviderResolver {
+  static async ensure(session: ProxySession): Promise<Response | null> {
+    session.setProvider(await ProxyProviderResolver.findReusable(session));
+
+    if (!session.provider) {
+      session.setProvider(await ProxyProviderResolver.pickRandomProvider());
+    }
+
+    if (session.provider) {
+      return null;
+    }
+
+    const status = 503;
+    const message = "暂无可用的上游服务";
+    console.error("No available providers");
+    await ProxyLogger.logFailure(session, new Error(message));
+    return ProxyResponses.buildError(status, message);
+  }
+
+  private static async findReusable(session: ProxySession): Promise<Provider | null> {
+    if (!session.shouldReuseProvider()) {
+      return null;
+    }
+
+    const apiKey = session.authState?.apiKey;
+    if (!apiKey) {
+      return null;
+    }
+
+    const latestRequest = await findLatestMessageRequestByKey(apiKey);
+    if (!latestRequest?.providerId) {
+      return null;
+    }
+
+    const provider = await findProviderById(latestRequest.providerId);
+    if (!provider || !provider.isEnabled) {
+      return null;
+    }
+
+    return provider;
+  }
+
+  private static async pickRandomProvider(): Promise<Provider | null> {
+    const providers = await findProviderList();
+    const enabledProviders = providers.filter((provider) => provider.isEnabled);
+
+    if (enabledProviders.length === 0) {
+      return null;
+    }
+
+    if (enabledProviders.length === 1) {
+      return enabledProviders[0];
+    }
+
+    const totalWeight = enabledProviders.reduce((sum, provider) => sum + provider.weight, 0);
+
+    if (totalWeight === 0) {
+      const randomIndex = Math.floor(Math.random() * enabledProviders.length);
+      return enabledProviders[randomIndex];
+    }
+
+    const random = Math.random() * totalWeight;
+    let cumulativeWeight = 0;
+
+    for (const provider of enabledProviders) {
+      cumulativeWeight += provider.weight;
+      if (random < cumulativeWeight) {
+        return provider;
+      }
+    }
+
+    return enabledProviders[enabledProviders.length - 1];
+  }
+}

+ 184 - 0
src/app/v1/_lib/proxy/response-handler.ts

@@ -0,0 +1,184 @@
+import { updateMessageRequestDuration, updateMessageRequestCost } from "@/repository/message";
+import { findLatestPriceByModel } from "@/repository/model-price";
+import { parseSSEData } from "@/lib/utils/sse";
+import { calculateRequestCost } from "@/lib/utils/cost-calculation";
+import type { ProxySession } from "./session";
+import { ProxyLogger } from "./logger";
+
+export type UsageMetrics = {
+  input_tokens?: number;
+  output_tokens?: number;
+  cache_creation_input_tokens?: number;
+  cache_read_input_tokens?: number;
+};
+
+export class ProxyResponseHandler {
+  static async dispatch(session: ProxySession, response: Response): Promise<Response> {
+    const contentType = response.headers.get("content-type") || "";
+    const isSSE = contentType.includes("text/event-stream");
+
+    if (!isSSE) {
+      return ProxyResponseHandler.handleNonStream(session, response);
+    }
+
+    return await ProxyResponseHandler.handleStream(session, response);
+  }
+
+  private static handleNonStream(session: ProxySession, response: Response): Response {
+    const provider = session.provider;
+    if (!provider) {
+      return response;
+    }
+
+    const responseForLog = response.clone();
+
+    void (async () => {
+      try {
+        const responseText = await responseForLog.text();
+        let responseLogContent = responseText;
+        let usageRecord: Record<string, unknown> | null = null;
+        let usageMetrics: UsageMetrics | null = null;
+
+        try {
+          const parsed = JSON.parse(responseText) as Record<string, unknown>;
+          responseLogContent = JSON.stringify(parsed, null, 2);
+          const usageValue = parsed.usage;
+          if (usageValue && typeof usageValue === "object") {
+            usageRecord = usageValue as Record<string, unknown>;
+            usageMetrics = extractUsageMetrics(usageValue);
+          }
+        } catch {
+          // 非 JSON 响应时保持原始日志
+        }
+
+        const messageContext = session.messageContext;
+        if (usageRecord && usageMetrics && messageContext) {
+          await updateRequestCostFromUsage(messageContext.id, session.request.model, usageMetrics);
+        }
+
+        if (messageContext) {
+          const duration = Date.now() - session.startTime;
+          await updateMessageRequestDuration(messageContext.id, duration);
+        }
+
+        await ProxyLogger.logNonStream(session, provider, responseLogContent);
+      } catch (error) {
+        console.error("Failed to handle non-stream log:", error);
+      }
+    })();
+
+    return response;
+  }
+
+  private static async handleStream(session: ProxySession, response: Response): Promise<Response> {
+    const messageContext = session.messageContext;
+    const provider = session.provider;
+
+    if (!messageContext || !provider || !response.body) {
+      return response;
+    }
+
+    const [clientStream, internalStream] = response.body.tee();
+
+    void (async () => {
+      const reader = internalStream.getReader();
+      const decoder = new TextDecoder();
+      const chunks: string[] = [];
+      let usageForCost: UsageMetrics | null = null;
+
+      try {
+        while (true) {
+          const { value, done } = await reader.read();
+          if (done) {
+            break;
+          }
+          if (value) {
+            chunks.push(decoder.decode(value, { stream: true }));
+          }
+        }
+
+        const flushed = decoder.decode();
+        if (flushed) {
+          chunks.push(flushed);
+        }
+
+        const allContent = chunks.join("");
+        const parsedEvents = parseSSEData(allContent);
+
+        const duration = Date.now() - session.startTime;
+        await updateMessageRequestDuration(messageContext.id, duration);
+
+        for (const event of parsedEvents) {
+          if (event.event === "message_delta" && typeof event.data === "object" && event.data !== null) {
+            const usageMetrics = extractUsageMetrics((event.data as Record<string, unknown>).usage);
+            if (usageMetrics) {
+              usageForCost = usageMetrics;
+            }
+          }
+        }
+
+        await updateRequestCostFromUsage(messageContext.id, session.request.model, usageForCost);
+      } catch (error) {
+        console.error("Failed to save SSE content:", error);
+      } finally {
+        reader.releaseLock();
+      }
+    })();
+
+    return new Response(clientStream, {
+      status: response.status,
+      statusText: response.statusText,
+      headers: new Headers(response.headers)
+    });
+  }
+}
+
+function extractUsageMetrics(value: unknown): UsageMetrics | null {
+  if (!value || typeof value !== "object") {
+    return null;
+  }
+
+  const usage = value as Record<string, unknown>;
+  const result: UsageMetrics = {};
+  let hasAny = false;
+
+  if (typeof usage.input_tokens === "number") {
+    result.input_tokens = usage.input_tokens;
+    hasAny = true;
+  }
+
+  if (typeof usage.output_tokens === "number") {
+    result.output_tokens = usage.output_tokens;
+    hasAny = true;
+  }
+
+  if (typeof usage.cache_creation_input_tokens === "number") {
+    result.cache_creation_input_tokens = usage.cache_creation_input_tokens;
+    hasAny = true;
+  }
+
+  if (typeof usage.cache_read_input_tokens === "number") {
+    result.cache_read_input_tokens = usage.cache_read_input_tokens;
+    hasAny = true;
+  }
+
+  return hasAny ? result : null;
+}
+
+async function updateRequestCostFromUsage(
+  messageId: number,
+  modelName: string | null,
+  usage: UsageMetrics | null
+): Promise<void> {
+  if (!modelName || !usage) {
+    return;
+  }
+
+  const priceData = await findLatestPriceByModel(modelName);
+  if (priceData?.priceData) {
+    const cost = calculateRequestCost(usage, priceData.priceData);
+    if (cost > 0) {
+      await updateMessageRequestCost(messageId, cost);
+    }
+  }
+}

+ 17 - 0
src/app/v1/_lib/proxy/responses.ts

@@ -0,0 +1,17 @@
+export class ProxyResponses {
+  static buildError(status: number, message: string): Response {
+    const payload = {
+      error: {
+        message,
+        type: String(status)
+      }
+    };
+
+    return new Response(JSON.stringify(payload), {
+      status,
+      headers: {
+        "content-type": "application/json; charset=utf-8"
+      }
+    });
+  }
+}

+ 163 - 0
src/app/v1/_lib/proxy/session.ts

@@ -0,0 +1,163 @@
+import type { Context } from "hono";
+import type { Provider } from "@/types/provider";
+import type { User } from "@/types/user";
+
+export interface AuthState {
+  user: User | null;
+  apiKey: string | null;
+  success: boolean;
+}
+
+export interface MessageContext {
+  id: number;
+  user: User;
+  apiKey: string;
+}
+
+export interface ProxyRequestPayload {
+  message: Record<string, unknown>;
+  buffer?: ArrayBuffer;
+  log: string;
+  note?: string;
+  model: string | null;
+}
+
+interface RequestBodyResult {
+  requestMessage: Record<string, unknown>;
+  requestBodyLog: string;
+  requestBodyLogNote?: string;
+  requestBodyBuffer?: ArrayBuffer;
+}
+
+export class ProxySession {
+  readonly startTime: number;
+  readonly method: string;
+  readonly requestUrl: URL;
+  readonly headers: Headers;
+  readonly headerLog: string;
+  readonly request: ProxyRequestPayload;
+  userName: string;
+  authState: AuthState | null;
+  provider: Provider | null;
+  messageContext: MessageContext | null;
+
+  private constructor(init: {
+    startTime: number;
+    method: string;
+    requestUrl: URL;
+    headers: Headers;
+    headerLog: string;
+    request: ProxyRequestPayload;
+  }) {
+    this.startTime = init.startTime;
+    this.method = init.method;
+    this.requestUrl = init.requestUrl;
+    this.headers = init.headers;
+    this.headerLog = init.headerLog;
+    this.request = init.request;
+    this.userName = "unknown";
+    this.authState = null;
+    this.provider = null;
+    this.messageContext = null;
+  }
+
+  static async fromContext(c: Context): Promise<ProxySession> {
+    const startTime = Date.now();
+    const method = c.req.method.toUpperCase();
+    const requestUrl = new URL(c.req.url);
+    const headers = new Headers(c.req.header());
+    const headerLog = formatHeadersForLog(headers);
+    const bodyResult = await parseRequestBody(c);
+
+    const request: ProxyRequestPayload = {
+      message: bodyResult.requestMessage,
+      buffer: bodyResult.requestBodyBuffer,
+      log: bodyResult.requestBodyLog,
+      note: bodyResult.requestBodyLogNote,
+      model: typeof bodyResult.requestMessage.model === "string" ? bodyResult.requestMessage.model : null
+    };
+
+    return new ProxySession({ startTime, method, requestUrl, headers, headerLog, request });
+  }
+
+  setAuthState(state: AuthState): void {
+    this.authState = state;
+    if (state.user) {
+      this.userName = state.user.name;
+    }
+  }
+
+  setProvider(provider: Provider | null): void {
+    this.provider = provider;
+  }
+
+  setMessageContext(context: MessageContext | null): void {
+    this.messageContext = context;
+    if (context?.user) {
+      this.userName = context.user.name;
+    }
+  }
+
+  shouldReuseProvider(): boolean {
+    const messages = (this.request.message as Record<string, unknown>).messages;
+    return Array.isArray(messages) && messages.length > 1;
+  }
+}
+
+function formatHeadersForLog(headers: Headers): string {
+  const collected: string[] = [];
+  headers.forEach((value, key) => {
+    collected.push(`${key}: ${value}`);
+  });
+
+  return collected.length > 0 ? collected.join("\n") : "(empty)";
+}
+
+function optimizeRequestMessage(message: Record<string, unknown>): Record<string, unknown> {
+  const optimized = { ...message };
+
+  if (Array.isArray(optimized.system)) {
+    optimized.system = new Array(optimized.system.length).fill(0);
+  }
+  if (Array.isArray(optimized.messages)) {
+    optimized.messages = new Array(optimized.messages.length).fill(0);
+  }
+  if (Array.isArray(optimized.tools)) {
+    optimized.tools = new Array(optimized.tools.length).fill(0);
+  }
+
+  return optimized;
+}
+
+async function parseRequestBody(c: Context): Promise<RequestBodyResult> {
+  const method = c.req.method.toUpperCase();
+  const hasBody = method !== "GET" && method !== "HEAD";
+
+  if (!hasBody) {
+    return { requestMessage: {}, requestBodyLog: "(empty)" };
+  }
+
+  const requestBodyBuffer = await c.req.raw.clone().arrayBuffer();
+  const requestBodyText = new TextDecoder().decode(requestBodyBuffer);
+
+  let requestMessage: Record<string, unknown> = {};
+  let requestBodyLog: string;
+  let requestBodyLogNote: string | undefined;
+
+  try {
+    const parsedMessage = JSON.parse(requestBodyText) as Record<string, unknown>;
+    requestMessage = optimizeRequestMessage(parsedMessage);
+    requestBodyLog = JSON.stringify(parsedMessage, null, 2);
+  } catch {
+    requestMessage = { raw: requestBodyText };
+    requestBodyLog = requestBodyText;
+    requestBodyLogNote = "请求体不是合法 JSON,已记录原始文本。";
+  }
+
+  return {
+    requestMessage,
+    requestBodyLog,
+    requestBodyLogNote,
+    requestBodyBuffer
+  };
+}

+ 30 - 0
src/app/v1/_lib/url.ts

@@ -0,0 +1,30 @@
+/**
+ * 构建代理目标URL
+ * @param baseUrl - 基础URL(如 https://open.bigmodel.cn/api/anthropic)
+ * @param requestUrl - 原始请求URL对象
+ * @returns 拼接后的完整URL字符串
+ */
+export function buildProxyUrl(baseUrl: string, requestUrl: URL): string {
+  try {
+    // 解析baseUrl
+    const baseUrlObj = new URL(baseUrl);
+    
+    // 合并路径:baseUrl的路径 + 请求的路径
+    // 确保路径拼接正确(处理斜杠)
+    const basePath = baseUrlObj.pathname.replace(/\/$/, ''); // 移除末尾斜杠
+    const requestPath = requestUrl.pathname; // 这已经包含 /v1/...
+    
+    // 构建最终URL
+    baseUrlObj.pathname = basePath + requestPath;
+    // 保留原始请求的查询参数
+    baseUrlObj.search = requestUrl.search;
+    
+    return baseUrlObj.toString();
+  } catch (error) {
+    console.error("URL构建失败:", error);
+    // 降级到字符串拼接
+    const normalizedBaseUrl = baseUrl.replace(/\/$/, ''); 
+    return `${normalizedBaseUrl}${requestUrl.pathname}${requestUrl.search}`;
+  }
+}
+

+ 119 - 0
src/components/error-boundary.tsx

@@ -0,0 +1,119 @@
+'use client';
+
+import React, { Component, ReactNode } from 'react';
+import { Button } from '@/components/ui/button';
+import { Card, CardDescription, CardFooter, CardHeader, CardTitle } from '@/components/ui/card';
+import { AlertTriangle, RefreshCw } from 'lucide-react';
+
+interface ErrorBoundaryState {
+  hasError: boolean;
+  error?: Error;
+}
+
+interface ErrorBoundaryProps {
+  children: ReactNode;
+  fallback?: React.ComponentType<{ error?: Error; resetError: () => void }>;
+}
+
+/**
+ * 简单的错误边界组件
+ */
+export class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
+  constructor(props: ErrorBoundaryProps) {
+    super(props);
+    this.state = { hasError: false };
+  }
+
+  static getDerivedStateFromError(error: Error): ErrorBoundaryState {
+    return {
+      hasError: true,
+      error,
+    };
+  }
+
+  componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
+    console.error('Error Boundary caught an error:', error, errorInfo);
+  }
+
+  resetError = () => {
+    this.setState({ hasError: false, error: undefined });
+  };
+
+  render() {
+    if (this.state.hasError) {
+      const FallbackComponent = this.props.fallback || DefaultErrorFallback;
+      return (
+        <FallbackComponent
+          error={this.state.error}
+          resetError={this.resetError}
+        />
+      );
+    }
+
+    return this.props.children;
+  }
+}
+
+/**
+ * 默认错误回退组件
+ */
+function DefaultErrorFallback({ error, resetError }: { error?: Error; resetError: () => void }) {
+  return (
+    <Card className="w-full max-w-md mx-auto my-8 border-destructive/50">
+      <CardHeader className="text-center">
+        <div className="flex justify-center mb-4">
+          <AlertTriangle className="h-12 w-12 text-destructive" />
+        </div>
+        <CardTitle className="text-destructive">出现了错误</CardTitle>
+        <CardDescription>
+          {error?.message || '页面加载时发生了未知错误,请尝试刷新页面。'}
+        </CardDescription>
+      </CardHeader>
+      <CardFooter className="flex gap-2 justify-center">
+        <Button variant="outline" onClick={resetError} size="sm">
+          <RefreshCw className="w-4 h-4 mr-2" />
+          重试
+        </Button>
+        <Button 
+          variant="secondary" 
+          onClick={() => window.location.reload()} 
+          size="sm"
+        >
+          刷新页面
+        </Button>
+      </CardFooter>
+    </Card>
+  );
+}
+
+/**
+ * 列表专用错误边界 - 用于列表组件的错误处理
+ */
+export function ListErrorBoundary({ children }: { children: ReactNode }) {
+  return (
+    <ErrorBoundary
+      fallback={({ error, resetError }) => (
+        <div className="rounded-lg border border-destructive/20 bg-destructive/5 p-4">
+          <div className="flex items-center gap-2 text-destructive">
+            <AlertTriangle className="h-4 w-4" />
+            <span className="text-sm font-medium">加载数据时出错</span>
+          </div>
+          <p className="mt-2 text-sm text-muted-foreground">
+            {error?.message || '无法加载数据,请稍后重试'}
+          </p>
+          <Button
+            variant="outline"
+            size="sm"
+            className="mt-3"
+            onClick={resetError}
+          >
+            <RefreshCw className="w-3 h-3 mr-1" />
+            重试
+          </Button>
+        </div>
+      )}
+    >
+      {children}
+    </ErrorBoundary>
+  );
+}

+ 34 - 0
src/components/form-error-boundary.tsx

@@ -0,0 +1,34 @@
+"use client";
+
+import type { ReactNode } from "react";
+import { ErrorBoundary } from "@/components/error-boundary";
+import { Button } from "@/components/ui/button";
+import { AlertTriangle, RefreshCw } from "lucide-react";
+
+/**
+ * 表单专用错误边界 - 用于对话框内表单的错误处理
+ */
+export function FormErrorBoundary({ children }: { children: ReactNode }) {
+  return (
+    <ErrorBoundary
+      fallback={({ error, resetError }) => (
+        <div className="p-2">
+          <div className="flex items-center gap-2 text-destructive">
+            <AlertTriangle className="h-4 w-4" />
+            <span className="text-sm font-medium">表单出错</span>
+          </div>
+          <p className="mt-2 text-xs text-muted-foreground">
+            {error?.message || '表单加载或提交失败,请重试'}
+          </p>
+          <div className="mt-3">
+            <Button variant="outline" size="sm" onClick={resetError}>
+              <RefreshCw className="w-3 h-3 mr-1" /> 重试
+            </Button>
+          </div>
+        </div>
+      )}
+    >
+      {children}
+    </ErrorBoundary>
+  );
+}

Деякі файли не було показано, через те що забагато файлів було змінено