@haposoft/cafekit 0.16.6 → 0.18.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.
Files changed (194) 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 +0 -36
  4. package/bin/__tests__/codex-native.test.js +46 -88
  5. package/bin/__tests__/develop-contract.test.js +26 -57
  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 +159 -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 +100 -541
  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 +0 -27
  20. package/bin/__tests__/specs-v2-execution-closeout.test.js +4 -69
  21. package/bin/__tests__/ui-ux-skill.test.js +101 -0
  22. package/bin/__tests__/usage-hook-retired.test.js +114 -0
  23. package/bin/__tests__/validator-grounding.test.js +9 -28
  24. package/bin/phases/codex-runtime.js +24 -0
  25. package/bin/phases/omp-runtime.js +22 -0
  26. package/package.json +1 -1
  27. package/src/claude/CLAUDE.md +3 -10
  28. package/src/claude/agents/brainstormer.md +7 -3
  29. package/src/claude/agents/code-auditor.md +35 -50
  30. package/src/claude/agents/debugger.md +5 -79
  31. package/src/claude/agents/deployer.md +3 -5
  32. package/src/claude/agents/docs-keeper.md +2 -7
  33. package/src/claude/agents/implementer.md +6 -9
  34. package/src/claude/agents/inspector.md +2 -3
  35. package/src/claude/agents/project-manager.md +7 -15
  36. package/src/claude/agents/researcher.md +9 -3
  37. package/src/claude/agents/spec-maker.md +11 -11
  38. package/src/claude/hooks/__tests__/fixtures/statusline-default.golden +1 -1
  39. package/src/claude/hooks/__tests__/spec-gate.test.js +112 -3
  40. package/src/claude/hooks/__tests__/spec-state-touched.test.js +134 -0
  41. package/src/claude/hooks/__tests__/state.test.js +68 -0
  42. package/src/claude/hooks/__tests__/statusline.test.js +140 -11
  43. package/src/claude/hooks/agent.cjs +2 -1
  44. package/src/claude/hooks/lib/context.cjs +0 -89
  45. package/src/claude/hooks/lib/hook-payload.cjs +2 -2
  46. package/src/claude/hooks/rules.cjs +2 -1
  47. package/src/claude/hooks/session.cjs +0 -1
  48. package/src/claude/hooks/spec-gate.cjs +18 -28
  49. package/src/claude/hooks/spec-state.cjs +95 -1
  50. package/src/claude/hooks/state.cjs +21 -2
  51. package/src/claude/migration-manifest.json +30 -15
  52. package/src/claude/rules/ai-dev-rules.md +1 -1
  53. package/src/claude/rules/hook-protocols.md +8 -15
  54. package/src/claude/rules/manage-docs.md +9 -23
  55. package/src/claude/rules/orchestrator.md +3 -4
  56. package/src/claude/rules/review-audit-self-decision.md +3 -3
  57. package/src/claude/rules/skill-domain-routing.md +1 -3
  58. package/src/claude/rules/skill-workflow-routing.md +4 -5
  59. package/src/claude/rules/state-sync.md +3 -12
  60. package/src/claude/rules/workflow.md +10 -18
  61. package/src/claude/runtime.schema.json +5 -3
  62. package/src/claude/scripts/generate-skill-catalog.cjs +7 -6
  63. package/src/claude/scripts/provenance.cjs +23 -5
  64. package/src/claude/scripts/spec-receipt.cjs +52 -6
  65. package/src/claude/scripts/spec-resolver.cjs +1 -2
  66. package/src/claude/scripts/validate-spec-output.cjs +5 -32
  67. package/src/claude/scripts/workflow-policy.cjs +22 -10
  68. package/src/claude/settings/settings.json +1 -32
  69. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  70. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  71. package/src/claude/skills/code-review/SKILL.md +48 -17
  72. package/src/claude/skills/code-review/references/spec-compliance-review.md +2 -3
  73. package/src/claude/skills/code-review/references/verification-gate.md +22 -27
  74. package/src/claude/skills/debug/SKILL.md +29 -149
  75. package/src/claude/skills/develop/SKILL.md +27 -36
  76. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  77. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  78. package/src/claude/skills/docs/SKILL.md +2 -2
  79. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  80. package/src/claude/skills/fix/SKILL.md +153 -0
  81. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  82. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  83. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  84. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  85. package/src/claude/skills/git/SKILL.md +48 -9
  86. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  87. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  88. package/src/claude/skills/orca/SKILL.md +9 -8
  89. package/src/claude/skills/research/SKILL.md +2 -0
  90. package/src/claude/skills/scout/SKILL.md +61 -0
  91. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  92. package/src/claude/skills/specs/SKILL.md +22 -29
  93. package/src/claude/skills/specs/references/review.md +8 -6
  94. package/src/claude/skills/specs/references/templates.md +38 -44
  95. package/src/claude/skills/sync/SKILL.md +8 -15
  96. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  97. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  98. package/src/claude/skills/test/SKILL.md +22 -3
  99. package/src/claude/skills/test/references/execution-strategy.md +83 -0
  100. package/src/claude/skills/test/references/failure-triage.md +1 -1
  101. package/src/claude/skills/ui-ux/LICENSE +21 -0
  102. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  103. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  104. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  105. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  106. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  107. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  108. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  109. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  110. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  111. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  112. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  113. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  114. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  115. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  116. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  117. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  118. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  119. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  120. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  121. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  122. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  123. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  124. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  125. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  126. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  127. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  128. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  129. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  130. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  131. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  132. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  133. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  134. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  135. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  136. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  137. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  138. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  139. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  140. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  141. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  142. package/src/claude/skills/ui-ux/references/review.md +528 -0
  143. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  144. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  145. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  146. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  147. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  148. package/src/claude/skills/ui-ux/references/system.md +219 -0
  149. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  150. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  151. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  152. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  153. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  154. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  155. package/src/claude/status.cjs +149 -144
  156. package/src/codex/AGENTS.md +8 -13
  157. package/src/codex/hooks/agent.cjs +2 -1
  158. package/src/codex/hooks/lib/spec-receipt.cjs +5 -0
  159. package/src/codex/hooks/lib/spec-utils.cjs +0 -7
  160. package/src/codex/hooks/rules.cjs +2 -1
  161. package/src/codex/hooks/session.cjs +0 -2
  162. package/src/codex/hooks/spec-gate.cjs +8 -18
  163. package/src/codex/hooks.json +0 -20
  164. package/src/codex/rules/state-sync.md +3 -12
  165. package/src/common/AGENTS.md +2 -2
  166. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  167. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  168. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  169. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  170. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  171. package/src/claude/hooks/completion-authority.cjs +0 -163
  172. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  173. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  174. package/src/claude/hooks/usage.cjs +0 -197
  175. package/src/claude/skills/delegate/SKILL.md +0 -124
  176. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  177. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  178. package/src/claude/skills/hotfix/SKILL.md +0 -289
  179. package/src/claude/skills/inspect/SKILL.md +0 -142
  180. package/src/claude/skills/loop/SKILL.md +0 -114
  181. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  182. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  183. package/src/claude/skills/route/SKILL.md +0 -94
  184. package/src/claude/skills/route/references/agent-timing.md +0 -46
  185. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  186. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  187. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  188. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  189. package/src/codex/hooks/completion-authority.cjs +0 -109
  190. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  191. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  192. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  193. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  194. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -0,0 +1,650 @@
1
+ # Thiết kế từ đầu như một designer — luật U
2
+
3
+ **Nhánh mặc định** (chủ dự án chốt): mọi đề dựng hay làm lại một màn trở lên đều
4
+ vào đây, tiếng Việt hay tiếng Anh, sản phẩm mới hay màn đã có. Chỉ không vào khi đề nói rõ lối
5
+ khác (bảng câu 1 của `SKILL.md`): soi, giữ brand, dựng lại theo gu skill, refactor, dựng
6
+ luôn, hoặc việc nhỏ hơn một màn.
7
+
8
+ Khác nhánh `V` (`review.md`): `V` giữ khung trang, sửa lỗi và làm gọn. Kết quả là bản hi-fi
9
+ sạch hơn của **đúng wireframe cũ**. Nhánh `U` bắt đầu từ câu *"người dùng đến màn này để
10
+ làm gì"*, nên được đổi cả khung: cái gì đứng đầu, lọc nằm đâu, card nói gì, có chế độ xem
11
+ nào. Bản dựng lại theo `V` sạch hết lỗi đo được mà người xem vẫn nói
12
+ "nhìn không khác gì bản cũ, vẫn cần người làm UX".
13
+
14
+ Hai cổng chờ của nhánh này là hai chỗ duy nhất skill dừng hỏi. Đề ghi sẵn đủ cho cổng nào
15
+ (brief đã rõ, "chọn A luôn") thì qua cổng đó không dừng.
16
+
17
+ **"Dựng luôn" thì không vẽ wireframe** (vẽ wireframe tốn nhiều token, chủ dự án chốt).
18
+ Đề có "dựng luôn", "just build it", hoặc người dùng trả lời `dựng luôn` ở cổng 1: vẫn làm `U1`,
19
+ `U2` và chọn phương án `U3` sẽ khuyên dùng, **trong đầu, không gửi, không dừng**, rồi dựng thẳng
20
+ theo phương án đó (`U4`), màu theo nấc sẽ khuyên. Lúc giao ghi một dòng: *"Bố cục: [phương án]
21
+ vì [việc chính]. Muốn xem các hướng khác thì nhắn `vẽ wireframe`."* Lúc gửi brief ở cổng 1, thêm một dòng cuối: *"Muốn bỏ wireframe, dựng luôn thì
22
+ trả lời `dựng luôn`."* (tiếng Anh: *"Reply `just build it` to skip the wireframes."*)
23
+
24
+ ---
25
+
26
+ ## Bốn bước, hai cổng
27
+
28
+ | Bước | Ra cái gì | Cổng |
29
+ | --- | --- | --- |
30
+ | `U1` Brief | Một khối ngắn: sản phẩm, người dùng, việc chính, nền tảng | Gộp với `U2`, **cổng 1** |
31
+ | `U2` Việc chính của từng màn | Bảng: đến để làm gì, so sánh bằng gì, hành động cuối, quy ước loại sản phẩm | **Cổng 1**: người dùng sửa hoặc trả lời `ok` |
32
+ | `U3` Wireframe | 2–3 phương án bố cục khác nhau thật, nội dung thật, có ảnh; biến thể nội dung D, E; một thanh công cụ: phương án, màu, desktop / mobile, trạng thái, khung lý do | **Cổng 2**: người dùng chọn |
33
+ | `U4` Dựng thật | Code theo phương án đã chọn, probe tới khi danh sách `P` trống | Như cổng 3 của `checklist.md` |
34
+
35
+ Chưa qua cổng 2 thì **không đụng file nào của dự án**. Wireframe và ảnh để ở
36
+ `$TMPDIR/evon-design/`.
37
+
38
+ ---
39
+
40
+ ## U1. Brief: đọc trước, hỏi sau ⚑
41
+
42
+ - **Chạy audit câu 2 của `SKILL.md` trước khi viết brief**, cho mọi dự án, sản phẩm mới hay
43
+ đã có UI: stack, component có sẵn, phong cách. Dòng `Audit:` đứng đầu tin cổng 1. Bỏ audit
44
+ thì wireframe vẽ control mà dự án đã có kiểu khác, `U4` dựng ra app thứ hai.
45
+ - Đọc README, file route, kiểu dữ liệu (type, mock), chữ trên các màn đang có. Từ đó ghi
46
+ một khối năm dòng: **sản phẩm gì**, **cho ai**, **một đến ba việc chính**, **nền tảng
47
+ dùng nhiều** (điện thoại hay máy tính), **điểm khác biệt** (thứ sản phẩm bán mà nơi khác
48
+ không có).
49
+ - Dòng nào không suy ra được thì hỏi, **tối đa năm câu, gửi một lần**, mỗi câu kèm câu trả
50
+ lời đoán sẵn để người dùng chỉ cần gõ `ok`.
51
+ - **Không viết persona, không vẽ hành trình người dùng, không bịa số liệu nghiên cứu.**
52
+ Model không phỏng vấn được ai. Brief chỉ ghi điều đọc được từ code hoặc người dùng đã nói,
53
+ mỗi dòng ghi nguồn: *đọc code*, *người dùng nói*, *đoán*.
54
+
55
+ ## U2. Việc chính của từng màn ⚑
56
+
57
+ Mỗi màn trong phạm vi một dòng:
58
+
59
+ | Màn | Đến để làm gì | So sánh, quyết định bằng gì | Hành động cuối | Loại sản phẩm này thường làm |
60
+ | --- | --- | --- | --- | --- |
61
+ | Danh sách khoá học | Tìm khoá hợp trình độ, trong ngân sách | Giá, thời lượng, trình độ, đánh giá | Mở chi tiết, lưu | Lọc dính đầu trang, card nói giá và trình độ trước, có sắp xếp cạnh số kết quả |
62
+
63
+ - Cột "so sánh bằng gì" quyết định card và bảng: thứ người dùng dùng để chọn giữa các mục
64
+ phải **nổi nhất và đứng đầu**. Thứ không giúp chọn thì lùi xuống hoặc để trang chi tiết.
65
+ - Cột cuối: **tra thật** cách vài sản phẩm cùng loại đang làm (tìm web nếu có công cụ), ghi
66
+ thành quy ước, không ghi tên sản phẩm vào code hay vào file của dự án. Không tra được thì
67
+ ghi *"theo trí nhớ, cần kiểm"*. Quy ước số đông thắng gu riêng.
68
+ - Điểm khác biệt ở `U1` phải **hiện trên màn chính**, không chỉ nằm trong bộ lọc.
69
+
70
+ Gửi `U1` và `U2` trong **một** tin, kết bằng *"Đúng thì trả lời `ok`, sai dòng nào thì sửa
71
+ dòng đó."* Dừng chờ.
72
+
73
+ ## U3. Wireframe: 2–3 phương án khác nhau thật ⚑
74
+
75
+ - **Khác ở chiến lược bố cục, không khác ở trang trí.** Ví dụ cho một trang danh sách:
76
+ A giữ lưới card với thanh lọc gọn dính đầu trang; B chia đôi danh sách và panel chi tiết;
77
+ C đặt ô tìm lên trước, lọc sau. Ba phương án chỉ khác bo góc hay màu là **một** phương án.
78
+ - **Mặc định xám.** Ảnh là khối xám có tỉ lệ thật. Mở ra người dùng nhìn bố cục trước, không
79
+ sa vào màu; màu nhấn chỉ bật bằng nút Màu trên thanh công cụ (dưới). Xám chỉ là tắt màu nhấn,
80
+ còn lại vẫn là token và component thật (dòng "Wireframe dựng bằng chính token…" dưới).
81
+ - **Icon lucide thật ở đúng chỗ các app đều đặt**, không vẽ ô vuông giữ chỗ (ô vuông cạnh mục
82
+ sidebar đọc ra checkbox). Nạp `lucide` từ `cdn.jsdelivr.net`, gọi
83
+ `lucide.createIcons()`. **Mỗi lần ghi lại `innerHTML` là phải vẽ lại icon**: `createIcons()`
84
+ chỉ thay các `<i data-lucide>` có lúc gọi, thẻ mới ghi sau đó nằm trống. Gắn một lần ở đầu
85
+ script `new MutationObserver(() => { if (document.querySelector("i[data-lucide]")) lucide.createIcons(); }).observe(document.body, { childList: true, subtree: true })`
86
+ thay vì gọi tay sau từng lần render. **Phải có điều kiện `i[data-lucide]`**: svg vẽ xong vẫn mang
87
+ `data-lucide`, gọi thẳng `createIcons()` trong observer thì nó thay svg mãi, trang treo. Không gắn
88
+ observer thì chọn xong một mục là ô select và ô ngày mất icon (svg thành `<i data-lucide>` trống),
89
+ phải mở lại mới thấy. Ba chỗ: **mỗi mục sidebar**; **ô icon 32px nền nhạt ở góc mỗi card số
90
+ liệu**; **ô icon hay avatar đầu dòng** khi dòng thuộc một loại (bộ phận, nhà cung cấp, loại
91
+ giao dịch). Tiêu đề khối, dòng meta thì không icon (`V1c`). Nấc Xám thì icon xám; ba icon
92
+ giống hệt nhau cho ba mục là thà bỏ (`F17`). Dashboard không icon trông "chán", người xem
93
+ nói ngay. **Xám vẫn có mức nhấn**: nút chính tô xám đậm chữ
94
+ trắng, nút phụ viền. Hai nút cùng một kiểu trong wireframe là chưa quyết thứ bậc, lúc dựng
95
+ thật sẽ lại thành hai nút tranh nhau (`V1b`).
96
+ - **Wireframe dựng bằng chính token, font, component mà bản dựng sẽ dùng** ⚑, vì `U4` chép
97
+ nguyên. Nấc Màu của wireframe phải **là** bản dựng, chỉ khác là file HTML: người dùng chọn thứ
98
+ họ thấy, dựng ra y hệt thì họ hài lòng; dựng ra khác là chọn nhầm.
99
+ - **Khung file như `layouts/app-kanban.html`**: nạp font, Tailwind v4 bản trình duyệt
100
+ (`@tailwindcss/browser@4`), rồi dán **nguyên** khối token vào `<style type="text/tailwindcss">`
101
+ (`:root` và `@theme inline`). Sản phẩm mới: `references/tokens.css`, màu nhấn theo nhóm Nhấn
102
+ (dưới). Dự án đã có UI: **file token của dự án** (`globals.css`, `index.css`, config theme),
103
+ màu xếp theo vai như `U4` sẽ làm (`review.md`, bảng vai màu), dáng theo bảng "Dáng lấy từ
104
+ skill". Không tự đặt mã màu, bo góc, bóng nào ngoài khối token.
105
+ **Dự án đã qua `D9`** (có trang `/design-system` hay story design system, file token đã sửa)
106
+ tính là **dự án đã có UI** dù chưa có màn nào: dán file token của dự án, không dán
107
+ `tokens.css` của skill, không thì mất font và màu nhấn vừa duyệt.
108
+ - **Component viết đúng class bản dựng sẽ dùng**: dự án có thư viện component (shadcn, bộ nội
109
+ bộ) thì mở source `Button`, `Input`, `Badge`, `Card`… của dự án, chép chuỗi class của biến
110
+ thể sẽ dùng; chưa có thì chép công thức trong `components/*.md`. Không vẽ lại "cho giống".
111
+ - **Khoảng cách, cỡ viết thẳng bằng class** ở từng khối (padding khung trang, `gap`, chiều cao
112
+ control, cỡ chữ, cỡ icon), theo thang của dự án hay của skill, không để trình duyệt tự canh.
113
+ Người dùng chọn wireframe là chọn luôn độ thoáng.
114
+ - **Nấc Xám chỉ đổi màu nhấn và màu trạng thái sang xám** (ghi đè `--primary` và các token
115
+ trạng thái trên `:root[data-mau="xam"]`); nền, viền, chữ, bo góc, bóng, font giữ nguyên token.
116
+ Bật Màu lên là thấy đúng bản dựng.
117
+ - **Đổi màu nhấn trên `:root`, không trên `body`, và kéo theo mọi token suy từ nó.** Biến
118
+ `--border-focus: var(--primary)` khai ở `:root` được tính xong ngay tại `:root` rồi mới truyền
119
+ xuống: đổi `--primary` ở `body` thì nút chính đổi màu mà viền focus vẫn màu cũ. Token viết cứng
120
+ theo màu nhấn (`--ring-focus`, `--primary-hover`, `--primary-light`) cũng phải suy lại ở từng nấc,
121
+ như `brand-tokens.md` dặn khi đổi `--primary`. Không suy lại thì chọn
122
+ xanh ngọc, nút xanh ngọc, nhưng ô tìm và select focus vẫn viền và ring màu chàm ở mọi nấc,
123
+ cả nấc Xám. Probe báo "Token focus không theo màu nhấn".
124
+ - **Nội dung thật**: chữ lấy từ dữ liệu của dự án, cả ca dài nhất và ca trống. Wireframe chữ
125
+ "Lorem" thì không thấy được card quá tải.
126
+ - **Dữ liệu mẫu có đủ mọi trạng thái, nhất là trạng thái suy từ giờ.** Màn có mốc "bây giờ"
127
+ (lịch hẹn, hạn chót, đơn đang giao) thì thứ người dùng cần thấy nhất thường là thứ **đã quá
128
+ mốc mà chưa xong**: khách trễ giờ hẹn chưa đến, việc quá hạn, đơn giao chậm. Nó không nằm
129
+ trong danh sách trạng thái lưu trong dữ liệu (chỉ có "Đã xác nhận"), mà tính từ giờ, nên
130
+ hay bị quên. Đặt giờ "bây giờ" của wireframe sao cho có ít nhất một mục như vậy, vẽ nó thành
131
+ trạng thái riêng (`M4`: hổ phách hay đỏ ở nấc Màu), và ghi vào `U2` là thứ phải nổi. Ví dụ lịch
132
+ hẹn lúc 10:40 mà mọi lịch trước đó đều đã đến, đang khám hoặc không đến, không có ai "trễ": việc
133
+ chính của lễ tân lúc đó (gọi người trễ) không có trên wireframe.
134
+ - **Đúng hình dạng dữ liệu**: mỗi mục có mấy ảnh, trường nào hay trống, danh sách dài bao
135
+ nhiêu. Dữ liệu chỉ có một ảnh mỗi tin mà wireframe vẽ lưới ba ảnh là hứa thứ dữ liệu
136
+ không có: người dùng chọn vì lưới ảnh, bản dựng ra một ảnh to quá khổ. Muốn phương án cần thêm dữ liệu thì vẽ đúng cái đang có và ghi *"đẹp hơn khi
137
+ có X"* ở dòng đánh đổi.
138
+ - Mỗi phương án ghi **ba dòng**: việc chính giờ thấy ở đâu, đổi gì so với bản cũ, đánh đổi.
139
+ Ba dòng đó cũng là nội dung khung lý do trên trang wireframe (dưới), hai chỗ cùng một chữ.
140
+ Phương án cần dữ liệu hay logic chưa có (khoảng cách, chế độ xem mới) thì ghi rõ
141
+ *"cần dữ liệu X, logic do bạn nối"* (`N10`).
142
+ - **Tính cả khung app vào bề ngang.** App đã có sidebar điều hướng mà phương án thêm một cột
143
+ lọc bên trái thì ghi rõ ở dòng đánh đổi: hai cột trái, nội dung còn lại bao nhiêu px ở 1280.
144
+ - **Đánh dấu một phương án khuyên dùng**, kèm một câu vì sao (bám `U2`).
145
+ - Một file HTML, mọi lựa chọn nằm trên tham số (`?v=a&mau=xam&kho=desktop&tt=du-lieu`) để probe
146
+ mở được từng cái, và người dùng chép link gửi đi thì mở ra đúng cái đang xem.
147
+ - **Đánh số từng khối chính** (header, hàng lọc, danh sách, panel, chân trang…): `data-wf-block="1"`,
148
+ số hiện nhỏ ở góc trên trái khối. Người dùng góp ý bằng số (*"bỏ khối 3"*, *"đưa khối 2 lên
149
+ đầu"*), không phải tả "cái thanh chữ nhỏ ở trên bảng". Cùng một khối ở các phương án giữ cùng
150
+ số; khối chỉ phương án đó có thì số mới.
151
+ **Số không được đè chữ hay icon của khối.** Khối không có padding (hàng công cụ, hàng chữ đếm
152
+ trạng thái) có chữ ngay góc trên trái: số đè thành "ThBa". Sau mỗi lần vẽ, `placeBlockNumbers()`
153
+ (mẫu dưới) đo chữ và icon dưới số, đè thì gắn `data-wf-block-out` để số lên ngay trên mép khối.
154
+ **`data-wf-block` đặt trên khối bọc không cuộn**: khối `overflow-x-auto` (hàng chip) cắt mất số
155
+ nằm ngoài mép, bọc thêm một `div` rồi đánh số lên đó.
156
+ - **Khối nào skill đã có mẫu thì wireframe vẽ đúng hình mẫu đó**, vì `U4` dựng đúng wireframe:
157
+ vẽ sai là bản dựng chép sai theo. Trước khi vẽ, liệt kê các khối của phương án rồi mở mẫu
158
+ tương ứng ở bảng mục 2 của `SKILL.md` (`components/`, `layouts/`): nút, ô nhập, select, ô
159
+ chọn ngày, checkbox, công tắc, tab, chip, phân trang, badge, avatar, dòng danh sách, card, card
160
+ số liệu, biểu đồ, khối rỗng, đường dẫn, header, sidebar, bảng. Chép **hình và class**: cỡ, số phần tử,
161
+ cách xếp, chữ nằm đâu, chuỗi class của mẫu; không cần chép code React. Control là thẻ thật (`<input>`, `<button>`,
162
+ `<select>` nếu mẫu dùng), không `div` giả. Khối chưa có mẫu mới tự vẽ. Hai
163
+ lỗi hay gặp: phân trang vẽ hai nút chữ "Trước / Sau" rộng khác nhau thay cho
164
+ `‹ 1 2 3 … ›`; ô tìm là `div` nên placeholder dài rớt xuống dòng hai.
165
+ **App đang có mà dùng control gốc của trình duyệt thì wireframe không chép theo**, kể cả khi
166
+ app đã tô viền, bo góc: `<select>`, ô ngày / giờ bấm vào vẫn bung menu và lịch của hệ điều hành;
167
+ checkbox, radio, thanh trượt, ô chọn tệp gốc lạc dáng giữa app. Vẽ theo mẫu tương ứng của skill
168
+ (`components/choice-controls.md`, `range-slider.md`, `file-upload.md`), tô bằng token của app. Chỉ giữ control gốc khi nó chỉ hiện trên mobile (luật `<select>` gốc cho màn
169
+ cảm ứng ở đó); ô nằm trong dialog dùng cho cả hai khổ thì dựng. Khối trong lớp nổi (dialog,
170
+ sheet, popover) cũng đối chiếu mẫu, mở ra rồi xem, không chỉ phần trang đang hiện.
171
+ - **Tự đối chiếu trước khi probe**: mỗi khối có mẫu, so wireframe với mẫu một dòng ("phân trang:
172
+ khớp", "ô tìm: `<input>`, placeholder vừa"). Probe bắt được một phần (placeholder dài hơn ô,
173
+ khối trông như ô nhập mà chữ xuống dòng, phân trang chỉ có nút chữ, hàng control lệch), phần
174
+ còn lại là mắt.
175
+ - **Đủ bốn trạng thái của `I19`**: có dữ liệu, đang tải, rỗng, lỗi, chuyển bằng nút Trạng thái.
176
+ Màn rỗng có câu và nút của `components/empty-state.md`, đang tải là khung chờ đúng hình dòng
177
+ thật. Người dùng góp ý màn rỗng từ lúc wireframe, không đợi dựng xong mới thấy.
178
+ - **Wireframe có đủ trạng thái như bản thật**: một mục đang chọn đánh dấu `aria-current` (hay
179
+ `aria-selected`), có nền rê. Wireframe tĩnh không có rê thì không ai thấy
180
+ "rê trùng nền đang chọn" cho tới khi đã dựng xong.
181
+ - **Probe từng phương án trước khi gửi**, ở 1280 và 375, sửa tới khi sạch các mục: danh sách
182
+ `P`, rê ra đúng màu mục đang chọn, vạch trái bị bo góc cắt, mục lặp dày chữ, cột dính cuộn
183
+ riêng, nội dung trôi giữa màn rộng, hàng nút rớt một nút lẻ (`R3`), select và ô ngày gốc (kể cả
184
+ trong dialog đang đóng), "khung wireframe làm hỏng
185
+ bản thiết kế" (số đè chữ, `sticky` mất, thanh tràn). Rồi chạy luật Cấu trúc (`V1b` trong `review.md`) bằng
186
+ mắt. Người dùng không tự thấy "card chữ quá trời" hay "vạch bị cắt" trên wireframe xám, họ
187
+ chọn theo bố cục rồi vấp lỗi ở bản dựng (năm dòng mỗi mục, vạch trái bị bo cắt, đang chọn và rê
188
+ cùng một xám; probe đo ra cả hai lỗi đầu trên chính file wireframe). Ghi một dòng khi gửi: *"Probe wireframe: A sạch, B sạch, C sạch"*.
189
+ - **Hai biến thể nội dung, D và E, trên phương án khuyên dùng.** Cùng bố cục, chỉ khác nội
190
+ dung, để người dùng thấy cạnh nhau cái họ không tự nghĩ ra:
191
+ - **D, gọn chữ:** mỗi mục chỉ giữ thứ dùng để chọn ở cột "so sánh bằng gì" của `U2`, tối
192
+ đa ba dòng. Phần còn lại để trang hay panel chi tiết.
193
+ - **E, bỏ lặp:** mỗi thông tin một chỗ trên màn: không lặp giữa mục và panel chi tiết, giữa
194
+ header và sidebar, giữa tên trang và mục đang chọn (`V1b`, "Hai chỗ một việc").
195
+
196
+ D và E cũng qua probe như các phương án bố cục.
197
+
198
+ - **Công tắc Màu** (tắt là Xám, bật là Màu), áp cho mọi phương án, không phải bản riêng:
199
+ - **Tắt, Xám:** mặc định lúc mở, chỉ xem bố cục.
200
+ - **Bật, Màu:** như bản dựng sẽ ra theo mặc định (`P6`): màu nhấn ở nút chính, mục đang chọn, link,
201
+ biểu đồ; **và màu trạng thái của `M4` trên mọi dữ liệu có trạng thái**: vượt ngân sách,
202
+ quá hạn đỏ hay hổ phách, đã xong xanh, thanh tiến độ tô theo ngưỡng. Nấc Màu mà thanh 103%
203
+ vẫn đen thì người xem hỏi *"chọn màu mà sao vẫn trắng đen"*. Dự án đã
204
+ có phong cách khác flat (`P4`) thì Màu là phong cách đó.
205
+
206
+ ⚠️ **Nấc thứ ba "Có màu" đã bỏ khỏi wireframe (chủ dự án: thêm vào cũng không
207
+ khác gì mấy).** Màu brand người dùng cần thấy nằm ở chỗ tương tác (control bấm được, dưới),
208
+ không ở dải màu trang trí. Người dùng tự xin "có màu" trong đề thì theo `P12` ở `styles.md`
209
+ lúc dựng, không vẽ thành nấc.
210
+
211
+ **Dự án chưa có màu brand** (màu nhấn là gần đen mặc định, `brand-tokens.md`; dự án đã qua
212
+ `D9` thì màu nhấn đã chốt ở cổng đó, kể cả khi chốt gần đen, không tính là chưa có) thì thêm nhóm
213
+ **Nhấn: ● ● ●** gồm ba màu gợi ý (chàm `#4f46e5`, xanh ngọc `#0d9488`, cam `#ea580c`), đổi
214
+ `--primary` tại chỗ. Không có nhóm này thì nấc Màu của dự án mới vẫn đen trắng. Màu người dùng
215
+ chọn thành màu nhấn lúc dựng (`brand-tokens.md`); không chọn thì dựng màu đầu, báo một dòng.
216
+ Đổi màu bằng biến CSS trên `:root[data-mau]`, `:root[data-nhan]` (không trên `body`, xem trên), không vẽ lại. Probe cả hai
217
+ nấc (tương phản chữ trắng trên nút chính, trên mục đang chọn).
218
+
219
+ **Dự án chưa có logo** thì thêm nhóm **Logo: 1 · 2 · 3** (`?logo=`): ba dấu theo ba hướng của
220
+ `components/logo.md`, đã kiểm ở 16px, đổi ở mọi chỗ có logo trên trang. Khung lý do có một dòng
221
+ vì sao cho từng dấu. Không có nhóm này thì đầu sidebar là ô chữ cái, người dùng duyệt xong vẫn
222
+ chưa thấy sản phẩm mình trông ra sao. Logo người dùng chọn thành logo lúc dựng; không chọn thì
223
+ dựng hướng khuyên dùng, báo một dòng.
224
+
225
+ - **Control trong wireframe bấm được và hiện trạng thái như bản thật** ⚑: ô nhập, ô tìm focus
226
+ thì viền và ring màu nhấn (`I13`: `--border-focus`, `--ring-focus`); select, dropdown, nút lọc
227
+ bấm là xổ ra danh sách mục thật theo `layouts/overlay.md` (khung, chuyển động), bấm ngoài hay
228
+ Esc thì đóng. Mục đang chọn có badge thì badge đảo màu như bản dựng (`layouts/app.md`,
229
+ Sidebar). Bật Màu lên là người dùng thấy màu brand đúng ở chỗ họ sẽ bấm (chủ dự án chốt:
230
+ "cho user thấy còn hay hơn" nấc Có màu).
231
+
232
+ - **Nút Khổ: Desktop · Mobile.** Mobile hiện chính trang đó trong một khung 375 × 812 giữa màn
233
+ (iframe cùng link, thêm `frame=1` để trong khung không có thanh công cụ), nên media query chạy
234
+ thật. Người dùng hầu như không tự thu cửa sổ, nên không thấy bảng thành danh sách, lọc thành
235
+ nút ra sao ở điện thoại.
236
+ - **Nút ☰ trong khung mobile bấm được**: mở panel trượt từ trái theo `layouts/app.md` (lớp
237
+ phủ, bấm ngoài hay Esc thì đóng, không nút ✕), để người dùng thấy menu có bao nhiêu mục, mục nào đang
238
+ chọn. ☰ không bấm được thì mobile chỉ là ảnh chụp.
239
+ - **App có từ 5 mục điều hướng chính trở xuống** thì thêm nhóm **Nav: ☰ · Thanh dưới** (chỉ
240
+ hiện khi Khổ là Mobile), vẽ thêm thanh điều hướng dưới theo `layouts/app.md`, và ghi trong
241
+ khung lý do nên dùng cái nào: app dùng hằng ngày, chuyển mục liên tục thì thanh dưới; app
242
+ quản trị ít mở trên điện thoại thì ☰. Từ 6 mục thì chỉ ☰.
243
+
244
+ - **Thanh công cụ ở đỉnh trang, bắt buộc, một dòng**, nằm ngoài bản thiết kế: dải **sáng** cao
245
+ 56px, nền trắng, viền dưới xám nhạt, chữ 14px, **không dính đỉnh**: thanh dính đè lên sidebar,
246
+ header, panel `sticky top-0` của chính bản thiết kế, người xem thấy sidebar mất logo khi cuộn và
247
+ tưởng bản dựng sẽ vậy. Các nhóm xếp liền từ trái, cách nhau
248
+ 24px, theo thứ tự: Màn (đề nhiều màn) · **Phương án** · **Màu** (công tắc) · Nhấn (dự án chưa có brand) · Logo (dự án chưa có logo) · Khổ · Nav (mobile, ít mục)
249
+ · **Trạng thái**.
250
+ - **Đề nhiều màn** (lịch và hồ sơ, danh sách và chi tiết) thì một file, nhóm **Màn** đứng đầu
251
+ (`?man=`), nhãn một hai chữ ("Lịch", "Hồ sơ"); mỗi màn có A, B, C riêng. **Ở 1280 thanh phải
252
+ vừa một dòng không cuộn** (probe đo): nhãn dài như "Lịch trong ngày", "Hồ sơ bệnh
253
+ nhân" đẩy thanh tràn, "Trạng thái" bị cắt mất.
254
+ - **Mỗi nhóm là một segmented control**: rãnh xám nhạt bo 10px, nút trong rãnh không nền, nút
255
+ đang bật (`aria-current="page"`) nền trắng, bóng mảnh, chữ đậm đen; nút khác chữ xám. Không
256
+ dải tối, không nút chữ trắng rời rạc: dải tối nặng hơn chính bản thiết kế, kéo mắt khỏi thứ
257
+ cần xem, và mười mấy nút cùng hình đọc không ra nhóm nào.
258
+ - **Phương án chỉ ghi chữ cái** `A B C D E`, có nhãn "Phương án" xám đứng trước; tên đầy đủ ở
259
+ `title` và ở đầu khung lý do. Phương án khuyên dùng có chấm nhỏ màu nhấn cạnh chữ cái. Tên
260
+ dài trên thanh ("A · Báo cáo một trang (khuyên dùng)") đẩy cả thanh phải cuộn ngang ở 1280.
261
+ - **Khổ có icon**: màn hình trước Desktop, điện thoại trước Mobile (icon 16px, nét 2).
262
+ - **Trạng thái là menu thả**, nhãn xám "Trạng thái:" kèm giá trị đang xem đậm và mũi tên nhỏ;
263
+ bấm thì ra bốn link. Bốn trạng thái ít đổi, không đáng chiếm bốn nút trên thanh.
264
+ - **Màu là công tắc** có nhãn "Màu" đứng trước, không phải segmented: chỉ còn hai nấc.
265
+ Nhóm Nhấn, Nav không cần nhãn: chữ trong nút đã tự nói.
266
+ - **Chữ trên thanh và khung lý do theo tiếng người dùng đang viết (`T27`)**, không theo
267
+ `T24`: đó là lời skill nói với người dùng, không phải chữ của sản phẩm. Mẫu HTML dưới viết
268
+ tiếng Việt; đề tiếng Anh thì đổi hết: Phương án → Option, Màu → Color, Nhấn (`aria-label`
269
+ Chàm, Xanh ngọc, Cam) → Indigo, Teal, Orange, Thanh dưới → Bottom bar, Trạng thái → State
270
+ (Có dữ liệu, Đang tải, Rỗng, Lỗi → Data, Loading, Empty, Error), Khuyên dùng → Recommended,
271
+ Ưu, nhược → Pros, cons, Ưu / Nhược / Hợp khi → Pros / Cons / Best when, Gợi ý góp ý →
272
+ Feedback ideas, tên phương án, câu lý do, câu gợi ý cũng tiếng Anh. Tham số URL (`v`, `mau`,
273
+ `kho`, `tt`…) giữ nguyên. Chữ **trong** bản thiết kế vẫn theo `T24`: dự án đang có nhãn tiếng
274
+ Việt thì wireframe tiếng Việt dù đề tiếng Anh. Thanh tiếng Việt trên đề tiếng Anh thì người
275
+ dùng không đọc được nút nào, mà thanh là chỗ duy nhất để chọn.
276
+
277
+ Mỗi nút là link giữ nguyên các lựa chọn khác, chỉ đổi đúng tham số của nó. Màn hẹp thì thanh
278
+ cuộn ngang, không xuống dòng. Mở không tham số thì: phương án khuyên dùng, Màu tắt, Desktop, Có
279
+ dữ liệu. Thiếu thanh thì người dùng phải tự gõ `?v=`.
280
+
281
+ - **Khung lý do ngay dưới thanh**, không modal (modal che mất bản thiết kế đúng lúc cần nhìn),
282
+ nền xám rất nhạt. **Dòng đóng**: tên phương án đậm đen, nhãn "Khuyên dùng" (chỉ phương án khuyên
283
+ dùng), một câu lý do chữ `#525252` cắt một dòng, nút "Ưu, nhược ⌄" dạt phải: *"**A · Lưới card**
284
+ [Khuyên dùng] Người dùng đến để so lương, nên lương đứng đầu mỗi dòng"* (bám việc chính ở `U2`,
285
+ không viết "gọn gàng, hiện đại"). Dưới 768px câu lý do xuống dòng riêng, nút chỉ còn mũi tên để
286
+ tên và nhãn giữ một dòng. Bấm mở ra đủ:
287
+ - **Ba cột** Ưu (chấm xanh), Nhược (chấm hổ phách), Hợp khi (chấm xám): tiêu đề 12px đậm đen,
288
+ Ưu 2–3 gạch đầu dòng, Nhược 1–2, Hợp khi một câu, chữ `#404040`. Màn hẹp xếp chồng. Đổi theo
289
+ phương án đang xem.
290
+ - **Gợi ý góp ý**: hàng riêng dưới đường kẻ, 3–4 câu ngắn người dùng chép gửi lại cho AI, **mỗi
291
+ câu là một nút** (chữ cả câu kèm icon chép, không xuống dòng, bấm xong icon thành dấu ✓ xanh
292
+ 1,5 giây). Chọn
293
+ theo chính trang này, bằng ngôn ngữ của đề: trang đang Xám nhạt thì *"Thêm màu brand ở header
294
+ và hàng lọc"*; tiêu đề mảnh thì *"Tiêu đề đậm hơn"*; khối sát nhau thì *"Thoáng hơn, tăng
295
+ khoảng cách giữa các khối"*; *"Font khác hợp sản phẩm hơn"*; *"Bỏ khối 3"*. Không gợi ý
296
+ thứ trang đã có (đã nhiều màu thì không "thêm màu").
297
+
298
+ Đừng viết mọi thứ thành chữ 13px xám `#737373` liền một khối (Ưu, Nhược, Hợp khi nối đuôi, nút
299
+ Chép chen giữa câu, một câu gợi ý gãy làm hai dòng): người xem thấy "màu chìm, cấu trúc loạn xạ".
300
+
301
+ ```html
302
+ <nav class="wf-bar" aria-label="Wireframe">
303
+ <div class="wf-group">
304
+ <span class="wf-label">Phương án</span>
305
+ <span class="wf-set" data-wf-param="v">
306
+ <a data-value="a" title="A · Lưới card (khuyên dùng)" data-recommended>A</a><a data-value="b" title="B · Danh sách + chi tiết">B</a><a data-value="d" title="D · A gọn chữ">D</a>
307
+ </span>
308
+ </div>
309
+ <a class="wf-switch" data-wf-toggle="mau" data-on="mau" data-off="xam" role="switch">Màu <i></i></a>
310
+ <span class="wf-set" data-wf-param="nhan"><a data-value="cham" aria-label="Chàm"><i></i></a><a data-value="ngoc" aria-label="Xanh ngọc"><i></i></a><a data-value="cam" aria-label="Cam"><i></i></a></span>
311
+ <span class="wf-set" data-wf-param="kho">
312
+ <a data-value="desktop"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="20" height="14" x="2" y="3" rx="2"/><path d="M8 21h8M12 17v4"/></svg>Desktop</a>
313
+ <a data-value="mobile"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="20" x="5" y="2" rx="2"/><path d="M12 18h.01"/></svg>Mobile</a>
314
+ </span>
315
+ <span class="wf-set" data-wf-param="nav"><a data-value="menu">☰ Menu</a><a data-value="duoi">Thanh dưới</a></span>
316
+ <details class="wf-menu">
317
+ <summary><span>Trạng thái:</span><b data-wf-current="tt"></b><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></summary>
318
+ <div class="wf-popover" data-wf-param="tt"><a data-value="du-lieu">Có dữ liệu</a><a data-value="dang-tai">Đang tải</a><a data-value="rong">Rỗng</a><a data-value="loi">Lỗi</a></div>
319
+ </details>
320
+ </nav>
321
+ <details class="wf-reason" data-wf-reason>
322
+ <summary>
323
+ <span class="wf-reason-name">A · Lưới card</span>
324
+ <span class="wf-reason-tag">Khuyên dùng</span>
325
+ <span class="wf-reason-why">Người dùng đến để so lương, nên lương đứng đầu mỗi dòng.</span>
326
+ <span class="wf-reason-toggle"><span>Ưu, nhược</span><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 9 6 6 6-6"/></svg></span>
327
+ </summary>
328
+ <div class="wf-reason-body">
329
+ <div class="wf-reason-cols">
330
+ <section><h3><i data-tone="uu"></i>Ưu</h3><ul><li>…</li><li>…</li></ul></section>
331
+ <section><h3><i data-tone="nhuoc"></i>Nhược</h3><ul><li>…</li></ul></section>
332
+ <section><h3><i></i>Hợp khi</h3><p>…</p></section>
333
+ </div>
334
+ <div class="wf-reason-tips">
335
+ <h3>Gợi ý góp ý</h3>
336
+ <button type="button" data-copy="Tiêu đề đậm hơn">Tiêu đề đậm hơn<svg data-icon="chep" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect width="14" height="14" x="8" y="8" rx="2"/><path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"/></svg><svg data-icon="da-chep" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 6 9 17l-5-5"/></svg></button>
337
+ …mỗi câu gợi ý một nút như trên…
338
+ </div>
339
+ </div>
340
+ </details>
341
+ <main id="wf-design">…khối có data-wf-block="1", "2"…</main>
342
+ <style>
343
+ .wf-bar { position: relative; z-index: 50; display: flex; align-items: center; gap: 24px; height: 56px;
344
+ padding: 0 16px; overflow-x: auto; white-space: nowrap; background: #fff; border-bottom: 1px solid #e5e5e5;
345
+ color: #737373; font: 14px/1 system-ui, -apple-system, sans-serif; }
346
+ .wf-bar *, .wf-reason * { box-sizing: border-box; }
347
+ .wf-group { display: flex; align-items: center; gap: 8px; flex-shrink: 0; }
348
+ .wf-set { display: flex; align-items: center; gap: 2px; flex-shrink: 0; padding: 3px; border-radius: 10px; background: #f4f4f5; }
349
+ .wf-set a { position: relative; display: inline-flex; align-items: center; gap: 6px; height: 30px; padding: 0 12px;
350
+ border-radius: 7px; color: #737373; text-decoration: none; }
351
+ .wf-set a:hover { color: #171717; }
352
+ .wf-switch { display: inline-flex; align-items: center; gap: 8px; flex-shrink: 0; color: #737373; text-decoration: none; }
353
+ .wf-switch i { position: relative; width: 36px; height: 20px; border-radius: 10px; background: #e4e4e7; transition: background .15s; }
354
+ .wf-switch i::after { content: ""; position: absolute; top: 2px; left: 2px; width: 16px; height: 16px; border-radius: 50%;
355
+ background: #fff; box-shadow: 0 1px 2px rgb(0 0 0 / .2); transition: translate .15s; }
356
+ .wf-switch[aria-checked="true"] i { background: #171717; }
357
+ .wf-switch[aria-checked="true"] i::after { translate: 16px 0; }
358
+ .wf-set a[aria-current="page"] { background: #fff; color: #171717; font-weight: 500;
359
+ box-shadow: 0 1px 2px rgb(0 0 0 / .08), 0 0 0 1px rgb(0 0 0 / .04); }
360
+ .wf-bar svg { width: 16px; height: 16px; flex-shrink: 0; }
361
+ [data-wf-param="v"] a { justify-content: center; min-width: 32px; padding: 0 10px; }
362
+ [data-wf-param="v"] a[data-recommended]::after { content: ""; position: absolute; top: 5px; right: 5px;
363
+ width: 5px; height: 5px; border-radius: 50%; background: #4f46e5; }
364
+ [data-wf-param="nhan"] a { padding: 0 8px; }
365
+ [data-wf-param="nhan"] i { width: 14px; height: 14px; border-radius: 50%; background: currentColor; }
366
+ [data-wf-param="nhan"] a[data-value="cham"] { color: #4f46e5; } [data-wf-param="nhan"] a[data-value="ngoc"] { color: #0d9488; }
367
+ [data-wf-param="nhan"] a[data-value="cam"] { color: #ea580c; }
368
+ .wf-menu { flex-shrink: 0; }
369
+ .wf-menu summary { display: flex; align-items: center; gap: 6px; height: 36px; padding: 0 10px; border-radius: 8px;
370
+ cursor: pointer; list-style: none; }
371
+ .wf-menu summary::-webkit-details-marker { display: none; }
372
+ .wf-menu summary:hover, .wf-menu[open] summary { background: #f4f4f5; }
373
+ .wf-menu b { color: #171717; font-weight: 500; }
374
+ .wf-popover { position: fixed; z-index: 60; display: grid; min-width: 168px; padding: 4px; background: #fff;
375
+ border: 1px solid #e5e5e5; border-radius: 10px; box-shadow: 0 8px 24px rgb(0 0 0 / .08); }
376
+ .wf-popover a { display: flex; align-items: center; height: 34px; padding: 0 10px; border-radius: 6px; color: #404040; text-decoration: none; }
377
+ .wf-popover a:hover, .wf-popover a[aria-current="page"] { background: #f4f4f5; color: #171717; }
378
+ .wf-popover a[aria-current="page"] { font-weight: 500; }
379
+ .wf-reason { border-bottom: 1px solid #e5e5e5; background: #fafafa; color: #404040; font: 13px/1.5 system-ui, -apple-system, sans-serif; }
380
+ .wf-reason summary { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 8px; min-height: 44px; padding: 8px 16px;
381
+ cursor: pointer; list-style: none; }
382
+ .wf-reason summary::-webkit-details-marker { display: none; }
383
+ .wf-reason-name { min-width: 0; overflow: hidden; color: #171717; font-weight: 600; text-overflow: ellipsis; white-space: nowrap; }
384
+ .wf-reason-tag { flex-shrink: 0; height: 20px; padding: 0 8px; border-radius: 10px; background: #eef2ff; color: #4338ca;
385
+ font-size: 12px; font-weight: 500; line-height: 20px; }
386
+ .wf-reason-why { flex: 1; min-width: 0; overflow: hidden; color: #525252; text-overflow: ellipsis; white-space: nowrap; }
387
+ .wf-reason[open] .wf-reason-why { white-space: normal; }
388
+ .wf-reason-toggle { display: inline-flex; flex-shrink: 0; align-items: center; gap: 4px; height: 28px; margin-left: auto;
389
+ padding: 0 8px; border-radius: 6px; color: #171717; font-weight: 500; }
390
+ .wf-reason summary:hover .wf-reason-toggle { background: #f0f0f0; }
391
+ .wf-reason-toggle svg { width: 16px; height: 16px; transition: rotate .15s; }
392
+ .wf-reason[open] .wf-reason-toggle svg { rotate: 180deg; }
393
+ /* minmax(0, 1fr) và min(240px, 100%): thiếu thì ở 375 lưới cột tính theo max-width, tràn ngang. */
394
+ .wf-reason-body { display: grid; grid-template-columns: minmax(0, 1fr); gap: 16px; padding: 4px 16px 16px; }
395
+ .wf-reason-cols { display: grid; max-width: 1120px; grid-template-columns: repeat(auto-fit, minmax(min(240px, 100%), 1fr)); gap: 16px 32px; }
396
+ .wf-reason h3 { display: flex; align-items: center; gap: 6px; margin: 0 0 4px; color: #171717; font-size: 12px; font-weight: 600; }
397
+ .wf-reason h3 i { width: 6px; height: 6px; border-radius: 50%; background: #a3a3a3; }
398
+ .wf-reason h3 i[data-tone="uu"] { background: #16a34a; }
399
+ .wf-reason h3 i[data-tone="nhuoc"] { background: #d97706; }
400
+ .wf-reason ul { display: grid; gap: 2px; margin: 0; padding-left: 16px; }
401
+ .wf-reason li::marker { color: #a3a3a3; }
402
+ .wf-reason p { margin: 0; }
403
+ .wf-reason-tips { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; padding-top: 12px; border-top: 1px solid #ebebeb; }
404
+ .wf-reason-tips h3 { margin: 0 4px 0 0; }
405
+ .wf-reason-tips button { display: inline-flex; align-items: center; gap: 6px; height: 28px; padding: 0 10px; border: 1px solid #e5e5e5;
406
+ border-radius: 8px; background: #fff; color: #262626; font: inherit; white-space: nowrap; cursor: pointer; }
407
+ .wf-reason-tips button:hover { border-color: #d4d4d4; background: #f5f5f5; }
408
+ .wf-reason-tips button svg { width: 14px; height: 14px; color: #737373; }
409
+ .wf-reason-tips button [data-icon="da-chep"], .wf-reason-tips button[data-copied] [data-icon="chep"] { display: none; }
410
+ .wf-reason-tips button[data-copied] [data-icon="da-chep"] { display: block; color: #16a34a; }
411
+ .wf-reason :focus-visible { outline: 2px solid #4f46e5; outline-offset: 2px; }
412
+ @media (max-width: 767px) {
413
+ .wf-reason-why { order: 1; flex-basis: 100%; }
414
+ /* Chỉ còn mũi tên để tên và nhãn giữ một dòng; chữ vẫn còn cho trình đọc màn hình. */
415
+ .wf-reason-toggle span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
416
+ .wf-reason-tips h3 { flex-basis: 100%; }
417
+ }
418
+ /* Trong layer: rule ngoài layer thắng mọi utility Tailwind, `sticky` của sidebar thành `relative`, sidebar
419
+ trôi khi cuộn. Không Tailwind thì class `sticky` của dự án vẫn thắng rule trong layer. */
420
+ @layer base { [data-wf-block] { position: relative; } }
421
+ [data-wf-block]::before { content: attr(data-wf-block); position: absolute; top: 4px; left: 4px; z-index: 5;
422
+ display: grid; place-items: center; width: 18px; height: 18px; border-radius: 9px; background: #1f1f1f; color: #fff; font-size: 11px; }
423
+ [data-wf-block][data-wf-block-out]::before { top: auto; bottom: 100%; } /* số đè chữ thì lên trên mép khối */
424
+ body[data-frame] .wf-bar, body[data-frame] .wf-reason { display: none; }
425
+ body:not([data-kho="mobile"]) [data-wf-param="nav"] { display: none; }
426
+ /* Màu nhấn đổi trên :root, cùng chỗ khai token: var(--primary) khai ở :root đã tính xong tại :root, đổi ở
427
+ body thì viền focus, ring, hover vẫn màu cũ. */
428
+ :root[data-mau="xam"] { --primary: #2c2c2c; } /* nấc Xám: bỏ màu nhấn, cả màu trạng thái */
429
+ :root:not([data-mau="xam"])[data-nhan="cham"] { --primary: #4f46e5; } /* …ngoc, cam tương tự */
430
+ /* Token suy từ màu nhấn, tính lại theo nấc (brand-tokens.md). Chỉ ở nấc Xám hoặc khi có nhóm Nhấn: dự án đã có
431
+ brand thì ở nấc Màu giữ nguyên token của dự án. */
432
+ :root[data-mau="xam"], :root:has([data-wf-param="nhan"]) {
433
+ --primary-hover: color-mix(in oklab, var(--primary) 85%, var(--background)); /* lệch về phía nền */
434
+ --primary-light: color-mix(in srgb, var(--primary) 5%, transparent);
435
+ --ring-focus: color-mix(in srgb, var(--primary) 10%, transparent);
436
+ --border-focus: var(--primary);
437
+ --chart-fill: var(--primary);
438
+ }
439
+ .wf-drawer { position: fixed; inset: 0 auto 0 0; width: 280px; translate: -100% 0; transition: translate .35s; }
440
+ body[data-menu-open] .wf-drawer { translate: 0 0; }
441
+ </style>
442
+ <script>
443
+ const params = new URLSearchParams(location.search);
444
+ // Mặc định: phương án khuyên dùng, Xám, màu nhấn gợi ý đầu, Desktop, ☰, Có dữ liệu.
445
+ const state = { v: "a", mau: "xam", nhan: "cham", kho: "desktop", nav: "menu", tt: "du-lieu" };
446
+ for (const key of Object.keys(state)) state[key] = params.get(key) || state[key];
447
+ Object.assign(document.body.dataset, state);
448
+ Object.assign(document.documentElement.dataset, { mau: state.mau, nhan: state.nhan }); // màu nhấn đổi trên :root
449
+ if (params.has("frame")) document.body.dataset.frame = "1";
450
+ for (const set of document.querySelectorAll("[data-wf-param]")) {
451
+ for (const link of set.querySelectorAll("a")) {
452
+ link.href = `?${new URLSearchParams({ ...state, [set.dataset.wfParam]: link.dataset.value })}`;
453
+ if (state[set.dataset.wfParam] !== link.dataset.value) continue;
454
+ link.setAttribute("aria-current", "page");
455
+ const currentLabel = document.querySelector(`[data-wf-current="${set.dataset.wfParam}"]`);
456
+ if (currentLabel) currentLabel.textContent = link.textContent;
457
+ }
458
+ }
459
+ // Công tắc Màu: một link, bấm là sang nấc kia.
460
+ for (const toggle of document.querySelectorAll("[data-wf-toggle]")) {
461
+ const param = toggle.dataset.wfToggle;
462
+ const isOn = state[param] === toggle.dataset.on;
463
+ toggle.setAttribute("aria-checked", String(isOn));
464
+ toggle.href = `?${new URLSearchParams({ ...state, [param]: isOn ? toggle.dataset.off : toggle.dataset.on })}`;
465
+ }
466
+ // Menu Trạng thái: thanh cuộn ngang cắt mất khối absolute, nên menu là fixed, đặt ngay dưới nút.
467
+ const statusMenu = document.querySelector(".wf-menu");
468
+ statusMenu?.addEventListener("toggle", () => {
469
+ const summaryRect = statusMenu.querySelector("summary").getBoundingClientRect();
470
+ Object.assign(statusMenu.querySelector(".wf-popover").style, { top: `${summaryRect.bottom + 6}px`, left: `${summaryRect.left}px` });
471
+ });
472
+ document.addEventListener("click", (event) => { if (statusMenu && !statusMenu.contains(event.target)) statusMenu.open = false; });
473
+ if (state.kho === "mobile" && !params.has("frame")) {
474
+ const frameSource = `?${new URLSearchParams({ ...state, kho: "desktop", frame: "1" })}`;
475
+ document.getElementById("wf-design").innerHTML =
476
+ `<div style="display:grid;place-items:center;padding:24px"><iframe src="${frameSource}" title="Mobile" style="width:375px;height:812px;border:1px solid #ddd;border-radius:24px;background:#fff"></iframe></div>`;
477
+ }
478
+ // ☰ trong khung mobile mở panel trượt; bấm lớp phủ hay Esc thì đóng.
479
+ for (const toggle of document.querySelectorAll("[data-wf-menu]")) {
480
+ toggle.addEventListener("click", () => document.body.toggleAttribute("data-menu-open"));
481
+ }
482
+ document.addEventListener("keydown", (event) => {
483
+ if (event.key !== "Escape") return;
484
+ document.body.removeAttribute("data-menu-open");
485
+ if (statusMenu) statusMenu.open = false;
486
+ });
487
+ for (const button of document.querySelectorAll("[data-copy]")) {
488
+ button.addEventListener("click", async () => {
489
+ await navigator.clipboard.writeText(button.dataset.copy);
490
+ button.setAttribute("data-copied", "");
491
+ setTimeout(() => button.removeAttribute("data-copied"), 1500);
492
+ });
493
+ }
494
+ // Số khối đè chữ hay icon thì lên trên mép khối. Gọi sau mỗi lần vẽ lại, sau hai khung hình: Tailwind bản
495
+ // trình duyệt dựng CSS sau khi HTML vào trang, đo ngay thì mọi khối chưa có layout, số nào cũng "đè".
496
+ function placeBlockNumbers() {
497
+ const isOverlap = (first, second) => first.left < second.right && first.right > second.left && first.top < second.bottom && first.bottom > second.top;
498
+ for (const block of document.querySelectorAll("[data-wf-block]")) {
499
+ const blockRect = block.getBoundingClientRect();
500
+ const numberRect = { left: blockRect.left + 4, top: blockRect.top + 4, right: blockRect.left + 22, bottom: blockRect.top + 22 };
501
+ const contentRects = [...block.querySelectorAll("svg, img")].map((node) => node.getBoundingClientRect());
502
+ const walker = document.createTreeWalker(block, NodeFilter.SHOW_TEXT);
503
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
504
+ if (!node.textContent.trim()) continue;
505
+ const range = document.createRange();
506
+ range.selectNodeContents(node);
507
+ contentRects.push(...range.getClientRects());
508
+ }
509
+ block.toggleAttribute("data-wf-block-out", contentRects.some((rect) => rect.width > 0 && isOverlap(rect, numberRect)));
510
+ }
511
+ }
512
+ requestAnimationFrame(() => requestAnimationFrame(placeBlockNumbers));
513
+ addEventListener("resize", placeBlockNumbers);
514
+ </script>
515
+ ```
516
+
517
+ Dự án đã có màu brand thì bỏ nhóm Nhấn; đã có logo thì bỏ nhóm Logo; app từ 6 mục chính thì bỏ nhóm Nav. Thanh dưới vẽ
518
+ sẵn trong trang, chỉ hiện khi `body[data-kho="mobile"][data-nav="duoi"]` (trong khung là
519
+ `frame=1` kèm `nav=duoi`).
520
+
521
+ - **Gửi link bấm được cho từng phương án**, không chỉ đường dẫn ảnh. Chạy một server tĩnh nền
522
+ trên thư mục wireframe (`python3 -m http.server <cổng> -d "$TMPDIR/evon-design"`, chạy nền),
523
+ rồi liệt kê mỗi phương án một dòng dạng link đầy đủ, người dùng bấm hoặc chép vào trình
524
+ duyệt được ngay:
525
+
526
+ ```
527
+ - A · Lưới card + hàng lọc gọn (khuyên dùng): http://localhost:<cổng>/wireframe.html?v=a
528
+ - B · Danh sách + bản đồ: http://localhost:<cổng>/wireframe.html?v=b
529
+ - D · A gọn chữ: http://localhost:<cổng>/wireframe.html?v=d
530
+ ```
531
+
532
+ Kèm một dòng: *"Mỗi trang có thanh trên cùng: bật Màu, xem Mobile, xem Rỗng / Lỗi, và khung lý
533
+ do có sẵn câu góp ý để chép."*
534
+
535
+ Mở thử từng link (probe đã mở là được) trước khi gửi. Không chạy được server thì ghi đường
536
+ dẫn tệp `file://…/wireframe.html` và nói tham số `?v=` chọn phương án.
537
+
538
+ Kết bằng *"Chọn A, B hay C, kèm D, E nếu muốn (ví dụ `C + D`, `B + E`). Góp ý theo số khối
539
+ cũng được."* Bản dựng theo nấc Màu. Dừng chờ.
540
+
541
+ Người dùng trả lời `ok`, `dựng luôn` mà không ghi chữ cái nào thì dựng **phương án khuyên dùng**,
542
+ màu và nhấn theo mức đã khuyên, không hỏi lại.
543
+
544
+ ## U4. Dựng thật ⚑
545
+
546
+ - **Dự án đã có UI:** giữ brand theo bảng vai màu (`review.md`, chế độ dựng lại giữ brand),
547
+ dáng theo gu skill: đi hết bảng **"Dáng lấy từ skill, không từ CSS cũ"** trong `review.md`
548
+ (dropdown, checkbox, viền, scrollbar, nút header…), lúc giao có dòng `Dáng:`. Giữ brand
549
+ chỉ là giữ màu theo vai, logo, font. Khung trang theo phương án đã chọn. Logic, handler, dữ liệu không đụng;
550
+ thứ cần dữ liệu mới thì để prop và handler rỗng, lúc giao liệt kê.
551
+ - **Sản phẩm mới:** audit câu 2 đã chạy ở `U1`; đi tiếp câu 3 của mục 0 trong `SKILL.md`,
552
+ rồi dựng theo phương án đã chọn thay cho bố cục mặc định của câu 4. Chưa có logo thì dựng
553
+ `ProductBrand` với dấu đã chọn ở nhóm Logo, kèm favicon từ cùng dấu (`components/logo.md`).
554
+ - **Đề nhiều hơn một màn:** chốt hợp đồng nguyên tố `D1` (`system.md`) ở đây, trước khi dựng
555
+ màn đầu tiên. Wireframe đã chọn nói khung, bảng `D1` nói control nào dùng kiểu nào cho cả bộ.
556
+ - Ráp bằng mẫu của skill (`SKILL.md` mục 2). Chạy probe `--sweep --wireframe "<link phương án
557
+ đã chọn>&mau=mau"` tới khi danh sách `P` trống, tối đa ba vòng. **Mục probe về dáng cũng sửa**, dù không nằm trong `P`: hàng nút
558
+ header không đồng cỡ, vòng focus, control gốc, lớp nổi không chuyển động, viền trang trí
559
+ đậm, thanh cuộn. Ở `U4` dáng là của skill, nên đó không phải "Lệch hệ để tuỳ" như lúc soi.
560
+ **Danh sách `P` tính cả khung app trên route đó** (header, sidebar,
561
+ thanh dưới, menu thông báo): người dùng nhìn cả màn, không chỉ phần mới dựng. Khung app lỗi
562
+ thì sửa luôn, sửa ở component dùng chung và nói nó đổi cả các màn khác. Trang
563
+ dựng lại đúng bố cục mà header vẫn bị bóp thì người xem vẫn chấm "xấu".
564
+ - **Wireframe vẽ khung app (header, sidebar) thì khung app cũng là phương án**: dựng lại
565
+ component dùng chung theo wireframe (số mục, mục nào nút đặc, mục nào chỉ icon), dáng theo
566
+ gu, màu theo vai màu. Không để nguyên header cũ rồi chỉ vá cho khỏi rớt dòng.
567
+ - **Dựng đúng wireframe đã chọn, không bịa.** Wireframe ở nấc Màu là bản đặc tả, đã dùng token
568
+ và component của bản dựng (`U3`): bản dựng là nó viết lại bằng code dự án. **Không thêm** mục, dòng chữ, badge, nút, khối mà wireframe không có;
569
+ **không bỏ** thứ wireframe có; không đổi thứ tự. Thấy wireframe thiếu gì thì hỏi hoặc ghi
570
+ một dòng lúc giao, không tự chêm vào.
571
+ - **Khoảng cách, cỡ, màu, chữ chép nguyên từ wireframe, tới từng px** ⚑. Padding của khung trang
572
+ và từng khối, `gap` giữa các khối và trong hàng, chiều cao header, ô tìm, nút, chip, bề rộng
573
+ sidebar, cỡ ảnh card, cỡ, độ đậm, dòng cao của chữ, cỡ icon; màu chữ, nền, viền theo token
574
+ wireframe đã dùng. Chữ cũng chép nguyên: tiêu đề, placeholder, câu đếm kết quả, dòng cuối danh
575
+ sách, nhãn nút. Cách làm: mở file wireframe, **với từng khối chép chuỗi class sang bản dựng**
576
+ (wireframe đã viết bằng class và token của dự án, `U3`), không viết lại theo trí nhớ hay theo
577
+ thói quen của dự án.
578
+ - **Khung trang có sẵn của dự án (container, layout, `PageShell`) padding khác wireframe thì
579
+ wireframe thắng**: đổi padding ở khung đó (dùng chung thì nói nó đổi các màn khác, như khung
580
+ app ở dòng trên), không để khung cũ đẩy cả trang lệch. Đây là chỗ lệch hay gặp nhất.
581
+ - Giá trị wireframe không có trong thang spacing của dự án thì dùng đúng px đó (`pt-[18px]`),
582
+ không làm tròn sang bậc gần nhất: làm tròn là lệch vài px ở mỗi khối, cộng dồn xuống cuối trang.
583
+ - **Chạy probe kèm `--wireframe "<link phương án đã chọn>&mau=mau"`** (link `U3`, server đã tắt
584
+ thì `file://$TMPDIR/evon-design/wireframe.html?v=<chữ cái>&mau=mau`). Probe mở cả hai ở 1440 và
585
+ 375, neo theo chữ, placeholder, icon, báo **đúng khoảng lệch** (*"«Phòng trọ» → ô tìm: bản dựng
586
+ 33px, wireframe 16px"*), cỡ chữ, độ đậm, cỡ icon khác, màu chữ, màu icon, nền khác (gom theo
587
+ cặp màu, một token sai là một dòng), chữ và icon thiếu hay thêm. Thiếu `mau=mau` thì probe
588
+ không so màu, vì nấc Xám cố ý tắt màu nhấn. Các mục đó vào danh sách
589
+ `P`, sửa tới khi trống như mọi mục `P`. Chỉ được lệch khi người dùng dặn hoặc dữ liệu thật khác
590
+ wireframe (chữ dài hơn nên thêm dòng); lúc giao ghi từng chỗ.
591
+
592
+ Lệch vài px mỗi khối, đổi chữ đếm, đổi placeholder, mất dòng cuối danh sách thì kéo thanh so
593
+ sánh wireframe với bản dựng qua lại là mọi khối nhảy.
594
+ - **Mục điều hướng trỏ tới màn ngoài đề** (sidebar có "Bệnh nhân" mà đề chỉ xin hồ sơ một người):
595
+ màn đó chưa qua `U2`, chưa có wireframe, nên không tự nghĩ bố cục. Dựng tối giản theo khuôn mặc
596
+ định của skill cho loại màn đó (danh sách thì "Danh sách có bộ lọc" trong `layouts/app.md`: ô tìm,
597
+ dòng theo `components/list-row.md` có giá trị so sánh bên phải), rồi ghi một dòng lúc giao:
598
+ *"Màn [X] ngoài đề, dựng tạm để menu không dẫn vào trang trống; muốn làm thật thì nhắn."* Đừng tự
599
+ nghĩ bố cục: ra cột tên + mã rộng 1500px, nửa phải trống, không ô tìm, không giá trị so sánh.
600
+ - **Đối chiếu wireframe từng khối trước khi giao.** Mở ảnh wireframe đã chọn cạnh ảnh 1440
601
+ của bản dựng, đi từng khối (header, sidebar, hàng lọc, danh sách, panel): số mục, thứ tự,
602
+ mục nào nút đặc, mục nào chỉ icon, thứ gì wireframe đã bỏ. Khoảng cách và chữ thì đã có
603
+ phần so của probe `--wireframe` (trên), mắt lo phần probe không neo được: khối không chữ, ảnh. Khác chỗ nào thì sửa, hoặc ghi
604
+ một dòng vì sao lệch (thiếu dữ liệu, người dùng dặn). Màu thì theo "Mỗi vai đúng một mã
605
+ màu" trong `review.md`: wireframe xám không nói màu, nhưng bản dựng phải ăn nhập từ viền
606
+ tới brand. Tin giao có bảng *"Đối chiếu wireframe"*: khối, wireframe có gì, bản dựng có
607
+ gì, khớp hay lý do lệch. Kèm số dòng chữ mỗi mục ở hai bên (probe "mục lặp dày chữ").
608
+ - **Dựng xong chạy một lượt làm gọn** trên các khối mới **và khung app của route**: `V1b` và `V1c` trong `review.md`
609
+ (card cao thấp theo dòng có dòng không, link trông như chữ thường, nửa khối trống ở màn
610
+ rộng). Sửa luôn, không đưa bảng: người dùng đã chọn phương án rồi.
611
+ - **Thứ không được tự sửa thì nêu ra, đừng giữ im lặng** ⚑ (dự án đã có UI). Lượt làm gọn chỉ
612
+ sửa dáng và sắp lại. Các dòng `V1b` loại bỏ, ẩn, gộp thông tin, gom màu trang trí, chỗ
613
+ vai màu tranh nhau (badge nhiều màu nặng hơn giá), và mọi chỗ **nhận diện** trông xấu theo
614
+ `V1` trong `review.md` (màu vai, font, logo, khối màu đậm) là quyết định sản phẩm và nhận diện: bản
615
+ dựng giữ nguyên, nhưng tin giao có mục **"Còn thấy"**, tối đa năm dòng đánh số. Mỗi dòng
616
+ một vấn đề người dùng cuối vấp kèm hướng sửa, nói bằng thứ bậc chứ không bằng màu
617
+ (`V1b`, "Không phải lối vòng để đổi màu"): *"1. Card nào cũng có badge, năm màu đè trên
618
+ ảnh, mắt dừng ở badge trước giá. Chỉ giữ badge ở phòng khác đi (giảm giá, đã xác thực)."*
619
+ Kết bằng *"Muốn sửa dòng nào thì trả lời số, ví dụ `sửa 1, 3`."* Không thấy gì thì ghi
620
+ "Còn thấy: không". Đây không phải cổng: bản dựng đã giao xong, người dùng trả lời hay không
621
+ tuỳ họ. **Lỗi dáng mà skill đã có luật thì sửa trước khi giao, không đẩy vào "Còn thấy"**:
622
+ mục chỉ dành cho thứ bản dựng không được tự quyết (ví dụ hàng đếm 375px cắt mục cuối không mép
623
+ mờ theo `R10`, email xuống dòng ở gạch nối tên miền theo `description-list.md`: sửa, không đưa
624
+ vào "Còn thấy").
625
+ - **Tự soi bằng mắt trước khi giao, ghi ra.** Mở ảnh 375, 1440 và 1920 của probe, trả lời
626
+ từng câu thành một dòng trong tin giao (câu nào có lỗi thì sửa trước, rồi mới ghi "không"):
627
+ 1. Card, dòng cùng loại có cao thấp khác nhau vì có dòng thiếu một mẩu không?
628
+ 2. Thứ bấm được (link "Xem thêm", nút chữ) có trông như chữ thường không?
629
+ 3. Trong một màn có bao nhiêu khung viền đứng cạnh hay lồng nhau? Gộp được khung nào?
630
+ 4. Ở 1920, chỗ nào trống mà không có lý do (nửa card, hai bên nội dung)?
631
+ 5. Thứ nặng nhất màn (đậm nhất, màu nhất) có đúng là việc chính ở `U2` không?
632
+
633
+ Không ghi mấy dòng này thì coi như chưa soi. Người dùng tự phát hiện ra lỗi nằm trong năm
634
+ câu này là skill chưa làm xong việc.
635
+ - **Lúc giao** nói bằng ngôn ngữ trải nghiệm, không bằng class: việc chính giờ làm trong mấy
636
+ bước, thấy ngay ở khổ nào; ảnh trước và sau ở 1280 và 375, là link bấm được và một trang
637
+ `so-sanh.html` như `V5` trong `review.md` (cả ảnh của mục "Còn thấy"); danh sách thứ cần bạn nối logic
638
+ hay thêm dữ liệu. Cuối tin một dòng **Muốn chỉnh thì nhắn** với 3–4 câu ngắn chọn theo bản vừa
639
+ dựng, như khung lý do của `U3` (*"Thêm màu ở header"*, *"Tiêu đề đậm hơn"*, *"Thoáng hơn"*,
640
+ *"Đổi font"*). Người dùng thường chỉ thấy "chưa đã" mà không gọi được tên.
641
+
642
+ ---
643
+
644
+ ## U5. Không làm
645
+
646
+ - Không moodboard, không hi-fi mock riêng rồi dựng lại: với skill này code chính là hi-fi.
647
+ - Không tự thêm tính năng ngoài `U2` (chat, thông báo, đánh giá) cho "đủ bộ".
648
+ - Không đổi vai màu của dự án đã có, trừ khi người dùng nói bỏ style cũ (`review.md`, chế độ
649
+ dựng lại theo gu skill).
650
+ - Không quay về nhánh `V` giữa chừng để "vá cho nhanh": người dùng đã xin nghĩ lại khung.