ai-developer-skill-os 9.3.1 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/AGENTS.md +73 -40
- package/.agents/DEV_PROFILE.md +36 -2
- package/.agents/LICENSE +21 -21
- package/.agents/docs/ARCHITECTURE.md +56 -120
- package/.agents/docs/GOVERNANCE.md +3 -3
- package/.agents/docs/SPEC.md +137 -60
- package/.agents/docs/VERSIONING.md +25 -57
- package/.agents/docs/adr/0005-v10-platform-consolidation.md +50 -0
- package/.agents/docs/schemas/learning.schema.yml +22 -57
- package/.agents/docs/schemas/skill.schema.yml +116 -161
- package/.agents/docs/schemas/workflow.schema.yml +51 -26
- package/.agents/docs/skill-classification.md +1 -1
- package/.agents/registry/graph.json +193 -346
- package/.agents/registry/index.yaml +90 -233
- package/.agents/rules/coding.md +30 -12
- package/.agents/rules/command-safety.md +20 -9
- package/.agents/rules/global.md +234 -28
- package/.agents/rules/prompt-compiler.md +170 -0
- package/.agents/rules/safety.md +1 -1
- package/.agents/rules/security.md +1 -1
- package/.agents/rules/skill-quality.md +18 -3
- package/.agents/skills/_template/SKILL.md +238 -88
- package/.agents/skills/qk-api-data-discovery/SKILL.md +456 -0
- package/.agents/skills/qk-api-data-discovery/references/bronze-record-format.md +47 -0
- package/.agents/skills/qk-api-data-discovery/references/data-contract-yaml.md +72 -0
- package/.agents/skills/qk-api-data-discovery/references/discovery-report-template.md +110 -0
- package/.agents/skills/qk-backend-data/SKILL.md +339 -0
- package/.agents/skills/qk-bug-resolution/SKILL.md +328 -248
- package/.agents/skills/qk-bug-resolution/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-code-cleaner/SKILL.md +399 -0
- package/.agents/skills/qk-code-review/SKILL.md +320 -247
- package/.agents/skills/qk-code-review/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-code-review/references/ai/{v8-schema-validation.md → schema-validation.md} +2 -2
- package/.agents/skills/qk-code-review/references/cross-cutting/async-concurrency-patterns.md +515 -515
- package/.agents/skills/qk-code-review/references/cross-cutting/error-handling-principles.md +492 -492
- package/.agents/skills/qk-code-review/references/cross-cutting/n-plus-one-queries.md +309 -309
- package/.agents/skills/qk-code-review/references/cross-cutting/sql-injection-prevention.md +307 -307
- package/.agents/skills/qk-code-review/references/cross-cutting/xss-prevention.md +263 -263
- package/.agents/skills/qk-code-review/references/languages/angular.md +768 -768
- package/.agents/skills/qk-code-review/references/languages/c.md +890 -890
- package/.agents/skills/qk-code-review/references/languages/cpp.md +893 -893
- package/.agents/skills/qk-code-review/references/languages/css-less-sass.md +661 -661
- package/.agents/skills/qk-code-review/references/languages/django.md +985 -985
- package/.agents/skills/qk-code-review/references/languages/fastapi.md +580 -580
- package/.agents/skills/qk-code-review/references/languages/go.md +993 -993
- package/.agents/skills/qk-code-review/references/languages/java.md +409 -409
- package/.agents/skills/qk-code-review/references/languages/java8.md +586 -586
- package/.agents/skills/qk-code-review/references/languages/kotlin.md +1018 -1018
- package/.agents/skills/qk-code-review/references/languages/nestjs.md +593 -593
- package/.agents/skills/qk-code-review/references/languages/php.md +684 -684
- package/.agents/skills/qk-code-review/references/languages/python.md +1073 -1073
- package/.agents/skills/qk-code-review/references/languages/qt.md +757 -757
- package/.agents/skills/qk-code-review/references/languages/react.md +871 -871
- package/.agents/skills/qk-code-review/references/languages/ruby.md +964 -964
- package/.agents/skills/qk-code-review/references/languages/rust.md +846 -846
- package/.agents/skills/qk-code-review/references/languages/svelte.md +1064 -1064
- package/.agents/skills/qk-code-review/references/languages/swift.md +936 -936
- package/.agents/skills/qk-code-review/references/languages/typescript.md +1016 -1016
- package/.agents/skills/qk-code-review/references/languages/vue.md +924 -924
- package/.agents/skills/qk-code-review/references/languages/zig.md +440 -440
- package/.agents/skills/qk-devops-release/SKILL.md +294 -0
- package/.agents/skills/qk-feature-delivery/SKILL.md +331 -247
- package/.agents/skills/qk-feature-delivery/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-orchestrator/SKILL.md +286 -152
- package/.agents/skills/qk-orchestrator/evals/scorecard.yaml +1 -1
- package/.agents/skills/qk-orchestrator/references/routing-table.md +1 -1
- package/.agents/skills/qk-product-spec/SKILL.md +262 -0
- package/.agents/skills/qk-prompt-compiler/SKILL.md +444 -0
- package/.agents/skills/qk-ui-engineer/SKILL.md +286 -0
- package/.agents/workflows/_schema.yml +146 -146
- package/.agents/workflows/bug-resolution.yml +155 -121
- package/.agents/workflows/code-review.yml +127 -93
- package/.agents/workflows/context-discovery.yml +128 -94
- package/.agents/workflows/documentation.yml +124 -90
- package/.agents/workflows/feature-delivery.yml +158 -124
- package/.agents/workflows/production-release.yml +207 -173
- package/.agents/workflows/prompt-compilation.yml +126 -0
- package/.agents/workflows/refactor.yml +136 -102
- package/.agents/workflows/security-audit.yml +149 -115
- package/.agents/workflows/shared/quality-gate.yml +3 -1
- package/.agents/workflows/skin-governance.yml +149 -115
- package/.agents/workflows/spec-driven-development.yml +116 -87
- package/CHANGELOG.md +77 -0
- package/README.md +125 -205
- package/bin/install.js +324 -329
- package/package.json +68 -74
- package/tooling/build-registry.js +226 -208
- package/tooling/run-aar.js +55 -126
- package/tooling/sync-versions.js +2 -2
- package/tooling/validate-graph.js +100 -87
- package/tooling/validate-skills.js +32 -14
- package/.agents/README.md +0 -90
- package/.agents/docs/CHI_TIET_SKILLS.md +0 -126
- package/.agents/docs/HUONG_DAN_SU_DUNG.md +0 -120
- package/.agents/docs/MIGRATION-CLEANUP-V8.1.3.md +0 -36
- package/.agents/docs/MIGRATION-STATUS.md +0 -35
- package/.agents/docs/MIGRATION-V8.md +0 -10
- package/.agents/docs/ROADMAP-V8.2.md +0 -78
- package/.agents/docs/V8-CERTIFICATION.md +0 -27
- package/.agents/docs/decisions/ADR-001-v8-migration.md +0 -58
- package/.agents/docs/decisions/ADR-002-workflow-separation.md +0 -50
- package/.agents/docs/decisions/ADR-003-registry-generated.md +0 -54
- package/.agents/docs/decisions/ADR-008-skill-boundary-review.md +0 -27
- package/.agents/registry/capability-graph.yml +0 -390
- package/.agents/registry/skills-index.yml +0 -305
- package/.agents/skills/_template/capability.yaml +0 -34
- package/.agents/skills/_template/evals/scorecard.yaml +0 -19
- package/.agents/skills/qk-access-policy/SKILL.md +0 -206
- package/.agents/skills/qk-access-policy/capability.yaml +0 -23
- package/.agents/skills/qk-access-policy/evals/scorecard.yaml +0 -36
- package/.agents/skills/qk-agent-observability/SKILL.md +0 -108
- package/.agents/skills/qk-agent-observability/capability.yaml +0 -29
- package/.agents/skills/qk-agent-observability/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-agent-observability/references/scorecard.yaml +0 -80
- package/.agents/skills/qk-ai-builder/SKILL.md +0 -254
- package/.agents/skills/qk-ai-builder/capability.yaml +0 -23
- package/.agents/skills/qk-ai-builder/evals/scorecard.yaml +0 -30
- package/.agents/skills/qk-api-consumer/SKILL.md +0 -256
- package/.agents/skills/qk-api-consumer/capability.yaml +0 -21
- package/.agents/skills/qk-api-consumer/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-api-lifecycle/SKILL.md +0 -251
- package/.agents/skills/qk-api-lifecycle/capability.yaml +0 -23
- package/.agents/skills/qk-api-lifecycle/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-bug-resolution/capability.yaml +0 -25
- package/.agents/skills/qk-code-review/capability.yaml +0 -23
- package/.agents/skills/qk-context-loader/SKILL.md +0 -198
- package/.agents/skills/qk-context-loader/capability.yaml +0 -23
- package/.agents/skills/qk-context-loader/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-data-engineer/SKILL.md +0 -253
- package/.agents/skills/qk-data-lifecycle/SKILL.md +0 -197
- package/.agents/skills/qk-data-lifecycle/capability.yaml +0 -23
- package/.agents/skills/qk-data-lifecycle/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-db-optimizer/SKILL.md +0 -210
- package/.agents/skills/qk-db-optimizer/capability.yaml +0 -22
- package/.agents/skills/qk-db-optimizer/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-design-system-engineering/SKILL.md +0 -193
- package/.agents/skills/qk-design-system-engineering/capability.yaml +0 -25
- package/.agents/skills/qk-design-system-engineering/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-devops-platform/SKILL.md +0 -198
- package/.agents/skills/qk-devops-platform/capability.yaml +0 -29
- package/.agents/skills/qk-devops-platform/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-docs/SKILL.md +0 -193
- package/.agents/skills/qk-docs/capability.yaml +0 -23
- package/.agents/skills/qk-docs/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-engineering-standard/SKILL.md +0 -89
- package/.agents/skills/qk-engineering-standard/capability.yaml +0 -23
- package/.agents/skills/qk-engineering-standard/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-engineering-standard/references/anti-patterns.md +0 -121
- package/.agents/skills/qk-engineering-standard/rules/backend.md +0 -122
- package/.agents/skills/qk-engineering-standard/rules/database.md +0 -3
- package/.agents/skills/qk-engineering-standard/rules/frontend.md +0 -152
- package/.agents/skills/qk-engineering-standard/rules/security.md +0 -3
- package/.agents/skills/qk-engineering-standard/rules/testing.md +0 -3
- package/.agents/skills/qk-fe-api-integration/SKILL.md +0 -704
- package/.agents/skills/qk-fe-api-integration/capability.yaml +0 -21
- package/.agents/skills/qk-fe-api-integration/evals/scorecard.yaml +0 -29
- package/.agents/skills/qk-feature-delivery/capability.yaml +0 -24
- package/.agents/skills/qk-frontend-architecture/SKILL.md +0 -127
- package/.agents/skills/qk-frontend-architecture/capability.yaml +0 -28
- package/.agents/skills/qk-frontend-architecture/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-help/SKILL.md +0 -107
- package/.agents/skills/qk-help/capability.yaml +0 -20
- package/.agents/skills/qk-help/evals/scorecard.yaml +0 -13
- package/.agents/skills/qk-orchestrator/capability.yaml +0 -22
- package/.agents/skills/qk-product-specification/SKILL.md +0 -187
- package/.agents/skills/qk-product-specification/capability.yaml +0 -27
- package/.agents/skills/qk-product-specification/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-production-release/SKILL.md +0 -188
- package/.agents/skills/qk-production-release/capability.yaml +0 -27
- package/.agents/skills/qk-production-release/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-project-audit/SKILL.md +0 -174
- package/.agents/skills/qk-project-bootstrap/SKILL.md +0 -372
- package/.agents/skills/qk-project-bootstrap/capability.yaml +0 -23
- package/.agents/skills/qk-project-bootstrap/evals/scorecard.yaml +0 -28
- package/.agents/skills/qk-project-health/SKILL.md +0 -202
- package/.agents/skills/qk-project-health/capability.yaml +0 -23
- package/.agents/skills/qk-project-health/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-project-memory/SKILL.md +0 -303
- package/.agents/skills/qk-project-memory/capability.yaml +0 -23
- package/.agents/skills/qk-project-memory/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-refactor/SKILL.md +0 -243
- package/.agents/skills/qk-refactor/capability.yaml +0 -26
- package/.agents/skills/qk-refactor/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-security-audit/SKILL.md +0 -280
- package/.agents/skills/qk-security-audit/capability.yaml +0 -29
- package/.agents/skills/qk-security-audit/evals/scorecard.yaml +0 -27
- package/.agents/skills/qk-system-evolution/SKILL.md +0 -625
- package/.agents/skills/qk-system-evolution/capability.yaml +0 -24
- package/.agents/skills/qk-system-evolution/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-test-engineering/SKILL.md +0 -215
- package/.agents/skills/qk-test-engineering/capability.yaml +0 -28
- package/.agents/skills/qk-test-engineering/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-ui-audit/SKILL.md +0 -175
- package/.agents/skills/qk-ui-audit/capability.yaml +0 -23
- package/.agents/skills/qk-ui-audit/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-ui-audit/references/anti-slop-checklist.md +0 -136
- package/.agents/skills/qk-ui-builder/SKILL.md +0 -521
- package/.agents/skills/qk-ui-builder/capability.yaml +0 -29
- package/.agents/skills/qk-ui-builder/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-ui-builder/references/anti-patterns.md +0 -295
- package/.agents/skills/qk-ui-builder/references/color.md +0 -115
- package/.agents/skills/qk-ui-builder/references/component-cookbook.md +0 -458
- package/.agents/skills/qk-ui-builder/references/copy.md +0 -250
- package/.agents/skills/qk-ui-builder/references/interaction-and-states.md +0 -115
- package/.agents/skills/qk-ui-builder/references/layout-and-space.md +0 -111
- package/.agents/skills/qk-ui-builder/references/macrostructures/01-bento-grid.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/02-long-document.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/03-marquee-hero.md +0 -51
- package/.agents/skills/qk-ui-builder/references/macrostructures/04-stat-led.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/05-workbench.md +0 -44
- package/.agents/skills/qk-ui-builder/references/macrostructures/06-conversational-faq.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/07-manifesto.md +0 -51
- package/.agents/skills/qk-ui-builder/references/macrostructures/08-photographic.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/09-quote-led.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/11-catalogue.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/12-letter.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/13-index-first.md +0 -49
- package/.agents/skills/qk-ui-builder/references/macrostructures/14-narrative-workflow.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/15-split-studio.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/16-feature-stack.md +0 -51
- package/.agents/skills/qk-ui-builder/references/macrostructures/17-type-specimen.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/18-portfolio-grid.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/19-map-diagram.md +0 -50
- package/.agents/skills/qk-ui-builder/references/macrostructures/20-ecosystem-index.md +0 -48
- package/.agents/skills/qk-ui-builder/references/macrostructures/21-component-playground.md +0 -45
- package/.agents/skills/qk-ui-builder/references/macrostructures.md +0 -38
- package/.agents/skills/qk-ui-builder/references/motion.md +0 -95
- package/.agents/skills/qk-ui-builder/references/responsive.md +0 -115
- package/.agents/skills/qk-ui-builder/references/slop-test.md +0 -135
- package/.agents/skills/qk-ui-builder/references/structure.md +0 -280
- package/.agents/skills/qk-ui-builder/references/themes/atmospheric.md +0 -53
- package/.agents/skills/qk-ui-builder/references/themes/carnival.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/cobalt.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/editorial.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/garden.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/hum.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/lumen.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/midnight.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/modern-minimal.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/playful.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/specimen.md +0 -52
- package/.agents/skills/qk-ui-builder/references/themes/terminal.md +0 -52
- package/.agents/skills/qk-ui-builder/references/typography.md +0 -129
- package/.agents/skills/qk-ui-system-builder/SKILL.md +0 -183
- package/.agents/skills/qk-ui-system-builder/capability.yaml +0 -25
- package/.agents/skills/qk-ui-system-builder/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-upgrade/SKILL.md +0 -301
- package/.agents/skills/qk-upgrade/capability.yaml +0 -24
- package/.agents/skills/qk-upgrade/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-validation-gate/SKILL.md +0 -88
- package/.agents/skills/qk-validation-gate/capability.yaml +0 -23
- package/.agents/skills/qk-validation-gate/evals/scorecard.yaml +0 -26
- package/.agents/skills/qk-web-quality-gate/SKILL.md +0 -197
- package/.agents/skills/qk-web-quality-gate/capability.yaml +0 -24
- package/.agents/skills/qk-web-quality-gate/evals/scorecard.yaml +0 -26
- package/.agents/workflows/research.yml +0 -75
- package/.agents/workflows/skill-evolution.yml +0 -97
- package/tooling/fix-refactor.js +0 -8
|
@@ -1,458 +0,0 @@
|
|
|
1
|
-
# 📘 Component Cookbook — Blueprint chuẩn
|
|
2
|
-
|
|
3
|
-
> Stack mặc định: React + TypeScript + Next.js (App Router) + Tailwind (Design Token qua CSS variable) + **TanStack Query (React Query)** cho toàn bộ server-state.
|
|
4
|
-
> Nếu dự án dùng stack khác (Vue, Redux, styled-components...), điều chỉnh cú pháp nhưng giữ nguyên **cấu trúc giải phẫu** bên dưới.
|
|
5
|
-
>
|
|
6
|
-
> **Quy tắc phân định state (quan trọng khi có React Query):**
|
|
7
|
-
> - **Server state** (dữ liệu đến từ API/DB: user, list sản phẩm, kết quả search...) → luôn qua React Query. KHÔNG copy vào `useState`/Redux/Zustand.
|
|
8
|
-
> - **Client state** (UI thuần: modal đang mở, tab đang chọn, giá trị input chưa submit) → `useState`/Context/Zustand như bình thường.
|
|
9
|
-
> - Hai loại state này không được trộn vào cùng một chỗ quản lý.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## 1. Giải phẫu Component chuẩn
|
|
14
|
-
|
|
15
|
-
Một component "sạch" luôn tách làm 3 lớp theo đúng thứ tự:
|
|
16
|
-
|
|
17
|
-
```
|
|
18
|
-
1. Types/Props → định nghĩa hợp đồng dữ liệu (interface)
|
|
19
|
-
2. Custom Hook → toàn bộ logic, state, side-effect, gọi service
|
|
20
|
-
3. JSX thuần → chỉ render, KHÔNG chứa logic nghiệp vụ
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
Quy tắc vàng: **nếu bạn phải "đọc" logic mới hiểu component render cái gì, tức là logic và UI đang bị trộn lẫn.**
|
|
24
|
-
|
|
25
|
-
```
|
|
26
|
-
components/
|
|
27
|
-
Button/
|
|
28
|
-
Button.tsx ← JSX thuần + import hook
|
|
29
|
-
useButton.ts ← logic (nếu có state/behavior phức tạp)
|
|
30
|
-
Button.types.ts ← Props interface
|
|
31
|
-
Button.stories.tsx ← (tuỳ chọn) Storybook
|
|
32
|
-
index.ts ← re-export
|
|
33
|
-
```
|
|
34
|
-
|
|
35
|
-
---
|
|
36
|
-
|
|
37
|
-
## 2. Ví dụ mẫu: Button chuẩn mực
|
|
38
|
-
|
|
39
|
-
### `Button.types.ts`
|
|
40
|
-
```ts
|
|
41
|
-
import type { ButtonHTMLAttributes, ReactNode } from "react";
|
|
42
|
-
|
|
43
|
-
export type ButtonVariant = "primary" | "secondary" | "danger" | "ghost";
|
|
44
|
-
export type ButtonSize = "sm" | "md" | "lg";
|
|
45
|
-
|
|
46
|
-
export interface ButtonProps
|
|
47
|
-
extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
|
|
48
|
-
/** Nội dung hiển thị bên trong button */
|
|
49
|
-
children: ReactNode;
|
|
50
|
-
/** Kiểu giao diện, mặc định 'primary' */
|
|
51
|
-
variant?: ButtonVariant;
|
|
52
|
-
/** Kích thước, mặc định 'md' */
|
|
53
|
-
size?: ButtonSize;
|
|
54
|
-
/** Trạng thái đang xử lý async */
|
|
55
|
-
isLoading?: boolean;
|
|
56
|
-
/** Icon hiển thị trước label (tuỳ chọn) */
|
|
57
|
-
leadingIcon?: ReactNode;
|
|
58
|
-
}
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
### `useButton.ts` (logic tách riêng — chỉ cần khi có behavior phức tạp)
|
|
62
|
-
```ts
|
|
63
|
-
import { useCallback } from "react";
|
|
64
|
-
import type { ButtonProps } from "./Button.types";
|
|
65
|
-
|
|
66
|
-
export function useButton({
|
|
67
|
-
isLoading,
|
|
68
|
-
disabled,
|
|
69
|
-
onClick,
|
|
70
|
-
}: Pick<ButtonProps, "isLoading" | "disabled" | "onClick">) {
|
|
71
|
-
const isDisabled = Boolean(isLoading || disabled);
|
|
72
|
-
|
|
73
|
-
const handleClick = useCallback<NonNullable<ButtonProps["onClick"]>>(
|
|
74
|
-
(event) => {
|
|
75
|
-
if (isDisabled) return;
|
|
76
|
-
onClick?.(event);
|
|
77
|
-
},
|
|
78
|
-
[isDisabled, onClick]
|
|
79
|
-
);
|
|
80
|
-
|
|
81
|
-
return { isDisabled, handleClick };
|
|
82
|
-
}
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
### `Button.tsx` (JSX thuần)
|
|
86
|
-
```tsx
|
|
87
|
-
import { forwardRef } from "react";
|
|
88
|
-
import { cva, type VariantProps } from "class-variance-authority";
|
|
89
|
-
import { cn } from "@/lib/utils";
|
|
90
|
-
import { useButton } from "./useButton";
|
|
91
|
-
import type { ButtonProps } from "./Button.types";
|
|
92
|
-
|
|
93
|
-
// Design Token được ánh xạ qua Tailwind class (không hardcode hex color)
|
|
94
|
-
const buttonStyles = cva(
|
|
95
|
-
"inline-flex items-center justify-center gap-2 rounded-md font-medium " +
|
|
96
|
-
"transition-colors focus-visible:outline-none focus-visible:ring-2 " +
|
|
97
|
-
"focus-visible:ring-offset-2 focus-visible:ring-ring " +
|
|
98
|
-
"disabled:pointer-events-none disabled:opacity-50",
|
|
99
|
-
{
|
|
100
|
-
variants: {
|
|
101
|
-
variant: {
|
|
102
|
-
primary: "bg-primary text-primary-foreground hover:bg-primary/90 active:bg-primary/80",
|
|
103
|
-
secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/90",
|
|
104
|
-
danger: "bg-danger text-danger-foreground hover:bg-danger/90",
|
|
105
|
-
ghost: "bg-transparent text-foreground hover:bg-muted",
|
|
106
|
-
},
|
|
107
|
-
size: {
|
|
108
|
-
sm: "h-8 px-3 text-sm",
|
|
109
|
-
md: "h-10 px-4 text-sm",
|
|
110
|
-
lg: "h-12 px-6 text-base",
|
|
111
|
-
},
|
|
112
|
-
},
|
|
113
|
-
defaultVariants: { variant: "primary", size: "md" },
|
|
114
|
-
}
|
|
115
|
-
);
|
|
116
|
-
|
|
117
|
-
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|
118
|
-
(
|
|
119
|
-
{ children, variant, size, isLoading, leadingIcon, className, onClick, disabled, ...rest },
|
|
120
|
-
ref
|
|
121
|
-
) => {
|
|
122
|
-
const { isDisabled, handleClick } = useButton({ isLoading, disabled, onClick });
|
|
123
|
-
|
|
124
|
-
return (
|
|
125
|
-
<button
|
|
126
|
-
ref={ref}
|
|
127
|
-
type="button"
|
|
128
|
-
className={cn(buttonStyles({ variant, size }), className)}
|
|
129
|
-
disabled={isDisabled}
|
|
130
|
-
aria-busy={isLoading || undefined}
|
|
131
|
-
aria-disabled={isDisabled || undefined}
|
|
132
|
-
onClick={handleClick}
|
|
133
|
-
{...rest}
|
|
134
|
-
>
|
|
135
|
-
{isLoading ? (
|
|
136
|
-
<span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent" aria-hidden="true" />
|
|
137
|
-
) : (
|
|
138
|
-
leadingIcon
|
|
139
|
-
)}
|
|
140
|
-
<span>{children}</span>
|
|
141
|
-
</button>
|
|
142
|
-
);
|
|
143
|
-
}
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
Button.displayName = "Button";
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### `index.ts`
|
|
150
|
-
```ts
|
|
151
|
-
export { Button } from "./Button";
|
|
152
|
-
export type { ButtonProps, ButtonVariant, ButtonSize } from "./Button.types";
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
**Vì sao mẫu này đạt chuẩn:**
|
|
156
|
-
- ✅ Props có type rõ ràng, kế thừa đúng HTML attributes gốc thay vì định nghĩa lại từ đầu.
|
|
157
|
-
- ✅ Logic (`isDisabled`, `handleClick`) tách khỏi JSX qua `useButton`.
|
|
158
|
-
- ✅ Style dùng Design Token (`bg-primary`, `text-danger-foreground`...) — không hardcode màu.
|
|
159
|
-
- ✅ Có đủ `variant`/`size` mở rộng qua `cva`, dễ thêm token mới.
|
|
160
|
-
- ✅ Có `aria-busy`, `aria-disabled`, dùng đúng thẻ `<button>` — hỗ trợ người dùng khuyết tật.
|
|
161
|
-
- ✅ `forwardRef` để component cha có thể focus/measure DOM node khi cần.
|
|
162
|
-
- ✅ Xử lý trạng thái loading (spinner) mà không phá layout.
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## 3. Mẫu component có gọi dữ liệu async — dùng React Query (ví dụ `UserCard`)
|
|
167
|
-
|
|
168
|
-
Đây là ví dụ minh hoạ nguyên tắc "không gọi API trực tiếp trong component trình bày", với React Query đảm nhiệm toàn bộ cache/loading/error thay vì tự viết `useState` + `useEffect`:
|
|
169
|
-
|
|
170
|
-
```
|
|
171
|
-
features/user/
|
|
172
|
-
components/UserCard.tsx ← JSX thuần, dùng hook query
|
|
173
|
-
hooks/useUserProfile.ts ← wrap useQuery, khai báo queryKey chuẩn
|
|
174
|
-
services/userService.ts ← fetch logic thuần, không biết gì về React
|
|
175
|
-
queryKeys.ts ← nơi tập trung mọi queryKey của feature
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
```ts
|
|
179
|
-
// services/userService.ts — fetch logic thuần, không import React/React Query
|
|
180
|
-
export async function fetchUserProfile(userId: string): Promise<UserProfile> {
|
|
181
|
-
const res = await fetch(`/api/users/${userId}`);
|
|
182
|
-
if (!res.ok) throw new Error("Failed to fetch user profile");
|
|
183
|
-
return res.json();
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function updateUserProfile(userId: string, patch: Partial<UserProfile>): Promise<UserProfile> {
|
|
187
|
-
const res = await fetch(`/api/users/${userId}`, {
|
|
188
|
-
method: "PATCH",
|
|
189
|
-
headers: { "Content-Type": "application/json" },
|
|
190
|
-
body: JSON.stringify(patch),
|
|
191
|
-
});
|
|
192
|
-
if (!res.ok) throw new Error("Failed to update user profile");
|
|
193
|
-
return res.json();
|
|
194
|
-
}
|
|
195
|
-
```
|
|
196
|
-
|
|
197
|
-
```ts
|
|
198
|
-
// queryKeys.ts — tập trung key, tránh string rải rác gây khó invalidate
|
|
199
|
-
export const userKeys = {
|
|
200
|
-
all: ["users"] as const,
|
|
201
|
-
detail: (userId: string) => [...userKeys.all, "detail", userId] as const,
|
|
202
|
-
};
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
```ts
|
|
206
|
-
// hooks/useUserProfile.ts
|
|
207
|
-
import { useQuery } from "@tanstack/react-query";
|
|
208
|
-
import { fetchUserProfile } from "../services/userService";
|
|
209
|
-
import { userKeys } from "../queryKeys";
|
|
210
|
-
|
|
211
|
-
export function useUserProfile(userId: string) {
|
|
212
|
-
return useQuery({
|
|
213
|
-
queryKey: userKeys.detail(userId),
|
|
214
|
-
queryFn: () => fetchUserProfile(userId),
|
|
215
|
-
enabled: Boolean(userId), // tránh gọi khi userId rỗng
|
|
216
|
-
staleTime: 60_000, // dữ liệu ít đổi → giảm refetch thừa
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
```
|
|
220
|
-
|
|
221
|
-
```tsx
|
|
222
|
-
// components/UserCard.tsx
|
|
223
|
-
import { useUserProfile } from "../hooks/useUserProfile";
|
|
224
|
-
|
|
225
|
-
export function UserCard({ userId }: { userId: string }) {
|
|
226
|
-
const { data, isPending, isError } = useUserProfile(userId);
|
|
227
|
-
|
|
228
|
-
if (isPending) return <UserCardSkeleton />;
|
|
229
|
-
if (isError) return <p role="alert">Không tải được thông tin người dùng.</p>;
|
|
230
|
-
|
|
231
|
-
return (
|
|
232
|
-
<article className="rounded-lg border p-4">
|
|
233
|
-
<h3 className="font-semibold">{data.name}</h3>
|
|
234
|
-
<p className="text-muted-foreground text-sm">{data.email}</p>
|
|
235
|
-
</article>
|
|
236
|
-
);
|
|
237
|
-
}
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
### Mutation chuẩn (ví dụ cập nhật hồ sơ)
|
|
241
|
-
|
|
242
|
-
```ts
|
|
243
|
-
// hooks/useUpdateUserProfile.ts
|
|
244
|
-
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
245
|
-
import { updateUserProfile } from "../services/userService";
|
|
246
|
-
import { userKeys } from "../queryKeys";
|
|
247
|
-
import type { UserProfile } from "../types";
|
|
248
|
-
|
|
249
|
-
export function useUpdateUserProfile(userId: string) {
|
|
250
|
-
const queryClient = useQueryClient();
|
|
251
|
-
|
|
252
|
-
return useMutation({
|
|
253
|
-
mutationFn: (patch: Partial<UserProfile>) => updateUserProfile(userId, patch),
|
|
254
|
-
onSuccess: (updated) => {
|
|
255
|
-
// Cập nhật cache ngay, không cần chờ refetch
|
|
256
|
-
queryClient.setQueryData(userKeys.detail(userId), updated);
|
|
257
|
-
},
|
|
258
|
-
// onError: hiển thị toast lỗi ở nơi gọi mutate, không nuốt lỗi im lặng
|
|
259
|
-
});
|
|
260
|
-
}
|
|
261
|
-
```
|
|
262
|
-
|
|
263
|
-
```tsx
|
|
264
|
-
// components/EditProfileForm.tsx
|
|
265
|
-
import { useUpdateUserProfile } from "../hooks/useUpdateUserProfile";
|
|
266
|
-
|
|
267
|
-
export function EditProfileForm({ userId }: { userId: string }) {
|
|
268
|
-
const { mutate, isPending, error } = useUpdateUserProfile(userId);
|
|
269
|
-
|
|
270
|
-
return (
|
|
271
|
-
<form
|
|
272
|
-
onSubmit={(e) => {
|
|
273
|
-
e.preventDefault();
|
|
274
|
-
const form = new FormData(e.currentTarget);
|
|
275
|
-
mutate({ name: String(form.get("name")) });
|
|
276
|
-
}}
|
|
277
|
-
>
|
|
278
|
-
<input name="name" defaultValue="" />
|
|
279
|
-
<Button type="submit" isLoading={isPending}>Lưu</Button>
|
|
280
|
-
{error && <p role="alert">Cập nhật thất bại, thử lại.</p>}
|
|
281
|
-
</form>
|
|
282
|
-
);
|
|
283
|
-
}
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
**Vì sao mẫu này đạt chuẩn:**
|
|
287
|
-
- ✅ `queryKey` được khai báo tập trung ở `queryKeys.ts`, không rải string tay ở nhiều nơi.
|
|
288
|
-
- ✅ `service` layer không biết gì về React/React Query — có thể tái dùng ở server, test độc lập.
|
|
289
|
-
- ✅ Dùng `isPending`/`isError` do React Query cung cấp thay vì tự quản lý state loading.
|
|
290
|
-
- ✅ Mutation cập nhật cache qua `setQueryData` thay vì `invalidateQueries` tràn lan gây refetch thừa (chỉ dùng `invalidateQueries` khi thực sự cần đồng bộ lại từ server).
|
|
291
|
-
- ✅ Component KHÔNG tự gọi `fetch` — chỉ gọi hook.
|
|
292
|
-
|
|
293
|
-
---
|
|
294
|
-
|
|
295
|
-
## 4. Blueprint nhanh: Table / Form / Virtual / Router
|
|
296
|
-
|
|
297
|
-
### 4.1 TanStack Table (headless — bảng có sort/filter/pagination)
|
|
298
|
-
|
|
299
|
-
```tsx
|
|
300
|
-
// components/UserTable.tsx
|
|
301
|
-
import { useReactTable, getCoreRowModel, getSortedRowModel, type ColumnDef } from "@tanstack/react-table";
|
|
302
|
-
|
|
303
|
-
const columns: ColumnDef<UserProfile>[] = [
|
|
304
|
-
{ accessorKey: "name", header: "Tên", enableSorting: true },
|
|
305
|
-
{ accessorKey: "email", header: "Email" },
|
|
306
|
-
];
|
|
307
|
-
|
|
308
|
-
export function UserTable({ data }: { data: UserProfile[] }) {
|
|
309
|
-
const table = useReactTable({
|
|
310
|
-
data,
|
|
311
|
-
columns,
|
|
312
|
-
getCoreRowModel: getCoreRowModel(),
|
|
313
|
-
getSortedRowModel: getSortedRowModel(),
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
return (
|
|
317
|
-
<table className="w-full text-sm">
|
|
318
|
-
<thead>
|
|
319
|
-
{table.getHeaderGroups().map((hg) => (
|
|
320
|
-
<tr key={hg.id}>
|
|
321
|
-
{hg.headers.map((h) => (
|
|
322
|
-
<th key={h.id} onClick={h.column.getToggleSortingHandler()} className="cursor-pointer text-left p-2">
|
|
323
|
-
{h.isPlaceholder ? null : h.column.columnDef.header as string}
|
|
324
|
-
</th>
|
|
325
|
-
))}
|
|
326
|
-
</tr>
|
|
327
|
-
))}
|
|
328
|
-
</thead>
|
|
329
|
-
<tbody>
|
|
330
|
-
{table.getRowModel().rows.map((row) => (
|
|
331
|
-
<tr key={row.id} className="border-t">
|
|
332
|
-
{row.getVisibleCells().map((cell) => (
|
|
333
|
-
<td key={cell.id} className="p-2">{String(cell.getValue())}</td>
|
|
334
|
-
))}
|
|
335
|
-
</tr>
|
|
336
|
-
))}
|
|
337
|
-
</tbody>
|
|
338
|
-
</table>
|
|
339
|
-
);
|
|
340
|
-
}
|
|
341
|
-
```
|
|
342
|
-
> Nguyên tắc: `columns` định nghĩa tách biệt khỏi JSX, JSX chỉ `.map()` để render — không tự viết sort/filter tay.
|
|
343
|
-
|
|
344
|
-
### 4.2 TanStack Virtual (danh sách lớn)
|
|
345
|
-
|
|
346
|
-
```tsx
|
|
347
|
-
// components/VirtualUserList.tsx
|
|
348
|
-
import { useRef } from "react";
|
|
349
|
-
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
350
|
-
|
|
351
|
-
export function VirtualUserList({ items }: { items: UserProfile[] }) {
|
|
352
|
-
const parentRef = useRef<HTMLDivElement>(null);
|
|
353
|
-
const virtualizer = useVirtualizer({
|
|
354
|
-
count: items.length,
|
|
355
|
-
getScrollElement: () => parentRef.current,
|
|
356
|
-
estimateSize: () => 56, // chiều cao ước tính mỗi row (px)
|
|
357
|
-
overscan: 8,
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
return (
|
|
361
|
-
<div ref={parentRef} className="h-[480px] overflow-auto">
|
|
362
|
-
<div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
|
|
363
|
-
{virtualizer.getVirtualItems().map((row) => (
|
|
364
|
-
<div
|
|
365
|
-
key={row.key}
|
|
366
|
-
style={{ position: "absolute", top: 0, left: 0, width: "100%", transform: `translateY(${row.start}px)`, height: row.size }}
|
|
367
|
-
>
|
|
368
|
-
{items[row.index].name}
|
|
369
|
-
</div>
|
|
370
|
-
))}
|
|
371
|
-
</div>
|
|
372
|
-
</div>
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
```
|
|
376
|
-
> Nguyên tắc: không render toàn bộ `items.map()` trực tiếp khi danh sách có thể vượt vài trăm phần tử.
|
|
377
|
-
|
|
378
|
-
### 4.3 TanStack Form (form nhiều field, validate type-safe)
|
|
379
|
-
|
|
380
|
-
```tsx
|
|
381
|
-
// components/EditProfileForm.tsx
|
|
382
|
-
import { useForm } from "@tanstack/react-form";
|
|
383
|
-
import { z } from "zod";
|
|
384
|
-
|
|
385
|
-
const schema = z.object({
|
|
386
|
-
name: z.string().min(1, "Tên không được để trống"),
|
|
387
|
-
email: z.string().email("Email không hợp lệ"),
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
export function EditProfileForm({ onSubmit }: { onSubmit: (v: z.infer<typeof schema>) => void }) {
|
|
391
|
-
const form = useForm({
|
|
392
|
-
defaultValues: { name: "", email: "" },
|
|
393
|
-
onSubmit: async ({ value }) => onSubmit(schema.parse(value)),
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
return (
|
|
397
|
-
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
|
|
398
|
-
<form.Field
|
|
399
|
-
name="name"
|
|
400
|
-
validators={{ onChange: ({ value }) => (value.length ? undefined : "Tên không được để trống") }}
|
|
401
|
-
>
|
|
402
|
-
{(field) => (
|
|
403
|
-
<>
|
|
404
|
-
<input
|
|
405
|
-
value={field.state.value}
|
|
406
|
-
onChange={(e) => field.handleChange(e.target.value)}
|
|
407
|
-
onBlur={field.handleBlur}
|
|
408
|
-
/>
|
|
409
|
-
{field.state.meta.errors.length > 0 && <p role="alert">{field.state.meta.errors[0]}</p>}
|
|
410
|
-
</>
|
|
411
|
-
)}
|
|
412
|
-
</form.Field>
|
|
413
|
-
<Button type="submit">Lưu</Button>
|
|
414
|
-
</form>
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
```
|
|
418
|
-
> Nguyên tắc: validate schema dùng chung (Zod) giữa client và server; mỗi field tự quản lý re-render qua `form.Field`, tránh re-render toàn form mỗi keystroke.
|
|
419
|
-
|
|
420
|
-
### 4.4 TanStack Router (route + loader prefetch)
|
|
421
|
-
|
|
422
|
-
```tsx
|
|
423
|
-
// routes/users.$userId.tsx
|
|
424
|
-
import { createFileRoute } from "@tanstack/react-router";
|
|
425
|
-
import { userKeys } from "../queryKeys";
|
|
426
|
-
import { fetchUserProfile } from "../services/userService";
|
|
427
|
-
|
|
428
|
-
export const Route = createFileRoute("/users/$userId")({
|
|
429
|
-
loader: ({ context: { queryClient }, params }) =>
|
|
430
|
-
queryClient.ensureQueryData({
|
|
431
|
-
queryKey: userKeys.detail(params.userId),
|
|
432
|
-
queryFn: () => fetchUserProfile(params.userId),
|
|
433
|
-
}),
|
|
434
|
-
component: UserPage,
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
function UserPage() {
|
|
438
|
-
const { userId } = Route.useParams();
|
|
439
|
-
// Data đã được prefetch trong loader — useQuery ở đây chỉ đọc cache, không fetch lại
|
|
440
|
-
return <UserCard userId={userId} />;
|
|
441
|
-
}
|
|
442
|
-
```
|
|
443
|
-
> Nguyên tắc: loader prefetch qua `queryClient`, component tiêu thụ lại qua `useQuery`/hook cùng `queryKey` — tránh fetch 2 lần.
|
|
444
|
-
|
|
445
|
-
---
|
|
446
|
-
|
|
447
|
-
## 5. Checklist khi sinh component mới
|
|
448
|
-
|
|
449
|
-
- [ ] Props có `interface` riêng, đặt tên `<Component>Props`
|
|
450
|
-
- [ ] Logic phức tạp (>1 state hoặc có side-effect) được tách vào custom hook
|
|
451
|
-
- [ ] Không gọi `fetch`/API trực tiếp trong file `.tsx` trình bày
|
|
452
|
-
- [ ] Style dùng Design Token/Tailwind class, không hardcode màu/spacing
|
|
453
|
-
- [ ] Có xử lý đủ trạng thái loading/error/empty nếu có dữ liệu async
|
|
454
|
-
- [ ] Có ARIA attributes và dùng đúng thẻ semantic
|
|
455
|
-
- [ ] Export qua `index.ts` để import path gọn (`@/components/Button`)
|
|
456
|
-
- [ ] Bảng dữ liệu dùng TanStack Table (`columns` tách khỏi JSX), danh sách lớn dùng TanStack Virtual
|
|
457
|
-
- [ ] Form nhiều field dùng TanStack Form với schema Zod dùng chung client/server
|
|
458
|
-
- [ ] Route có params/search dùng TanStack Router type-safe, dữ liệu prefetch qua `loader` + `ensureQueryData`
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
# UI Copy Rules
|
|
2
|
-
|
|
3
|
-
Microcopy is the small bits of text that guide, inform, and reassure users. Good copy is invisible; bad copy creates friction.
|
|
4
|
-
|
|
5
|
-
## Button Labels
|
|
6
|
-
|
|
7
|
-
**Rule:** Verb-first, 1-3 words. Start with the action the user takes.
|
|
8
|
-
|
|
9
|
-
**Good:**
|
|
10
|
-
- Save changes
|
|
11
|
-
- Delete account
|
|
12
|
-
- Upload file
|
|
13
|
-
- Send message
|
|
14
|
-
- Continue to payment
|
|
15
|
-
|
|
16
|
-
**Bad:**
|
|
17
|
-
- Submission (noun, not verb)
|
|
18
|
-
- Click here (generic)
|
|
19
|
-
- Yes / No (without context)
|
|
20
|
-
- Submit your information here (too long)
|
|
21
|
-
|
|
22
|
-
**Guidelines:**
|
|
23
|
-
- Use imperative verbs: Save, Delete, Upload, Send, Continue
|
|
24
|
-
- Avoid articles: "Save" not "Save the changes"
|
|
25
|
-
- Match case to surrounding UI (usually sentence case)
|
|
26
|
-
- For destructive actions, include the object: "Delete file" not just "Delete"
|
|
27
|
-
|
|
28
|
-
---
|
|
29
|
-
|
|
30
|
-
## Error Messages
|
|
31
|
-
|
|
32
|
-
**Rule:** What happened + how to fix. Two sentences max.
|
|
33
|
-
|
|
34
|
-
**Formula:**
|
|
35
|
-
1. State the problem clearly
|
|
36
|
-
2. Provide a specific solution
|
|
37
|
-
|
|
38
|
-
**Good:**
|
|
39
|
-
- "Email address is invalid. Please include an @ symbol."
|
|
40
|
-
- "Password must be at least 8 characters. Try adding a number."
|
|
41
|
-
- "Connection lost. Check your internet and try again."
|
|
42
|
-
|
|
43
|
-
**Bad:**
|
|
44
|
-
- "Error 500" (technical, unhelpful)
|
|
45
|
-
- "Something went wrong" (vague)
|
|
46
|
-
- "Invalid input" (no fix guidance)
|
|
47
|
-
- "Please try again later" (no context)
|
|
48
|
-
|
|
49
|
-
**Guidelines:**
|
|
50
|
-
- Never blame the user ("You entered...")
|
|
51
|
-
- Use plain language, not technical jargon
|
|
52
|
-
- Include actionable next steps
|
|
53
|
-
- Keep it concise: 1-2 sentences
|
|
54
|
-
|
|
55
|
-
---
|
|
56
|
-
|
|
57
|
-
## Placeholder Text
|
|
58
|
-
|
|
59
|
-
**Rule:** Example, not instruction. Show what good looks like, don't tell users what to do.
|
|
60
|
-
|
|
61
|
-
**Good:**
|
|
62
|
-
- "you@example.com" (email input)
|
|
63
|
-
- "12/25/2024" (date input)
|
|
64
|
-
- "Project description..." (textarea)
|
|
65
|
-
- "Search by name or email..." (search input)
|
|
66
|
-
|
|
67
|
-
**Bad:**
|
|
68
|
-
- "Enter your email address" (instruction, redundant with label)
|
|
69
|
-
- "Type here..." (no helpful example)
|
|
70
|
-
- "Required" (states the obvious)
|
|
71
|
-
- "Your full name" (duplicates label)
|
|
72
|
-
|
|
73
|
-
**Guidelines:**
|
|
74
|
-
- Use actual example values
|
|
75
|
-
- Show format for structured data (dates, phone numbers)
|
|
76
|
-
- Avoid repeating the label text
|
|
77
|
-
- Placeholder disappears on focus, so it must be self-explanatory
|
|
78
|
-
|
|
79
|
-
---
|
|
80
|
-
|
|
81
|
-
## CTAs (Call to Action)
|
|
82
|
-
|
|
83
|
-
**Rule:** Specific, not generic. Tell users exactly what they get.
|
|
84
|
-
|
|
85
|
-
**Good:**
|
|
86
|
-
- Start free trial
|
|
87
|
-
- Download whitepaper
|
|
88
|
-
- Book a demo
|
|
89
|
-
- Get started
|
|
90
|
-
- Create account
|
|
91
|
-
- Upgrade to Pro
|
|
92
|
-
|
|
93
|
-
**Bad:**
|
|
94
|
-
- Submit (generic, no value)
|
|
95
|
-
- Click here (no context)
|
|
96
|
-
- Learn more (vague destination)
|
|
97
|
-
- Sign up (missing context: "Sign up for free")
|
|
98
|
-
- Get started (missing context: what are they starting?)
|
|
99
|
-
|
|
100
|
-
**Guidelines:**
|
|
101
|
-
- Include the value proposition: "Start free trial" not "Submit"
|
|
102
|
-
- Be specific about the outcome: "Download report" not "Continue"
|
|
103
|
-
- Avoid ambiguity: "Create account" not "Join us"
|
|
104
|
-
- Match CTA intensity to action weight (primary vs secondary)
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## Ban List
|
|
109
|
-
|
|
110
|
-
These phrases are forbidden in UI copy:
|
|
111
|
-
|
|
112
|
-
### Absolutely Banned
|
|
113
|
-
- "Lorem ipsum" — placeholder text in production
|
|
114
|
-
- "Click here" — generic link text
|
|
115
|
-
- "Read more" — lazy, no context
|
|
116
|
-
- "Learn more" — vague, no value proposition
|
|
117
|
-
- "Submit" — generic, no action clarity
|
|
118
|
-
- "John Doe" / "Jane Smith" — fake names
|
|
119
|
-
- "Test User" — lazy placeholder data
|
|
120
|
-
- "Coming soon" — broken promise
|
|
121
|
-
- "Under construction" — unprofessional
|
|
122
|
-
- "TBD" — visible planning state
|
|
123
|
-
- "Error 404" — technical error without guidance
|
|
124
|
-
- "Something went wrong" — unhelpful error
|
|
125
|
-
- "Please try again later" — no context or fix
|
|
126
|
-
|
|
127
|
-
### Contextual Bans
|
|
128
|
-
- "Welcome to our website" — generic, no value
|
|
129
|
-
- "Thank you for your submission" — assumes success before validation
|
|
130
|
-
- "We value your feedback" — insincere, no action
|
|
131
|
-
- "Powered by AI" — unless AI is a selling point
|
|
132
|
-
- "Best in class" — unsubstantiated claim
|
|
133
|
-
- "Seamless" — meaningless buzzword
|
|
134
|
-
- "Synergy" — meaningless buzzword
|
|
135
|
-
- "Leverage" — pretentious verb
|
|
136
|
-
- "Robust" — vague adjective
|
|
137
|
-
- "Cutting-edge" — cliché
|
|
138
|
-
|
|
139
|
-
---
|
|
140
|
-
|
|
141
|
-
## Tone Guidelines
|
|
142
|
-
|
|
143
|
-
**Voice:** Confident, clear, helpful. Not overly casual or corporate.
|
|
144
|
-
|
|
145
|
-
**Person:** Second person ("you") for user actions. First person plural ("we") for company statements. Avoid third person.
|
|
146
|
-
|
|
147
|
-
**Length:** Short for actions, longer for explanations. Buttons: 1-3 words. Headlines: 2-8 words. Body: 1-3 sentences.
|
|
148
|
-
|
|
149
|
-
**Jargon:** Avoid unless audience is technical. If jargon is necessary, define it on first use.
|
|
150
|
-
|
|
151
|
-
**Humor:** Use sparingly. Only in error states, empty states, or onboarding. Never in critical flows or legal text.
|
|
152
|
-
|
|
153
|
-
---
|
|
154
|
-
|
|
155
|
-
## Accessibility Copy
|
|
156
|
-
|
|
157
|
-
**Alt Text:** Describe the image content, not "image of" or "photo of."
|
|
158
|
-
- Good: "Bar chart showing 40% increase in sales"
|
|
159
|
-
- Bad: "Image" or "Chart"
|
|
160
|
-
|
|
161
|
-
**ARIA Labels:** Be specific and contextual.
|
|
162
|
-
- Good: `aria-label="Close dialog"`
|
|
163
|
-
- Bad: `aria-label="Button"`
|
|
164
|
-
|
|
165
|
-
**Link Text:** Descriptive, not generic.
|
|
166
|
-
- Good: "View pricing plans"
|
|
167
|
-
- Bad: "Click here"
|
|
168
|
-
|
|
169
|
-
**Form Labels:** Clear and concise.
|
|
170
|
-
- Good: "Email address"
|
|
171
|
-
- Bad: "Please enter your email address in the field below"
|
|
172
|
-
|
|
173
|
-
---
|
|
174
|
-
|
|
175
|
-
## Validation States
|
|
176
|
-
|
|
177
|
-
**Success:**
|
|
178
|
-
- "Changes saved"
|
|
179
|
-
- "Account created"
|
|
180
|
-
- "File uploaded"
|
|
181
|
-
|
|
182
|
-
**Error:**
|
|
183
|
-
- "Invalid email address. Example: you@company.com"
|
|
184
|
-
- "Password must include a number and special character"
|
|
185
|
-
- "Connection failed. Please try again."
|
|
186
|
-
|
|
187
|
-
**Warning:**
|
|
188
|
-
- "You have unsaved changes. Leave without saving?"
|
|
189
|
-
- "Your session expires in 5 minutes"
|
|
190
|
-
|
|
191
|
-
**Info:**
|
|
192
|
-
- "New features available. Refresh to see updates."
|
|
193
|
-
- "Your trial ends in 3 days"
|
|
194
|
-
|
|
195
|
-
---
|
|
196
|
-
|
|
197
|
-
## Empty States
|
|
198
|
-
|
|
199
|
-
**No Data:**
|
|
200
|
-
- "No projects yet. Create your first project to get started."
|
|
201
|
-
- "No messages. Start a conversation with your team."
|
|
202
|
-
|
|
203
|
-
**No Search Results:**
|
|
204
|
-
- "No results for 'query'. Try different keywords or clear filters."
|
|
205
|
-
- "We couldn't find anything matching your search."
|
|
206
|
-
|
|
207
|
-
**Error States:**
|
|
208
|
-
- "Something went wrong on our end. Refresh the page or contact support if it persists."
|
|
209
|
-
- "Unable to load data. Check your connection and try again."
|
|
210
|
-
|
|
211
|
-
---
|
|
212
|
-
|
|
213
|
-
## Confirmation Messages
|
|
214
|
-
|
|
215
|
-
**Destructive Actions:**
|
|
216
|
-
- "Are you sure you want to delete this project? This cannot be undone."
|
|
217
|
-
- "Deactivate account? You will lose access to all projects."
|
|
218
|
-
|
|
219
|
-
**Success Actions:**
|
|
220
|
-
- "Account created! Check your email to verify."
|
|
221
|
-
- "File uploaded successfully."
|
|
222
|
-
|
|
223
|
-
**Conditional Actions:**
|
|
224
|
-
- "You have unsaved changes. Save before leaving?"
|
|
225
|
-
- "Discard draft? Your changes will be lost."
|
|
226
|
-
|
|
227
|
-
---
|
|
228
|
-
|
|
229
|
-
## Legal & Compliance
|
|
230
|
-
|
|
231
|
-
**Privacy:**
|
|
232
|
-
- "We use cookies to improve your experience. [Accept] [Settings]"
|
|
233
|
-
- "By signing up, you agree to our Terms and Privacy Policy."
|
|
234
|
-
|
|
235
|
-
**Consent:**
|
|
236
|
-
- "Send me emails with product updates and offers." (opt-in)
|
|
237
|
-
- "I agree to the Terms of Service." (required)
|
|
238
|
-
|
|
239
|
-
**Age:**
|
|
240
|
-
- "You must be 18 or older to create an account."
|
|
241
|
-
|
|
242
|
-
---
|
|
243
|
-
|
|
244
|
-
## Measurement
|
|
245
|
-
|
|
246
|
-
Copy is done when:
|
|
247
|
-
- Users complete tasks without reading the full text
|
|
248
|
-
- Error messages reduce support tickets
|
|
249
|
-
- CTAs have measurable click-through rates
|
|
250
|
-
- No placeholder text remains in production
|