TokenToSea提示词广场
English 控制台

通用 React / Next.js 项目指令

编程开发·系统提示 / 角色

在聊天中使用

面向 React、Next.js、TypeScript 与 Tailwind 项目的通用 AI 代理规则,可作为 AGENTS.md 或 CLAUDE.md 使用。

# 通用 React / Next.js 项目指令

> 目的:为使用 React + TypeScript、Next.js + TypeScript 和 Tailwind CSS 开发各类项目提供通用规则。  
> 使用方式:将此文件置于新项目的根目录下,命名为 `AGENTS.md`、`CLAUDE.md` 或 `PROJECT_RULES.md`,或将其用作 AI 代理的基础指令集。  
> 重要提示:这些指令不包含特定于产品的规则。所有与单个项目相关的内容应保留在单独的 `PROJECT_RULES.md` 文件中。

---

# 1. 核心原则

构建一个可投入生产运行的应用程序,而不是一组互不关联的组件。

始终遵循以下顺序:

1. 审查当前的项目结构、`package.json`、路由、UI 原语、状态管理、自定义 Hook、数据模式(schema)以及项目规则。
2. 查找可复用的现有操作、辅助函数、数据模式和组件。
3. 明确完成任务所需的最小变更。
4. 保持现有行为不变。
5. 端到端地实现每个新功能:模型、校验、UI、存储/导入/导出、边界情况和验证。
6. 运行相关检查并如实报告结果。

除非确实必要,否则不要添加依赖项、抽象层、全局状态管理或架构层。  
默认使用 `shadcn/ui` 进行 UI 开发。除非有明确理由,否则不要在其之上添加另一个 UI 组件库。

---

# 2. React 与 Next.js 的选择

当项目需要以下功能时,使用 Next.js:

- 路由;
- SEO;
- 服务端渲染(SSR)/ 服务端组件;
- 服务端操作(Server Actions);
- 路由处理器 / API 路由;
- 认证;
- 数据库访问;
- 私有环境变量;
- 内容发布。

当满足以下条件时,使用 React + Vite:

- 应用完全运行在客户端;
- 不需要 SEO;
- 是本地工具、仪表板、编辑器、管理面板或类桌面 UI;
- 服务端已作为独立服务存在。

不要仅仅因为 Next.js 流行而选择它。除非有特定理由,否则不要添加 Redux、Zustand、React Query、表单库或其他 UI 组件库。

---

# 3. 默认技术栈与检查

默认使用以下技术:

- React;
- 启用严格模式的 TypeScript;
- Tailwind CSS;
- `shadcn/ui` 作为实现简洁设计和快速界面开发所必需的 UI 方案;
- Lucide React 或当前 shadcn 配置所使用的图标库;
- ESLint;
- 共享的 `cn()` 辅助函数;
- 对外部数据进行运行时校验;
- 可访问的 HTML 元素。

将 `shadcn/ui` 作为 UI 原语的主要来源:按钮、输入框、选择器、对话框、侧边面板、下拉菜单、工具提示、标签页、轮播图、卡片、徽章、骨架屏、滚动区域及其他所需组件。仅当 shadcn 未提供合适组件,或项目已有稳定本地原语时,才创建自定义原语。

对于 MVP,首先使用模拟数据/JSON/localStorage 数据,优先验证本地用户流程。待 UI、数据模型和流程明确后,再最后添加后端、数据库、支付、认证和外部集成。

代码更改后,至少运行以下命令:

```bash
npm run typecheck
npm run lint
npm run build
```

如果这些命令未运行或运行出错,不得声称项目可以正常工作。

---

# 4. 架构

对于预计会增长的 Next.js 项目,默认将源代码放在 `src/` 目录下:`src/app`、`src/components`、`src/lib`、`src/data`、`src/hooks` 和 `src/features`。将根级别的支持性文件夹和文件(如 `public`、配置文件、锁文件和 README)保留在项目根目录中。

对于小型项目,以下结构是可接受的:

```text
src/
  app/ 或 pages/
  components/
  features/
  lib/
  shared/
```

对于中大型项目,采用类似 FSD 的方式:

```text
src/
  app/       # 启动文件、提供者(providers)、布局、路由
  views/     # 页面级组合
  widgets/   # 大型 UI 模块
  features/  # 用户工作流
  entities/  # 领域模型
  shared/    # 通用辅助函数、配置、对 shadcn/ui 的轻量封装
```

导入方向应为:

```text
app/views -> widgets -> features -> entities -> shared
```

禁止:

- 将 `widgets` 导入到 `features` 中;
- 在 `shared` 中放置业务逻辑;
- 将 `shared/lib` 变成存放无关函数的垃圾场;
- 在多个 UI 组件中重复编写相同的变更逻辑;
- 当某个模块已暴露公共 API 时,仍深度导入其内部实现。  

---

# 5. 公共 API

每当一个功能模块(feature)、实体(entity)或共享 UI 文件夹被外部使用时,应通过 `index.ts` 暴露清晰的公共 API。对于 shadcn 原语,公共 API 通常已经存在于 `components/ui/*` 或项目的本地 UI 层中。

良好示例:

```ts
import { createTask } from "@/features/create-task";
```

错误示例:

```ts
import { createTask } from "@/features/create-task/model/createTask";
```

例外情况:同一功能或实体内部的代码。  

---

# 6. TypeScript

必须遵守以下规则:

- 启用 `strict: true`;
- 除非在隔离的互操作代码中,否则不得使用 `any`;
- 不得使用 `as` 类型断言来掩盖类型错误;
- 使用可辨识联合(discriminated unions)处理复杂状态;
- 使用 schema 验证运行时 JSON;
- 不要无合理理由创建多个完全相同的类型。

示例状态类型:

```ts
type LoadState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; message: string };
```

---

# 7. React 状态与副作用

将状态存储在它本应归属的位置:

| 状态类型     | 存储位置                                                   |
| ------------ | ----------------------------------------------------------- |
| 本地 UI      | `useState`, `useReducer`                                    |
| URL 状态     | 路由/查询参数                                               |
| 服务器状态   | 服务端渲染或缓存/查询层                                     |
| 表单状态     | 表单钩子/库                                                 |
| 全局 UI      | 必要时使用小型状态库                                        |
| 领域状态     | 当状态在多个流程间共享时,使用实体/状态库                   |

不要将以下内容放入全局状态库:

- 悬停状态;
- 单个下拉框的状态;
- 单个输入框的草稿值;
- 单个模态框的状态;
- 单个组件的临时选中标签页。

使用 `useEffect` 与外部系统同步:

- 浏览器 API;
- 定时器;
- 订阅;
- 外部状态库;
- DOM 集成。

不要使用 `useEffect` 计算派生值。

错误示例:

```tsx
const [fullName, setFullName] = useState("");

useEffect(() => {
  setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
```

正确示例:

```tsx
const fullName = `${firstName} ${lastName}`;
```

---

# 8. Next.js 边界

在 App Router 中,组件默认为服务端组件。

仅在需要时添加 `"use client"`:

- 事件处理器;
- 本地状态;
- 副作用;
- `window`, `document`, 或 `localStorage`;
- 拖放;
- `contenteditable`;
- 仅限客户端的库。

不要在没有明确需求的情况下将整个布局设为客户端组件。

仅限服务端的代码包括:

- 数据库访问;
- 身份验证;
- 私有 API 客户端;
- 秘密环境变量;
- Webhook;
- 访问权限检查。

切勿将仅限服务端的模块导入客户端组件中。

---

# 9. 运行时验证与迁移

在边界处验证所有外部数据:

- 请求体;
- 表单数据;
- URL/查询参数;
- 上传的文件;
- 导入的 JSON;
- localStorage/IndexedDB 数据;
- 外部 API 的响应。

当添加新的模型字段时,需更新整个生命周期:

1. TypeScript 类型;
2. 运行时 schema;
3. 工厂/默认值;
4. 旧数据的解析器/迁移逻辑;
5. 归一化辅助函数;
6. 导入/导出;
7. 搜索/过滤索引(如果该字段应支持搜索);
8. 撤销/重做快照(如果用户可编辑该字段);
9. 创建、编辑和清空该字段的 UI;
10. 边界情况与校验。

示例:

```ts
return {
  ...item,
  status: item.status ?? "active",
  tags: normalizeTags(item.tags),
  dueDate: normalizeDate(item.dueDate),
};
```

不要仅在 UI 中添加模型字段。
| 功能大小                       | 放置位置                         | 示例                             |
| ---------------------------------- | --------------------------------- | ----------------------------------- |
| 1-5 个快速设置                 | 上下文菜单 / 下拉菜单 / 气泡框 | 状态、截止日期、标签              |
| 5-12 个分组设置                | 分区的、可滚动的气泡框           | 实体属性、紧凑型筛选器            |
| 大型数据集或批量操作           | 侧边栏 / 抽屉                    | 筛选器、工具面板                  |
| 复杂表单或危险操作             | 模态框                           | 导入/导出、删除确认               |
| 永久性工作区                   | 专用视图/页面/小组件             | 仪表板、日历、编辑器              |

规则:

> 如果某个控件是偶尔使用,就将其保留在菜单中。  
> 如果某个控件是持续使用,就让其在主界面上可见。  
> 如果某个控件复杂且操作流程长,就将其移至侧边栏或模态框中。

不要将一小组控件变成页面上的一个大型卡片。在生产力型界面中,这会浪费宝贵的空间。

---

# 13. 编辑器、仪表板和工作区的紧凑 UI

在生产力型应用中,主要内容必须保持焦点。

必需:

- 标题、正文、看板或编辑器不得因次要控件而被向下推移;
- 实体属性通常应通过标题旁的图标按钮打开;
- 设置按钮必须具有 `aria-label`;
- 重要状态可显示为小徽章;
- 创建/添加操作必须出现在明确的上下文中;
- 重度依赖侧边栏的流程必须包含适合移动设备的菜单或切换器;
- 不要让生产力工具看起来像着陆页。

不佳示例:

```tsx
${largepropertiescard}
  <Select>Status</Select>
  <Select>Task</Select>
  <Input>Date</Input>
  <Input>Tags</Input>
</LargePropertiesCard>
```

良好示例:

```tsx
${titlerow}
  <TitleInput />
  <PropertiesMenu />
</TitleRow>
```

---

# 14. 叠加层、下拉菜单、气泡框和上下文菜单

每个菜单都必须表现为真正的叠加层。

规则:

- 如果菜单可能超出其容器,应通过 `createPortal(..., document.body)` 渲染;
- 使用 `position: fixed` 或可靠的定位辅助工具;
- 设置明确的 `z-index`;
- 使用不透明的 `backgroundColor`;
- 不要仅依赖半透明的 `bg-black/50` 背景或模糊效果;
- 添加边框、轮廓或阴影;
- 设置 `max-height` 并启用 `overflow-y-auto`;
- 按下 `Escape` 键时关闭;
- 在外部点击/触摸时关闭;
- 防止页面文本穿透或渲染在菜单之上;
- 悬停和激活状态不得改变菜单项的尺寸。

最小叠加层样式:

```tsx
<div
  role="menu"
  className="rounded-2xl border p-2 shadow-2xl"
  style=${backgroundcolor:"#151a21",
    boxShadow: "0 24px 70px rgb(0 0 0 / 78%)",
    isolation: "isolate",
    zIndex: 1000,}
>
  ...
</div>
```

如果菜单背景未正确渲染或内容出现在其上方,请检查:

- portal;
- `position`;
- `z-index`;
- 父级堆叠上下文;
- `isolation`;
- 不透明度/背景;
- 父级 overflow/裁剪。

---

# 15. 菜单中的选项列表

菜单中的任务、项目、用户、标签或其他选项列表不应看起来像一堵密集的文字墙。

对于两行项目:

- 使用 40-44px 的 `min-height`;
- 在图标、文本和勾选标记之间包含 `gap`;
- 使用如 `py-1.5` 的垂直内边距;
- 为标题和元数据使用不同的行高;
- 在标题和元数据之间添加 `mt-0.5`;
- 在包含文本的父元素上应用 `min-w-0`;
- 对标题和元数据应用 `truncate`;
- 对勾选标记和图标应用 `shrink-0`。

示例:

```tsx
<button className="flex min-h-11 items-center gap-2.5 rounded-lg px-2.5 py-1.5">
  <span className="min-w-0 flex-1">
    <span className="block truncate font-medium leading-5">${title}</span>
    <span className="mt-0.5 block truncate text-xs leading-4 text-muted">
      {meta}
    </span>
  </span>
  {isActive ? <Check className="shrink-0" /> : null}
</button>
```

---

# 16. 长文本与溢出

任何用户提供的文本都可能包含无空格的长单词。

对于编辑器、`contenteditable` 元素、Markdown、卡片标题和评论:

- 在 flex/grid 子元素上使用 `min-w-0`;
- 使用当前 Tailwind 工具类来换行长单词;
- 在较新的 Tailwind 版本中,`break-words` 可能写作 `wrap-break-word`;
- 在使用 wrapping、overflow、text-wrap、grid、spacing 或任意值类之前,请查阅项目当前 Tailwind 版本的文档;
- 如果元素位于 flex 容器内且长文本破坏其宽度,请检查 `wrap-anywhere` 是否合适;
- 在卡片中的短行上使用 `truncate`;
- 对正文文本进行换行,而不是允许水平溢出;
- 文本不得渲染在菜单、气泡框或模态框之上;
- 使用不含空格的长字符串进行测试。

对于可编辑块:

```tsx
className = "min-w-0 wrap-break-word whitespace-pre-wrap";
```
  
如果项目使用的 Tailwind 版本较旧,其中 `wrap-break-word` 不可用,则需检查已安装的 Tailwind 版本以及官方文档或版本说明,然后使用受支持的等效类:`break-words`、任意值或 CSS 属性。

对于徽章(badge):

```tsx
className = "inline-flex whitespace-nowrap";
```

徽章不得在垂直方向上压缩文本。如果内容无法容纳,应将其移至新行,或结合明确且易于理解的宽度使用 `truncate`。

---

# 17. Tailwind CSS:验证当前类名

AI 代理在使用新类或可能依赖版本的类之前,必须检查项目中安装的 Tailwind 版本。

流程如下:

1. 检查 `package.json` 和锁文件(lockfile)。
2. 确定 Tailwind 的主版本号。
3. 如果某个类在不同版本间可能存在差异,请查阅该确切版本的官方文档。
4. 不得在未经验证的情况下机械替换类名。
5. 使用任意值时,需确认其已包含在构建输出中。

需特别注意以下类:

- `break-words` / `wrap-break-word` / `wrap-anywhere`;
- `text-wrap`、`text-balance` 和 `text-pretty`;
- `overflow-*`;
- `size-*`;
- 任意颜色,例如 `bg-[#151a21]`;
- 任意阴影;
- 任意网格模板;
- 动态类名。

不要以如下方式构建动态 Tailwind 类:

```tsx
const color = "red";
return <div className={`bg-${color}-500`} />;
```

Tailwind 可能在构建期间无法检测到该类。应使用映射(map):

```tsx
const colorClassName = {
  danger: "bg-red-500",
  success: "bg-emerald-500",
}${variant};
```

如果重要的遮罩背景不能依赖 Tailwind 的构建输出,则可以使用内联 `style.backgroundColor`。

---

# 18. 布局与侧边栏收起

收起侧边栏或抽屉时,不得改变页面高度,也不得留下空白区块。

规则如下:

- 应用外壳(app shell):`h-dvh min-h-dvh overflow-hidden`;
- 内部区域:`flex min-h-0 flex-1 overflow-hidden`;
- 仅在适当区域启用滚动,使用 `overflow-y-auto`;
- 收起时应改变宽度或 flex-basis,而非高度;
- 收起的侧边栏必须具有稳定的宽度;
- 提供清晰的控件以恢复侧边栏;
- 破坏性操作或创建操作不得作为孤立按钮存在而无上下文;
- 偏好设置可持久化存储于 localStorage 中。

示例:

```tsx
<main className="flex h-dvh min-h-dvh flex-col overflow-hidden">
  <div className="flex min-h-0 flex-1 overflow-hidden">
    <Sidebar className="h-full min-h-0 shrink-0" />
    <section className="min-h-0 flex-1 overflow-y-auto" />
  </div>
</main>
```

---

# 19. 浏览器 API 与 localStorage

在 Next.js 中,浏览器 API 仅可在客户端组件(Client Components)中使用。

规则如下:

- 使用 `localStorage`、`window`、`document`、拖放或 `contenteditable` 的文件必须包含 `"use client"`;
- 不得在服务端组件(Server Component)中读取 `localStorage`;
- 不得因初始值不同而导致 hydration 错误;
- 将存储操作包裹在 `try/catch` 中;
- 存储失败不得导致 UI 崩溃;
- 重新加载后需验证持久化的 UI 偏好;
- 构建过程不得因 `window is not defined` 而失败。

示例:

```tsx
const toggle = useCallback(() => {
  setIsCollapsed((current) => {
    const next = !current;

    try {
      window.localStorage.setItem(KEY, next ? "true" : "false");
    } catch {
      // 即使没有浏览器存储,UI 仍可正常工作。
    }

    return next;
  });
}, []);
```

需验证以下几点:

- 默认状态在存储为空时仍能正常工作;
- 重新加载后状态得以保留;
- 私密模式或存储错误不会导致界面崩溃;
- 构建不会因 `window is not defined` 而失败。

---

# 20. 实体间的关系

如果一个实体关联到另一个实体,则该关系必须真实存在:

- 在模型中存储该关系;
- 在 UI 中展示该关系;
- 点击时应打开关联的实体;
- 创建相关实体时,应立即保存该关系;
- 导入/导出过程中需保留该关系;
- 在搜索/过滤行为中有用时应包含该关系;
- 如果关联实体被删除,应在 UI 中显示回退内容。

若关系未被持久化,则不得创建装饰性的“链接”按钮。

---

# 21. 统一的领域操作

每个用户操作必须有单一事实来源。

不得:

- 通过斜杠菜单以一种方式创建实体;
- 通过工具栏以另一种方式创建;
- 通过命令面板绕过验证;
- 在上下文菜单中重复 mutation 逻辑。

而应:

- 将领域操作集中于一处;
- 让 UI 组件调用该操作;
- 对所有入口点使用相同的验证和约束。

---

# 23. 可访问性

必须满足以下要求:

- 使用 `<button>` 表示操作;
- 使用 `<a>` 表示导航;
- 为仅含图标的按钮添加 `aria-label`;
- 为输入框提供标签;
- 显示可见的焦点状态;
- 支持键盘导航;
- 按 `Escape` 键关闭模态框和弹出层;
- 点击外部区域关闭弹出层;
- 在模态框中使用焦点陷阱(focus trap)。
- 不要仅使用颜色来传达含义;
- 不要使用 `${div_onclick}` 替代 `<button>`。

---

# 24. 加载、空状态与错误状态

数据驱动的界面必须考虑以下状态:

- 加载中;
- 成功;
- 空状态;
- 权限被拒绝;
- 网络错误;
- 服务器错误;
- 重试。

没有说明的空白界面属于缺陷。

---

# 25. 安全

必须做到:

- 仅在服务器端保存密钥;
- 使用运行时验证;
- 在服务器端强制执行访问控制;
- 验证文件的 MIME 类型和大小;
- 对用户提供的 HTML 进行净化处理;
- 除非使用净化器,否则不要使用 `dangerouslySetInnerHTML`;
- 不要记录令牌或个人数据;
- 不要信任浏览器提供的 `role` 或 `userId` 值。

---

# 26. 性能

先测量,再优化。

使用:

- 动态导入用于重型编辑器、图表、地图和 PDF 模块;
- 图像优化;
- 大型列表的虚拟化;
- 搜索请求的中止/过期请求防护;
- 选择器以减少重渲染。

不要无理由地添加记忆化(memoization)。

---

# 27. 使用真实内容测试设计

有关界面质量的进一步指导,可参考:

- https://jakub.kr/skills/make-interfaces-feel-better

该资源在打磨排版、悬停状态、阴影、边框、间距、视觉对齐、微交互以及整体界面感受方面非常有用。

在完成 UI 任务前,使用以下内容进行测试:

- 一个无空格的长单词;
- 一个长俄语标题;
- 一个短标题;
- 一个空标题;
- 多个标签;
- 一个长列表/分类名称;
- 下拉菜单中的多个选项;
- 激活和非激活状态;
- 一个日期和一个缺失的日期。

验证以下事项:

- 没有元素重叠;
- 浮层覆盖底层内容;
- 文本不会透过菜单显示;
- 徽章不会在垂直方向上压缩文本;
- 元素之间不显得拥挤;
- 滚动条不会遮挡重要文本;
- 悬停和焦点状态易于阅读;
- 桌面和移动端宽度均显示正确。

---

# 28. 更改后的检查

代码更改后,运行:

```bash
npm run typecheck
npm run lint
npm run build
```

如果 UI 发生更改:

- 在浏览器中打开页面;
- 完成主要用户流程;
- 测试键盘和鼠标交互;
- 测试 `Escape` 键和点击外部区域的行为;
- 测试页面重载;
- 测试长文本;
- 测试移动端视口宽度;
- 若视觉层发生变化,请截取屏幕截图。

如果无法进行浏览器验证,需明确说明。不要将 `typecheck` 视为视觉验证。

---

# 29. Git 与工作树

在进行更改前,检查当前状态:

```bash
git status --short
```

规则:

- 未经明确请求,不得撤销他人的更改;
- 未经明确许可,不得使用破坏性命令;
- 不得进行无关的重构;
- 除非用户要求,否则不得自动提交;
- 不要不必要地更改换行符或重新格式化整个项目。

---

# 30. 最终报告

在最终回复中说明:

- 哪些内容发生了变更;
- 哪些文件是重要的;
- 运行了哪些检查;
- 哪些内容未能验证;
- 仍存在哪些风险。

保持报告简洁且诚实。