From 95e2d846555516eea1c892f6631ad7b647764329 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Oct 2025 09:38:41 +0800 Subject: [PATCH 001/129] docs: add comprehensive refactoring documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add three key documents to guide the project restructure: - REFACTORING_MASTER_PLAN.md: Complete refactoring roadmap with 6 stages - REFACTORING_CHECKLIST.md: Detailed task checklist for tracking progress - REFACTORING_REFERENCE.md: Technical reference and implementation guide This refactoring aims to modernize the codebase with React Query, react-hook-form, zod validation, and shadcn/ui components while maintaining the current Tailwind CSS 4.x stack. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- docs/REFACTORING_CHECKLIST.md | 489 +++++++++ docs/REFACTORING_MASTER_PLAN.md | 1679 +++++++++++++++++++++++++++++++ docs/REFACTORING_REFERENCE.md | 834 +++++++++++++++ 3 files changed, 3002 insertions(+) create mode 100644 docs/REFACTORING_CHECKLIST.md create mode 100644 docs/REFACTORING_MASTER_PLAN.md create mode 100644 docs/REFACTORING_REFERENCE.md diff --git a/docs/REFACTORING_CHECKLIST.md b/docs/REFACTORING_CHECKLIST.md new file mode 100644 index 0000000..dbdafce --- /dev/null +++ b/docs/REFACTORING_CHECKLIST.md @@ -0,0 +1,489 @@ +# CC Switch 重构实施清单 + +> 用于跟踪重构进度的详细检查清单 + +**开始日期**: ___________ +**预计完成**: ___________ +**当前阶段**: ___________ + +--- + +## 📋 阶段 0: 准备阶段 (预计 1 天) + +### 环境准备 + +- [ ] 创建新分支 `refactor/modernization` +- [ ] 创建备份标签 `git tag backup-before-refactor` +- [ ] 备份用户配置文件 `~/.cc-switch/config.json` +- [ ] 通知团队成员重构开始 + +### 依赖安装 + +```bash +pnpm add @tanstack/react-query +pnpm add react-hook-form @hookform/resolvers +pnpm add zod +pnpm add sonner +pnpm add next-themes +pnpm add @radix-ui/react-dialog @radix-ui/react-dropdown-menu +pnpm add @radix-ui/react-label @radix-ui/react-select +pnpm add @radix-ui/react-slot @radix-ui/react-switch @radix-ui/react-tabs +pnpm add class-variance-authority clsx tailwind-merge tailwindcss-animate +``` + +- [ ] 安装核心依赖 (上述命令) +- [ ] 验证依赖安装成功 `pnpm install` +- [ ] 验证编译通过 `pnpm typecheck` + +### 配置文件 + +- [ ] 创建 `components.json` +- [ ] 更新 `tsconfig.json` 添加路径别名 +- [ ] 更新 `vite.config.mts` 添加路径解析 +- [ ] 验证开发服务器启动 `pnpm dev` + +**完成时间**: ___________ +**遇到的问题**: ___________ + +--- + +## 📋 阶段 1: 基础设施 (预计 2-3 天) + +### 1.1 工具函数和基础组件 + +- [ ] 创建 `src/lib/utils.ts` (cn 函数) +- [ ] 创建 `src/components/ui/button.tsx` +- [ ] 创建 `src/components/ui/dialog.tsx` +- [ ] 创建 `src/components/ui/input.tsx` +- [ ] 创建 `src/components/ui/label.tsx` +- [ ] 创建 `src/components/ui/textarea.tsx` +- [ ] 创建 `src/components/ui/select.tsx` +- [ ] 创建 `src/components/ui/switch.tsx` +- [ ] 创建 `src/components/ui/tabs.tsx` +- [ ] 创建 `src/components/ui/sonner.tsx` +- [ ] 创建 `src/components/ui/form.tsx` + +**测试**: +- [ ] 验证所有 UI 组件可以正常导入 +- [ ] 创建一个测试页面验证组件样式 + +### 1.2 Query Client 设置 + +- [ ] 创建 `src/lib/query/queryClient.ts` +- [ ] 配置默认选项 (retry, staleTime 等) +- [ ] 导出 queryClient 实例 + +### 1.3 API 层 + +- [ ] 创建 `src/lib/api/providers.ts` + - [ ] getAll + - [ ] getCurrent + - [ ] add + - [ ] update + - [ ] delete + - [ ] switch + - [ ] importDefault + - [ ] updateTrayMenu + +- [ ] 创建 `src/lib/api/settings.ts` + - [ ] get + - [ ] save + +- [ ] 创建 `src/lib/api/mcp.ts` + - [ ] getConfig + - [ ] upsertServer + - [ ] deleteServer + +- [ ] 创建 `src/lib/api/index.ts` (聚合导出) + +**测试**: +- [ ] 验证 API 调用不会出现运行时错误 +- [ ] 确认类型定义正确 + +### 1.4 Query Hooks + +- [ ] 创建 `src/lib/query/queries.ts` + - [ ] useProvidersQuery + - [ ] useSettingsQuery + - [ ] useMcpConfigQuery + +- [ ] 创建 `src/lib/query/mutations.ts` + - [ ] useAddProviderMutation + - [ ] useSwitchProviderMutation + - [ ] useDeleteProviderMutation + - [ ] useUpdateProviderMutation + - [ ] useSaveSettingsMutation + +- [ ] 创建 `src/lib/query/index.ts` (聚合导出) + +**测试**: +- [ ] 在临时组件中测试每个 hook +- [ ] 验证 loading/error 状态正确 +- [ ] 验证缓存和自动刷新工作 + +**完成时间**: ___________ +**遇到的问题**: ___________ + +--- + +## 📋 阶段 2: 核心功能重构 (预计 3-4 天) + +### 2.1 主题系统 + +- [ ] 创建 `src/components/theme-provider.tsx` +- [ ] 创建 `src/components/mode-toggle.tsx` +- [ ] 更新 `src/index.css` 添加主题变量 +- [ ] 删除 `src/hooks/useDarkMode.ts` +- [ ] 更新所有组件使用新的主题系统 + +**测试**: +- [ ] 验证主题切换正常工作 +- [ ] 验证系统主题跟随功能 +- [ ] 验证主题持久化 + +### 2.2 更新 main.tsx + +- [ ] 引入 QueryClientProvider +- [ ] 引入 ThemeProvider +- [ ] 添加 Toaster 组件 +- [ ] 移除旧的 API 导入 + +**测试**: +- [ ] 验证应用可以正常启动 +- [ ] 验证 Context 正确传递 + +### 2.3 重构 App.tsx + +- [ ] 使用 useProvidersQuery 替代手动状态管理 +- [ ] 移除所有 loadProviders 相关代码 +- [ ] 移除手动 notification 状态 +- [ ] 简化事件监听逻辑 +- [ ] 更新对话框为新的 Dialog 组件 + +**目标**: 将 412 行代码减少到 ~100 行 + +**测试**: +- [ ] 验证供应商列表正常加载 +- [ ] 验证切换 Claude/Codex 正常工作 +- [ ] 验证事件监听正常工作 + +### 2.4 重构 ProviderList + +- [ ] 创建 `src/components/providers/ProviderList.tsx` +- [ ] 使用 mutation hooks 处理操作 +- [ ] 移除 onNotify prop +- [ ] 移除手动状态管理 + +**测试**: +- [ ] 验证供应商列表渲染 +- [ ] 验证切换操作 +- [ ] 验证删除操作 + +### 2.5 重构表单系统 + +- [ ] 创建 `src/lib/schemas/provider.ts` (Zod schema) +- [ ] 创建 `src/components/providers/ProviderForm.tsx` + - [ ] 使用 react-hook-form + - [ ] 使用 zodResolver + - [ ] 字段级验证 + +- [ ] 创建 `src/components/providers/AddProviderDialog.tsx` + - [ ] 使用新的 Dialog 组件 + - [ ] 集成 ProviderForm + - [ ] 使用 useAddProviderMutation + +- [ ] 创建 `src/components/providers/EditProviderDialog.tsx` + - [ ] 使用新的 Dialog 组件 + - [ ] 集成 ProviderForm + - [ ] 使用 useUpdateProviderMutation + +**测试**: +- [ ] 验证表单验证正常工作 +- [ ] 验证错误提示显示正确 +- [ ] 验证提交操作成功 +- [ ] 验证表单重置功能 + +### 2.6 清理旧组件 + +- [ ] 删除 `src/components/AddProviderModal.tsx` +- [ ] 删除 `src/components/EditProviderModal.tsx` +- [ ] 更新所有引用这些组件的地方 + +**完成时间**: ___________ +**遇到的问题**: ___________ + +--- + +## 📋 阶段 3: 设置和辅助功能 (预计 2-3 天) + +### 3.1 重构 SettingsDialog + +- [ ] 创建 `src/components/settings/SettingsDialog.tsx` + - [ ] 使用 Tabs 组件 + - [ ] 集成各个设置子组件 + +- [ ] 创建 `src/components/settings/GeneralSettings.tsx` + - [ ] 语言设置 + - [ ] 配置目录设置 + - [ ] 其他通用设置 + +- [ ] 创建 `src/components/settings/AboutSection.tsx` + - [ ] 版本信息 + - [ ] 更新检查 + - [ ] 链接 + +- [ ] 创建 `src/components/settings/ImportExportSection.tsx` + - [ ] 导入功能 + - [ ] 导出功能 + +**目标**: 将 643 行拆分为 4-5 个小组件,每个 100-150 行 + +**测试**: +- [ ] 验证设置保存功能 +- [ ] 验证导入导出功能 +- [ ] 验证更新检查功能 + +### 3.2 重构通知系统 + +- [ ] 在所有 mutations 中使用 `toast` 替代 `showNotification` +- [ ] 移除 App.tsx 中的 notification 状态 +- [ ] 移除自定义通知组件 + +**测试**: +- [ ] 验证成功通知显示 +- [ ] 验证错误通知显示 +- [ ] 验证通知自动消失 + +### 3.3 重构确认对话框 + +- [ ] 更新 `src/components/ConfirmDialog.tsx` 使用新的 Dialog +- [ ] 或者直接使用 shadcn/ui 的 AlertDialog + +**测试**: +- [ ] 验证删除确认对话框 +- [ ] 验证其他确认场景 + +**完成时间**: ___________ +**遇到的问题**: ___________ + +--- + +## 📋 阶段 4: 清理和优化 (预计 1-2 天) + +### 4.1 移除旧代码 + +- [ ] 删除 `src/lib/styles.ts` +- [ ] 从 `src/lib/tauri-api.ts` 移除 `window.api` 绑定 +- [ ] 精简 `src/lib/tauri-api.ts`,只保留事件监听相关 +- [ ] 删除或更新 `src/vite-env.d.ts` 中的过时类型 + +### 4.2 代码审查 + +- [ ] 检查所有 TODO 注释 +- [ ] 检查是否还有 `window.api` 调用 +- [ ] 检查是否还有手动状态管理 +- [ ] 统一代码风格 + +### 4.3 类型检查 + +- [ ] 运行 `pnpm typecheck` 确保无错误 +- [ ] 修复所有类型错误 +- [ ] 更新类型定义 + +### 4.4 性能优化 + +- [ ] 检查是否有不必要的重渲染 +- [ ] 添加必要的 React.memo +- [ ] 优化 Query 缓存配置 + +**完成时间**: ___________ +**遇到的问题**: ___________ + +--- + +## 📋 阶段 5: 测试和修复 (预计 2-3 天) + +### 5.1 功能测试 + +#### 供应商管理 +- [ ] 添加供应商 (Claude) +- [ ] 添加供应商 (Codex) +- [ ] 编辑供应商 +- [ ] 删除供应商 +- [ ] 切换供应商 +- [ ] 导入默认配置 + +#### 应用切换 +- [ ] Claude <-> Codex 切换 +- [ ] 切换后数据正确加载 +- [ ] 切换后托盘菜单更新 + +#### 设置 +- [ ] 保存通用设置 +- [ ] 切换语言 +- [ ] 配置目录选择 +- [ ] 导入配置 +- [ ] 导出配置 + +#### UI 交互 +- [ ] 主题切换 (亮色/暗色) +- [ ] 对话框打开/关闭 +- [ ] 表单验证 +- [ ] Toast 通知 + +#### MCP 管理 +- [ ] 列表显示 +- [ ] 添加 MCP +- [ ] 编辑 MCP +- [ ] 删除 MCP +- [ ] 启用/禁用 MCP + +### 5.2 边界情况测试 + +- [ ] 空供应商列表 +- [ ] 无效配置文件 +- [ ] 网络错误 +- [ ] 后端错误响应 +- [ ] 并发操作 +- [ ] 表单输入边界值 + +### 5.3 兼容性测试 + +- [ ] Windows 测试 +- [ ] macOS 测试 +- [ ] Linux 测试 + +### 5.4 性能测试 + +- [ ] 100+ 供应商加载速度 +- [ ] 快速切换供应商 +- [ ] 内存使用情况 +- [ ] CPU 使用情况 + +### 5.5 Bug 修复 + +**Bug 列表** (发现后记录): + +1. ___________ + - [ ] 已修复 + - [ ] 已验证 + +2. ___________ + - [ ] 已修复 + - [ ] 已验证 + +**完成时间**: ___________ +**遇到的问题**: ___________ + +--- + +## 📋 最终检查 + +### 代码质量 + +- [ ] 所有 TypeScript 错误已修复 +- [ ] 运行 `pnpm format` 格式化代码 +- [ ] 运行 `pnpm typecheck` 通过 +- [ ] 代码审查完成 + +### 文档更新 + +- [ ] 更新 `CLAUDE.md` 反映新架构 +- [ ] 更新 `README.md` (如有必要) +- [ ] 添加 Migration Guide (可选) + +### 性能基准 + +记录性能数据: + +**旧版本**: +- 启动时间: _____ms +- 供应商加载: _____ms +- 内存占用: _____MB + +**新版本**: +- 启动时间: _____ms +- 供应商加载: _____ms +- 内存占用: _____MB + +### 代码统计 + +**代码行数对比**: + +| 文件 | 旧版本 | 新版本 | 减少 | +|------|--------|--------|------| +| App.tsx | 412 | ~100 | -76% | +| tauri-api.ts | 712 | ~50 | -93% | +| ProviderForm.tsx | 271 | ~150 | -45% | +| SettingsModal.tsx | 643 | ~400 (拆分) | -38% | +| **总计** | 2038 | ~700 | **-66%** | + +--- + +## 📦 发布准备 + +### Pre-release 测试 + +- [ ] 创建 beta 版本 `v4.0.0-beta.1` +- [ ] 在测试环境验证 +- [ ] 收集用户反馈 + +### 正式发布 + +- [ ] 合并到 main 分支 +- [ ] 创建 Release Tag `v4.0.0` +- [ ] 更新 Changelog +- [ ] 发布 GitHub Release +- [ ] 通知用户更新 + +--- + +## 🚨 回滚触发条件 + +如果出现以下情况,考虑回滚: + +- [ ] 重大功能无法使用 +- [ ] 用户数据丢失 +- [ ] 严重性能问题 +- [ ] 无法修复的兼容性问题 + +**回滚命令**: +```bash +git reset --hard backup-before-refactor +# 或 +git revert +``` + +--- + +## 📝 总结报告 + +### 成功指标 + +- [ ] 所有现有功能正常工作 +- [ ] 代码量减少 40%+ +- [ ] 无用户数据丢失 +- [ ] 性能未下降 + +### 经验教训 + +**遇到的主要挑战**: +1. ___________ +2. ___________ +3. ___________ + +**解决方案**: +1. ___________ +2. ___________ +3. ___________ + +**未来改进**: +1. ___________ +2. ___________ +3. ___________ + +--- + +**重构完成日期**: ___________ +**总耗时**: _____ 天 +**参与人员**: ___________ diff --git a/docs/REFACTORING_MASTER_PLAN.md b/docs/REFACTORING_MASTER_PLAN.md new file mode 100644 index 0000000..e2af742 --- /dev/null +++ b/docs/REFACTORING_MASTER_PLAN.md @@ -0,0 +1,1679 @@ +# CC Switch 现代化重构完整方案 + +## 📋 目录 + +- [第一部分: 战略规划](#第一部分-战略规划) + - [重构背景与目标](#重构背景与目标) + - [当前问题全面分析](#当前问题全面分析) + - [技术选型与理由](#技术选型与理由) +- [第二部分: 架构设计](#第二部分-架构设计) + - [新的目录结构](#新的目录结构) + - [数据流架构](#数据流架构) + - [组件拆分详细方案](#组件拆分详细方案) +- [第三部分: 实施计划](#第三部分-实施计划) + - [分阶段实施路线图](#分阶段实施路线图) + - [详细实施步骤](#详细实施步骤) +- [第四部分: 质量保障](#第四部分-质量保障) + - [测试策略](#测试策略) + - [风险控制](#风险控制) + - [回滚方案](#回滚方案) + +--- + +# 第一部分: 战略规划 + +## 🎯 重构背景与目标 + +### 为什么要重构? + +当前代码库存在以下核心问题: + +1. **状态管理混乱** + - 手动管理 20+ `useState` + - 大量复杂的 `useEffect` 依赖链 + - 数据同步逻辑分散 + +2. **组件过于臃肿** + - `SettingsModal.tsx`: **1046 行** 😱 + - `ProviderList.tsx`: **418 行** + - `ProviderForm.tsx`: **271 行** + +3. **代码重复严重** + - 相似的数据获取逻辑在多个组件重复 + - 表单验证逻辑手动编写 + - 错误处理不统一 + +4. **UI 缺乏统一性** + - 自定义样式分散 + - 缺乏设计系统 + - 响应式支持不足 + +5. **可维护性差** + - 组件职责不清晰 + - 耦合度高 + - 难以测试 + +### 重构目标 + +| 维度 | 目标 | 衡量标准 | +| -------------- | -------------------- | -------------- | +| **代码质量** | 减少 40-60% 样板代码 | 代码行数统计 | +| **开发效率** | 提升 50%+ 开发速度 | 新功能开发时间 | +| **用户体验** | 统一设计系统 | UI 一致性检查 | +| **可维护性** | 清晰的架构分层 | 代码审查时间 | +| **功能完整性** | 100% 功能无回归 | 全量测试通过 | + +--- + +## 🔍 当前问题全面分析 + +### 问题 1: App.tsx - 状态管理混乱 (412行) + +**现状**: + +```typescript +// 10+ 个 useState,状态管理混乱 +const [providers, setProviders] = useState>({}) +const [currentProviderId, setCurrentProviderId] = useState("") +const [notification, setNotification] = useState<{...} | null>(null) +const [isNotificationVisible, setIsNotificationVisible] = useState(false) +const [confirmDialog, setConfirmDialog] = useState<{...} | null>(null) +const [isSettingsOpen, setIsSettingsOpen] = useState(false) +const [isMcpOpen, setIsMcpOpen] = useState(false) +// ... 更多 + +// 手动数据加载,缺少 loading/error 状态 +const loadProviders = async () => { + const loadedProviders = await window.api.getProviders(activeApp) + const currentId = await window.api.getCurrentProvider(activeApp) + setProviders(loadedProviders) + setCurrentProviderId(currentId) +} + +// 复杂的 useEffect 依赖 +useEffect(() => { + loadProviders() +}, [activeApp]) +``` + +**核心问题**: + +- ❌ 状态同步困难 +- ❌ 没有 loading/error 处理 +- ❌ 错误处理不统一 +- ❌ 组件责任过重 + +**目标**: + +```typescript +// React Query: 3 行搞定 +const { data, isLoading, error } = useProvidersQuery(activeApp); +const providers = data?.providers || {}; +const currentProviderId = data?.currentProviderId || ""; +``` + +--- + +### 问题 2: SettingsModal.tsx - 超级巨无霸组件 (1046行) + +**现状结构**: + +``` +SettingsModal.tsx (1046 行) +├── 20+ useState (settings, configPath, version, isChecking...) +├── 15+ 处理函数 +│ ├── loadSettings() +│ ├── saveSettings() +│ ├── handleLanguageChange() +│ ├── handleCheckUpdate() +│ ├── handleExportConfig() +│ ├── handleImportConfig() +│ ├── handleBrowseConfigDir() +│ └── ... 更多 +├── 语言设置 UI +├── 窗口行为设置 UI +├── 配置文件位置 UI +├── 配置目录覆盖 UI (3个输入框) +├── 导入导出 UI +├── 关于和更新 UI +└── 2个子对话框 (ImportProgress, RestartConfirm) +``` + +**核心问题**: + +- ❌ 单个文件超过 1000 行 +- ❌ 多种职责混杂 +- ❌ 难以理解和维护 +- ❌ 无法并行开发 +- ❌ 难以测试 + +**目标**: 拆分为 **7 个小组件** (~470 行总计) + +--- + +### 问题 3: ProviderList.tsx - 内嵌组件和逻辑混杂 (418行) + +**现状结构**: + +``` +ProviderList.tsx (418 行) +├── SortableProviderItem (内嵌子组件, ~100行) +├── 拖拽排序逻辑 +├── 用量配置逻辑 +├── URL 处理逻辑 +├── Claude 插件同步逻辑 +└── 空状态 UI +``` + +**核心问题**: + +- ❌ 内嵌组件导致代码难读 +- ❌ 拖拽逻辑和 UI 混在一起 +- ❌ 业务逻辑分散 + +**目标**: 拆分为 **4 个独立组件** + **1 个自定义 Hook** + +--- + +### 问题 4: tauri-api.ts - 全局污染 (712行) + +**现状**: + +```typescript +// 问题 1: 污染全局命名空间 +if (typeof window !== "undefined") { + (window as any).api = tauriAPI; +} + +// 问题 2: 无缓存机制 +getProviders: async (app?: AppType) => { + try { + return await invoke("get_providers", { app_type: app, app }); + } catch (error) { + console.error("获取供应商列表失败:", error); + return {}; // 错误被吞掉 + } +}; +``` + +**核心问题**: + +- ❌ 全局 `window.api` 污染命名空间 +- ❌ 无缓存,重复请求 +- ❌ 无自动重试 +- ❌ 错误处理不统一 + +**目标**: + +- 封装为 API 层 (`lib/api/`) +- React Query 管理缓存和状态 + +--- + +### 问题 5: 表单验证 - 手动编写 (ProviderForm.tsx) + +**现状**: + +```typescript +const [name, setName] = useState(""); +const [nameError, setNameError] = useState(""); +const [apiKey, setApiKey] = useState(""); +const [apiKeyError, setApiKeyError] = useState(""); + +const validate = () => { + let valid = true; + if (!name) { + setNameError("请填写名称"); + valid = false; + } else { + setNameError(""); + } + if (!apiKey) { + setApiKeyError("请填写 API Key"); + valid = false; + } else if (apiKey.length < 10) { + setApiKeyError("API Key 长度不足"); + valid = false; + } else { + setApiKeyError(""); + } + return valid; +}; +``` + +**核心问题**: + +- ❌ 每个字段需要 2 个 state (值 + 错误) +- ❌ 验证逻辑手动编写 +- ❌ 代码冗长 + +**目标**: 使用 `react-hook-form` + `zod` + +```typescript +const schema = z.object({ + name: z.string().min(1, "请填写名称"), + apiKey: z.string().min(10, "API Key 长度不足"), +}); + +const form = useForm({ resolver: zodResolver(schema) }); +``` + +--- + +## 🛠 技术选型与理由 + +### 核心技术栈 + +| 技术 | 版本 | 用途 | 替代方案 | 为何选它? | +| ------------------------- | ------- | -------------- | --------------- | -------------------- | +| **@tanstack/react-query** | ^5.90.2 | 服务端状态管理 | SWR, RTK Query | 功能最全,生态最好 | +| **react-hook-form** | ^7.63.0 | 表单管理 | Formik | 性能更好,API 更简洁 | +| **zod** | ^4.1.11 | 运行时类型验证 | yup, joi | TypeScript 原生支持 | +| **shadcn/ui** | latest | UI 组件库 | Radix UI 原生 | 可定制,代码归属权 | +| **sonner** | ^2.0.7 | Toast 通知 | react-hot-toast | 更现代,动画更好 | +| **next-themes** | ^0.4.6 | 主题管理 | 自定义实现 | 开箱即用,SSR 友好 | + +--- + +# 第二部分: 架构设计 + +## 📁 新的目录结构 + +### 完整目录树 + +``` +src/ +├── components/ +│ ├── ui/ # shadcn/ui 基础组件 (由 CLI 生成) +│ │ ├── button.tsx +│ │ ├── dialog.tsx +│ │ ├── input.tsx +│ │ ├── label.tsx +│ │ ├── form.tsx +│ │ ├── select.tsx +│ │ ├── switch.tsx +│ │ ├── tabs.tsx +│ │ ├── card.tsx +│ │ ├── badge.tsx +│ │ └── sonner.tsx # Toast 组件 +│ │ +│ ├── providers/ # 供应商管理模块 +│ │ ├── ProviderList.tsx # 列表容器 (~100行) +│ │ ├── ProviderCard.tsx # 供应商卡片 (~120行) +│ │ ├── ProviderActions.tsx # 操作按钮组 (~80行) +│ │ ├── ProviderEmptyState.tsx # 空状态 (~30行) +│ │ ├── AddProviderDialog.tsx # 添加对话框 (~60行) +│ │ ├── EditProviderDialog.tsx # 编辑对话框 (~60行) +│ │ └── forms/ # 表单子模块 +│ │ ├── ProviderForm.tsx # 主表单 (~150行) +│ │ ├── PresetSelector.tsx # 预设选择器 (~60行) +│ │ ├── ApiKeyInput.tsx # API Key 输入 (~40行) +│ │ ├── ConfigEditor.tsx # 配置编辑器 (~80行) +│ │ └── KimiModelSelector.tsx # Kimi 模型选择器 (~40行) +│ │ +│ ├── settings/ # 设置管理模块 (拆分自 SettingsModal) +│ │ ├── SettingsDialog.tsx # 设置对话框容器 (~80行) +│ │ ├── LanguageSettings.tsx # 语言设置 (~40行) +│ │ ├── WindowSettings.tsx # 窗口行为设置 (~50行) +│ │ ├── ConfigPathDisplay.tsx # 配置路径显示 (~40行) +│ │ ├── DirectorySettings/ # 目录设置子模块 +│ │ │ ├── index.tsx # 目录设置容器 (~60行) +│ │ │ └── DirectoryInput.tsx # 单个目录输入组件 (~50行) +│ │ ├── ImportExportSection.tsx # 导入导出 (~120行) +│ │ ├── AboutSection.tsx # 关于和更新 (~100行) +│ │ └── RestartDialog.tsx # 重启确认对话框 (~40行) +│ │ +│ ├── usage/ # 用量查询模块 +│ │ ├── UsageFooter.tsx # 用量信息展示 +│ │ ├── UsageScriptModal.tsx # 用量脚本配置 +│ │ └── UsageEditor.tsx # 脚本编辑器 +│ │ +│ ├── mcp/ # MCP 管理模块 +│ │ ├── McpPanel.tsx # MCP 管理面板 +│ │ ├── McpList.tsx # MCP 列表 +│ │ ├── McpForm.tsx # MCP 表单 +│ │ └── McpTemplates.tsx # MCP 模板选择 +│ │ +│ ├── shared/ # 共享组件 +│ │ ├── AppSwitcher.tsx # Claude/Codex 切换器 +│ │ ├── ConfirmDialog.tsx # 确认对话框 +│ │ ├── UpdateBadge.tsx # 更新徽章 +│ │ ├── JsonEditor.tsx # JSON 编辑器 +│ │ ├── BrandIcons.tsx # 品牌图标 +│ │ └── ImportProgressModal.tsx # 导入进度 +│ │ +│ ├── theme-provider.tsx # 主题 Provider +│ └── mode-toggle.tsx # 主题切换按钮 +│ +├── hooks/ # 自定义 Hooks (业务逻辑层) +│ ├── useSettings.ts # 设置管理逻辑 +│ ├── useImportExport.ts # 导入导出逻辑 +│ ├── useDragSort.ts # 拖拽排序逻辑 +│ ├── useProviderActions.ts # 供应商操作 (可选) +│ ├── useVSCodeSync.ts # VS Code 同步 +│ ├── useClaudePlugin.ts # Claude 插件管理 +│ └── useAppVersion.ts # 版本信息 +│ +├── lib/ +│ ├── query/ # React Query 层 +│ │ ├── index.ts # 导出所有 hooks +│ │ ├── queryClient.ts # QueryClient 配置 +│ │ ├── queries.ts # 所有查询 hooks +│ │ └── mutations.ts # 所有变更 hooks +│ │ +│ ├── api/ # API 调用层 (封装 Tauri invoke) +│ │ ├── providers.ts # 供应商 API +│ │ ├── settings.ts # 设置 API +│ │ ├── mcp.ts # MCP API +│ │ ├── usage.ts # 用量查询 API +│ │ ├── vscode.ts # VS Code API +│ │ └── index.ts # 聚合导出 +│ │ +│ ├── schemas/ # Zod 验证 Schemas +│ │ ├── provider.ts # 供应商验证规则 +│ │ ├── settings.ts # 设置验证规则 +│ │ └── mcp.ts # MCP 验证规则 +│ │ +│ ├── utils/ # 工具函数 +│ │ ├── errorHandling.ts # 错误处理 +│ │ ├── providerUtils.ts # 供应商工具 +│ │ └── configUtils.ts # 配置工具 +│ │ +│ └── utils.ts # shadcn/ui 工具函数 (cn) +│ +├── types/ # TypeScript 类型定义 +│ └── index.ts +│ +├── contexts/ # React Contexts (保留现有) +│ └── UpdateContext.tsx # 更新管理 Context +│ +├── i18n/ # 国际化 (保留现有) +│ ├── index.ts +│ └── locales/ +│ +├── App.tsx # 主应用组件 (简化到 ~100行) +├── main.tsx # 入口文件 (添加 Providers) +└── index.css # 全局样式 +``` + +### 目录结构设计原则 + +1. **按功能模块分组** (providers/, settings/, mcp/) +2. **按技术层次分层** (components/, hooks/, lib/) +3. **UI 组件独立** (ui/ 目录) +4. **业务逻辑提取** (hooks/ 目录) +5. **数据层封装** (api/ 目录) + +--- + +## 🏗 数据流架构 + +### 分层架构图 + +``` +┌─────────────────────────────────────────┐ +│ UI 层 (Components) │ +│ ProviderList, SettingsDialog, etc. │ +└────────────────┬────────────────────────┘ + │ 使用 + ↓ +┌─────────────────────────────────────────┐ +│ 业务逻辑层 (Custom Hooks) │ +│ useSettings, useDragSort, etc. │ +└────────────────┬────────────────────────┘ + │ 调用 + ↓ +┌─────────────────────────────────────────┐ +│ 数据管理层 (React Query Hooks) │ +│ useProvidersQuery, useMutation, etc. │ +└────────────────┬────────────────────────┘ + │ 调用 + ↓ +┌─────────────────────────────────────────┐ +│ API 层 (API Functions) │ +│ providersApi, settingsApi, etc. │ +└────────────────┬────────────────────────┘ + │ invoke + ↓ +┌─────────────────────────────────────────┐ +│ Tauri Backend (Rust) │ +│ Commands, State, File System │ +└─────────────────────────────────────────┘ +``` + +### 数据流示例 + +**场景**: 切换供应商 + +``` +1. 用户点击按钮 + ↓ +2. ProviderCard 调用 onClick={() => switchMutation.mutate(id)} + ↓ +3. useSwitchProviderMutation (lib/query/mutations.ts) + - mutationFn: 调用 providersApi.switch(id, appType) + ↓ +4. providersApi.switch (lib/api/providers.ts) + - 调用 invoke('switch_provider', { id, app_type }) + ↓ +5. Tauri Backend (Rust) + - 执行切换逻辑 + - 更新配置文件 + - 返回结果 + ↓ +6. useSwitchProviderMutation + - onSuccess: invalidateQueries(['providers', appType]) + - onSuccess: updateTrayMenu() + - onSuccess: toast.success('切换成功') + ↓ +7. useProvidersQuery 自动重新获取数据 + ↓ +8. UI 自动更新 +``` + +### 关键设计原则 + +1. **单一职责**: 每层只做一件事 +2. **依赖倒置**: UI 依赖抽象 (hooks),不依赖具体实现 +3. **开闭原则**: 易于扩展,无需修改现有代码 +4. **状态分离**: + - 服务端状态 → React Query + - 客户端 UI 状态 → useState + - 全局状态 → Context + +--- + +## 🔧 组件拆分详细方案 + +### 拆分策略: SettingsModal (1046行 → 7个组件) + +#### 拆分前后对比 + +``` +┌───────────────────────────────────┐ +│ SettingsModal.tsx (1046 行) │ ❌ 过于臃肿 +│ │ +│ - 20+ useState │ +│ - 15+ 函数 │ +│ - 600+ 行 JSX │ +│ - 难以理解和维护 │ +└───────────────────────────────────┘ + + ↓ 重构 + +┌─────────────────────────────────────────────────┐ +│ settings/ 模块 (7个组件, ~470行) │ +│ │ +│ ├── SettingsDialog.tsx (容器, ~80行) │ +│ │ └── 使用 useSettings hook │ +│ │ │ +│ ├── LanguageSettings.tsx (~40行) │ +│ ├── WindowSettings.tsx (~50行) │ +│ ├── ConfigPathDisplay.tsx (~40行) │ +│ ├── DirectorySettings/ (~110行) │ +│ │ ├── index.tsx (~60行) │ +│ │ └── DirectoryInput.tsx (~50行) │ +│ ├── ImportExportSection.tsx (~120行) │ +│ │ └── 使用 useImportExport hook │ +│ └── AboutSection.tsx (~100行) │ +│ └── 使用 useAppVersion, useUpdate hooks │ +└─────────────────────────────────────────────────┘ + +✅ 每个组件 30-120 行 +✅ 职责清晰 +✅ 易于测试 +✅ 可独立开发 +``` + +#### 拆分详细方案 + +**1. SettingsDialog.tsx (容器组件, ~80行)** + +职责: 组织整体布局,协调子组件 + +```typescript +import { LanguageSettings } from './LanguageSettings' +import { WindowSettings } from './WindowSettings' +import { DirectorySettings } from './DirectorySettings' +import { ImportExportSection } from './ImportExportSection' +import { AboutSection } from './AboutSection' +import { useSettings } from '@/hooks/useSettings' + +export function SettingsDialog({ open, onOpenChange }) { + const { settings, updateSettings, saveSettings, isPending } = useSettings() + + return ( + + + + 设置 + + + + + 通用 + 高级 + 关于 + + + + updateSettings({ language: lang })} + /> + + + + + + + + + + + + + + + + + + + + + ) +} +``` + +**2. LanguageSettings.tsx (~40行)** + +职责: 语言切换 UI + +```typescript +interface LanguageSettingsProps { + value: 'zh' | 'en' + onChange: (lang: 'zh' | 'en') => void +} + +export function LanguageSettings({ value, onChange }: LanguageSettingsProps) { + return ( +
+

语言设置

+
+ + +
+
+ ) +} +``` + +**3. DirectoryInput.tsx (~50行)** + +职责: 可复用的目录选择输入框 + +```typescript +import { FolderSearch, Undo2 } from 'lucide-react' + +interface DirectoryInputProps { + label: string + description?: string + value?: string + onChange: (value: string | undefined) => void + type: 'app' | 'claude' | 'codex' +} + +export function DirectoryInput({ label, description, value, onChange }: DirectoryInputProps) { + const handleBrowse = async () => { + const selected = await window.api.selectConfigDirectory(value) + if (selected) onChange(selected) + } + + const handleReset = () => { + onChange(undefined) + } + + return ( +
+ + {description &&

{description}

} +
+ onChange(e.target.value)} + className="flex-1 font-mono text-xs" + /> + + +
+
+ ) +} +``` + +**4. useSettings Hook (业务逻辑提取)** + +```typescript +export function useSettings() { + const queryClient = useQueryClient(); + + // 获取设置 + const { data: settings, isLoading } = useQuery({ + queryKey: ["settings"], + queryFn: async () => await settingsApi.get(), + }); + + // 保存设置 + const saveMutation = useMutation({ + mutationFn: async (newSettings: Settings) => + await settingsApi.save(newSettings), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ["settings"] }); + toast.success("设置已保存"); + }, + }); + + // 本地临时状态 (保存前) + const [localSettings, setLocalSettings] = useState(null); + const currentSettings = localSettings || settings || {}; + + return { + settings: currentSettings, + updateSettings: (updates: Partial) => { + setLocalSettings((prev) => ({ ...prev, ...updates })); + }, + saveSettings: () => { + if (localSettings) saveMutation.mutate(localSettings); + }, + resetSettings: () => setLocalSettings(null), + isPending: saveMutation.isPending, + isLoading, + }; +} +``` + +--- + +### 拆分策略: ProviderList (418行 → 4个组件 + 1个Hook) + +#### 拆分方案 + +``` +ProviderList.tsx (418 行) ❌ 内嵌组件、逻辑混杂 + + ↓ 重构 + +providers/ 模块 (4个组件 + 1个Hook, ~330行) + +├── ProviderList.tsx (容器, ~100行) +│ └── 使用 useDragSort hook +│ +├── ProviderCard.tsx (~120行) +│ └── 显示单个供应商信息 +│ +├── ProviderActions.tsx (~80行) +│ └── 操作按钮组 (switch, edit, delete, usage) +│ +├── ProviderEmptyState.tsx (~30行) +│ └── 空状态提示 +│ +└── hooks/useDragSort.ts (~100行) + └── 拖拽排序逻辑 +``` + +#### 代码示例 + +**ProviderList.tsx (容器)** + +```typescript +import { ProviderCard } from './ProviderCard' +import { ProviderEmptyState } from './ProviderEmptyState' +import { useDragSort } from '@/hooks/useDragSort' + +export function ProviderList({ providers, currentProviderId, appType }) { + const { sortedProviders, handleDragEnd, sensors } = useDragSort(providers, appType) + + if (sortedProviders.length === 0) { + return + } + + return ( + + p.id)} + strategy={verticalListSortingStrategy} + > +
+ {sortedProviders.map(provider => ( + + ))} +
+
+
+ ) +} +``` + +**useDragSort.ts (逻辑提取)** + +```typescript +export function useDragSort( + providers: Record, + appType: AppType +) { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + // 排序逻辑 + const sortedProviders = useMemo(() => { + return Object.values(providers).sort((a, b) => { + if (a.sortIndex !== undefined && b.sortIndex !== undefined) { + return a.sortIndex - b.sortIndex; + } + const timeA = a.createdAt || 0; + const timeB = b.createdAt || 0; + if (timeA === 0 && timeB === 0) { + return a.name.localeCompare(b.name, "zh-CN"); + } + return timeA === 0 ? -1 : timeB === 0 ? 1 : timeA - timeB; + }); + }, [providers]); + + // 拖拽传感器 + const sensors = useSensors( + useSensor(PointerSensor, { activationConstraint: { distance: 8 } }), + useSensor(KeyboardSensor) + ); + + // 拖拽结束处理 + const handleDragEnd = useCallback( + async (event: DragEndEvent) => { + const { active, over } = event; + if (!over || active.id === over.id) return; + + const oldIndex = sortedProviders.findIndex((p) => p.id === active.id); + const newIndex = sortedProviders.findIndex((p) => p.id === over.id); + + const reordered = arrayMove(sortedProviders, oldIndex, newIndex); + const updates = reordered.map((p, i) => ({ id: p.id, sortIndex: i })); + + try { + await providersApi.updateSortOrder(updates, appType); + queryClient.invalidateQueries({ queryKey: ["providers", appType] }); + toast.success(t("provider.sortUpdated")); + } catch (error) { + toast.error(t("provider.sortUpdateFailed")); + } + }, + [sortedProviders, appType, queryClient, t] + ); + + return { sortedProviders, sensors, handleDragEnd }; +} +``` + +--- + +### 代码量对比总结 + +| 组件 | 重构前 | 重构后 | 变化 | +| -------------------- | ---------- | ---------------- | -------- | +| **SettingsModal** | 1046 行 | 7个组件 ~470行 | **-55%** | +| **ProviderList** | 418 行 | 4个组件 ~330行 | **-21%** | +| **业务逻辑 (Hooks)** | 混在组件中 | 5个 hooks ~400行 | 提取独立 | +| **总计** | 1464 行 | ~1200 行 | **-18%** | + +**注意**: 代码总量略有减少,但**可维护性大幅提升**: + +- ✅ 每个文件 30-120 行,易于理解 +- ✅ 关注点分离,职责清晰 +- ✅ 业务逻辑可复用 +- ✅ 易于测试和调试 + +--- + +# 第三部分: 实施计划 + +## 📅 分阶段实施路线图 + +### 总览 + +| 阶段 | 目标 | 工期 | 产出 | +| ---------- | -------------- | ------------ | ---------------------------- | +| **阶段 0** | 准备环境 | 1 天 | 依赖安装、配置完成 | +| **阶段 1** | 搭建基础设施 | 2-3 天 | API 层、Query Hooks 完成 | +| **阶段 2** | 重构核心功能 | 3-4 天 | App.tsx、ProviderList 完成 | +| **阶段 3** | 重构设置和辅助 | 2-3 天 | SettingsDialog、通知系统完成 | +| **阶段 4** | 清理和优化 | 1-2 天 | 旧代码删除、优化完成 | +| **阶段 5** | 测试和修复 | 2-3 天 | 测试通过、Bug 修复 | +| **总计** | - | **11-16 天** | v4.0.0 发布 | + +--- + +### 阶段 0: 准备阶段 (1天) + +**目标**: 环境准备和依赖安装 + +#### 任务清单 + +- [ ] 创建新分支 `refactor/modernization` +- [ ] 创建备份标签 `git tag backup-before-refactor` +- [ ] 安装核心依赖 +- [ ] 配置 shadcn/ui +- [ ] 配置 TypeScript 路径别名 +- [ ] 配置 Vite 路径解析 +- [ ] 验证开发服务器启动 + +#### 详细步骤 + +**1. 创建分支和备份** + +```bash +# 创建新分支 +git checkout -b refactor/modernization + +# 创建备份标签 +git tag backup-before-refactor + +# 推送标签到远程 (可选) +git push origin backup-before-refactor +``` + +**2. 安装依赖** + +```bash +# 核心依赖 +pnpm add @tanstack/react-query +pnpm add react-hook-form @hookform/resolvers +pnpm add zod +pnpm add sonner +pnpm add next-themes + +# Radix UI 组件 (shadcn/ui 依赖) +pnpm add @radix-ui/react-dialog +pnpm add @radix-ui/react-dropdown-menu +pnpm add @radix-ui/react-label +pnpm add @radix-ui/react-select +pnpm add @radix-ui/react-slot +pnpm add @radix-ui/react-switch +pnpm add @radix-ui/react-tabs +pnpm add @radix-ui/react-checkbox + +# 样式工具 +pnpm add class-variance-authority +pnpm add clsx +pnpm add tailwind-merge +``` + +**3. 创建 `components.json`** + +```json +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} +``` + +**4. 更新 `tsconfig.json`** + +```json +{ + "compilerOptions": { + // ... 现有配置 + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + } +} +``` + +**5. 更新 `vite.config.mts`** + +```typescript +import path from "path"; +import react from "@vitejs/plugin-react"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); +``` + +**6. 验证** + +```bash +pnpm dev # 确保开发服务器正常启动 +pnpm typecheck # 确保类型检查通过 +``` + +--- + +### 阶段 1: 基础设施 (2-3天) + +**目标**: 搭建新架构的基础层 + +#### 任务清单 + +- [ ] 创建工具函数 (`lib/utils.ts`) +- [ ] 添加基础 UI 组件 (Button, Dialog, Input, Form 等) +- [ ] 创建 Query Client 配置 +- [ ] 封装 API 层 (providers, settings, mcp) +- [ ] 创建 Query Hooks (queries, mutations) +- [ ] 创建 Zod Schemas + +#### 详细步骤 + +**Step 1.1: 创建 `src/lib/utils.ts`** + +```typescript +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} +``` + +**Step 1.2: 添加 shadcn/ui 基础组件** + +创建 `src/components/ui/button.tsx`: + +```typescript +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } +``` + +类似地创建: + +- `dialog.tsx` +- `input.tsx` +- `label.tsx` +- `form.tsx` +- `select.tsx` +- `switch.tsx` +- `tabs.tsx` +- `textarea.tsx` +- `sonner.tsx` + +**参考**: https://ui.shadcn.com/docs/components + +**Step 1.3: 创建 Query Client** + +`src/lib/query/queryClient.ts`: + +```typescript +import { QueryClient } from "@tanstack/react-query"; + +export const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: 1, + refetchOnWindowFocus: false, + staleTime: 1000 * 60 * 5, // 5 分钟 + }, + mutations: { + retry: false, + }, + }, +}); +``` + +**Step 1.4: 封装 API 层** + +`src/lib/api/providers.ts`: + +```typescript +import { invoke } from "@tauri-apps/api/core"; +import { Provider } from "@/types"; + +export type AppType = "claude" | "codex"; + +export const providersApi = { + getAll: async (appType: AppType): Promise> => { + return await invoke("get_providers", { app_type: appType, app: appType }); + }, + + getCurrent: async (appType: AppType): Promise => { + return await invoke("get_current_provider", { + app_type: appType, + app: appType, + }); + }, + + add: async (provider: Provider, appType: AppType): Promise => { + return await invoke("add_provider", { + provider, + app_type: appType, + app: appType, + }); + }, + + update: async (provider: Provider, appType: AppType): Promise => { + return await invoke("update_provider", { + provider, + app_type: appType, + app: appType, + }); + }, + + delete: async (id: string, appType: AppType): Promise => { + return await invoke("delete_provider", { + id, + app_type: appType, + app: appType, + }); + }, + + switch: async (id: string, appType: AppType): Promise => { + return await invoke("switch_provider", { + id, + app_type: appType, + app: appType, + }); + }, + + importDefault: async (appType: AppType): Promise => { + return await invoke("import_default_config", { + app_type: appType, + app: appType, + }); + }, + + updateTrayMenu: async (): Promise => { + return await invoke("update_tray_menu"); + }, + + updateSortOrder: async ( + updates: Array<{ id: string; sortIndex: number }>, + appType: AppType + ): Promise => { + return await invoke("update_providers_sort_order", { + updates, + app_type: appType, + app: appType, + }); + }, +}; +``` + +类似地创建: + +- `src/lib/api/settings.ts` +- `src/lib/api/mcp.ts` +- `src/lib/api/index.ts` (聚合导出) + +**Step 1.5: 创建 Query Hooks** + +`src/lib/query/queries.ts`: + +```typescript +import { useQuery } from "@tanstack/react-query"; +import { providersApi, AppType } from "@/lib/api"; +import { Provider } from "@/types"; + +// 排序辅助函数 +const sortProviders = ( + providers: Record +): Record => { + return Object.fromEntries( + Object.values(providers) + .sort((a, b) => { + const timeA = a.createdAt || 0; + const timeB = b.createdAt || 0; + if (timeA === 0 && timeB === 0) { + return a.name.localeCompare(b.name, "zh-CN"); + } + if (timeA === 0) return -1; + if (timeB === 0) return 1; + return timeA - timeB; + }) + .map((provider) => [provider.id, provider]) + ); +}; + +export const useProvidersQuery = (appType: AppType) => { + return useQuery({ + queryKey: ["providers", appType], + queryFn: async () => { + let providers: Record = {}; + let currentProviderId = ""; + + try { + providers = await providersApi.getAll(appType); + } catch (error) { + console.error("获取供应商列表失败:", error); + } + + try { + currentProviderId = await providersApi.getCurrent(appType); + } catch (error) { + console.error("获取当前供应商失败:", error); + } + + // 自动导入默认配置 + if (Object.keys(providers).length === 0) { + try { + const success = await providersApi.importDefault(appType); + if (success) { + providers = await providersApi.getAll(appType); + currentProviderId = await providersApi.getCurrent(appType); + } + } catch (error) { + console.error("导入默认配置失败:", error); + } + } + + return { providers: sortProviders(providers), currentProviderId }; + }, + }); +}; +``` + +`src/lib/query/mutations.ts`: + +```typescript +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { providersApi, AppType } from "@/lib/api"; +import { Provider } from "@/types"; +import { toast } from "sonner"; +import { useTranslation } from "react-i18next"; + +export const useAddProviderMutation = (appType: AppType) => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (provider: Omit) => { + const newProvider: Provider = { + ...provider, + id: crypto.randomUUID(), + createdAt: Date.now(), + }; + await providersApi.add(newProvider, appType); + return newProvider; + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); + await providersApi.updateTrayMenu(); + toast.success(t("notifications.providerAdded")); + }, + onError: (error: Error) => { + toast.error(t("notifications.addFailed", { error: error.message })); + }, + }); +}; + +export const useSwitchProviderMutation = (appType: AppType) => { + const queryClient = useQueryClient(); + const { t } = useTranslation(); + + return useMutation({ + mutationFn: async (providerId: string) => { + return await providersApi.switch(providerId, appType); + }, + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ["providers", appType] }); + await providersApi.updateTrayMenu(); + toast.success( + t("notifications.switchSuccess", { appName: t(`apps.${appType}`) }) + ); + }, + onError: (error: Error) => { + toast.error(t("notifications.switchFailed") + ": " + error.message); + }, + }); +}; + +// 类似地创建: useDeleteProviderMutation, useUpdateProviderMutation +``` + +**Step 1.6: 创建 Zod Schemas** + +`src/lib/schemas/provider.ts`: + +```typescript +import { z } from "zod"; + +export const providerSchema = z.object({ + name: z.string().min(1, "请填写供应商名称"), + websiteUrl: z.string().url("请输入有效的网址").optional().or(z.literal("")), + settingsConfig: z + .string() + .min(1, "请填写配置内容") + .refine( + (val) => { + try { + JSON.parse(val); + return true; + } catch { + return false; + } + }, + { message: "配置 JSON 格式错误" } + ), +}); + +export type ProviderFormData = z.infer; +``` + +--- + +### 阶段 2: 核心功能重构 (3-4天) + +**目标**: 重构 App.tsx 和供应商管理 + +#### 任务清单 + +- [ ] 更新 `main.tsx` (添加 Providers) +- [ ] 创建主题 Provider +- [ ] 重构 `App.tsx` (412行 → ~100行) +- [ ] 拆分 ProviderList (4个组件) +- [ ] 创建 `useDragSort` Hook +- [ ] 重构表单组件 (使用 react-hook-form) +- [ ] 创建 AddProvider / EditProvider Dialog + +#### 详细步骤 + +**Step 2.1: 更新 `main.tsx`** + +```typescript +import React from 'react' +import ReactDOM from 'react-dom/client' +import App from './App' +import { UpdateProvider } from './contexts/UpdateContext' +import './index.css' +import './i18n' +import { QueryClientProvider } from '@tanstack/react-query' +import { queryClient } from '@/lib/query' +import { ThemeProvider } from '@/components/theme-provider' +import { Toaster } from '@/components/ui/sonner' + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + + + + + + +) +``` + +**Step 2.2: 创建 `theme-provider.tsx`** + +```typescript +import { createContext, useContext, useEffect, useState } from 'react' + +type Theme = 'dark' | 'light' | 'system' + +type ThemeProviderProps = { + children: React.ReactNode + defaultTheme?: Theme + storageKey?: string +} + +type ThemeProviderState = { + theme: Theme + setTheme: (theme: Theme) => void +} + +const ThemeProviderContext = createContext({ + theme: 'system', + setTheme: () => null, +}) + +export function ThemeProvider({ + children, + defaultTheme = 'system', + storageKey = 'ui-theme', + ...props +}: ThemeProviderProps) { + const [theme, setTheme] = useState( + () => (localStorage.getItem(storageKey) as Theme) || defaultTheme + ) + + useEffect(() => { + const root = window.document.documentElement + root.classList.remove('light', 'dark') + + if (theme === 'system') { + const systemTheme = window.matchMedia('(prefers-color-scheme: dark)').matches + ? 'dark' + : 'light' + root.classList.add(systemTheme) + return + } + + root.classList.add(theme) + }, [theme]) + + const value = { + theme, + setTheme: (theme: Theme) => { + localStorage.setItem(storageKey, theme) + setTheme(theme) + }, + } + + return ( + + {children} + + ) +} + +export const useTheme = () => { + const context = useContext(ThemeProviderContext) + if (context === undefined) { + throw new Error('useTheme must be used within a ThemeProvider') + } + return context +} +``` + +**Step 2.3: 重构 `App.tsx`** + +(参考前面的代码示例,从 412 行简化到 ~100 行) + +**Step 2.4-2.7: 拆分 ProviderList** + +(参考前面的组件拆分详细方案) + +--- + +### 阶段 3: 设置和辅助功能 (2-3天) + +**目标**: 重构 SettingsModal 和通知系统 + +#### 任务清单 + +- [ ] 拆分 SettingsDialog (7个组件) +- [ ] 创建 `useSettings` Hook +- [ ] 创建 `useImportExport` Hook +- [ ] 替换通知系统为 Sonner +- [ ] 重构 ConfirmDialog + +#### 详细步骤 + +(参考前面的组件拆分详细方案) + +--- + +### 阶段 4: 清理和优化 (1-2天) + +**目标**: 清理旧代码,优化性能 + +#### 任务清单 + +- [ ] 删除 `lib/styles.ts` +- [ ] 删除旧的 Modal 组件 +- [ ] 移除 `window.api` 全局绑定 +- [ ] 清理无用的 state 和函数 +- [ ] 更新类型定义 +- [ ] 代码格式化 +- [ ] TypeScript 检查 + +--- + +### 阶段 5: 测试和修复 (2-3天) + +**目标**: 全面测试,修复 Bug + +#### 功能测试清单 + +- [ ] 添加供应商 (Claude/Codex) +- [ ] 编辑供应商 +- [ ] 删除供应商 +- [ ] 切换供应商 +- [ ] 拖拽排序 +- [ ] 设置保存 +- [ ] 导入导出配置 +- [ ] 主题切换 +- [ ] MCP 管理 +- [ ] 用量查询 +- [ ] 托盘菜单同步 + +#### 边界情况测试 + +- [ ] 空供应商列表 +- [ ] 网络错误 +- [ ] 表单验证 +- [ ] 并发操作 +- [ ] 大量数据 (100+ 供应商) + +--- + +# 第四部分: 质量保障 + +## 🧪 测试策略 + +### 手动测试 + +每完成一个阶段后进行全量功能测试。 + +### 自动化测试 (可选) + +可以考虑添加: + +- Vitest 单元测试 (hooks, utils) +- Testing Library 组件测试 + +--- + +## 🚨 风险控制 + +### 潜在风险 + +1. **功能回归**: 重构可能引入 bug +2. **用户数据丢失**: 配置文件操作失败 +3. **性能下降**: 新架构可能影响性能 +4. **兼容性问题**: 依赖库平台兼容性 + +### 缓解措施 + +1. **逐步重构**: 按阶段进行,每阶段后测试 +2. **保留备份**: Git tag + 配置文件备份 +3. **Beta 测试**: 先发布 beta 版本 +4. **回滚方案**: 准备快速回滚机制 + +--- + +## ⏪ 回滚方案 + +### 如果需要回滚 + +```bash +# 方案 1: 回到重构前 +git reset --hard backup-before-refactor + +# 方案 2: 创建回滚分支 +git checkout -b rollback-refactor +git revert +``` + +### 用户数据保护 + +在重构前自动备份配置: + +```rust +// Rust 后端 +fn backup_config_before_refactor() -> Result<()> { + let config_path = get_app_config_path()?; + let backup_path = config_path.with_extension("backup.json"); + fs::copy(config_path, backup_path)?; + Ok(()) +} +``` + +--- + +## 🎯 成功标准 + +### 必须达成 (Must Have) + +- ✅ 所有现有功能正常工作 +- ✅ 无用户数据丢失 +- ✅ 性能不下降 +- ✅ TypeScript 检查通过 + +### 期望达成 (Should Have) + +- ✅ 代码量减少 40%+ +- ✅ 用户反馈积极 +- ✅ 开发体验提升明显 + +### 可选达成 (Nice to Have) + +- ⭕ 添加自动化测试 +- ⭕ 性能优化 20%+ + +--- + +## 📊 预期成果 + +### 代码质量 + +- **代码行数**: 减少 40-60% +- **文件数量**: UI 组件增加,但单文件更小 +- **可维护性**: 大幅提升 + +### 开发效率 + +- **新功能开发**: 提升 50%+ +- **Bug 修复**: 提升 30%+ +- **代码审查**: 提升 40%+ + +### 用户体验 + +- **界面一致性**: 统一的设计语言 +- **响应速度**: 更好的加载反馈 +- **错误提示**: 更友好的错误信息 + +--- + +## 📚 参考资料 + +- [TanStack Query 文档](https://tanstack.com/query/latest) +- [react-hook-form 文档](https://react-hook-form.com/) +- [shadcn/ui 文档](https://ui.shadcn.com/) +- [Zod 文档](https://zod.dev/) +- [原始 PR #76](https://github.com/farion1231/cc-switch/pull/76) + +--- + +## 📝 注意事项 + +1. **分支管理**: 在新分支进行,不要直接在 main 上修改 +2. **提交粒度**: 每完成一小步就提交,便于回滚 +3. **文档更新**: 同步更新 CLAUDE.md +4. **依赖锁定**: 锁定依赖版本 +5. **沟通协作**: 定期同步进度 + +--- + +**祝重构顺利! 🚀** diff --git a/docs/REFACTORING_REFERENCE.md b/docs/REFACTORING_REFERENCE.md new file mode 100644 index 0000000..ee6c876 --- /dev/null +++ b/docs/REFACTORING_REFERENCE.md @@ -0,0 +1,834 @@ +# 重构快速参考指南 + +> 常见模式和代码示例的速查表 + +--- + +## 📑 目录 + +1. [React Query 使用](#react-query-使用) +2. [react-hook-form 使用](#react-hook-form-使用) +3. [shadcn/ui 组件使用](#shadcnui-组件使用) +4. [代码迁移示例](#代码迁移示例) + +--- + +## React Query 使用 + +### 基础查询 + +```typescript +// 定义查询 Hook +export const useProvidersQuery = (appType: AppType) => { + return useQuery({ + queryKey: ['providers', appType], + queryFn: async () => { + const data = await providersApi.getAll(appType) + return data + }, + }) +} + +// 在组件中使用 +function MyComponent() { + const { data, isLoading, error } = useProvidersQuery('claude') + + if (isLoading) return
Loading...
+ if (error) return
Error: {error.message}
+ + return
{/* 使用 data */}
+} +``` + +### Mutation (变更操作) + +```typescript +// 定义 Mutation Hook +export const useAddProviderMutation = (appType: AppType) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (provider: Provider) => { + return await providersApi.add(provider, appType) + }, + onSuccess: () => { + // 重新获取数据 + queryClient.invalidateQueries({ queryKey: ['providers', appType] }) + toast.success('添加成功') + }, + onError: (error: Error) => { + toast.error(`添加失败: ${error.message}`) + }, + }) +} + +// 在组件中使用 +function AddProviderDialog() { + const mutation = useAddProviderMutation('claude') + + const handleSubmit = (data: Provider) => { + mutation.mutate(data) + } + + return ( + + ) +} +``` + +### 乐观更新 + +```typescript +export const useSwitchProviderMutation = (appType: AppType) => { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (providerId: string) => { + return await providersApi.switch(providerId, appType) + }, + // 乐观更新: 在请求发送前立即更新 UI + onMutate: async (providerId) => { + // 取消正在进行的查询 + await queryClient.cancelQueries({ queryKey: ['providers', appType] }) + + // 保存当前数据(以便回滚) + const previousData = queryClient.getQueryData(['providers', appType]) + + // 乐观更新 + queryClient.setQueryData(['providers', appType], (old: any) => ({ + ...old, + currentProviderId: providerId, + })) + + return { previousData } + }, + // 如果失败,回滚 + onError: (err, providerId, context) => { + queryClient.setQueryData(['providers', appType], context?.previousData) + toast.error('切换失败') + }, + // 无论成功失败,都重新获取数据 + onSettled: () => { + queryClient.invalidateQueries({ queryKey: ['providers', appType] }) + }, + }) +} +``` + +### 依赖查询 + +```typescript +// 第二个查询依赖第一个查询的结果 +const { data: providers } = useProvidersQuery(appType) +const currentProviderId = providers?.currentProviderId + +const { data: currentProvider } = useQuery({ + queryKey: ['provider', currentProviderId], + queryFn: () => providersApi.getById(currentProviderId!), + enabled: !!currentProviderId, // 只有当 ID 存在时才执行 +}) +``` + +--- + +## react-hook-form 使用 + +### 基础表单 + +```typescript +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { z } from 'zod' + +// 定义验证 schema +const schema = z.object({ + name: z.string().min(1, '请输入名称'), + email: z.string().email('邮箱格式不正确'), + age: z.number().min(18, '年龄必须大于18'), +}) + +type FormData = z.infer + +function MyForm() { + const form = useForm({ + resolver: zodResolver(schema), + defaultValues: { + name: '', + email: '', + age: 0, + }, + }) + + const onSubmit = (data: FormData) => { + console.log(data) + } + + return ( +
+ + {form.formState.errors.name && ( + {form.formState.errors.name.message} + )} + + +
+ ) +} +``` + +### 使用 shadcn/ui Form 组件 + +```typescript +import { useForm } from 'react-hook-form' +import { zodResolver } from '@hookform/resolvers/zod' +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from '@/components/ui/form' +import { Input } from '@/components/ui/input' +import { Button } from '@/components/ui/button' + +function MyForm() { + const form = useForm({ + resolver: zodResolver(schema), + }) + + return ( +
+ + ( + + 名称 + + + + + + )} + /> + + + + + ) +} +``` + +### 动态表单验证 + +```typescript +// 根据条件动态验证 +const schema = z.object({ + type: z.enum(['official', 'custom']), + apiKey: z.string().optional(), + baseUrl: z.string().optional(), +}).refine( + (data) => { + // 如果是自定义供应商,必须填写 baseUrl + if (data.type === 'custom') { + return !!data.baseUrl + } + return true + }, + { + message: '自定义供应商必须填写 Base URL', + path: ['baseUrl'], + } +) +``` + +### 手动触发验证 + +```typescript +function MyForm() { + const form = useForm() + + const handleBlur = async () => { + // 验证单个字段 + await form.trigger('name') + + // 验证多个字段 + await form.trigger(['name', 'email']) + + // 验证所有字段 + const isValid = await form.trigger() + } + + return
...
+} +``` + +--- + +## shadcn/ui 组件使用 + +### Dialog (对话框) + +```typescript +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogDescription, + DialogFooter, +} from '@/components/ui/dialog' +import { Button } from '@/components/ui/button' + +function MyDialog() { + const [open, setOpen] = useState(false) + + return ( + + + + 标题 + 描述信息 + + + {/* 内容 */} +
对话框内容
+ + + + + +
+
+ ) +} +``` + +### Select (选择器) + +```typescript +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select' + +function MySelect() { + const [value, setValue] = useState('') + + return ( + + ) +} +``` + +### Tabs (标签页) + +```typescript +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' + +function MyTabs() { + return ( + + + 标签1 + 标签2 + 标签3 + + + +
标签1的内容
+
+ + +
标签2的内容
+
+ + +
标签3的内容
+
+
+ ) +} +``` + +### Toast 通知 (Sonner) + +```typescript +import { toast } from 'sonner' + +// 成功通知 +toast.success('操作成功') + +// 错误通知 +toast.error('操作失败') + +// 加载中 +const toastId = toast.loading('处理中...') +// 完成后更新 +toast.success('处理完成', { id: toastId }) +// 或 +toast.dismiss(toastId) + +// 自定义持续时间 +toast.success('消息', { duration: 5000 }) + +// 带操作按钮 +toast('确认删除?', { + action: { + label: '删除', + onClick: () => handleDelete(), + }, +}) +``` + +--- + +## 代码迁移示例 + +### 示例 1: 状态管理迁移 + +**旧代码** (手动状态管理): + +```typescript +const [providers, setProviders] = useState>({}) +const [currentProviderId, setCurrentProviderId] = useState('') +const [loading, setLoading] = useState(false) +const [error, setError] = useState(null) + +useEffect(() => { + const load = async () => { + setLoading(true) + setError(null) + try { + const data = await window.api.getProviders(appType) + const currentId = await window.api.getCurrentProvider(appType) + setProviders(data) + setCurrentProviderId(currentId) + } catch (err) { + setError(err as Error) + } finally { + setLoading(false) + } + } + load() +}, [appType]) +``` + +**新代码** (React Query): + +```typescript +const { data, isLoading, error } = useProvidersQuery(appType) +const providers = data?.providers || {} +const currentProviderId = data?.currentProviderId || '' +``` + +**减少**: 从 20+ 行到 3 行 + +--- + +### 示例 2: 表单验证迁移 + +**旧代码** (手动验证): + +```typescript +const [name, setName] = useState('') +const [nameError, setNameError] = useState('') +const [apiKey, setApiKey] = useState('') +const [apiKeyError, setApiKeyError] = useState('') + +const validate = () => { + let valid = true + + if (!name.trim()) { + setNameError('请输入名称') + valid = false + } else { + setNameError('') + } + + if (!apiKey.trim()) { + setApiKeyError('请输入 API Key') + valid = false + } else if (apiKey.length < 10) { + setApiKeyError('API Key 长度不足') + valid = false + } else { + setApiKeyError('') + } + + return valid +} + +const handleSubmit = () => { + if (validate()) { + // 提交 + } +} + +return ( +
+ setName(e.target.value)} /> + {nameError && {nameError}} + + setApiKey(e.target.value)} /> + {apiKeyError && {apiKeyError}} + + +
+) +``` + +**新代码** (react-hook-form + zod): + +```typescript +const schema = z.object({ + name: z.string().min(1, '请输入名称'), + apiKey: z.string().min(10, 'API Key 长度不足'), +}) + +const form = useForm({ + resolver: zodResolver(schema), +}) + +return ( +
+ + ( + + + + + + + )} + /> + + ( + + + + + + + )} + /> + + + + +) +``` + +**减少**: 从 40+ 行到 30 行,且更健壮 + +--- + +### 示例 3: 通知系统迁移 + +**旧代码** (自定义通知): + +```typescript +const [notification, setNotification] = useState<{ + message: string + type: 'success' | 'error' +} | null>(null) +const [isVisible, setIsVisible] = useState(false) + +const showNotification = (message: string, type: 'success' | 'error') => { + setNotification({ message, type }) + setIsVisible(true) + setTimeout(() => { + setIsVisible(false) + setTimeout(() => setNotification(null), 300) + }, 3000) +} + +return ( + <> + {notification && ( +
+ {notification.message} +
+ )} + {/* 其他内容 */} + +) +``` + +**新代码** (Sonner): + +```typescript +import { toast } from 'sonner' + +// 在需要的地方直接调用 +toast.success('操作成功') +toast.error('操作失败') + +// 在 main.tsx 中只需添加一次 +import { Toaster } from '@/components/ui/sonner' + + +``` + +**减少**: 从 20+ 行到 1 行调用 + +--- + +### 示例 4: 对话框迁移 + +**旧代码** (自定义 Modal): + +```typescript +const [isOpen, setIsOpen] = useState(false) + +return ( + <> + + + {isOpen && ( +
setIsOpen(false)}> +
e.stopPropagation()}> +
+

标题

+ +
+
+ {/* 内容 */} +
+
+ + +
+
+
+ )} + +) +``` + +**新代码** (shadcn/ui Dialog): + +```typescript +import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog' + +const [isOpen, setIsOpen] = useState(false) + +return ( + <> + + + + + + 标题 + + {/* 内容 */} + + + + + + + +) +``` + +**优势**: +- 无需自定义样式 +- 内置无障碍支持 +- 自动管理焦点和 ESC 键 + +--- + +### 示例 5: API 调用迁移 + +**旧代码** (window.api): + +```typescript +// 添加供应商 +const handleAdd = async (provider: Provider) => { + try { + await window.api.addProvider(provider, appType) + await loadProviders() + showNotification('添加成功', 'success') + } catch (error) { + showNotification('添加失败', 'error') + } +} +``` + +**新代码** (React Query Mutation): + +```typescript +// 在组件中 +const addMutation = useAddProviderMutation(appType) + +const handleAdd = (provider: Provider) => { + addMutation.mutate(provider) + // 成功和错误处理已在 mutation 定义中处理 +} +``` + +**优势**: +- 自动处理 loading 状态 +- 统一的错误处理 +- 自动刷新数据 +- 更少的样板代码 + +--- + +## 常见问题 + +### Q: 如何在 mutation 成功后关闭对话框? + +```typescript +const mutation = useAddProviderMutation(appType) + +const handleSubmit = (data: Provider) => { + mutation.mutate(data, { + onSuccess: () => { + setIsOpen(false) // 关闭对话框 + }, + }) +} +``` + +### Q: 如何在表单中使用异步验证? + +```typescript +const schema = z.object({ + name: z.string().refine( + async (name) => { + // 检查名称是否已存在 + const exists = await checkNameExists(name) + return !exists + }, + { message: '名称已存在' } + ), +}) +``` + +### Q: 如何手动刷新 Query 数据? + +```typescript +const queryClient = useQueryClient() + +// 方式1: 使缓存失效,触发重新获取 +queryClient.invalidateQueries({ queryKey: ['providers', appType] }) + +// 方式2: 直接刷新 +queryClient.refetchQueries({ queryKey: ['providers', appType] }) + +// 方式3: 更新缓存数据 +queryClient.setQueryData(['providers', appType], newData) +``` + +### Q: 如何在组件外部使用 toast? + +```typescript +// 直接导入并使用即可 +import { toast } from 'sonner' + +export const someUtil = () => { + toast.success('工具函数中的通知') +} +``` + +--- + +## 调试技巧 + +### React Query DevTools + +```typescript +// 在 main.tsx 中添加 +import { ReactQueryDevtools } from '@tanstack/react-query-devtools' + + + + + +``` + +### 查看表单状态 + +```typescript +const form = useForm() + +// 在开发模式下打印表单状态 +console.log('Form values:', form.watch()) +console.log('Form errors:', form.formState.errors) +console.log('Is valid:', form.formState.isValid) +``` + +--- + +## 性能优化建议 + +### 1. 避免不必要的重渲染 + +```typescript +// 使用 React.memo +export const ProviderCard = React.memo(({ provider, onEdit }: Props) => { + // ... +}) + +// 或使用 useMemo +const sortedProviders = useMemo( + () => Object.values(providers).sort(...), + [providers] +) +``` + +### 2. Query 配置优化 + +```typescript +const { data } = useQuery({ + queryKey: ['providers', appType], + queryFn: fetchProviders, + staleTime: 1000 * 60 * 5, // 5分钟内不重新获取 + gcTime: 1000 * 60 * 10, // 10分钟后清除缓存 +}) +``` + +### 3. 表单性能优化 + +```typescript +// 使用 mode 控制验证时机 +const form = useForm({ + mode: 'onBlur', // 失去焦点时验证 + // mode: 'onChange', // 每次输入都验证(较慢) + // mode: 'onSubmit', // 提交时验证(最快) +}) +``` + +--- + +**提示**: 将此文档保存在浏览器书签或编辑器中,方便随时查阅! From cc0b7053aa61674c3882e10d57c4d4a556e95070 Mon Sep 17 00:00:00 2001 From: Jason Date: Thu, 16 Oct 2025 10:00:22 +0800 Subject: [PATCH 002/129] feat: complete stage 1 infrastructure --- components.json | 21 + docs/REFACTORING_MASTER_PLAN.md | 14 +- package.json | 19 +- pnpm-lock.yaml | 1012 +++++++++++++++++++++++++++++++ src/components/ui/button.tsx | 56 ++ src/components/ui/dialog.tsx | 117 ++++ src/components/ui/form.tsx | 165 +++++ src/components/ui/input.tsx | 23 + src/components/ui/label.tsx | 20 + src/components/ui/select.tsx | 122 ++++ src/components/ui/sonner.tsx | 22 + src/components/ui/switch.tsx | 26 + src/components/ui/tabs.tsx | 52 ++ src/components/ui/textarea.tsx | 22 + src/lib/api/index.ts | 6 + src/lib/api/mcp.ts | 69 +++ src/lib/api/providers.ts | 75 +++ src/lib/api/settings.ts | 52 ++ src/lib/api/types.ts | 1 + src/lib/api/usage.ts | 15 + src/lib/api/vscode.ts | 113 ++++ src/lib/query/index.ts | 3 + src/lib/query/mutations.ts | 155 +++++ src/lib/query/queries.ts | 79 +++ src/lib/query/queryClient.ts | 14 + src/lib/schemas/mcp.ts | 24 + src/lib/schemas/provider.ts | 23 + src/lib/schemas/settings.ts | 21 + src/lib/utils.ts | 6 + tsconfig.json | 6 +- vite.config.mts | 6 + 31 files changed, 2350 insertions(+), 9 deletions(-) create mode 100644 components.json create mode 100644 src/components/ui/button.tsx create mode 100644 src/components/ui/dialog.tsx create mode 100644 src/components/ui/form.tsx create mode 100644 src/components/ui/input.tsx create mode 100644 src/components/ui/label.tsx create mode 100644 src/components/ui/select.tsx create mode 100644 src/components/ui/sonner.tsx create mode 100644 src/components/ui/switch.tsx create mode 100644 src/components/ui/tabs.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/lib/api/index.ts create mode 100644 src/lib/api/mcp.ts create mode 100644 src/lib/api/providers.ts create mode 100644 src/lib/api/settings.ts create mode 100644 src/lib/api/types.ts create mode 100644 src/lib/api/usage.ts create mode 100644 src/lib/api/vscode.ts create mode 100644 src/lib/query/index.ts create mode 100644 src/lib/query/mutations.ts create mode 100644 src/lib/query/queries.ts create mode 100644 src/lib/query/queryClient.ts create mode 100644 src/lib/schemas/mcp.ts create mode 100644 src/lib/schemas/provider.ts create mode 100644 src/lib/schemas/settings.ts create mode 100644 src/lib/utils.ts diff --git a/components.json b/components.json new file mode 100644 index 0000000..6977d86 --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + } +} diff --git a/docs/REFACTORING_MASTER_PLAN.md b/docs/REFACTORING_MASTER_PLAN.md index e2af742..caf5fa8 100644 --- a/docs/REFACTORING_MASTER_PLAN.md +++ b/docs/REFACTORING_MASTER_PLAN.md @@ -870,7 +870,7 @@ export function useDragSort( | 阶段 | 目标 | 工期 | 产出 | | ---------- | -------------- | ------------ | ---------------------------- | | **阶段 0** | 准备环境 | 1 天 | 依赖安装、配置完成 | -| **阶段 1** | 搭建基础设施 | 2-3 天 | API 层、Query Hooks 完成 | +| **阶段 1** | 搭建基础设施(✅ 已完成) | 2-3 天 | API 层、Query Hooks 完成 | | **阶段 2** | 重构核心功能 | 3-4 天 | App.tsx、ProviderList 完成 | | **阶段 3** | 重构设置和辅助 | 2-3 天 | SettingsDialog、通知系统完成 | | **阶段 4** | 清理和优化 | 1-2 天 | 旧代码删除、优化完成 | @@ -1006,12 +1006,12 @@ pnpm typecheck # 确保类型检查通过 #### 任务清单 -- [ ] 创建工具函数 (`lib/utils.ts`) -- [ ] 添加基础 UI 组件 (Button, Dialog, Input, Form 等) -- [ ] 创建 Query Client 配置 -- [ ] 封装 API 层 (providers, settings, mcp) -- [ ] 创建 Query Hooks (queries, mutations) -- [ ] 创建 Zod Schemas +- [x] 创建工具函数 (`lib/utils.ts`) +- [x] 添加基础 UI 组件 (Button, Dialog, Input, Form 等) +- [x] 创建 Query Client 配置 +- [x] 封装 API 层 (providers, settings, mcp) +- [x] 创建 Query Hooks (queries, mutations) +- [x] 创建 Zod Schemas #### 详细步骤 diff --git a/package.json b/package.json index 3f3b086..4d3e087 100644 --- a/package.json +++ b/package.json @@ -35,20 +35,37 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@hookform/resolvers": "^5.2.2", + "@radix-ui/react-checkbox": "^1.3.3", + "@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-select": "^2.2.6", + "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-switch": "^1.2.6", + "@radix-ui/react-tabs": "^1.1.13", "@tailwindcss/vite": "^4.1.13", + "@tanstack/react-query": "^5.90.3", "@tauri-apps/api": "^2.8.0", "@tauri-apps/plugin-dialog": "^2.4.0", "@tauri-apps/plugin-process": "^2.0.0", "@tauri-apps/plugin-store": "^2.0.0", "@tauri-apps/plugin-updater": "^2.0.0", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", "codemirror": "^6.0.2", "i18next": "^25.5.2", "jsonc-parser": "^3.2.1", "lucide-react": "^0.542.0", + "next-themes": "^0.4.6", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-hook-form": "^7.65.0", "react-i18next": "^16.0.0", "smol-toml": "^1.4.2", - "tailwindcss": "^4.1.13" + "sonner": "^2.0.7", + "tailwind-merge": "^3.3.1", + "tailwindcss": "^4.1.13", + "zod": "^4.1.12" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f46cb7c..46b1213 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,9 +35,39 @@ importers: '@dnd-kit/utilities': specifier: ^3.2.2 version: 3.2.2(react@18.3.1) + '@hookform/resolvers': + specifier: ^5.2.2 + version: 5.2.2(react-hook-form@7.65.0(react@18.3.1)) + '@radix-ui/react-checkbox': + specifier: ^1.3.3 + version: 1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dialog': + specifier: ^1.1.15 + version: 1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-dropdown-menu': + specifier: ^2.1.16 + version: 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-label': + specifier: ^2.1.7 + version: 2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-select': + specifier: ^2.2.6 + version: 2.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': + specifier: ^1.2.3 + version: 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-switch': + specifier: ^1.2.6 + version: 1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-tabs': + specifier: ^1.1.13 + version: 1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@tailwindcss/vite': specifier: ^4.1.13 version: 4.1.13(vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1)) + '@tanstack/react-query': + specifier: ^5.90.3 + version: 5.90.3(react@18.3.1) '@tauri-apps/api': specifier: ^2.8.0 version: 2.8.0 @@ -53,6 +83,12 @@ importers: '@tauri-apps/plugin-updater': specifier: ^2.0.0 version: 2.9.0 + class-variance-authority: + specifier: ^0.7.1 + version: 0.7.1 + clsx: + specifier: ^2.1.1 + version: 2.1.1 codemirror: specifier: ^6.0.2 version: 6.0.2 @@ -65,21 +101,36 @@ importers: lucide-react: specifier: ^0.542.0 version: 0.542.0(react@18.3.1) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.2.0 version: 18.3.1 react-dom: specifier: ^18.2.0 version: 18.3.1(react@18.3.1) + react-hook-form: + specifier: ^7.65.0 + version: 7.65.0(react@18.3.1) react-i18next: specifier: ^16.0.0 version: 16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2) smol-toml: specifier: ^1.4.2 version: 1.4.2 + sonner: + specifier: ^2.0.7 + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tailwind-merge: + specifier: ^3.3.1 + version: 3.3.1 tailwindcss: specifier: ^4.1.13 version: 4.1.13 + zod: + specifier: ^4.1.12 + version: 4.1.12 devDependencies: '@tauri-apps/cli': specifier: ^2.8.0 @@ -389,6 +440,26 @@ packages: cpu: [x64] os: [win32] + '@floating-ui/core@1.7.3': + resolution: {integrity: sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==} + + '@floating-ui/dom@1.7.4': + resolution: {integrity: sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==} + + '@floating-ui/react-dom@2.1.6': + resolution: {integrity: sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.10': + resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==} + + '@hookform/resolvers@5.2.2': + resolution: {integrity: sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==} + peerDependencies: + react-hook-form: ^7.55.0 + '@isaacs/fs-minipass@4.0.1': resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} engines: {node: '>=18.0.0'} @@ -430,6 +501,375 @@ packages: '@marijn/find-cluster-break@1.0.2': resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-arrow@1.1.7': + 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/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + 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/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + 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/react-dropdown-menu@2.1.16': + 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/react-focus-guards@1.1.3': + 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/react-focus-scope@1.1.7': + 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/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.7': + 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/react-menu@2.1.16': + 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/react-popper@1.2.8': + 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/react-portal@1.1.9': + 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/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + 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/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + 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/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + 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/react-use-rect@1.1.1': + 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/react-use-size@1.1.1': + 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/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + 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/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@rolldown/pluginutils@1.0.0-beta.27': resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==} @@ -544,6 +984,9 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + '@tailwindcss/node@4.1.13': resolution: {integrity: sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw==} @@ -638,6 +1081,14 @@ packages: peerDependencies: vite: ^5.2.0 || ^6 || ^7 + '@tanstack/query-core@5.90.3': + resolution: {integrity: sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==} + + '@tanstack/react-query@5.90.3': + resolution: {integrity: sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==} + peerDependencies: + react: ^18 || ^19 + '@tauri-apps/api@2.8.0': resolution: {integrity: sha512-ga7zdhbS2GXOMTIZRT0mYjKJtR9fivsXzsyq5U3vjDL0s6DTMwYRm0UHNjzTY5dh4+LSC68Sm/7WEiimbQNYlw==} @@ -764,6 +1215,10 @@ packages: peerDependencies: vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + browserslist@4.25.1: resolution: {integrity: sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -776,6 +1231,13 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + codemirror@6.0.2: resolution: {integrity: sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==} @@ -801,6 +1263,9 @@ packages: resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} engines: {node: '>=8'} + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + electron-to-chromium@1.5.197: resolution: {integrity: sha512-m1xWB3g7vJ6asIFz+2pBUbq3uGmfmln1M9SSvBe4QIFWYrRHylP73zL/3nMjDmwz8V+1xAXQDfBd6+HPW0WvDQ==} @@ -826,6 +1291,10 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} @@ -964,6 +1433,12 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + next-themes@0.4.6: + 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 + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -984,6 +1459,12 @@ packages: peerDependencies: react: ^18.3.1 + react-hook-form@7.65.0: + resolution: {integrity: sha512-xtOzDz063WcXvGWaHgLNrNzlsdFgtUWcb32E6WFaGTd7kPZG3EeDusjdZfUsPwKCKVXy1ZlntifaHZ4l8pAsmw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + react-i18next@16.0.0: resolution: {integrity: sha512-JQ+dFfLnFSKJQt7W01lJHWRC0SX7eDPobI+MSTJ3/gP39xH2g33AuTE7iddAfXYHamJdAeMGM0VFboPaD3G68Q==} peerDependencies: @@ -1004,6 +1485,36 @@ packages: resolution: {integrity: sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==} engines: {node: '>=0.10.0'} + react-remove-scroll-bar@2.3.8: + 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 + + react-remove-scroll@2.7.1: + 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 + + react-style-singleton@2.2.3: + 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 + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -1024,6 +1535,12 @@ packages: resolution: {integrity: sha512-rInDH6lCNiEyn3+hH8KVGFdbjc099j47+OSgbMrfDYX1CmXLfdKd7qi6IfcWj2wFxvSVkuI46M+wPGYfEOEj6g==} engines: {node: '>= 18'} + sonner@2.0.7: + 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 + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1031,6 +1548,9 @@ packages: style-mod@4.1.2: resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + tailwind-merge@3.3.1: + resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} + tailwindcss@4.1.13: resolution: {integrity: sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w==} @@ -1059,6 +1579,26 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + vite@5.4.19: resolution: {integrity: sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1104,6 +1644,9 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} + zod@4.1.12: + resolution: {integrity: sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==} + snapshots: '@ampproject/remapping@2.3.0': @@ -1387,6 +1930,28 @@ snapshots: '@esbuild/win32-x64@0.21.5': optional: true + '@floating-ui/core@1.7.3': + dependencies: + '@floating-ui/utils': 0.2.10 + + '@floating-ui/dom@1.7.4': + dependencies: + '@floating-ui/core': 1.7.3 + '@floating-ui/utils': 0.2.10 + + '@floating-ui/react-dom@2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/dom': 1.7.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + '@floating-ui/utils@0.2.10': {} + + '@hookform/resolvers@5.2.2(react-hook-form@7.65.0(react@18.3.1))': + dependencies: + '@standard-schema/utils': 0.3.0 + react-hook-form: 7.65.0(react@18.3.1) + '@isaacs/fs-minipass@4.0.1': dependencies: minipass: 7.1.2 @@ -1436,6 +2001,370 @@ snapshots: '@marijn/find-cluster-break@1.0.2': {} + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-context@1.1.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-direction@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-id@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-label@2.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@floating-ui/react-dom': 2.1.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-rect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-select@2.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-slot': 1.2.3(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + aria-hidden: 1.2.6 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-remove-scroll: 2.7.1(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-slot@1.2.3(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-previous': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-size': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-direction': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-id': 1.1.1(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@18.3.23)(react@18.3.1) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-previous@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-rect@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-use-size@1.1.1(@types/react@18.3.23)(react@18.3.1)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.23)(react@18.3.1) + react: 18.3.1 + optionalDependencies: + '@types/react': 18.3.23 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.7(@types/react@18.3.23))(@types/react@18.3.23)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + '@types/react-dom': 18.3.7(@types/react@18.3.23) + + '@radix-ui/rect@1.1.1': {} + '@rolldown/pluginutils@1.0.0-beta.27': {} '@rollup/rollup-android-arm-eabi@4.46.2': @@ -1498,6 +2427,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.46.2': optional: true + '@standard-schema/utils@0.3.0': {} + '@tailwindcss/node@4.1.13': dependencies: '@jridgewell/remapping': 2.3.5 @@ -1569,6 +2500,13 @@ snapshots: tailwindcss: 4.1.13 vite: 5.4.19(@types/node@20.19.9)(lightningcss@1.30.1) + '@tanstack/query-core@5.90.3': {} + + '@tanstack/react-query@5.90.3(react@18.3.1)': + dependencies: + '@tanstack/query-core': 5.90.3 + react: 18.3.1 + '@tauri-apps/api@2.8.0': {} '@tauri-apps/cli-darwin-arm64@2.8.1': @@ -1684,6 +2622,10 @@ snapshots: transitivePeerDependencies: - supports-color + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + browserslist@4.25.1: dependencies: caniuse-lite: 1.0.30001731 @@ -1695,6 +2637,12 @@ snapshots: chownr@3.0.0: {} + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + clsx@2.1.1: {} + codemirror@6.0.2: dependencies: '@codemirror/autocomplete': 6.18.7 @@ -1717,6 +2665,8 @@ snapshots: detect-libc@2.0.4: {} + detect-node-es@1.1.0: {} + electron-to-chromium@1.5.197: {} enhanced-resolve@5.18.3: @@ -1757,6 +2707,8 @@ snapshots: gensync@1.0.0-beta.2: {} + get-nonce@1.0.1: {} + graceful-fs@4.2.11: {} html-parse-stringify@3.0.1: @@ -1852,6 +2804,11 @@ snapshots: nanoid@3.3.11: {} + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + node-releases@2.0.19: {} picocolors@1.1.1: {} @@ -1870,6 +2827,10 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-hook-form@7.65.0(react@18.3.1): + dependencies: + react: 18.3.1 + react-i18next@16.0.0(i18next@25.5.2(typescript@5.9.2))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(typescript@5.9.2): dependencies: '@babel/runtime': 7.28.4 @@ -1882,6 +2843,33 @@ snapshots: react-refresh@0.17.0: {} + react-remove-scroll-bar@2.3.8(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + react-remove-scroll@2.7.1(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + react-remove-scroll-bar: 2.3.8(@types/react@18.3.23)(react@18.3.1) + react-style-singleton: 2.2.3(@types/react@18.3.23)(react@18.3.1) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@18.3.23)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.23)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.23 + + react-style-singleton@2.2.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + get-nonce: 1.0.1 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -1920,10 +2908,17 @@ snapshots: smol-toml@1.4.2: {} + sonner@2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + source-map-js@1.2.1: {} style-mod@4.1.2: {} + tailwind-merge@3.3.1: {} + tailwindcss@4.1.13: {} tapable@2.2.3: {} @@ -1949,6 +2944,21 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + use-callback-ref@1.3.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + + use-sidecar@1.1.3(@types/react@18.3.23)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.23 + vite@5.4.19(@types/node@20.19.9)(lightningcss@1.30.1): dependencies: esbuild: 0.21.5 @@ -1966,3 +2976,5 @@ snapshots: yallist@3.1.1: {} yallist@5.0.0: {} + + zod@4.1.12: {} diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx new file mode 100644 index 0000000..6b1375f --- /dev/null +++ b/src/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; +import { cn } from "@/lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: + "bg-primary text-primary-foreground shadow hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", + outline: + "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2", + sm: "h-8 rounded-md px-3 text-xs", + lg: "h-10 rounded-md px-8", + icon: "h-9 w-9", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean; +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ( + + ); + } +); +Button.displayName = "Button"; + +export { Button, buttonVariants }; diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx new file mode 100644 index 0000000..285c6db --- /dev/null +++ b/src/components/ui/dialog.tsx @@ -0,0 +1,117 @@ +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogClose = DialogPrimitive.Close; + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + 关闭 + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+); +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { + Dialog, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, + DialogClose, +}; diff --git a/src/components/ui/form.tsx b/src/components/ui/form.tsx new file mode 100644 index 0000000..89ed363 --- /dev/null +++ b/src/components/ui/form.tsx @@ -0,0 +1,165 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { Slot } from "@radix-ui/react-slot"; +import { + Controller, + FormProvider, + useFormContext, + type ControllerProps, + type FieldPath, + type FieldValues, +} from "react-hook-form"; +import { cn } from "@/lib/utils"; + +const Form = FormProvider; + +type FormFieldContextValue< + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +> = { + name: TName; +}; + +const FormFieldContext = React.createContext( + {} as FormFieldContextValue +); + +const FormField = < + TFieldValues extends FieldValues = FieldValues, + TName extends FieldPath = FieldPath +>({ + ...props +}: ControllerProps) => { + return ( + + + + ); +}; + +const useFormField = () => { + const fieldContext = React.useContext(FormFieldContext); + const itemContext = React.useContext(FormItemContext); + const { getFieldState, formState } = useFormContext(); + + const fieldState = getFieldState(fieldContext.name, formState); + + if (!fieldContext) { + throw new Error("useFormField should be used within "); + } + + const id = itemContext.id; + + return { + id, + name: fieldContext.name, + formItemId: `${id}-form-item`, + formDescriptionId: `${id}-form-item-description`, + formMessageId: `${id}-form-item-message`, + ...fieldState, + }; +}; + +const FormItemContext = React.createContext<{ id: string }>( + {} as { id: string } +); + +const FormItem = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const id = React.useId(); + + return ( + +
+ + ); +}); +FormItem.displayName = "FormItem"; + +const FormLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => { + const { error, formItemId } = useFormField(); + + return ( + + ); +}); +FormLabel.displayName = LabelPrimitive.Root.displayName; + +const FormControl = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ ...props }, ref) => { + const { formItemId, formDescriptionId, formMessageId } = useFormField(); + + return ( + + ); +}); +FormControl.displayName = "FormControl"; + +const FormDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => { + const { formDescriptionId } = useFormField(); + + return ( +

+ ); +}); +FormDescription.displayName = "FormDescription"; + +const FormMessage = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, children, ...props }, ref) => { + const { error, formMessageId } = useFormField(); + const body = error?.message ?? children; + + if (!body) { + return null; + } + + return ( +

+ {body} +

+ ); +}); +FormMessage.displayName = "FormMessage"; + +export { + Form, + FormField, + FormItem, + FormLabel, + FormControl, + FormDescription, + FormMessage, + useFormField, +}; diff --git a/src/components/ui/input.tsx b/src/components/ui/input.tsx new file mode 100644 index 0000000..dc50ca3 --- /dev/null +++ b/src/components/ui/input.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export type InputProps = React.InputHTMLAttributes; + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ); + } +); +Input.displayName = "Input"; + +export { Input }; diff --git a/src/components/ui/label.tsx b/src/components/ui/label.tsx new file mode 100644 index 0000000..ced2949 --- /dev/null +++ b/src/components/ui/label.tsx @@ -0,0 +1,20 @@ +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cn } from "@/lib/utils"; + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/src/components/ui/select.tsx b/src/components/ui/select.tsx new file mode 100644 index 0000000..c7db9af --- /dev/null +++ b/src/components/ui/select.tsx @@ -0,0 +1,122 @@ +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown, ChevronUp } from "lucide-react"; +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + + + + {children} + + + + + + +)); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, +}; diff --git a/src/components/ui/sonner.tsx b/src/components/ui/sonner.tsx new file mode 100644 index 0000000..08392e7 --- /dev/null +++ b/src/components/ui/sonner.tsx @@ -0,0 +1,22 @@ +import { Toaster as SonnerToaster } from "sonner"; + +export function Toaster() { + return ( + + ); +} diff --git a/src/components/ui/switch.tsx b/src/components/ui/switch.tsx new file mode 100644 index 0000000..4187e3a --- /dev/null +++ b/src/components/ui/switch.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import * as SwitchPrimitives from "@radix-ui/react-switch"; +import { cn } from "@/lib/utils"; + +const Switch = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +Switch.displayName = SwitchPrimitives.Root.displayName; + +export { Switch }; diff --git a/src/components/ui/tabs.tsx b/src/components/ui/tabs.tsx new file mode 100644 index 0000000..1971862 --- /dev/null +++ b/src/components/ui/tabs.tsx @@ -0,0 +1,52 @@ +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx new file mode 100644 index 0000000..3b200f3 --- /dev/null +++ b/src/components/ui/textarea.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; +import { cn } from "@/lib/utils"; + +export type TextareaProps = React.TextareaHTMLAttributes; + +const Textarea = React.forwardRef( + ({ className, ...props }, ref) => { + return ( +