@haposoft/cafekit 0.16.7 → 0.18.1

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.
Files changed (231) hide show
  1. package/README.md +14 -22
  2. package/bin/__tests__/codex-hooks-ownership.test.js +2 -2
  3. package/bin/__tests__/codex-hooks.test.js +169 -272
  4. package/bin/__tests__/codex-native.test.js +100 -202
  5. package/bin/__tests__/develop-contract.test.js +122 -1413
  6. package/bin/__tests__/docs-sync-on-demand.test.js +93 -0
  7. package/bin/__tests__/grok-envelope.test.js +0 -56
  8. package/bin/__tests__/hook-payload.test.js +4 -6
  9. package/bin/__tests__/legacy-authority-decoupled.test.js +154 -0
  10. package/bin/__tests__/legacy-hooks-retired.test.js +221 -0
  11. package/bin/__tests__/omp-bridge.test.js +20 -17
  12. package/bin/__tests__/omp-hooks.test.js +0 -19
  13. package/bin/__tests__/optional-skill-inventory.test.js +112 -4
  14. package/bin/__tests__/orca-session.test.js +0 -2
  15. package/bin/__tests__/package-inventory.test.js +90 -925
  16. package/bin/__tests__/runtime-dir.test.js +0 -3
  17. package/bin/__tests__/runtime-schema.test.js +3 -2
  18. package/bin/__tests__/skill-routing-source.test.js +27 -15
  19. package/bin/__tests__/spec-narrowing.test.js +30 -161
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +0 -437
  21. package/bin/__tests__/test-runtime-dependency-closure.cjs +1 -2
  22. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  23. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  24. package/bin/__tests__/validator-grounding.test.js +18 -2949
  25. package/bin/lib/managed-writer.js +2 -3
  26. package/bin/phases/codex-runtime.js +27 -0
  27. package/bin/phases/copy-payload.js +13 -28
  28. package/bin/phases/omp-runtime.js +25 -0
  29. package/package.json +2 -3
  30. package/src/claude/CLAUDE.md +3 -10
  31. package/src/claude/agents/brainstormer.md +7 -3
  32. package/src/claude/agents/code-auditor.md +44 -56
  33. package/src/claude/agents/debugger.md +8 -88
  34. package/src/claude/agents/deployer.md +3 -5
  35. package/src/claude/agents/docs-keeper.md +6 -14
  36. package/src/claude/agents/git-ops.md +2 -4
  37. package/src/claude/agents/implementer.md +12 -16
  38. package/src/claude/agents/inspector.md +4 -3
  39. package/src/claude/agents/project-manager.md +9 -17
  40. package/src/claude/agents/researcher.md +9 -3
  41. package/src/claude/agents/spec-maker.md +15 -15
  42. package/src/claude/agents/test-runner.md +8 -11
  43. package/src/claude/agents/ui-ux-designer.md +0 -1
  44. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  45. package/src/claude/hooks/__tests__/spec-gate.test.js +244 -333
  46. package/src/claude/hooks/__tests__/spec-state-touched.test.js +160 -0
  47. package/src/claude/hooks/__tests__/state.test.js +68 -0
  48. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  49. package/src/claude/hooks/agent.cjs +2 -1
  50. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  51. package/src/claude/hooks/rules.cjs +2 -1
  52. package/src/claude/hooks/session.cjs +0 -1
  53. package/src/claude/hooks/spec-gate.cjs +17 -83
  54. package/src/claude/hooks/spec-state.cjs +142 -39
  55. package/src/claude/hooks/state.cjs +21 -2
  56. package/src/claude/migration-manifest.json +42 -22
  57. package/src/claude/output-styles/coding-level-0-eli5.md +9 -9
  58. package/src/claude/output-styles/coding-level-1-junior.md +3 -3
  59. package/src/claude/output-styles/coding-level-2-mid.md +3 -3
  60. package/src/claude/output-styles/coding-level-3-senior.md +2 -2
  61. package/src/claude/output-styles/coding-level-4-lead.md +5 -5
  62. package/src/claude/output-styles/coding-level-5-god.md +6 -6
  63. package/src/claude/rules/ai-dev-rules.md +2 -3
  64. package/src/claude/rules/hook-protocols.md +8 -15
  65. package/src/claude/rules/manage-docs.md +9 -23
  66. package/src/claude/rules/orchestrator.md +3 -4
  67. package/src/claude/rules/review-audit-self-decision.md +3 -3
  68. package/src/claude/rules/skill-domain-routing.md +1 -3
  69. package/src/claude/rules/skill-workflow-routing.md +4 -5
  70. package/src/claude/rules/state-sync.md +3 -12
  71. package/src/claude/rules/workflow.md +11 -19
  72. package/src/claude/runtime.schema.json +5 -3
  73. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  74. package/src/claude/scripts/provenance.cjs +23 -5
  75. package/src/claude/scripts/spec-receipt.cjs +24 -121
  76. package/src/claude/scripts/spec-resolver.cjs +41 -458
  77. package/src/claude/scripts/workflow-policy.cjs +3 -1801
  78. package/src/claude/settings/settings.json +1 -32
  79. package/src/claude/skills/agent-browser/SKILL.md +1 -1
  80. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  81. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  82. package/src/claude/skills/chrome-devtools/SKILL.md +14 -31
  83. package/src/claude/skills/code-review/SKILL.md +29 -81
  84. package/src/claude/skills/code-review/references/spec-compliance-review.md +4 -8
  85. package/src/claude/skills/debug/SKILL.md +29 -149
  86. package/src/claude/skills/develop/SKILL.md +26 -83
  87. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  88. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  89. package/src/claude/skills/develop/references/subagent-patterns.md +0 -6
  90. package/src/claude/skills/docs/SKILL.md +2 -2
  91. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  92. package/src/claude/skills/fix/SKILL.md +153 -0
  93. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  94. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  95. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  96. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  97. package/src/claude/skills/git/SKILL.md +48 -9
  98. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  99. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  100. package/src/claude/skills/orca/SKILL.md +9 -8
  101. package/src/claude/skills/research/SKILL.md +17 -46
  102. package/src/claude/skills/scout/SKILL.md +61 -0
  103. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  104. package/src/claude/skills/specs/SKILL.md +22 -29
  105. package/src/claude/skills/specs/references/review.md +8 -6
  106. package/src/claude/skills/specs/references/templates.md +34 -41
  107. package/src/claude/skills/sync/SKILL.md +16 -45
  108. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  109. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  110. package/src/claude/skills/test/SKILL.md +39 -104
  111. package/src/claude/skills/test/references/execution-strategy.md +93 -21
  112. package/src/claude/skills/test/references/failure-triage.md +1 -1
  113. package/src/claude/skills/ui-ux/LICENSE +21 -0
  114. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  115. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  116. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  117. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  118. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  119. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  120. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  121. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  122. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  123. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  124. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  125. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  126. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  127. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  128. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  129. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  130. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  131. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  132. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  133. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  134. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  135. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  136. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  137. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  138. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  139. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  140. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  141. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  142. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  143. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  144. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  145. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  146. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  147. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  148. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  149. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  150. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  151. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  152. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  153. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  154. package/src/claude/skills/ui-ux/references/review.md +528 -0
  155. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  156. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  157. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  158. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  159. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  160. package/src/claude/skills/ui-ux/references/system.md +219 -0
  161. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  162. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  163. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  164. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  165. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  166. package/src/claude/skills/ui-ux-pro-max/SKILL.md +25 -80
  167. package/src/claude/skills/web-testing/SKILL.md +2 -20
  168. package/src/claude/status.cjs +149 -144
  169. package/src/codex/AGENTS.md +8 -13
  170. package/src/codex/hooks/agent.cjs +2 -1
  171. package/src/codex/hooks/lib/spec-receipt.cjs +4 -32
  172. package/src/codex/hooks/lib/spec-utils.cjs +6 -52
  173. package/src/codex/hooks/rules.cjs +2 -1
  174. package/src/codex/hooks/session.cjs +0 -2
  175. package/src/codex/hooks/spec-gate.cjs +13 -81
  176. package/src/codex/hooks/spec-state.cjs +37 -41
  177. package/src/codex/hooks.json +0 -20
  178. package/src/codex/rules/state-sync.md +3 -12
  179. package/src/common/AGENTS.md +6 -12
  180. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  181. package/bin/__tests__/change-firewall.semantic-firewall.test.js +0 -2133
  182. package/bin/__tests__/finalizer-pass-gate.semantic-firewall.test.js +0 -342
  183. package/bin/__tests__/release-preflight.semantic-firewall.test.js +0 -586
  184. package/bin/__tests__/review-receipt-c2.semantic-firewall.test.js +0 -378
  185. package/bin/__tests__/specs-v2-policy-and-scaffold.test.js +0 -665
  186. package/bin/__tests__/specs-v2-validator-grounder-contract.test.js +0 -1132
  187. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  188. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  189. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  190. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  191. package/src/claude/hooks/completion-authority.cjs +0 -163
  192. package/src/claude/hooks/lib/context.cjs +0 -616
  193. package/src/claude/hooks/lib/detect.cjs +0 -474
  194. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  195. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  196. package/src/claude/hooks/usage.cjs +0 -197
  197. package/src/claude/scripts/change-firewall.cjs +0 -1748
  198. package/src/claude/scripts/spec-authoring-digest.cjs +0 -151
  199. package/src/claude/scripts/spec-authoring-validation.cjs +0 -126
  200. package/src/claude/scripts/spec-final-state.cjs +0 -290
  201. package/src/claude/scripts/spec-ground.cjs +0 -650
  202. package/src/claude/scripts/spec-readiness.cjs +0 -425
  203. package/src/claude/scripts/spec-scaffold.cjs +0 -2317
  204. package/src/claude/scripts/spec-semantic-model.cjs +0 -396
  205. package/src/claude/scripts/validate-spec-output.cjs +0 -3941
  206. package/src/claude/skills/code-review/references/verification-gate.md +0 -46
  207. package/src/claude/skills/delegate/SKILL.md +0 -124
  208. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  209. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  210. package/src/claude/skills/hotfix/SKILL.md +0 -289
  211. package/src/claude/skills/inspect/SKILL.md +0 -142
  212. package/src/claude/skills/loop/SKILL.md +0 -114
  213. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  214. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  215. package/src/claude/skills/route/SKILL.md +0 -94
  216. package/src/claude/skills/route/references/agent-timing.md +0 -46
  217. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  218. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  219. package/src/claude/skills/specs/templates/design.md +0 -82
  220. package/src/claude/skills/specs/templates/requirements-init.md +0 -8
  221. package/src/claude/skills/specs/templates/requirements.md +0 -42
  222. package/src/claude/skills/specs/templates/spec-state.json +0 -56
  223. package/src/claude/skills/specs/templates/task.md +0 -47
  224. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  225. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  226. package/src/codex/hooks/completion-authority.cjs +0 -109
  227. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  228. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  229. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  230. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  231. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -0,0 +1,408 @@
1
+ # Nguyên tắc chung — luật N
2
+
3
+ Mười hai nguyên tắc **đứng sau** các luật M, T, F, I, R và các file component. Đây
4
+ **không phải luật mới**: mỗi dòng gom từ những lỗi đã dính ở nhiều component
5
+ khác nhau, và dẫn về luật gốc. Đọc file này **trước khi dựng bất kỳ thứ gì**,
6
+ nhất là thứ chưa có file mẫu trong `components/`: không skill nào viết đủ spec
7
+ cho mọi UI, nên chỗ nào không có spec thì nguyên tắc là thứ duy nhất để bám.
8
+
9
+ Mỗi nguyên tắc có một **phép thử**: câu hỏi tự trả lời được bằng cách nhìn bản
10
+ dựng. Trả lời "không" là đang vi phạm, dù chưa có luật cụ thể nào cho component
11
+ đó.
12
+
13
+ **Đứng trên cả mười hai nguyên tắc: theo quy ước số đông.** Chỗ nào đã có một cách làm
14
+ mà hầu hết app đều làm và người dùng đã quen (dấu `*` đỏ cho trường bắt buộc, logo
15
+ góc trái về trang chủ, ✕ góc phải để đóng, Huỷ bên trái nút chính), thì làm đúng
16
+ như thế, **kể cả khi một cách khác trông gọn hơn**. Người dùng không nên phải dừng lại
17
+ tự hỏi. Gu của skill (nhạt, ít tín hiệu) chỉ quyết những chỗ chưa có quy ước. Chủ
18
+ dự án chốt.
19
+
20
+ *Phép thử:* người dùng lần đầu mở màn này có chỗ nào phải hỏi "cái này nghĩa là
21
+ gì" hay "bấm đâu để…" không? Có thì đang phá cách ở đó.
22
+
23
+ **Cũng đứng trên mười hai nguyên tắc: dự án đã có ngôn ngữ màu thì theo dự án.** Gu ít
24
+ màu của skill (xám + một màu nhấn, `M4`, `M5`, biểu đồ đậm nhạt một màu) là **mặc
25
+ định cho dự án trống**, không phải bộ lọc để chạy qua dự án có sẵn. Dự án đã tô chip
26
+ bằng màu nhấn nhạt, khối bước nền xanh nhạt, nhãn nhỏ đầu mục dạng pill tím, nút chính
27
+ có quầng sáng, biểu đồ mỗi loại một màu, thì màn mới dựng **cùng cách đó** (`N5`), và
28
+ refactor **không trung tính hoá** màu của họ. Vào dự án nhiều màu mà rút về xám là làm
29
+ hỏng nhận diện của họ, không phải làm đẹp (chủ dự án chốt). Cách nhận ra: đếm như phong
30
+ cách, `P4` trong `styles.md` (dòng "màu" ở tầng 3, `SKILL.md` câu 2).
31
+
32
+ Theo dự án là theo **cách dùng màu**, còn **luật về nghĩa và về đọc được** thì giữ ở
33
+ mọi dự án, vì đó là đúng sai chứ không phải gu:
34
+ - Đỏ chỉ cho lỗi và việc không lấy lại được (`M30`). Màu thương hiệu đỏ trong nội dung
35
+ (logo, ảnh) không tính.
36
+ - Một bảng trạng thái cho cả app: "xong" ở đâu cũng một màu (`M7`).
37
+ - Màu không bao giờ là thứ duy nhất mang nghĩa hay giá trị: luôn có chữ, số, icon đi kèm (`N4`).
38
+ - Chữ đạt tương phản 4.5:1, kể cả chữ màu trên nền màu nhạt.
39
+ - Một màu thương hiệu dùng cho một vai: màu nhấn của dự án là xanh thì nút chính, tab
40
+ đang chọn, link cùng xanh đó, không thêm màu nhấn thứ hai.
41
+
42
+ Dự án dùng màu lung tung, không ra quy luật (mỗi màn một kiểu) thì theo phần **mới nhất**
43
+ như `P5`, và báo một dòng lúc giao.
44
+
45
+ *Phép thử:* đặt màn mới cạnh màn cũ của dự án. Nhìn có như cùng một sản phẩm không, hay
46
+ màn mới xám nhạt lạc giữa các màn có màu?
47
+
48
+ ---
49
+
50
+ **N1. Đổi trạng thái thì giao diện không nhảy chỗ, không co giãn.**
51
+
52
+ Người dùng bấm, rê chuột, chuyển trang, dữ liệu về: mọi thứ **xung quanh** chỗ vừa
53
+ đổi phải đứng yên. Hai cách giữ: chừa sẵn chỗ cho trạng thái lớn nhất, hoặc đổi
54
+ bằng màu và opacity thay vì thêm bớt phần tử. Số tự đổi (đồng hồ đếm ngược, phần trăm,
55
+ bộ đếm) luôn `tabular-nums`.
56
+
57
+ Đã dính: tab thêm viền lúc chọn làm cả hàng xô (tab luôn có `border`); nav phân
58
+ trang đổi số ô theo trang (luôn 7 ô); select "Mỗi trang" trôi theo chuỗi đếm;
59
+ spinner chèn vào nút làm nút phình (spinner thay chỗ icon); cột % lệch vì nút
60
+ cuối hàng rộng hẹp khác nhau (cột hành động `w-20`); lịch 5 hay 6 hàng (luôn 6);
61
+ khung chờ sai hình (`I19`); thanh cuộn chiếm chỗ lúc hiện (`I18`); đổi độ đậm
62
+ chữ tab lúc chọn; câu lỗi OTP chèn vào đẩy nút Xác nhận tụt khỏi con trỏ (dòng
63
+ lỗi giữ chỗ sẵn khi nó nằm giữa ô và nút bấm); câu lỗi hay lý do khoá thay chỗ mô tả
64
+ `text-sm` mà xuống `text-xs`, hàng cài đặt co lại lúc bật tắt (câu thay chỗ giữ cỡ của câu nó thay).
65
+
66
+ **Giữ chỗ là để khớp với phần tử bên cạnh**, không phải để giữ hình. Không có
67
+ gì bên cạnh (màn hẹp xếp một cột, cả hàng cùng thiếu) thì bỏ chỗ giữ, để không
68
+ thành khoảng trắng vô nghĩa (đã dính: sparkline tháng đầu giữ chỗ ở mobile).
69
+
70
+ **Sang hẳn màn khác thì không tính** (đổi bước form, đổi trang): cả màn đã thay,
71
+ không còn gì "bên cạnh" để giữ. Đừng `truncate` chữ cần đọc chỉ để giữ chiều cao
72
+ qua các màn (đã dính: tên bước ở thanh thu gọn bị cắt "…").
73
+
74
+ **Mở/đóng có chủ ý đẩy phần bên dưới** (accordion, nhóm thu gọn) thì không giữ chỗ
75
+ được, nên phải **trượt**, không giật: `grid-rows` 0fr ↔ 1fr, không `<details>`, không
76
+ render có điều kiện (`I30`).
77
+
78
+ *Phép thử:* bật lần lượt từng trạng thái, nhìn **phần tử bên cạnh**, không nhìn
79
+ phần tử vừa đổi. Có cái nào xê dịch dù 1px không?
80
+
81
+ ---
82
+
83
+ **N2. Mỗi trạng thái đều được dựng, và liếc là phân biệt được.**
84
+
85
+ Liệt kê trạng thái trước khi dựng: thường, rê chuột, focus bàn phím (chỉ ô nhập và
86
+ mục menu; phần còn lại không vẽ vòng focus theo `I13`), đang chọn, khoá, đang tải,
87
+ rỗng, lỗi, xong, và **ca biên** (không có gì, một cái, rất dài, rất nhiều). Mỗi trạng
88
+ thái một ví dụ tĩnh (`SKILL.md` phạm vi). Hai trạng thái khác nghĩa thì phải khác hình rõ
89
+ ràng.
90
+
91
+ Đã dính: xong mà thanh vẫn đen đầy như đang chạy (xong là emerald); trang đang
92
+ chọn trông như ô input; nút phụ trông như bị khoá (`I8`); hôm nay và ngày đang
93
+ chọn lẫn nhau (chữ đậm + chấm, khác nền đặc); một trang và 0 dòng vẫn hiện đủ
94
+ control chết; bước lỗi không có hình riêng; tệp bị từ chối vì quá cỡ vẫn có rãnh thanh tiến độ rỗng, đọc như "đang chờ chạy"; khung tải tệp bị khoá cùng nền với lúc đang kéo tệp vào; thanh bước thu gọn tô đậm cả đoạn đang
95
+ làm nên "đang ở bước cuối" giống "đã xong hết", sửa thành để xám thì "Bước 2 / 3"
96
+ lại đọc như thanh thiếu (đang làm là tầng thứ ba, nửa đậm).
97
+
98
+ Ngoại lệ có tên: dòng bảng **đã chọn** và dòng **đang rê chuột** cùng một nền mờ, vì
99
+ hai trạng thái đã tách bằng checkbox đã tick (chủ dự án chốt, `I10`). Hình
100
+ khác nhau không nhất thiết phải là nền khác nhau. Ngoại lệ này **chỉ cho dòng bảng tick
101
+ checkbox**. Sidebar và cây thư mục không có checkbox nên đang chọn đậm hơn rê một bậc: rê
102
+ `hover:bg-item-hover`, đang chọn `bg-secondary` + `font-medium`, không màu nhấn, không viền
103
+ (chủ dự án chốt).
104
+
105
+ *Phép thử:* che chữ đi, chỉ nhìn hình. Còn nói được đây là trạng thái nào không?
106
+
107
+ ---
108
+
109
+ **N3. Một tín hiệu cho một ý. Tín hiệu mạnh nhất để dành cho đúng một chỗ.**
110
+
111
+ Nền màu nhấn, khối tô đặc, màu đỏ, chữ đậm là tín hiệu **đắt**: mỗi màn tiêu một
112
+ lần. Thứ bậc đi bằng cỡ chữ, độ đậm, vị trí trước; màu và bóng sau cùng (`M13`,
113
+ `T8`). Đã có một dấu hiệu thì không thêm dấu hiệu thứ hai cho cùng ý (`M6`, `F6`).
114
+
115
+ Đã dính: ba khối đen trong ô chọn giờ (một dải nhạt); icon thùng rác ở đầu hộp
116
+ và ở nút (nút chỉ chữ); banner tô màu cả mô tả (chỉ icon và tiêu đề); câu lỗi
117
+ ghi "rồi thử lại" cạnh nút Thử lại; "Xem tất cả" muốn tô đen ở mọi card (`I1`,
118
+ `I3`); tên trang nhạt hơn tiêu đề khối bên dưới; màn OTP hết hạn có cả "Gửi mã
119
+ mới" trong câu lỗi lẫn "Gửi lại mã" bên dưới (hai nút một việc: giữ một); tệp tải xong có
120
+ thanh xanh lá đầy + `100%` + "Đã tải xong" (ba tín hiệu một ý: bỏ thanh và số, giữ chữ).
121
+
122
+ **Cùng một câu ở mọi ô, mọi hàng cũng là một ý nói nhiều lần**: kéo ra ghi một lần
123
+ ở đầu nhóm. Đã dính: "so với 2025" ở cả bốn ô số liệu
124
+ (chính cái đuôi đó làm ô hẹp vỡ dòng; đừng chữa bằng cách xếp một cột thay vì
125
+ bỏ đuôi); khách mới thì "Chưa có kỳ trước" bốn lần; `/2026` ở mọi mốc giờ; cột Trạng
126
+ thái badge xanh "Đang hoạt động" trên 14/18 dòng. **Trạng thái thường không cần dấu, chỉ ngoại lệ mới có** (lời mời "Chờ chấp nhận"). Trong
127
+ panel khách hàng, số liệu `text-3xl` to hơn tên khách cũng sai: thứ nặng nhất phải là thứ trả lời "đang xem
128
+ cái gì".
129
+
130
+ **Thứ vừa bấm để mở cũng là một lần nói.** Đã dính: bấm
131
+ avatar ra menu, đầu menu lại một avatar 40px to hơn chính nút vừa bấm; header có avatar
132
+ mà chân sidebar vẫn còn hàng profile mở cùng menu đó (hai lối vào một chỗ); màn hẹp xổ
133
+ danh sách tài khoản ngay dưới đầu menu, tài khoản đang dùng hiện hai lần liền nhau.
134
+
135
+ **Đã nói bằng vị trí thì không nói thêm bằng hình. Một việc đang chạy, một spinner.**
136
+ Đã dính ở khung chat: bong bóng căn phải đã nói "ai đang nói" mà mỗi câu trả
137
+ lời vẫn có một vòng robot; mở danh sách công cụ ra thấy spinner ở cả hàng đầu lẫn hàng
138
+ bước; một công cụ lỗi mà câu trả lời đã giải thích vẫn có "1 lỗi" đỏ, icon đỏ, dòng mô
139
+ tả, bốn lần một ý (`components/chat.md`). Cùng màn đó, tên bước công cụ nặng ngang câu
140
+ trả lời: **quá trình luôn nhẹ hơn kết quả**, và không nói trước kết quả (mô tả bước
141
+ "tổng 409.000.000 đ" ngay trên câu trả lời mở đầu bằng đúng con số đó).
142
+
143
+ *Phép thử:* đếm số chỗ tô đặc hoặc có màu trên màn. Mỗi chỗ trả lời được "nó nói
144
+ điều gì mà chỗ khác chưa nói" không?
145
+
146
+ ---
147
+
148
+ **N4. Màu nói trạng thái, theo đúng một bảng cho cả app, và luôn có chữ đi kèm.**
149
+
150
+ Màu không để trang trí, không để phân loại (`M4`, `M5`). Trạng thái nào màu gì
151
+ lấy từ **một** bảng (`M7`, `D2`, `M30`): xám chờ, xanh lá xong, hổ phách cần chú
152
+ ý, đỏ hỏng. Màu theo **tốt hay xấu**, không theo lên hay xuống. Màu không bao giờ
153
+ đứng một mình: luôn có chữ hoặc icon nói cùng ý, vì người mù màu và trình đọc màn
154
+ hình không thấy màu.
155
+
156
+ Đã dính: màu tăng giảm suy từ dấu con số (chi phí tăng mà xanh); chuỗi xám
157
+ nhạt nhất của biểu đồ gần như trắng, may còn số trên đầu cột; thanh tiến độ
158
+ đổi màu mà không có dòng chữ; chỉ có đoạn đỏ trên thanh bước ở màn hẹp mà không
159
+ nói bước nào sai.
160
+
161
+ *Phép thử:* chuyển màn sang đen trắng. Mọi trạng thái còn đọc ra không?
162
+
163
+ ---
164
+
165
+ **N5. Cùng vai thì cùng khuôn, cùng class.**
166
+
167
+ Thứ đã có mẫu thì chép mẫu, không nặn biến thể (`SKILL.md` mục 2 "ráp, không vẽ
168
+ lại", `D1`, `D8`). Component mới có phần giống component cũ thì mượn đúng phần
169
+ đó: ô mở popover trông y như ô nhập, nút trong form cao bằng ô nhập, lịch nào
170
+ cũng một lưới.
171
+
172
+ Đã dính: hai đầu khoảng ngày khác sắc; vòng bước lỗi khác khuôn vòng bước xong;
173
+ "Tải báo cáo" mượn nhầm khuôn "Xem tất cả" (nút theo loại hành động, không theo
174
+ chỗ đứng); `₫` chỗ này `đ` chỗ kia; `12,4 / 20` cạnh `4/6`; thang xám của cột nhóm khác thang xám của donut; `2,8 %` ở số chính cạnh `27,3%` ở dòng
175
+ so sánh; khối nhãn và
176
+ giá trị viết trạng thái, nhãn phân loại, tiền thành chữ trơn thay vì dùng badge,
177
+ pill, `đ` đã có sẵn; avatar cỡ to trên trang hồ sơ dựng riêng nên khác màu
178
+ avatar của cùng người ấy trên header; email trong hộp xác nhận không dùng cách bẻ
179
+ dòng đã có ở khối nhãn và giá trị. **Một giá trị có khuôn riêng thì ở đâu cũng dùng khuôn đó**,
180
+ kể cả khi nó nằm trong một component khác. **Mượn khuôn là mượn cả class**, không chỉ
181
+ mượn dáng (cỡ chữ, độ dày đường nối, cách căn dòng đầu với vòng).
182
+
183
+ *Phép thử:* với từng phần tử mới, trong skill đã có thứ nào **cùng vai** chưa?
184
+ Có thì class có giống không?
185
+
186
+ ---
187
+
188
+ **N6. Chữ nói được việc: chuyện gì, vì sao, làm gì tiếp.**
189
+
190
+ Câu dài thì tách **hai tầng**: tầng trên chuyện gì xảy ra, tầng dưới vì sao hoặc
191
+ hệ quả, có số và mốc cụ thể. Câu lỗi nói cách sửa, không lặp lời nhãn hay
192
+ placeholder (`T20`, `T22`, `layouts/form.md`). Khoá thì nói vì sao khoá. Chỉ dẫn
193
+ sang chỗ khác thì là nút hoặc link, không phải câu chữ trơn.
194
+
195
+ Đã dính: toast lỗi vỡ ba dòng; lỗi tải chỉ một câu không lý do; "Chọn ngày hết
196
+ hạn" làm câu lỗi (đọc như hướng dẫn); "ví dụ 31/12/2026" cho ô không gõ được;
197
+ "Đổi trong Cài đặt" không bấm được; dòng dưới lịch vẫn "Chọn ngày bắt đầu" khi
198
+ đã chọn xong. Màn OTP không có "Đổi email": gõ nhầm email là kẹt, không có
199
+ đường lùi. **Mỗi bước phải có lối ra khi người dùng đi nhầm.** Tự tay dừng cũng là
200
+ đi nhầm được: câu trả lời bị dừng mà không có Tạo lại thì phải gõ lại cả câu hỏi
201
+ (`components/chat.md`).
202
+
203
+ **Câu lỗi không được bịa ra một luật mà hệ thống không hề kiểm.** Nó dạy sai người
204
+ dùng, và mâu thuẫn ngay với dữ liệu đang hiện trên màn (ô nhập
205
+ nhiều tag ghi "cần có dấu @ và đuôi .com" trong khi các email hợp lệ ngay trên đó là
206
+ `@saoviet.vn`). Viết đúng cái đang kiểm: "cần có dấu @ và tên miền".
207
+
208
+ **Rỗng mà người dùng là người phải mở đầu thì trạng thái rỗng là việc bấm được ngay**,
209
+ không phải câu báo "chưa có". Đã dính: khung chat mới chỉ có "Chưa có tin
210
+ nhắn nào" giữa màn (thay bằng 2–3 gợi ý mở đầu, `components/chat.md`). Danh sách do hệ
211
+ thống đổ về (đơn hàng, thông báo) thì vẫn một dòng chữ mờ (`components/empty-state.md`).
212
+
213
+ *Phép thử:* người dùng đọc xong câu này, họ biết phải làm gì tiếp không?
214
+
215
+ ---
216
+
217
+ **N7. Không làm hộ, không đoán hộ người dùng.**
218
+
219
+ Chưa chọn thì để trống, hiện placeholder. Gợi ý thì hiện ở chỗ gợi ý, không ghi
220
+ vào ô. Không bịa số, không tick sẵn đồng ý.
221
+
222
+ Đã dính: mở ô chọn giờ là ô tự điền `00:00:00`; bấm ngày xong ô ngày giờ tự lấy
223
+ giờ; tab có số đếm bịa cho có. Ngoại lệ có tên: nhóm radio luôn có sẵn một lựa
224
+ chọn (`components/choice-controls.md`).
225
+
226
+ *Phép thử:* có giá trị nào xuất hiện trong ô mà người dùng chưa hề chạm vào không?
227
+
228
+ ---
229
+
230
+ **N8. Không che, không cắt mất thứ người dùng cần để quyết định.**
231
+
232
+ Trang không cuộn ngang (`R1`, `T13`). Lớp nổi không che chính ô mở ra nó. Thứ
233
+ dùng để xác nhận (tên đối tượng sắp xoá) không `truncate`. Mô tả xuống dòng, chỉ
234
+ tiêu đề một dòng mới cắt, cắt thì có `title` (`T14`). **Mô tả được xuống dòng thì
235
+ luôn `text-pretty`** (`T10`), nhất là trong cột hẹp: không để trơ một chữ ở dòng cuối. Không có chữ bị xén nửa.
236
+
237
+ Đã dính: lịch khoảng ngày lật lên che ô của nó; số trong bánh xe bị cắt nửa ở mép;
238
+ nửa trên popover trống trơn vì chèn đệm. Mô tả bước ở thanh các bước dọc rớt "thoại", "hệ",
239
+ "doanh" xuống một mình (cột ~200px, thiếu `text-pretty`). Link "Đổi email" bị bẻ đôi ở cuối dòng. **Link ngắn nằm
240
+ trong câu không bị bẻ giữa chừng**: `whitespace-nowrap` để nó xuống dòng nguyên
241
+ cụm. Nhãn nút thì ngược lại, được xuống dòng (`T15`). **Chữ ghi đè lên hình (số trên
242
+ biểu đồ) đặt về phía trống**, không đặt cố định một phía: số "44,4 tr đ" đặt trên chấm
243
+ nằm đúng trên đoạn nối đi lên, nền sau chữ cắt đôi đường. Một con số không
244
+ kèm mốc khi trục chỉ ghi vài nhãn cũng là thiếu thứ cần đọc: không biết số của ngày nào.
245
+
246
+ **Phải cắt thì cắt phần giống nhau, giữ phần phân biệt.** Cắt ở cuối không phải cách
247
+ duy nhất, và xuống dòng không phải cách thay duy nhất. Email giữ tên miền, cắt phần
248
+ trước `@` (`layouts/overlay.md`, "Cắt email"); tên tệp giữ đuôi `.pdf`. Đã dính ở menu
249
+ chuyển tài khoản: sợ cắt mất tên miền nên cho email xuống dòng, mỗi hàng
250
+ thành ba dòng, danh sách con nặng hơn menu cha. Trong menu, hàng chọn, ô hẹp: mỗi
251
+ trường một dòng.
252
+
253
+ *Phép thử:* ở 375px và với dữ liệu dài nhất, thứ người dùng cần đọc để bấm có
254
+ còn đọc được hết không?
255
+
256
+ ---
257
+
258
+ **N9. Mọi thao tác đi được bằng chuột, bằng phím, và bằng tay trên điện thoại.**
259
+
260
+ Bấm được thì có hover và `cursor-pointer` trên đúng phần tử bấm, vùng bấm rộng
261
+ hết hàng (`I9`, `I29`). Tab tới được; không vẽ vòng focus (`I13`, chủ dự án chốt). Không có
262
+ hover trên màn chạm thì thứ ẩn-hiện-khi-rê phải luôn hiện (`I11`). Thứ chọn được
263
+ thì chọn được bằng nhiều đường, không chỉ một cử chỉ. `aria-*` cho thứ chỉ nói
264
+ bằng hình (`aria-pressed`, `aria-current`, `role="progressbar"`).
265
+
266
+ **Thứ bấm được mà hình nhỏ hơn 32px** (nút chữ giữa câu "Thử lại", "Gửi lại", nút icon `size-7`
267
+ cạnh một giá trị) **giữ hình, nới vùng bấm bằng `relative before:absolute before:-inset-*`** cho
268
+ tới ~32–40px, không phóng to hình: to hình thì đẩy lệch hàng và nặng hơn việc của nó. Số âm ở đây
269
+ buộc phải giữ (`N11`). Nếu hai vùng nới chạm nhau thì tách thứ đó ra hàng riêng trước. Đã dính:
270
+ nút sao chép 28px, "Gửi lại · Huỷ" 18px, "Thử lại" 16px; công thức
271
+ từng chỗ ở `components/description-list.md`, `layouts/app.md` (Hồ sơ), `components/file-upload.md`.
272
+
273
+ Đã dính: bánh xe giờ chỉ cuộn mới chọn được, và cuộn khựng giữa chừng; chỉ có
274
+ mũi tên ‹ › để đổi tháng, đi xa là mỏi tay (tiêu đề bấm ra lưới tháng/năm).
275
+
276
+ *Phép thử:* rút chuột ra, dùng Tab + Enter + mũi tên đi hết màn. Rồi mở trên điện
277
+ thoại. Có chỗ nào kẹt không?
278
+
279
+ ---
280
+
281
+ **N10. Skill lo hình, người dùng lo logic.**
282
+
283
+ Mọi thứ có hệ quả dữ liệu là quyết định của người dùng: lưu lúc nào, gọi gì, ngày
284
+ nào bị khoá, ngưỡng đổi màu, đóng rồi có hiện lại không. Skill để prop hoặc
285
+ handler rỗng, và chỉ quyết **mỗi lựa chọn đó trông ra sao**. Chi tiết ở phần
286
+ phạm vi đầu `SKILL.md`.
287
+
288
+ Công cụ cũng vậy: thư viện nào, bộ component nào là của dự án. **Kiểm trước
289
+ khi dựng** thứ hay có thư viện riêng (biểu đồ, lịch, bảng, danh sách ảo): có thì
290
+ dùng đúng cái đó, chỉnh cho khớp hình (tắt thứ nó bật mặc định, màu lấy từ
291
+ token), không tự vẽ lại bên cạnh. Chưa có thì không tự cài: dựng bình thường,
292
+ và chỉ đề xuất thư viện khi có nhu cầu thật mà tự dựng sẽ tốn. Chọn theo tiêu
293
+ chí (nhẹ, giải quyết đúng việc, hợp hệ sinh thái), không theo tên quen.
294
+
295
+ *Phép thử:* đoạn code vừa viết có gọi API, đặt ngưỡng, lưu trạng thái, hay hẹn
296
+ giờ mà đề không yêu cầu không?
297
+
298
+ ---
299
+
300
+ **N11. Không dùng số âm cho khoảng cách và vị trí, trừ khi không còn cách nào khác.**
301
+
302
+ Margin âm (`-mt-*`, `-mx-*`), `-space-*`, `-translate-*`, `-inset-*`, `top-[-…]`: số âm
303
+ kéo phần tử ra khỏi chỗ của nó, nên khung bao không còn nói thật kích thước bên trong.
304
+ Sửa padding một chỗ là chỗ khác lệch theo, và hay lộ lỗi ở trạng thái khác (margin
305
+ âm trong accordion làm câu đang đóng lòi dòng đầu câu trả lời; nút ⋯
306
+ `size-8` trên thẻ kanban kéo `-mr-2` thì khối bọc co còn 24px, `max-w-full` của Button bóp
307
+ nút theo thành 24×32). Chủ
308
+ dự án chốt: ưu tiên mọi giá là không dùng.
309
+
310
+ Làm theo thứ tự:
311
+
312
+ 1. **Đặt padding ở đúng phần tử cần nó.** Đường chia muốn tràn mép thì khung không có
313
+ padding ngang, từng hàng tự có `px`: vạch tự chạm mép, không phải kéo ra.
314
+ 2. **Chấp nhận khoảng cách mà padding cố định cho ra**, thay vì kéo cho sát hơn. Đừng
315
+ đổi padding của một khối theo trạng thái để bù cho khối bên cạnh: tô nền khối đó là
316
+ chữ lệch về một mép (accordion bớt `pb` của nút khi mở, `I30`).
317
+ 3. **`gap`, căn `items-*`, đổi `leading`** để thẳng hàng, thay vì nhích bằng `translate`.
318
+ **Căn giữa quanh một điểm** (chấm trên biểu đồ, nhãn trên tay cầm): đặt khối `absolute
319
+ w-0 flex justify-center` đúng tại điểm, phần tử nằm trong nó, không `-translate-x-1/2`.
320
+ 4. Không cách nào ở trên làm được: dùng số âm, và **ghi comment lý do ngay trên dòng đó**, như
321
+ `eslint-disable`. Các chỗ đã thử và giữ: avatar xếp chồng (`avatar.md`); vùng
322
+ bấm nở ra ngoài một phần tử nhỏ (`before:-inset-*`, `N9`); khung tên sửa tại chỗ tràn ra
323
+ ngoài chữ để chữ thẳng cột (`inline-edit.md`); hàng icon có nền rê nằm giữa một cột chữ
324
+ (`button.md`, mục `ghost`); đoạn đậm của đường dọc cây thư mục (`tree.md`).
325
+
326
+ **Không tính là số âm của `N11`**: điểm xuất phát của chuyển động (`-translate-y-1 → 0` của
327
+ dropdown, `-translate-y-full → 0` của toast ở đỉnh). Đó là hướng trượt vào, không phải khoảng cách
328
+ hay vị trí đứng yên; phần tử đứng yên luôn ở `translate-0`.
329
+
330
+ *Phép thử:* grep `-m[trblxy]?-|-space-|-translate-|-inset-` trong file vừa dựng. Mỗi kết
331
+ quả phải có comment giải thích vì sao không làm bằng cách 1–3 được (trừ điểm xuất phát chuyển động).
332
+
333
+ ---
334
+
335
+ **N12. Chữ trong một khối có thứ bậc, có nhịp, và tên không bị cắt cụt** ⚑.
336
+
337
+ Áp cho **mọi khối lặp**: card tin đăng, card việc làm, card sản phẩm, dòng danh sách, ô
338
+ lưới, kể cả kiểu chưa có mẫu. Không cần mẫu riêng cho từng kiểu, ba câu này là đủ:
339
+
340
+ 1. **Thang chữ trong khối chênh nhau một bậc.** Tối đa ba cỡ chữ, thứ to nhất chỉ hơn tên
341
+ mục một bậc của thang (`budgets.md`): tên `text-sm` thì giá, số chính `text-base`
342
+ `font-semibold`, không nhảy lên `text-lg`, `text-xl`. Thứ bậc còn lại nói bằng độ đậm
343
+ và màu. Giá to gấp rưỡi tên thì card đọc như bảng giá, tên thành chữ phụ. Ngoại lệ: card
344
+ số liệu, nơi con số chính là cả khối (`components/charts.md`).
345
+ 2. **Nhịp theo nhóm: trong nhóm gần, giữa nhóm xa.** Gom chữ thành nhóm theo nghĩa (giá +
346
+ tên; diện tích · khu vực · mốc gần; thời gian đăng). Dòng trong một nhóm cách **4px
347
+ (`gap-1`)**, không `gap-0.5`: 2px thì hai dòng dính nhau, nhất là dòng có dấu tiếng Việt
348
+ (chủ dự án chốt). Giữa các nhóm 8–12px, khoảng từ chữ tới mép khối không nhỏ
349
+ hơn khoảng giữa nhóm. Mọi dòng cách đều nhau là không có nhóm, mắt đọc thành một cục chữ.
350
+ 3. **Tên để nhận ra mục không cắt cụt.** Khối lặp mà mỗi mục là một khối riêng (card trong
351
+ lưới) thì tên `line-clamp-2`; `truncate` một dòng chỉ cho danh sách dày (dòng bảng,
352
+ sidebar, `T14`). Tên cắt sau hai mươi mấy ký tự ("Cho thuê phòng trọ khép kín …") là
353
+ mất đúng thứ người dùng đọc để chọn (`N8`).
354
+
355
+ Chủ dự án chốt: đừng viết thêm mẫu cho từng kiểu, viết phép thử chung.
356
+
357
+ *Phép thử:* probe mục "KHỐI LẶP" (card có ảnh: chữ to nhất so với tên, khoảng giữa các dòng,
358
+ tên cắt một dòng). Khối không ảnh thì tự soi ba câu trên bằng ảnh chụp.
359
+
360
+ ---
361
+
362
+ ## Dựng một thứ chưa có mẫu
363
+
364
+ Stepper dọc, dòng thời gian, cây thư mục, bình luận lồng nhau… không có file
365
+ trong `components/` thì:
366
+
367
+ 1. **Tìm thứ gần nhất đã có mẫu và mượn khuôn** (`N5`). Stepper dọc mượn vòng,
368
+ đường nối và bốn trạng thái của thanh các bước (`layouts/form.md`); bình luận
369
+ lồng nhau mượn dòng danh sách (`components/list-row.md`); cây thư mục mượn link
370
+ sidebar có menu con (`layouts/app.md`); khung chat mượn cây thư mục, nút viền và
371
+ "Lỗi tải" (`components/chat.md`).
372
+ **Chỉ mượn từ file trong skill**, không mượn từ bản dựng chưa duyệt trong dự án.
373
+ Khuôn lấy từ file thì dự án sau vẫn có, và lỗi không nhân đôi.
374
+ **Mượn khuôn, không mượn nội dung.** Khung mới làm việc khác thì chọn lại nội dung
375
+ theo việc của nó: trang chi tiết mượn hàng tên, ô số của panel xem nhanh, nhưng bộ
376
+ tab phải có bản ghi con chính (trang khách chép nguyên tab
377
+ Tin nhắn / Tệp / Hoạt động của panel thì 24 đơn không có chỗ xem, `layouts/app.md`).
378
+ 2. **Dựng luôn các ca biên vào trang**, không chỉ ca đẹp (`N2`, `S8`). Khối lặp (card,
379
+ dòng, ô) thì mỗi bản sao một ca: tên một dòng và tên rất dài, số `0` và số rất lớn,
380
+ thiếu ảnh, thiếu mô tả, một mục và nhiều mục. Không lặp được (một form, một panel)
381
+ thì mỗi trạng thái một ví dụ tĩnh cạnh nhau.
382
+ 3. **Chạy probe và tự sửa** như cổng 3 (`checklist.md`): `--sweep`, sửa tới khi danh
383
+ sách `P` trống, tối đa ba vòng.
384
+ 4. **Soi bằng mắt năm câu mà máy không đo được** ⚑. Mở ảnh 375px và một ảnh desktop,
385
+ nhìn đúng các ca biên vừa dựng:
386
+ - **Ca biên có trông cố ý không?** Số đếm bằng `0` ("0 ảnh", "0 bình luận") thì ẩn,
387
+ hoặc nói bằng chữ ("Chưa có ảnh"). Ảnh thiếu thì là trạng thái "chưa có ảnh", không
388
+ để badge đếm nằm trên ảnh giữ chỗ. Mô tả thiếu thì khối co lại, không để dòng trống.
389
+ - **Có khoảng trống nào chỉ để giữ chỗ không?** Giữ chỗ được khi nó làm thứ quan
390
+ trọng thẳng hàng trong lưới (giá, nút ở cùng độ cao giữa các card). Khi đó nói một
391
+ dòng lúc giao. Không làm được việc đó thì bỏ.
392
+ - **Các bản sao của khối lặp có cùng một cách viết không?** Cùng kiểu số và đơn vị
393
+ trong một danh sách ("4,5 triệu" với "0,85 triệu", không lẫn "850.000 đ"), cùng
394
+ thứ tự các dòng, cùng nhãn (`N5`).
395
+ - **Một vùng có bị đè quá nhiều thứ không?** Trên ảnh, trên header, trong một góc:
396
+ quá ba thứ chồng lên nhau thì gom hay dời bớt ra khỏi vùng đó (`N3`).
397
+ - **Thứ cần để quyết định còn nguyên ở ca dài nhất không?** Tên dài nhất, giá lớn
398
+ nhất, ở 375px (`N8`).
399
+
400
+ Câu nào ra "không" thì sửa, rồi quay lại bước 3.
401
+ 5. **Chạy mười hai phép thử** ở trên trước khi báo xong.
402
+ 6. Lúc giao nói một dòng: *"X chưa có mẫu đã duyệt, mình mượn khuôn của Y"*, cộng mỗi
403
+ lựa chọn đánh đổi một dòng (vd *"tên giữ chỗ hai dòng để giá thẳng hàng giữa các
404
+ card"*).
405
+
406
+ Luồng này dùng cho cả dựng mới lẫn chế độ dựng lại của nhánh `V` (`review.md`). Phần tử
407
+ lạ không có trong skill là chuyện thường, nên không cần mẫu cho mọi thứ: mượn khuôn gần
408
+ nhất, dựng cả ca biên, để máy đo, rồi mắt soi năm câu trên.
@@ -0,0 +1,245 @@
1
+ # Refactor codebase đã có — luật L
2
+
3
+ Mở file này khi việc là **sửa giao diện một dự án đang chạy**, không phải dựng
4
+ màn mới. Hai việc khác hẳn nhau về rủi ro và về thứ tự các bước.
5
+
6
+ Đúc rút từ một đợt refactor dự án thật: Next 16 + React 19, 237k dòng
7
+ TS/TSX, 14.218 dòng CSS, 4,5 tháng tuổi, có tiền thật chạy qua.
8
+
9
+ ---
10
+
11
+ ## Bốn mặc định trước khi gõ dòng đầu tiên
12
+
13
+ Không hỏi. Lấy mặc định dưới đây, **nói lại cả bốn ở đầu lúc giao**. Đề nói khác
14
+ thì theo đề.
15
+
16
+ 1. **Mục tiêu mặc định là "chụp lại hiện trạng", không "đổi diện mạo".** Đề có chữ "làm lại giao diện", "cho đẹp hơn", "theo gu" thì mới là đổi diện mạo. Hai việc khác hẳn nhau về rủi ro. Đổi diện mạo thì đi nhánh `U` (`design-process.md`) trước, dọn theo `L` sau; đề nói soi hay giữ brand thì nhánh `V` (`review.md`) thay chỗ `U`, người dùng chọn dòng nào mới sửa dòng đó (`SKILL.md` câu 1).
17
+ 2. **Mặc định giữ pixel.** Mọi bước refactor không được đổi giao diện; thấy chỗ trái luật skill thì ghi vào danh sách "đề xuất sửa" lúc giao (chia hạng theo `V1` trong `review.md`), không tự sửa trong cùng đợt. **Dự án nhiều màu hơn gu của skill thì không phải trái luật**: không đề xuất rút về xám; chỉ đề xuất khi phạm luật về nghĩa hay đọc được (đầu `principles.md`: đỏ cho thứ không phải lỗi, chữ màu dưới 4.5:1, màu đứng một mình mang nghĩa). Đây là ràng buộc quyết định cách chọn token.
18
+ 3. **Người duyệt "trông vẫn đúng" mặc định là người giao việc.** Lúc giao đưa danh sách màn cần họ mở ra đối chiếu, kèm ảnh trước/sau nếu chụp được.
19
+ 4. **Tài liệu hiện có: không xoá, chỉ đánh dấu chỗ sai.** Ở dự án đó, `design-system.md` mô tả một sản phẩm khác chứ không mô tả chính nó — nhưng vẫn có một mục là kiến thức thật, xoá trắng là mất.
20
+
21
+ ---
22
+
23
+ ## L1. Đo trước khi kết luận. Luôn luôn.
24
+
25
+ Cảm giác "code loạn quá" gần như luôn **đúng về triệu chứng** và gần như luôn
26
+ **sai về nguyên nhân**. Ở đợt refactor đó, mọi giả định ban đầu đều lệch sau khi đo:
27
+
28
+ | Tưởng là | Đo ra |
29
+ | --- | --- |
30
+ | "Không có design system" | Có 3 tầng: tài liệu 20KB + `tokens.css` 304 dòng + 93 biến màu |
31
+ | "`globals.css` là chỗ chứa CSS" | Kho thật là `prototype.css` — 8.668 dòng, gấp 2,4 lần |
32
+ | "Không ai dùng token" | 2.245 lần dùng `var(--…)`, tỉ lệ token/màu-thô 3.6:1 |
33
+ | "Tailwind chưa cài" | Cài rồi, `@import` rồi, chỉ thiếu `@theme` |
34
+ | "Code ẩu" | Comment ghi rõ ngày + nguyên nhân + vì sao cách sửa hiển nhiên là sai |
35
+
36
+ Bộ lệnh đo mở màn, chạy **trước khi mở file nào**:
37
+
38
+ ```bash
39
+ # CSS nằm ở đâu, bao nhiêu
40
+ find app components src -name "*.css" 2>/dev/null | xargs wc -l | sort -rn | head
41
+
42
+ # mức tuân thủ token
43
+ grep -roE 'var\(--' --include='*.css' . | wc -l # dùng đúng
44
+ grep -roE '#[0-9a-fA-F]{3,8}\b' --include='*.css' . | wc -l # màu thô
45
+
46
+ # nợ ở tầng component — đổi *.tsx cho khớp dự án:
47
+ # .vue, .svelte, .html, .php, .erb... Ra 0 vì grep sai đuôi file
48
+ # thì kết luận "sạch" là sai.
49
+ grep -rl "<button" --include="*.tsx" . | wc -l
50
+ grep -rl "style={{\|style=\"" --include="*.tsx" --include="*.html" . | wc -l
51
+
52
+ # selector trùng = dấu hiệu bồi đắp
53
+ grep -hoE '^\s*\.[a-zA-Z][a-zA-Z0-9_-]*\s*\{' $(find . -name '*.css') \
54
+ | tr -d ' {' | sort | uniq -d | wc -l
55
+ ```
56
+
57
+ ---
58
+
59
+ ## L2. Phân biệt nợ kỷ luật với nợ kiến trúc
60
+
61
+ Nợ **kỷ luật** = người viết ẩu. Nợ **kiến trúc** = người viết **không có chỗ nào
62
+ khác để đặt code**.
63
+
64
+ Dấu hiệu nhận biết nợ kiến trúc: **comment trong code chất lượng cao nhưng cấu
65
+ trúc vẫn tệ**. Người viết biết mình đang làm gì, họ chỉ hết đường.
66
+
67
+ Gặp nợ kiến trúc thì chửi người viết là vô ích và sai. Phải **dựng chỗ chứa
68
+ trước** — tầng primitive, CSS Module, thư mục component — rồi mới dọn được.
69
+
70
+ ---
71
+
72
+ ## L3. Đừng tin tài liệu. Tin CSS đã build.
73
+
74
+ Ở dự án đó, ba nguồn cùng mô tả một thang chữ, cả ba khác nhau:
75
+
76
+ | Token | `docs/design-system.md` | `AGENTS.md` | file token (chạy thật) |
77
+ | --- | --- | ---: | ---: |
78
+ | `--text-sm` | *(mô tả sản phẩm khác)* | 13px | **15px** |
79
+ | `--text-base` | *(mô tả sản phẩm khác)* | 14px | **16px** |
80
+
81
+ Cách kiểm chân lý duy nhất — đọc CSS mà **trình duyệt thật nhận được**:
82
+
83
+ ```bash
84
+ CSS=$(curl -s localhost:3000/ | grep -oE '/_next/static/[^"]+\.css' | head -1)
85
+ curl -s "localhost:3000$CSS" | grep -oE '\-\-text-sm: *[^;]*'
86
+ ```
87
+
88
+ Bước này bắt được lỗi mà đọc code không bao giờ thấy. Xem `tailwind-v4-traps.md`.
89
+
90
+ Hệ quả cho cách làm việc: khi tài liệu và code đá nhau, **sửa tài liệu**, đừng
91
+ sửa code cho khớp tài liệu.
92
+
93
+ ---
94
+
95
+ ## L4. Ảnh "trước" không dựng lại được
96
+
97
+ Xoá CSS cũ xong là mất luôn khả năng render trạng thái cũ. Không có ảnh thì không
98
+ còn căn cứ nào để cãi *"trông vẫn ổn mà"*.
99
+
100
+ Chụp **trước khi gõ dòng đầu tiên**, ở đủ các khổ màn (ít nhất 375px và desktop).
101
+
102
+ ⚠️ Chờ thêm **~1,2 giây sau `networkidle2`** — ảnh nền và webfont vào sau, chụp
103
+ sớm ra khung xám và bro sẽ so hai cái khung xám với nhau.
104
+
105
+ ---
106
+
107
+ ## L5. Quy trình cho từng trang, tám bước
108
+
109
+ ```
110
+ 1. Chụp trước ảnh before, đủ khổ màn
111
+ 2. Thay class class cũ → utility trong .tsx
112
+ 3. XOÁ CSS cũ gỡ đúng rule vừa thành thừa khỏi file CSS cũ
113
+ 4. Chụp sau ảnh after, cùng khổ màn
114
+ 5. Đối chiếu đặt hai ảnh cạnh nhau, người duyệt xem
115
+ 6. Kiểm sót grep tiền tố class cũ trong toàn repo
116
+ 7. Test + commit commit RIÊNG từng trang
117
+ 8. Ghi nhật ký một mục vào đầu file nhật ký của ngày hôm nay
118
+ ```
119
+
120
+ **Bước 3 là bước hay bị bỏ nhất.** Không xoá thì CSS chỉ phình thêm, mà giảm nó
121
+ mới là mục tiêu. Chỉ số theo dõi dễ nhất: **số dòng file CSS lớn nhất, mỗi tuần**.
122
+
123
+ **Bước 6 dễ quên.** Class cũ còn sót ở component khác thì xoá CSS sẽ làm vỡ một
124
+ trang mà bro không mở.
125
+
126
+ ```bash
127
+ grep -rn "dc-" --include="*.tsx" app components # tiền tố của trang vừa làm
128
+ ```
129
+
130
+ ---
131
+
132
+ ## L6. Migrate tăng dần chỉ an toàn khi hai hệ BẰNG NHAU về giá trị
133
+
134
+ Sai lầm suýt mắc ở dự án đó: định đổi tên **3.419 chỗ** để tách hai không gian
135
+ tên biến.
136
+
137
+ Không cần. Chỉ cần khai **đúng giá trị dự án đang chạy** vào hệ mới. Khi đó trang
138
+ đã migrate và trang chưa migrate **giống hệt nhau**. Việc "lấy đúng thang chuẩn"
139
+ lùi về bước cuối, khi không còn CSS cũ nào đọc biến đó — lúc ấy là một lần đổi có
140
+ kiểm soát, không phải rủi ro rải suốt đường.
141
+
142
+ **Ưu tiên hoà giá trị hơn là tách tên.**
143
+
144
+ ---
145
+
146
+ ## L7. Đừng "sửa" một token đang là `transparent`
147
+
148
+ Ở dự án đó, `--border-subtle` và `--border-strong` đều là `transparent`, nghĩa
149
+ là hàng trăm rule CSS cũ dạng `border: 1px solid var(--border-subtle)` đang vẽ
150
+ ra… không gì cả.
151
+
152
+ Gán màu cho hai biến đó để "sửa" = **bật viền cho hàng trăm chỗ cùng một lúc**,
153
+ không ai duyệt nổi.
154
+
155
+ Cách đúng: trang nào refactor thì viết viền thẳng ở đó, xoá dần rule cũ. Khi
156
+ không còn ai đọc hai biến kia nữa thì xoá luôn chúng.
157
+
158
+ Cùng loại bẫy: một khối `NO-BORDER CLEANUP` **635 dòng** sinh ra vì set
159
+ `--border-subtle: transparent` làm hỏng mọi chỗ dùng token đó làm *nền*. Sửa tận
160
+ gốc thì phải tách token viền khỏi token nền — tới giờ vẫn chưa làm.
161
+
162
+ **Bài học chung: một token mang hai vai thì không sửa được vai nào.** Xem `M25`.
163
+
164
+ ---
165
+
166
+ ## L8. Đi từ ít liên đới nhất tới nhiều nhất
167
+
168
+ 1. Khối đã tự gom theo feature (`.sp-*` sales page 667 dòng, pinned posts 173)
169
+ 2. Trang có ít selector (Discovery, 57 selector `dc-*`)
170
+ 3. Trang lớn (Challenge 297 `ch-*`, Marketplace 131 `mk-*`)
171
+ 4. Thứ dùng chung khắp nơi — shell, sidebar, modal (làm cuối, hoặc không làm)
172
+
173
+ Đếm quy mô trước khi chọn:
174
+
175
+ ```bash
176
+ grep -hoE '^\s*\.[a-z][a-z0-9-]*' $(find . -name '*.css') | tr -d ' .' \
177
+ | sed 's/-.*//' | sort | uniq -c | sort -rn | head -15
178
+ ```
179
+
180
+ ---
181
+
182
+ ## L9. CSS cũ trong `@layer base` thua utility — đừng vá bằng `!important`
183
+
184
+ Đừng viết rule CSS theo class cha để điều khiển một component
185
+ đã sang utility: CSS cũ nằm trong `@layer base` nên **thua** utility, phải rải
186
+ `!important` khắp nơi để thắng lại.
187
+
188
+ Cách đúng: khai một **variant** trong hệ utility (ví dụ một variant
189
+ `rail-collapsed:`) rồi dùng nó ngay trên component. Để lại trong CSS cũ đúng
190
+ phần nào utility không làm được — thường chỉ là bề rộng của khung ngoài.
191
+
192
+ ---
193
+
194
+ ## L10. Ghi nhật ký, và ghi VÌ SAO
195
+
196
+ Mỗi lần thêm/sửa/xoá gì đều ghi một mục. Mục đích: **ba tháng nữa nhìn lại vẫn
197
+ biết vì sao đã làm thế**, không chỉ là đã làm gì — phần "vì sao" mới là thứ
198
+ `git log` không kể được đầy đủ.
199
+
200
+ Quy ước ghi: **Ngày · Việc** → *thay đổi gì* · *vì sao* · *cái gì suýt sai*.
201
+
202
+ Vận hành file nhật ký:
203
+
204
+ - Tách **theo ngày**, mục mới lên **đầu** file của ngày hôm nay. Một file chung sẽ chạm ngưỡng không đọc nổi chỉ sau hai ngày.
205
+ - Ngày nào dài quá ~620 dòng thì cắt thành `<ngày>-part-N.md`. **Part 1 là sớm nhất**, mục mới luôn vào part số cao nhất — đánh số theo chiều đó thì part cũ đứng yên vĩnh viễn; đánh ngược lại là part 1 phình mãi và mọi link tới nó lệch dần.
206
+ - Giữ một file mục lục riêng, chỉ chứa link.
207
+ - Bài học rút ra thì gom vào một file riêng — chính là file bro đang đọc.
208
+
209
+ ---
210
+
211
+ ## L11. Đọc code UI trước khi bịa dữ liệu
212
+
213
+ Thẻ cộng đồng hiện `_count.memberships`, không phải cột `memberCount`. Seed
214
+ `memberCount: 761` thì thẻ vẫn hiện **0**.
215
+
216
+ Trước khi seed, grep xem component **thật sự đọc field nào**. Đừng suy từ tên cột.
217
+
218
+ Và seed phải phủ **ca biên**, không phải dữ liệu đẹp: giá 9 chữ số, tên tràn 2
219
+ dòng, thiếu ảnh, thiếu mô tả, số 1 chữ số, danh sách rỗng. Dữ liệu đẹp làm CSS
220
+ trông ổn cho tới lúc gặp khách thật.
221
+
222
+ ---
223
+
224
+ ## L12. Lỗi môi trường đội lốt lỗi code
225
+
226
+ Test đang xanh mà đột nhiên đỏ sau khi đụng vào môi trường → **nghi môi trường
227
+ trước khi nghi code**. Kiểm bằng cách chạy đúng file test đó riêng.
228
+
229
+ Ba ca đã dính, không cái nào là lỗi code: package
230
+ manager sai phiên bản; shell nạp nvm sai đường; và thiếu partial index vì
231
+ `db push` bỏ qua **im lặng** những thứ không diễn đạt nổi trong schema (partial
232
+ index `WHERE`, trigger, generated column) — dựng lại DB local xong phải grep
233
+ migration tìm `CREATE UNIQUE INDEX` / `CREATE TRIGGER` / `GENERATED` rồi chạy tay
234
+ phần thiếu.
235
+
236
+ ---
237
+
238
+ ## L13. Đo tác động thật, không chỉ đo số dòng
239
+
240
+ Không có số Lighthouse trước/sau thì không chứng minh được refactor cải thiện gì ngoài số
241
+ dòng CSS.
242
+
243
+ Số dòng CSS giảm là một chỉ số **dễ đo**, không phải chỉ số **quan trọng**. Nếu
244
+ chốt được một chỉ số thật ngay từ đầu (thời gian tải, CLS, số lần phải prompt
245
+ chỉnh lại) thì đợt refactor có cách để nói nó đã xong.