A general rule set for AI agents working on React, Next.js, TypeScript and Tailwind projects, meant to be saved as AGENTS.md or CLAUDE.md.
# Universal Instructions for React / Next.js Projects
> Purpose: General rules for developing various projects with React + TypeScript, Next.js + TypeScript, and Tailwind CSS.
> Usage: Place this file in the root of a new project as `AGENTS.md`, `CLAUDE.md`, or `PROJECT_RULES.md`, or use it as a base instruction set for an AI agent.
> Important: These instructions do not contain product-specific rules. Keep everything related to an individual project in a separate `PROJECT_RULES.md` file.
---
# 1. Core Principle
Build a production-ready application, not a collection of disconnected components.
Always follow this sequence:
1. Review the current project structure, `package.json`, routing, UI primitives, stores, hooks, schemas, and project rules.
2. Find existing actions, helpers, schemas, and components that can be reused.
3. Identify the smallest change required for the task.
4. Preserve existing behavior.
5. Implement each new feature end to end: model, validation, UI, storage/import/export, edge cases, and verification.
6. Run the relevant checks and report the results honestly.
Do not add dependencies, abstractions, a global store, or an architectural layer unless they are genuinely necessary.
Use `shadcn/ui` by default for UI work. Do not add another UI kit on top of it without a clear reason.
---
# 2. Choosing Between React and Next.js
Use Next.js when the project needs:
- routing;
- SEO;
- SSR / Server Components;
- Server Actions;
- Route Handlers / API routes;
- authentication;
- database access;
- private environment variables;
- content publishing.
Use React + Vite when:
- the application is entirely client-side;
- SEO is not required;
- it is a local tool, dashboard, editor, admin panel, or desktop-like UI;
- the server already exists as a separate service.
Do not choose Next.js simply because it is popular. Do not add Redux, Zustand, React Query, a form library, or another UI kit without a specific reason.
---
# 3. Default Stack and Checks
Use the following by default:
- React;
- TypeScript in strict mode;
- Tailwind CSS;
- `shadcn/ui` as the required UI approach for clean design and rapid interface development;
- Lucide React or the icon library used by the current shadcn configuration;
- ESLint;
- a shared `cn()` helper;
- runtime validation for external data;
- accessible HTML elements.
Use `shadcn/ui` as the primary source of UI primitives: buttons, inputs, selects, dialogs, sheets, dropdowns, tooltips, tabs, carousels, cards, badges, skeletons, scroll areas, and other required components. Create custom primitives only when shadcn does not provide a suitable component or when the project already has a stable local primitive.
For an MVP, begin with mock/JSON/localStorage data and validate local user flows first. Add the backend, database, payments, authentication, and external integrations last, once the UI, models, and flows are clear.
At a minimum, run these commands after code changes:
```bash
npm run typecheck
npm run lint
npm run build
```
Do not claim that the project works if these commands were not run or completed with errors.
---
# 4. Architecture
For Next.js projects expected to grow, keep source code inside `src/` by default: `src/app`, `src/components`, `src/lib`, `src/data`, `src/hooks`, and `src/features`. Keep root-level support folders and files (`public`, configuration files, lockfiles, and README) in the project root.
For small projects, the following structure is acceptable:
```text
src/
app/ or pages/
components/
features/
lib/
shared/
```
For medium and large projects, use an FSD-like approach:
```text
src/
app/ # bootstrap, providers, layouts, routes
views/ # page-level composition
widgets/ # large UI blocks
features/ # user workflows
entities/ # domain model
shared/ # generic helpers, config, thin wrappers around shadcn/ui
```
Import direction:
```text
app/views -> widgets -> features -> entities -> shared
```
Do not:
- import `widgets` into `features`;
- place business logic in `shared`;
- turn `shared/lib` into a dumping ground for unrelated functions;
- duplicate mutation logic across multiple UI components;
- use deep imports into another module's internals when that module exposes a public API.
---
# 5. Public API
Every feature, entity, or shared UI folder should expose a clear public API through `index.ts` when the module is used externally. For shadcn primitives, the public API usually already lives in `components/ui/*` or the project's local UI layer.
Good:
```ts
import { createTask } from "@/features/create-task";
```
Bad:
```ts
import { createTask } from "@/features/create-task/model/createTask";
```
Exception: internal code within the same feature or entity.
---
# 6. TypeScript
Required:
- enable `strict: true`;
- do not use `any` except in isolated interoperability code;
- do not hide type errors with `as` assertions;
- use discriminated unions for complex state;
- validate runtime JSON with a schema;
- do not create multiple identical types without a meaningful reason.
Example state type:
```ts
type LoadState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; message: string };
```
---
# 7. React State and Effects
Store state where it actually belongs:
| State type | Where to store it |
| ------------ | ----------------------------------------------------------- |
| Local UI | `useState`, `useReducer` |
| URL state | route/search parameters |
| Server state | server rendering or a cache/query layer |
| Form state | form hook/library |
| Global UI | a small store when necessary |
| Domain state | entity/store when the state is shared across multiple flows |
Do not put the following in a global store:
- hover state;
- the state of a single dropdown;
- the draft value of a single input;
- the state of a single modal;
- the temporary selected tab of one component.
Use `useEffect` to synchronize with external systems:
- browser APIs;
- timers;
- subscriptions;
- external stores;
- DOM integrations.
Do not use `useEffect` for derived values.
Bad:
```tsx
const [fullName, setFullName] = useState("");
useEffect(() => {
setFullName(`${firstName} ${lastName}`);
}, [firstName, lastName]);
```
Good:
```tsx
const fullName = `${firstName} ${lastName}`;
```
---
# 8. Next.js Boundaries
In the App Router, components are Server Components by default.
Add `"use client"` only where you need:
- event handlers;
- local state;
- effects;
- `window`, `document`, or `localStorage`;
- drag and drop;
- `contenteditable`;
- client-only libraries.
Do not make an entire layout a Client Component without a clear need.
Server-only code includes:
- database access;
- authentication;
- private API clients;
- secret environment variables;
- webhooks;
- access checks.
Never import a server-only module into a Client Component.
---
# 9. Runtime Validation and Migrations
Validate all external data at the boundary:
- request bodies;
- form data;
- URL/search parameters;
- uploaded files;
- imported JSON;
- localStorage/IndexedDB data;
- responses from external APIs.
When adding a new model field, update the entire lifecycle:
1. TypeScript type.
2. Runtime schema.
3. Factory/default values.
4. Parser/migration for legacy data.
5. Normalization helpers.
6. Import/export.
7. Search/filter indexing, if the field should be searchable.
8. Undo/redo snapshots, if users can edit the field.
9. UI for creating, editing, and clearing the field.
10. Edge cases and checks.
Example:
```ts
return {
...item,
status: item.status ?? "active",
tags: normalizeTags(item.tags),
dueDate: normalizeDate(item.dueDate),
};
```
Do not add a model field only in the UI.
---
# 10. Forms
Every form must include:
- a validation schema;
- field errors;
- a submitting/loading state;
- a disabled submit button while submitting;
- protection against duplicate submissions;
- an error state;
- success behavior;
- reset/draft behavior, when applicable.
A form is not complete if it works only when the request succeeds perfectly.
---
# 11. shadcn/ui and Shared UI
Use `shadcn/ui` by default to build clean, consistent interfaces quickly.
Rules:
- first check whether the required component exists in the shadcn registry;
- add shadcn components through the CLI or the project's established local method;
- do not create a custom Button, Input, Modal, Dropdown, Tooltip, Tabs, or Card if shadcn already covers the use case;
- adapt shadcn components through `className`, variants, and composition instead of copying similar components;
- keep business components separate from primitives: `components/marketplace`, `features/*/ui`, `widgets/*`, or `entities/*/ui`;
- keep only shadcn primitives and thin reusable wrappers in `components/ui` or `shared/ui`;
- do not place product-specific business components there;
- if shadcn does not provide a component, create a minimal local wrapper consistent with the current shadcn configuration.
Base set of shadcn components for productivity interfaces:
```text
button
input
select
textarea
checkbox
switch
dialog
sheet
dropdown-menu
popover
tooltip
tabs
card
badge
avatar
separator
scroll-area
skeleton
carousel
accordion
collapsible
hover-card
```
For marketplace, chat, and support flows, also plan for these newer shadcn components:
```text
message
message-scroller
attachment
marker
```
Always use `cn()`:
```ts
export function cn(...values: Array<string | false | null | undefined>) {
return values.filter(Boolean).join(" ");
}
```
---
# 12. Choosing the Right UI Surface
Before adding a new tool, choose the right surface:
| Feature size | Placement | Example |
| ---------------------------------- | --------------------------------- | ----------------------------------- |
| 1-5 quick settings | context menu / dropdown / popover | status, due date, tags |
| 5-12 grouped settings | sectioned, scrollable popover | entity properties, compact filters |
| large data sets or bulk actions | sidebar / drawer | filters, tools panel |
| complex form or dangerous action | modal | import/export, delete confirmation |
| permanent workspace | dedicated view/page/widget | dashboard, calendar, editor |
Rule:
> If a control is used occasionally, keep it in a menu.
> If a control is used constantly, keep it visible on the main surface.
> If a control is complex and lengthy, move it to a sidebar or modal.
Do not turn a small group of controls into a large card on the page. In productivity interfaces, this wastes valuable space.
---
# 13. Compact UI for Editors, Dashboards, and Workspaces
In productivity applications, the primary content must remain the focus.
Required:
- the title, body, board, or editor must not be pushed downward by secondary controls;
- entity properties should generally open from an icon button next to the title;
- settings buttons must have an `aria-label`;
- an important status can be shown as a small badge;
- create/add actions must appear in a clear context;
- sidebar-heavy flows must include a mobile-friendly menu or switcher;
- do not make a productivity tool look like a landing page.
Bad:
```tsx
${largepropertiescard}
<Select>Status</Select>
<Select>Task</Select>
<Input>Date</Input>
<Input>Tags</Input>
</LargePropertiesCard>
```
Good:
```tsx
${titlerow}
<TitleInput />
<PropertiesMenu />
</TitleRow>
```
---
# 14. Overlays, Dropdowns, Popovers, and Context Menus
Every menu must behave as a true overlay.
Rules:
- if a menu may extend beyond its container, render it through `createPortal(..., document.body)`;
- use `position: fixed` or a reliable positioning helper;
- set an explicit `z-index`;
- use an opaque `backgroundColor`;
- do not rely only on a translucent `bg-black/50` background or blur;
- add a border, ring, or shadow;
- set `max-height` and `overflow-y-auto`;
- close on `Escape`;
- close on outside click/tap;
- prevent page text from showing through or rendering over the menu;
- hover and active states must not change the item's dimensions.
Minimal overlay style:
```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>
```
If the menu background does not render correctly or content appears above it, check:
- the portal;
- `position`;
- `z-index`;
- parent stacking contexts;
- `isolation`;
- opacity/background;
- parent overflow/clipping.
---
# 15. Option Lists in Menus
A list of tasks, projects, users, tags, or other options in a menu must not look like a dense wall of text.
For a two-line item:
- use a `min-height` of 40-44px;
- include a `gap` between the icon, text, and checkmark;
- use vertical padding such as `py-1.5`;
- give the title and metadata different line heights;
- add `mt-0.5` between the title and metadata;
- apply `min-w-0` to the parent containing the text;
- apply `truncate` to the title and metadata;
- apply `shrink-0` to checkmarks and icons.
Example:
```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. Long Text and Overflow
Any user-provided text may contain a long word with no spaces.
For editors, `contenteditable` elements, Markdown, card titles, and comments:
- use `min-w-0` on flex/grid children;
- use the current Tailwind utilities for wrapping long words;
- in newer Tailwind versions, `break-words` may be written as `wrap-break-word`;
- check the documentation for the project's current Tailwind version before using wrapping, overflow, text-wrap, grid, spacing, or arbitrary-value classes;
- if an element is inside a flex container and long text breaks its width, check whether `wrap-anywhere` is appropriate;
- use `truncate` for short lines in cards;
- wrap body text instead of allowing horizontal overflow;
- text must not render over a menu, popover, or modal;
- test with a long string containing no spaces.
For an editable block:
```tsx
className = "min-w-0 wrap-break-word whitespace-pre-wrap";
```
If the project uses an older Tailwind version where `wrap-break-word` is unavailable, check the installed Tailwind version and the official documentation or version notes, then use a supported equivalent: `break-words`, an arbitrary value, or a CSS property.
For a badge:
```tsx
className = "inline-flex whitespace-nowrap";
```
A badge must not compress text vertically. If it does not fit, move it to a new line or use `truncate` with an explicit, understandable width.
---
# 17. Tailwind CSS: Verify Current Class Names
The AI agent must check the Tailwind version installed in the project before using new or potentially version-dependent classes.
Process:
1. Inspect `package.json` and the lockfile.
2. Determine the Tailwind major version.
3. If a class may differ between versions, check the official documentation for that exact version.
4. Do not replace classes mechanically without verification.
5. When using an arbitrary value, confirm that it is included in the build output.
Pay particular attention to:
- `break-words` / `wrap-break-word` / `wrap-anywhere`;
- `text-wrap`, `text-balance`, and `text-pretty`;
- `overflow-*`;
- `size-*`;
- arbitrary colors such as `bg-[#151a21]`;
- arbitrary shadows;
- arbitrary grid templates;
- dynamic class names.
Do not build dynamic Tailwind classes like this:
```tsx
const color = "red";
return <div className={`bg-${color}-500`} />;
```
Tailwind may not detect that class during the build. Use a map:
```tsx
const colorClassName = {
danger: "bg-red-500",
success: "bg-emerald-500",
}${variant};
```
If an important overlay background must not depend on Tailwind's build output, using an inline `style.backgroundColor` is acceptable.
---
# 18. Layout and Sidebar Collapse
Collapsing a sidebar or drawer must not change the page height or leave an empty block.
Rules:
- app shell: `h-dvh min-h-dvh overflow-hidden`;
- internal regions: `flex min-h-0 flex-1 overflow-hidden`;
- enable scrolling only on the appropriate region with `overflow-y-auto`;
- when collapsing, change width/flex-basis rather than height;
- a collapsed sidebar must have a stable width;
- provide a clear control for restoring the sidebar;
- destructive or creation actions must not remain as isolated buttons without context;
- preferences may be persisted in localStorage.
Example:
```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. Browser APIs and localStorage
In Next.js, browser APIs are available only in Client Components.
Rules:
- a file that uses `localStorage`, `window`, `document`, drag and drop, or `contenteditable` must include `"use client"`;
- do not read `localStorage` in a Server Component;
- do not cause hydration errors with different initial values;
- wrap storage operations in `try/catch`;
- storage failures must not break the UI;
- verify persisted UI preferences after a reload;
- the build must not fail with `window is not defined`.
Example:
```tsx
const toggle = useCallback(() => {
setIsCollapsed((current) => {
const next = !current;
try {
window.localStorage.setItem(KEY, next ? "true" : "false");
} catch {
// UI still works without browser storage.
}
return next;
});
}, []);
```
Verify that:
- the default state works with empty storage;
- a reload preserves the state;
- private mode or storage errors do not break the screen;
- the build does not fail with `window is not defined`.
---
# 20. Relationships Between Tools
If one entity is linked to another, the relationship must be real:
- store it in the model;
- show it in the UI;
- clicking it opens the linked entity;
- when creating the related entity, save the relationship immediately;
- preserve the relationship during import/export;
- include the relationship in search/filter behavior when useful;
- if the related entity is deleted, show a fallback in the UI.
Do not create a decorative "Link" button if the relationship is not persisted.
---
# 21. Unified Domain Operations
Each user operation must have a single source of truth.
Do not:
- create an entity one way from the slash menu;
- create it another way from the toolbar;
- bypass validation from the command palette;
- duplicate mutation logic in the context menu.
Instead:
- keep the domain operation in one place;
- have UI components call that operation;
- use the same validation and constraints for every entry point.
---
# 23. Accessibility
Required:
- use `<button>` for actions;
- use `<a>` for navigation;
- add `aria-label` to icon-only buttons;
- provide labels for inputs;
- show a visible focus state;
- support keyboard navigation;
- close modals and popovers on `Escape`;
- close popovers on outside click;
- use a focus trap in modals;
- do not use color as the only way to communicate meaning;
- do not replace `<button>` with `${div_onclick}`.
---
# 24. Loading, Empty, and Error States
Data-driven screens must account for:
- loading;
- success;
- empty state;
- permission denied;
- network error;
- server error;
- retry.
A blank screen with no explanation is a bug.
---
# 25. Security
Required:
- keep secrets on the server only;
- use runtime validation;
- enforce access control on the server;
- validate file MIME types and sizes;
- sanitize user-provided HTML;
- do not use `dangerouslySetInnerHTML` without a sanitizer;
- do not log tokens or personal data;
- do not trust `role` or `userId` values supplied by the browser.
---
# 26. Performance
Measure first, then optimize.
Use:
- dynamic imports for heavy editor, chart, map, and PDF modules;
- image optimization;
- virtualization for large lists;
- abort/stale-request protection for search;
- selectors to reduce rerenders.
Do not add memoization without a reason.
---
# 27. Test the Design with Realistic Content
For additional guidance on interface quality, you may refer to:
- https://jakub.kr/skills/make-interfaces-feel-better
This resource is useful when polishing typography, hover states, shadows, borders, spacing, optical alignment, micro-interactions, and the overall feel of the interface.
Before completing a UI task, test it with:
- a long word with no spaces;
- a long Russian title;
- a short title;
- an empty title;
- multiple tags;
- a long list/category name;
- multiple options in a dropdown;
- active and inactive statuses;
- a date and a missing date.
Verify that:
- nothing overlaps;
- overlays cover the underlying content;
- text does not show through menus;
- badges do not compress text vertically;
- elements do not crowd each other;
- scrollbars do not cover important text;
- hover and focus states are easy to read;
- desktop and mobile widths both look correct.
---
# 28. Checks After Changes
After code changes, run:
```bash
npm run typecheck
npm run lint
npm run build
```
If the UI was changed:
- open the page in a browser;
- complete the primary user flow;
- test keyboard and mouse interaction;
- test `Escape` and outside-click behavior;
- test reloading;
- test long text;
- test a mobile viewport width;
- take a screenshot if the visual layer changed.
If browser verification is impossible, say so explicitly. Do not present `typecheck` as visual verification.
---
# 29. Git and the Working Tree
Before making changes, inspect the current state:
```bash
git status --short
```
Rules:
- do not revert someone else's changes without an explicit request;
- do not use destructive commands without explicit permission;
- do not perform unrelated refactoring;
- do not commit automatically unless the user asks you to;
- do not change line endings or reformat the entire project unnecessarily.
---
# 30. Final Report
In the final response, state:
- what changed;
- which files are important;
- which checks were run;
- what could not be verified;
- which risks remain.
Keep the report concise and honest.# 通用 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. 最终报告
在最终回复中说明:
- 哪些内容发生了变更;
- 哪些文件是重要的;
- 运行了哪些检查;
- 哪些内容未能验证;
- 仍存在哪些风险。
保持报告简洁且诚实。