## Language
Always respond in Chinese (中文). Use the user's language for all conversations and explanations. Code, commands, and technical terms can remain in English.

页面内容（UI 文案、标签、提示、错误信息）默认使用 **英语**。仅当用户明确要求时才做多语言。

# Project Guidelines — appwebks

## Tech Stack
- **Runtime**: Bun (packageManager, scripts, lockfile)
- **Framework**: React 19, TypeScript 6.0, Vite 8.0
- **Styling**: Tailwind CSS 4.3 + clsx + tailwind-merge（CVA 变体模式）
- **State**: @tanstack/react-query v5（服务端状态）+ URL search/path params（客户端状态）
- **Routing**: react-router-dom v7
- **HTTP**: ky（优先）+ axios
- **Validation**: zod v4
- **Icons**: lucide-react

## Project Structure
```
src/
  pages/          # 页面组件，每个 page 一个文件夹
  components/     # 共享组件，按功能域分文件夹
  hooks/          # 自定义 hooks（含 react-query hooks）
  client/         # 自动生成 API 代码（只读！）
  lib/            # 工具函数、常量
  types/          # 共享类型定义
```

## Routing

使用 React Router v7 **Data Mode**：`createBrowserRouter` + Route Object 配置。

- 路由配置集中在 `src/routes.ts`，通过 `createBrowserRouter` 组装
- 每个路由模块（Component + loader + action）为独立文件，放在 `src/pages/` 下
- 路由文件不容纳 JSX，只 export RouteObject[]

```ts
// src/routes.ts — 路由配置，只组装不渲染
import { createBrowserRouter } from 'react-router';
import { RepoListPage } from '@/pages/RepoList/RepoListPage';
import { RepoDetailPage } from '@/pages/RepoDetail/RepoDetailPage';
import { RootLayout } from '@/pages/RootLayout';

export const router = createBrowserRouter([
  {
    path: '/',
    Component: RootLayout,
    children: [
      { index: true, Component: RepoListPage },
      { path: 'repo/:id', Component: RepoDetailPage },
    ],
  },
]);
```

```tsx
// src/pages/RepoList/RepoListPage.tsx — 路由模块
export function RepoListPage() {
  const { data } = useRepoList();
  return <div>...</div>;
}
```

## Code Style

### File Naming
- React 组件文件：**PascalCase**（`Button.tsx`、`UserList.tsx`）
- Hooks / utils / types 文件：**camelCase**（`useAuth.ts`、`formatDate.ts`）
- 页面文件夹：**PascalCase**（`UserProfile/`、`RepoList/`）

### Component Conventions
- 组件用 **function 声明**，不用 arrow function
- Props 类型用 **interface**，写在组件文件顶部，不单独导出：
  ```tsx
  interface ButtonProps {
    size?: 'sm' | 'md' | 'lg';
    variant?: 'primary' | 'secondary';
    children: React.ReactNode;
  }

  export function Button({ size = 'md', variant = 'primary', children }: ButtonProps) {
    // ...
  }
  ```
- 原生 HTML 元素 Props 用 `React.ComponentProps<'button'>` 派生
- 单文件不超过 **200 行**，函数不超过 **50 行**
- 复杂逻辑抽成 hooks

### Import Order
Biome 的 `organizeImports` 会自动按以下顺序排列 import，组间空行分隔：
1. React / React Router / react-query 等核心库
2. 第三方 UI / 工具库（lucide-react、clsx、zod...）
3. `@/` 别名导入
4. 相对路径导入（`./`、`../`）

```tsx
import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { Link } from 'react-router-dom';

import { Search, Plus } from 'lucide-react';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

import { repoService } from '@/client/services/RepoService';
import { Button } from '@/components/ui/Button';

import { useRepoList } from './useRepoList';
```

## Styling

### Tailwind + CVA
用 `clsx` + `tailwind-merge` 管理组件变体，按 CVA 模式组织：

```tsx
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';

function cn(...inputs: (string | undefined | false | null)[]) {
  return twMerge(clsx(inputs));
}

const variants = {
  size: {
    sm: 'px-2 py-1 text-sm',
    md: 'px-4 py-2 text-base',
    lg: 'px-6 py-3 text-lg',
  },
  variant: {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
  },
} as const;

interface ButtonProps {
  size?: keyof typeof variants.size;
  variant?: keyof typeof variants.variant;
}

export function Button({ size = 'md', variant = 'primary', ... }: ButtonProps) {
  return <button className={cn(variants.size[size], variants.variant[variant])} ... />;
}
```

- **禁止新增任何 `.css` 文件**。项目只保留一个全局入口 CSS（`src/index.css`），仅用于：
  - `@import "tailwindcss"`
  - CSS 自定义属性（主题变量）
  - 全局 reset
- 组件样式**必须**使用 Tailwind 原子类，不得引用局部 CSS 文件
- 禁止 `@apply` 抽取类名
- `cn()` 函数放在 `@/lib/cn.ts` 中统一导入

## State Management

### URL-Driven State
- UI 状态（分页、筛选、排序、tab 切换）**必须**编码到 URL search/path params
- 用 `useSearchParams()` 读写，组件内部 `useState` 最小化
- 好处：可分享、可书签、浏览器前进后退自然工作

### Server State (react-query)
- 所有 API 调用必须通过 react-query hooks（`useQuery` / `useMutation`）
- 禁止在组件中直接调用 `src/client/` 的服务方法
- Query key 约定：`['domain', 'action', ...params]`，如 `['repos', 'list', { page, q }]`

**注意**：TanStack Query v5 已移除 `useQuery` 上的 `onError` / `onSuccess` / `onSettled` 回调：
- `useQuery` 错误处理：组件内检查 `error` 状态，或全局通过 `QueryClient.setDefaultOptions` 配置
- `useMutation` 错误处理：`onError` 回调仍然可用

```tsx
// useQuery — 错误在 error 字段上处理
import { useQuery } from '@tanstack/react-query';

const { data, error, isLoading } = useQuery({
  queryKey: ['repos', 'list', { page }],
  queryFn: () => repoService.listRepos({ page }),
});

// useMutation — onError 仍然可用
import { useMutation } from '@tanstack/react-query';
import { toast } from 'sonner';

const mutation = useMutation({
  mutationFn: (params) => repoService.createRepo(params),
  onError: (error) => toast.error(error.message),
});
```

## API Layer

### Generated Code（src/client/）
- **视为只读**，禁止手动修改任何 `src/client/` 下的文件
- 需要变更时修改 `openapi.json` 或 `genapi.js`，然后运行 `bun run genapi` 重新生成

### Custom Hooks 封装
每个业务域在 `src/hooks/` 下创建对应的 react-query hook 文件：

```
src/hooks/
  useRepoList.ts       # useQuery: repo list + filters
  useRepoDetail.ts     # useQuery: single repo
  useCreateRepo.ts     # useMutation: create repo
  useBranchList.ts     # useQuery: branch list
```

hook 职责：
- 组装 queryKey（如 `['repos', 'list', { page, q }]`）
- 调用 `src/client/` 服务方法
- 转换入参（筛选条件 → API 参数）、归一化错误
- **默认返回 React Query 原始结果**：不隐藏 `isPending` / `isFetching` / `error` / `refetch` 等状态

```tsx
// src/hooks/useRepoList.ts — 典型范式
import { useQuery, keepPreviousData } from '@tanstack/react-query';
import { repoService } from '@/client/services/RepoService';

export function useRepoList(filters: RepoFilters) {
  return useQuery({
    queryKey: ['repos', 'list', filters],
    queryFn: ({ signal }) => repoService.listRepos({ ...filters }, { signal }),
    placeholderData: keepPreviousData,
  });
}
// 组件侧直接解构：const { data, isPending, isFetching, error, refetch } = useRepoList(filters);
```

### Error Handling
- 全局 **ErrorBoundary** 捕获 React 渲染错误
- `useMutation` 的 API 错误通过 `onError` 回调统一分发 toast
- `useQuery` 的 API 错误通过组件内判断 `error` 状态处理，或使用全局 `throwOnError` + ErrorBoundary
- 禁止在组件中 try/catch API 调用后自己做 UI 错误展示

```tsx
// 全局 QueryClient 配置（在 App 入口）
import { QueryClient } from '@tanstack/react-query';
import { toast } from 'sonner';

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      throwOnError: true,  // Query 抛错 → ErrorBoundary 捕获
    },
    mutations: {
      onError: (error) => toast.error(error.message),
    },
  },
});
```

## Git

### Branching
- **Trunk-based**：直接在 `main` 上做小而频繁的提交
- 不做长期 feature 分支，未完成功能用 feature flag 或路由守卫控制
- 需要时可以从 main 拉短期分支，但合入后立即删除

### Commits
- **Conventional Commits**：`feat` / `fix` / `refactor` / `docs` / `test` / `chore`
- 原子提交：一个 commit 只做一件事
- 禁止 force push 到 main

## Formatter（Biome）
项目使用 **Biome** 作为唯一的格式化 & lint 工具。配置见 `biome.json`。

### 格式化规则
| 配置项 | 值 |
|--------|-----|
| 缩进 | 2 空格 |
| 引号 | 单引号（JS/TS），双引号（JSX） |
| 分号 | 总是 |
| 尾逗号 | 总是 |
| 行宽 | 100 |
| 换行符 | LF |
| 箭头函数括号 | 总是保留 |

### 命令
```bash
bun run format     # 格式化 src/ 下所有文件
bun run lint       # 仅 lint 检查，不修改
bun run check      # 格式化 + lint，写入修复
```

### Lint 规则
- 启用 **recommended** 规则集
- `noUnusedVariables`: error — 禁止未使用变量
- `useHookAtTopLevel`: error — React hooks 必须在顶层调用
- `noParameterAssign`: error — 禁止给参数重新赋值
- `useConst`: error — 能用 const 就别用 let
- `useTemplate`: warn — 字符串拼接改用模板字符串

## Quality Gate
AI 助手完成代码修改后，**必须依次运行以下命令**确保通过：

```bash
bun run check      # Biome 格式化 + lint
bun run build      # TypeScript 类型检查 + Vite 构建
```

任一步骤失败不允许提交。

## Testing
当前阶段 **不强制写测试**，快速迭代优先。后续需要时优先补关键路径的单元测试（vitest）。

## Environment Variables
- **禁止**在业务代码中直接读取 `import.meta.env.*`
- 所有环境变量必须通过 `@/lib/env` 集中访问，统一提供默认值与类型安全

```ts
// src/lib/env.ts
export const env = {
  API_BASE_URL: import.meta.env.VITE_API_BASE_URL ?? '/api',
  DEV: import.meta.env.DEV,
  MODE: import.meta.env.MODE,
} as const;
```

## Import Discipline
- **禁止**页面组件直接导入 `@/client/**`
- 只有 `src/hooks/**` 或 `src/features/**/api/**` 可以导入 generated client
- 页面通过自定义 hook 访问 API 数据，不接触底层 client

```
✅ src/hooks/useRepoList.ts  →  import { RepoService } from '@/client/services/RepoService'
✅ src/features/repos/api.ts →  import { RepoService } from '@/client/services/RepoService'
❌ src/pages/RepoList.tsx    →  import { RepoService } from '@/client/services/RepoService'
```

## Safety
- 禁止硬编码 secrets / API keys / tokens
- 所有用户输入必须验证（zod schema）
- 错误必须显式处理，禁止 silent failure
- 禁止使用 `// ── xxxx ──────────` 分隔注释

## Reference
- `hooks.txt` — 项目已安装依赖的完整 Hooks 清单
- `biome.json` — Biome 格式化 / Lint 配置
- `openapi.json` — API Schema（用于生成 `src/client/`）
- `genapi.js` — API 代码生成脚本（`bun run genapi`）
