@haposoft/cafekit 0.16.7 → 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 (193) 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 +74 -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 +17 -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 +24 -7
  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/settings/settings.json +1 -32
  68. package/src/claude/skills/{question → ask}/SKILL.md +9 -75
  69. package/src/claude/skills/brainstorm/SKILL.md +43 -131
  70. package/src/claude/skills/code-review/SKILL.md +48 -17
  71. package/src/claude/skills/code-review/references/spec-compliance-review.md +2 -3
  72. package/src/claude/skills/code-review/references/verification-gate.md +22 -27
  73. package/src/claude/skills/debug/SKILL.md +29 -149
  74. package/src/claude/skills/develop/SKILL.md +27 -36
  75. package/src/claude/skills/develop/references/parallel-waves.md +0 -8
  76. package/src/claude/skills/develop/references/quality-gate.md +15 -13
  77. package/src/claude/skills/docs/SKILL.md +2 -2
  78. package/src/claude/skills/docs/references/update-workflow.md +1 -1
  79. package/src/claude/skills/fix/SKILL.md +153 -0
  80. package/src/claude/skills/{hotfix → fix}/references/diagnosis-protocol.md +2 -2
  81. package/src/claude/skills/{hotfix → fix}/references/parallel-patterns.md +2 -2
  82. package/src/claude/skills/{hotfix → fix}/references/prevention-gate.md +3 -3
  83. package/src/claude/skills/{hotfix → fix}/references/review-cycle.md +1 -1
  84. package/src/claude/skills/git/SKILL.md +48 -9
  85. package/src/claude/skills/git/references/commit-protocols.md +35 -16
  86. package/src/claude/skills/git/references/worktree-blueprint.md +42 -6
  87. package/src/claude/skills/orca/SKILL.md +9 -8
  88. package/src/claude/skills/research/SKILL.md +2 -0
  89. package/src/claude/skills/scout/SKILL.md +61 -0
  90. package/src/claude/skills/{inspect → scout}/references/internal-inspection.md +26 -10
  91. package/src/claude/skills/specs/SKILL.md +22 -29
  92. package/src/claude/skills/specs/references/review.md +8 -6
  93. package/src/claude/skills/specs/references/templates.md +34 -41
  94. package/src/claude/skills/sync/SKILL.md +8 -15
  95. package/src/claude/skills/sync/references/rebind-and-audit.md +59 -0
  96. package/src/claude/skills/sync/references/sync-protocols.md +4 -13
  97. package/src/claude/skills/test/SKILL.md +22 -3
  98. package/src/claude/skills/test/references/execution-strategy.md +83 -0
  99. package/src/claude/skills/test/references/failure-triage.md +1 -1
  100. package/src/claude/skills/ui-ux/LICENSE +21 -0
  101. package/src/claude/skills/ui-ux/SKILL.md +497 -0
  102. package/src/claude/skills/ui-ux/references/brand-tokens.md +90 -0
  103. package/src/claude/skills/ui-ux/references/budgets.md +113 -0
  104. package/src/claude/skills/ui-ux/references/checklist.md +271 -0
  105. package/src/claude/skills/ui-ux/references/components/accordion.md +246 -0
  106. package/src/claude/skills/ui-ux/references/components/avatar.md +153 -0
  107. package/src/claude/skills/ui-ux/references/components/banner.md +43 -0
  108. package/src/claude/skills/ui-ux/references/components/breadcrumb.md +201 -0
  109. package/src/claude/skills/ui-ux/references/components/button.md +142 -0
  110. package/src/claude/skills/ui-ux/references/components/card.md +113 -0
  111. package/src/claude/skills/ui-ux/references/components/charts.md +376 -0
  112. package/src/claude/skills/ui-ux/references/components/chat.md +132 -0
  113. package/src/claude/skills/ui-ux/references/components/choice-controls.md +369 -0
  114. package/src/claude/skills/ui-ux/references/components/comment-thread.md +17 -0
  115. package/src/claude/skills/ui-ux/references/components/description-list.md +116 -0
  116. package/src/claude/skills/ui-ux/references/components/empty-state.md +98 -0
  117. package/src/claude/skills/ui-ux/references/components/file-upload.md +107 -0
  118. package/src/claude/skills/ui-ux/references/components/inline-edit.md +128 -0
  119. package/src/claude/skills/ui-ux/references/components/input.md +161 -0
  120. package/src/claude/skills/ui-ux/references/components/list-row.md +56 -0
  121. package/src/claude/skills/ui-ux/references/components/loading.md +198 -0
  122. package/src/claude/skills/ui-ux/references/components/logo.md +147 -0
  123. package/src/claude/skills/ui-ux/references/components/otp-input.md +43 -0
  124. package/src/claude/skills/ui-ux/references/components/quantity-input.md +100 -0
  125. package/src/claude/skills/ui-ux/references/components/range-slider.md +16 -0
  126. package/src/claude/skills/ui-ux/references/components/small-controls.md +340 -0
  127. package/src/claude/skills/ui-ux/references/components/sortable-header.md +86 -0
  128. package/src/claude/skills/ui-ux/references/components/tag-input.md +14 -0
  129. package/src/claude/skills/ui-ux/references/components/timeline.md +38 -0
  130. package/src/claude/skills/ui-ux/references/components/tree.md +30 -0
  131. package/src/claude/skills/ui-ux/references/design-process.md +650 -0
  132. package/src/claude/skills/ui-ux/references/layouts/app-kanban.html +303 -0
  133. package/src/claude/skills/ui-ux/references/layouts/app.md +1369 -0
  134. package/src/claude/skills/ui-ux/references/layouts/form.md +430 -0
  135. package/src/claude/skills/ui-ux/references/layouts/overlay.md +552 -0
  136. package/src/claude/skills/ui-ux/references/layouts/pricing.md +204 -0
  137. package/src/claude/skills/ui-ux/references/locked-rules.md +35 -0
  138. package/src/claude/skills/ui-ux/references/principles.md +408 -0
  139. package/src/claude/skills/ui-ux/references/refactor.md +245 -0
  140. package/src/claude/skills/ui-ux/references/responsive.md +195 -0
  141. package/src/claude/skills/ui-ux/references/review.md +528 -0
  142. package/src/claude/skills/ui-ux/references/rules-color.md +623 -0
  143. package/src/claude/skills/ui-ux/references/rules-form.md +235 -0
  144. package/src/claude/skills/ui-ux/references/rules-state.md +680 -0
  145. package/src/claude/skills/ui-ux/references/rules-type.md +369 -0
  146. package/src/claude/skills/ui-ux/references/styles.md +311 -0
  147. package/src/claude/skills/ui-ux/references/system.md +219 -0
  148. package/src/claude/skills/ui-ux/references/tailwind-v4-traps.md +237 -0
  149. package/src/claude/skills/ui-ux/references/tokens.css +362 -0
  150. package/src/claude/skills/ui-ux/scripts/lint-skill.mjs +216 -0
  151. package/src/claude/skills/ui-ux/scripts/package.json +9 -0
  152. package/src/claude/skills/ui-ux/scripts/probe.mjs +4447 -0
  153. package/src/claude/skills/ui-ux-pro-max/SKILL.md +1 -1
  154. package/src/claude/status.cjs +149 -144
  155. package/src/codex/AGENTS.md +8 -13
  156. package/src/codex/hooks/agent.cjs +2 -1
  157. package/src/codex/hooks/lib/spec-receipt.cjs +2 -2
  158. package/src/codex/hooks/lib/spec-utils.cjs +0 -7
  159. package/src/codex/hooks/rules.cjs +2 -1
  160. package/src/codex/hooks/session.cjs +0 -2
  161. package/src/codex/hooks/spec-gate.cjs +7 -18
  162. package/src/codex/hooks.json +0 -20
  163. package/src/codex/rules/state-sync.md +3 -12
  164. package/src/common/AGENTS.md +2 -2
  165. package/src/omp/extensions/cafekit-bridge.mjs +6 -6
  166. package/src/claude/hooks/__tests__/completion-authority.test.js +0 -990
  167. package/src/claude/hooks/__tests__/semantic-review-authority.test.js +0 -259
  168. package/src/claude/hooks/completion-authority-check.cjs +0 -56
  169. package/src/claude/hooks/completion-authority-state.cjs +0 -527
  170. package/src/claude/hooks/completion-authority.cjs +0 -163
  171. package/src/claude/hooks/semantic-review-authority.cjs +0 -154
  172. package/src/claude/hooks/task-scaffold-guard.cjs +0 -114
  173. package/src/claude/hooks/usage.cjs +0 -197
  174. package/src/claude/skills/delegate/SKILL.md +0 -124
  175. package/src/claude/skills/delegate/references/codex-delegation.md +0 -71
  176. package/src/claude/skills/delegate/references/grok-delegation.md +0 -84
  177. package/src/claude/skills/hotfix/SKILL.md +0 -289
  178. package/src/claude/skills/inspect/SKILL.md +0 -142
  179. package/src/claude/skills/loop/SKILL.md +0 -114
  180. package/src/claude/skills/loop/references/bounded-loop-protocol.md +0 -104
  181. package/src/claude/skills/loop/references/metric-and-guard-contract.md +0 -76
  182. package/src/claude/skills/route/SKILL.md +0 -94
  183. package/src/claude/skills/route/references/agent-timing.md +0 -46
  184. package/src/claude/skills/route/references/chaining-patterns.md +0 -53
  185. package/src/claude/skills/route/references/task-taxonomy.md +0 -48
  186. package/src/codex/hooks/completion-authority-check.cjs +0 -67
  187. package/src/codex/hooks/completion-authority-state.cjs +0 -527
  188. package/src/codex/hooks/completion-authority.cjs +0 -109
  189. package/src/codex/hooks/semantic-review-authority.cjs +0 -130
  190. package/src/codex/hooks/task-scaffold-guard.cjs +0 -63
  191. /package/src/claude/skills/{question → ask}/templates/question.md +0 -0
  192. /package/src/claude/skills/{hotfix → fix}/references/escalation-tactics.md +0 -0
  193. /package/src/claude/skills/{hotfix → fix}/references/workflow-specialized.md +0 -0
@@ -0,0 +1,376 @@
1
+ # Biểu đồ và số liệu
2
+
3
+ Biểu đồ là chỗ luật một màu nhấn hay bị phá nhất. Bốn ô số liệu bốn màu, cột
4
+ xanh cột đỏ, biểu đồ tròn bảy múi bảy sắc. Nhìn thì tưởng nhiều thông tin, thật
5
+ ra là không quyết định được cái nào quan trọng.
6
+
7
+ ---
8
+
9
+ ## Màu
10
+
11
+ **Một chuỗi dữ liệu là một màu.** Tám cột của cùng một chỉ số thì cả tám cùng
12
+ `--chart-fill` (bản sáng chính là `--primary`, bản tối hạ còn 70%, xem "Nền tối" dưới). Cột cao thấp đã nói lên khác biệt rồi, không cần màu nói lại.
13
+
14
+ **Hai tới bốn chuỗi thì phân biệt bằng đậm nhạt, không bằng sắc** (dự án chưa có thang màu biểu đồ). Đổi hue là bắt
15
+ người đọc học một bảng chú giải màu mà họ không xin. Một thang cho cả app, biểu
16
+ đồ cột nhóm và phần donut dùng chung (`N5`):
17
+
18
+ | Số chuỗi / phần | Bậc |
19
+ | --- | --- |
20
+ | 2 | `bg-chart-fill` · `bg-chart-fill/45` |
21
+ | 3 | `bg-chart-fill` · `bg-chart-fill/45` · `bg-chart-fill/15` |
22
+ | 4 | `bg-chart-fill` · `bg-chart-fill/65` · `bg-chart-fill/35` · `bg-chart-fill/15` |
23
+
24
+ Không dùng `--muted` làm một bậc: nó gần bằng `primary/60`, hai chuỗi trông như
25
+ một. Bậc `/15` và `/35` dưới 3:1 trên nền
26
+ trắng, nên **mỗi cột, mỗi phần phải có con số đi kèm** (số trên đầu cột, số
27
+ trong bảng chú giải donut); màu nhạt không bao giờ là thứ duy nhất mang giá trị
28
+ (`N4`). Chấm chú giải dùng đúng class của cột.
29
+
30
+ **Phân loại từ 5 nhóm trở lên thì mỗi nhóm một sắc.** Ngành, kênh, quốc gia, loại
31
+ khách: năm bậc đậm nhạt của một màu thì bậc 4 và bậc 5 mắt không tách được nữa, chấm
32
+ màu trong bảng không còn chỉ về đúng thanh nào. Các app phân tích phổ biến đều tô mỗi
33
+ nhóm một sắc ở đây (thanh chia phần, donut, cột chồng). Cách làm:
34
+
35
+ - **Dự án có thang màu biểu đồ** (`--chart-1`…`--chart-5` của shadcn, hay thang riêng) thì
36
+ dùng đúng thang đó theo thứ tự. Dự án có ngôn ngữ màu riêng (`P4`) thì thang theo dự
37
+ án kể cả khi dưới 5 nhóm.
38
+ - **Chưa có** thì dùng thang mặc định, xếp theo thứ tự nhóm lớn tới nhỏ: `blue-500` ·
39
+ `sky-400` · `violet-500` · `fuchsia-400` · `teal-500` · `indigo-300`. Tránh sắc trùng
40
+ bảng trạng thái (`red`, `rose`, `amber`, `emerald`) khi cùng màn có badge trạng thái,
41
+ để một nhóm không bị đọc thành "lỗi" hay "xong".
42
+ - **Tối đa 6 sắc**, nhóm thứ 7 trở đi gộp thành "Khác" màu `slate-300`. Bảy tám sắc
43
+ thì lại thành cầu vồng không ai nhớ.
44
+ - **Một nhóm một màu cố định trong cả app** (như `M8`): "Media" tím ở thanh chia phần
45
+ thì tím ở donut, ở chấm trong bảng.
46
+ - **Chấm màu trong bảng dùng đúng màu của thanh**, và mỗi nhóm luôn có số và phần trăm
47
+ bằng chữ: màu chỉ giúp nối bảng với biểu đồ, không mang giá trị (`N4`).
48
+
49
+ **Màu chỉ được đổi sắc khi nó mang nghĩa trạng thái** (ngoài ca phân loại từ 5 nhóm ở trên), và phải trùng đúng bảng
50
+ trạng thái của app: đỏ là hỏng hoặc thất bại, amber là quá hạn hoặc cần chú ý,
51
+ emerald là xong. Cột "quá hạn" tô amber thì được (`M7`, `list-row.md`). Cột "tháng 3" tô đỏ thì không.
52
+
53
+ **Ô số liệu thì con số để nguyên màu chữ chính.** Đừng tô "Đang làm" xanh,
54
+ "Quá hạn" đỏ, "Hoàn thành" xanh lá. Bốn màu trong một hàng là dấu hiệu chưa
55
+ chọn được cái nào đáng nhìn nhất. Cần nhấn một ô thì nhấn bằng vị trí hoặc bằng
56
+ dòng phụ, không bằng màu.
57
+
58
+ **Nền tối** (`M32`). **Mảng dữ liệu là `--chart-fill`, không `--primary` trần.** Nền tối màu
59
+ nhấn gần trắng, cột 100% là khối chói nhất màn, nặng hơn cả tên trang (chủ dự án chọn 70%;
60
+ đừng dùng xám ánh xanh: nhạt quá, lẫn với cột kỳ đang chạy). Khối `.dark` của `tokens.css` đã hạ token này
61
+ còn 70%, thang `/45`, `/15` nhân theo nên giữ đúng tỉ lệ. Thanh mảnh (thanh tiến độ, `h-2`)
62
+ vẫn `bg-primary`: diện tích nhỏ, không chói. Lưới `stroke-border` và nhãn `text-muted` tự đổi
63
+ theo theme. Phải khai riêng:
64
+
65
+ - **Thang phân loại** viết bằng class sắc thì thêm bản tối, sáng lên một bậc:
66
+ `blue-500 dark:blue-400` · `sky-400` giữ · `violet-500 dark:violet-400` · `fuchsia-400` giữ ·
67
+ `teal-500 dark:teal-400` · `indigo-300` giữ. Nhóm "Khác" `slate-300` thành **`dark:slate-600`**:
68
+ để nguyên thì xám sáng thành mảng nổi nhất biểu đồ tối.
69
+ - **Thư viện vẽ bằng mã màu** (Recharts `fill="#…"`, Chart.js) thì truyền `var(--…)` hoặc đọc
70
+ token lúc vẽ, không viết hex: hex cứng không lật theo theme. shadcn chart: `ChartConfig` nhận
71
+ `theme: { light, dark }`, hoặc `--chart-1…5` có bản `.dark`.
72
+
73
+ ---
74
+
75
+ ## Bỏ bớt đi
76
+
77
+ - **Không lưới, không trục Y** ở biểu đồ trong card dashboard, panel, ô số liệu. Ghi thẳng con số lên đầu cột hoặc cuối dòng. Người ta muốn biết giá trị, không muốn dóng mắt sang trục.
78
+ **Ngoại lệ: biểu đồ chính của trang báo cáo** (biểu đồ là thứ người ta vào trang để đọc, rộng hết khung, từ ~15 mốc). Ở đó người ta so ngày này với ngày kia ("13/09 khoảng 70 tr, 22/09 khoảng 40 tr") mà không muốn rê 30 lần. Thêm **3–4 đường lưới ngang** mảnh (`stroke-border`, 1px, không nét đứt) ở các mức số tròn (0, 20 tr, 40 tr, 60 tr), nhãn mức `text-xs text-muted tabular-nums` ở mép trái, không đường trục dọc, không lưới dọc. Bộ component dashboard phổ biến nhất bật lưới ngang và bỏ nhãn trục Y; một thư viện dashboard lớn và trang báo cáo của một nền tảng bán hàng lớn bật cả hai. Cả ba nguồn đều có lưới ngang, nên lưới ngang là số đông; nhãn mức hai trên ba, giữ vì trang báo cáo là để đọc số. Recharts: `<CartesianGrid vertical={false} stroke="var(--border)" />` + `<YAxis axisLine={false} tickLine={false} tickCount={4} width={48} tick={{ fill: "var(--muted)", fontSize: 12 }} />`.
79
+ - **Không chú giải rời** khi có thể ghi nhãn ngay cạnh dữ liệu.
80
+ - **Không biểu đồ tròn cho quá bốn phần.** Quá bốn thì mắt không so được, chuyển sang thanh ngang xếp theo thứ tự lớn dần.
81
+ - **Không hiệu ứng lúc vào trang.** Cột không mọc lên, đường không tự vẽ, số không đếm tăng dần. Xem luật `F23` trong `../rules-form.md`.
82
+ - **Không đổ bóng, không gradient, không 3D** cho cột và mảng.
83
+
84
+ ---
85
+
86
+ ## Vẽ bằng thư viện, skill lo hình
87
+
88
+ Biểu đồ đường, cột, donut ở dự án thật hầu như luôn vẽ bằng thư viện: nó lo tỉ
89
+ lệ trục, co giãn theo khung, rê chuột, bàn phím. Nên (`N10`, bảng thư viện
90
+ chuyên dụng trong `SKILL.md`):
91
+
92
+ 1. **Dự án đã có** thư viện biểu đồ (Recharts, shadcn chart, Chart.js, ECharts,
93
+ Tremor…) thì **dùng đúng cái đó**, cấu hình theo bảng dưới. Audit grep
94
+ `package.json` trước khi viết một dòng.
95
+ 2. **Chưa có thì không tự cài.** Dựng như bình thường theo các mẫu ở mục "Công
96
+ thức dùng được ngay". Hình của skill đã bỏ gần hết thứ thư viện bán (lưới,
97
+ trục Y, tooltip, chú giải, hiệu ứng), nên vài biểu đồ tĩnh trên một trang
98
+ dashboard thường **không cần thư viện nào**: `<svg>` và div là đủ, nhẹ nhất.
99
+ 3. **Chỉ đề xuất thư viện khi có nhu cầu mà tự dựng sẽ tốn**, và lúc giao nói
100
+ rõ nhu cầu đó: nhiều loại biểu đồ khắp app, dữ liệu hàng nghìn điểm, phóng to
101
+ hay kéo chọn vùng, cập nhật theo thời gian thực, trục thời gian có khoảng
102
+ trống. Không có nhu cầu nào như vậy thì không đề xuất.
103
+ 4. **Chọn thư viện theo tiêu chí, không theo tên quen**: nhẹ (xem kích thước
104
+ thật trên bundlephobia lúc đề xuất, đừng nhớ số), làm được đúng các loại
105
+ biểu đồ đang cần, tô được bằng token CSS, còn được bảo trì, chạy được với
106
+ SSR nếu dự án có. Hợp với hệ sinh thái sẵn có cũng là một tiêu chí: dự án
107
+ dùng shadcn thì shadcn chart nhất quán với phần còn lại, dù Recharts bên dưới
108
+ không phải thư viện nhẹ. Đề xuất ghi một dòng, có lý do: "Nếu sau này cần
109
+ phóng to biểu đồ 10.000 điểm, cân nhắc X vì Y".
110
+
111
+ Thanh tiến độ, thanh ngang trong danh sách thì luôn dựng bằng div, kể cả khi
112
+ có thư viện: chúng chỉ là một thanh có `width`. **Sparkline** cũng không cần thư
113
+ viện: một `<svg>` với `<polyline>`, không trục, không rê chuột, không cần đề
114
+ xuất thư viện chỉ vì nó. Dự án đã có Recharts thì dùng `<LineChart>` trơn cũng
115
+ được, nhưng bốn `ResponsiveContainer` trong một hàng là nặng hơn cần thiết.
116
+
117
+ **Skill chỉ quy định hình.** Mọi thư viện bật sẵn thứ skill cấm, phải tắt tay.
118
+ Ví dụ với Recharts / shadcn chart (vì hay gặp nhất, không phải vì được chọn):
119
+
120
+ | Luật hình | Cấu hình |
121
+ | --- | --- |
122
+ | Không lưới | bỏ `<CartesianGrid>` (biểu đồ chính trang báo cáo: lưới ngang, xem "Bỏ bớt đi") |
123
+ | Không trục Y | `<YAxis hide domain={[0, "auto"]} />` (đáy luôn 0; trang báo cáo: hiện nhãn mức) |
124
+ | Trục X chỉ nhãn, đáy một đường mảnh | `<XAxis tickLine={false} axisLine={{ stroke: "var(--border)" }} tick={{ fill: "var(--muted)", fontSize: 12 }} />` |
125
+ | Không hiệu ứng vào trang (`F23`) | `isAnimationActive={false}` trên mọi `<Line>`, `<Bar>`, `<Pie>` |
126
+ | Số ghi trên đầu cột | `<LabelList position="top" />`, cùng màu `--muted`, `text-xs` |
127
+ | Đường: chỉ chấm và số ở điểm cuối (hoặc điểm đang rê) | `dot={false}`, `activeDot` tắt, tự vẽ chấm + nhãn bằng `<ReferenceDot>` hoặc `label` theo chỉ số điểm đang chọn |
128
+ | Không tooltip nổi cho đường | bỏ `<Tooltip>`, đọc điểm đang rê qua `onMouseMove` để dời chấm và số |
129
+ | Cột bo góc | `radius={[8, 8, 0, 0]}` (cột nhóm hẹp: `[6, 6, 0, 0]`) |
130
+ | Màu từ token, đúng thang đậm nhạt | `fill="var(--chart-fill)"` + `fillOpacity` theo bảng ở mục Màu, không để bảng màu mặc định của thư viện |
131
+ | Donut | `<Pie innerRadius="72%" startAngle={90} endAngle={-270} paddingAngle={1} stroke="var(--surface)">`, dữ liệu đã xếp lớn dần |
132
+ | Chưa có số (khác 0) | giá trị `null` + `connectNulls={false}` để đường đứt |
133
+ | Chú giải | bỏ `<Legend>` của thư viện, dựng chú giải bằng HTML theo spec (chấm cùng class cột) |
134
+
135
+ Thư viện khác thì tìm đúng các tuỳ chọn tương ứng; hình cuối cùng phải y như
136
+ bảng trên. Các mẫu HTML bên dưới là **hình cần đạt**: có thư viện thì để so,
137
+ chưa có thư viện thì dựng theo đúng mẫu đó.
138
+
139
+ ## Cột hay đường
140
+
141
+ | Dữ liệu | Loại | Vì sao |
142
+ | --- | --- | --- |
143
+ | **Số đếm của từng kỳ**, tới ~12 kỳ: việc xong mỗi tuần, đơn mỗi ngày, khách mới mỗi tháng | **Cột**, số trên đầu mỗi cột | Mỗi kỳ là một con số rời người ta muốn đọc; cột ghi đủ số, không phải rê chuột |
144
+ | Mức tại từng thời điểm hoặc tỷ lệ: số người dùng đang hoạt động, tỷ lệ huỷ, số dư, luỹ kế. Doanh thu theo tháng khi cái cần đọc là xu hướng | Đường | Giá trị chảy liền, cái cần thấy là dáng lên xuống |
145
+ | Số đếm từng kỳ nhưng quá ~12 kỳ (30 ngày, 52 tuần) | Đường | Cột mảnh quá, số trên đầu cột chồng nhau |
146
+
147
+ Các công cụ quản lý dự án phổ biến vẽ "việc xong mỗi tuần" bằng cột. Vẽ 8
148
+ tuần bằng đường thì chỉ đọc được số tuần cuối.
149
+
150
+ ## Công thức dùng được ngay
151
+
152
+ **Vùng vẽ giãn theo card, không đóng cứng.** Card biểu đồ thường bị lưới kéo cao
153
+ hơn nội dung. Dùng `flex-1 min-h-[14rem]` cho vùng vẽ để cột nở ra lấp đầy, cột
154
+ cao lên thì chênh lệch giữa các giá trị cũng đọc rõ hơn. Đừng đóng `h-56` rồi
155
+ `mt-auto` đẩy xuống đáy.
156
+
157
+ Giãn hết cỡ rồi mà vẫn dư nhiều thì **cho card ngắn lại** (bỏ `row-span`), đừng
158
+ tự nghĩ ra nội dung lấp vào. Lúc giao gợi ý một dòng chỗ đó có thể thêm gì, xem
159
+ luật `S5` trong `../../SKILL.md`.
160
+
161
+ **Biểu đồ cột**, hình cần đạt (có thư viện thì dựng bằng thư viện, xem mục trên):
162
+
163
+ ```html
164
+ <div class="flex min-h-[14rem] flex-1 items-end gap-2 sm:gap-3">
165
+ <div class="flex h-full flex-1 flex-col justify-end gap-2">
166
+ <p class="text-center text-xs font-medium text-muted">16</p>
167
+ <!-- mx-auto w-full max-w-8: cột tối đa 32px, nằm giữa khe của nó -->
168
+ <div class="mx-auto w-full max-w-8 rounded-t-md bg-chart-fill" style="height: 80%"></div>
169
+ </div>
170
+ <!-- các cột khác, cùng bg-chart-fill; kỳ đang chạy (cột cuối) bg-chart-fill/35 -->
171
+ </div>
172
+
173
+ <div class="mt-3 flex gap-2 border-t border-border pt-3 sm:gap-3">
174
+ <p class="flex-1 text-center text-xs text-muted">T1</p>
175
+ <!-- nhãn trục, cùng gap với cụm cột để thẳng hàng -->
176
+ </div>
177
+ ```
178
+
179
+ Nhãn trục phải dùng **đúng `gap` với cụm cột**, lệch một bậc là cả hàng nhãn
180
+ trượt khỏi cột.
181
+
182
+ - **Cột tối đa 32px** (`max-w-8`), khe còn lại để trống. Để `flex-1` giãn hết thì 8 cột trong card rộng thành 8 khối đen 56px, cả biểu đồ là một mảng đen nặng nhất màn, nặng hơn tên trang và hàng số (ở 32px mảng đen mất, số trên đầu cột vẫn đọc như cũ). Màn hẹp cột tự co theo khe.
183
+ - **Kỳ đang chạy (tuần này, tháng này) là cột nhạt `bg-chart-fill/35`, nhãn trục ghi "Tuần này"**, số trên đầu vẫn ghi. Kỳ chưa hết mà vẽ cùng màu với các kỳ đủ thì cột thấp đọc ra "tuần này tụt", cột cao đọc ra "đã vượt" trong khi tuần còn hai ngày. Hàng số liệu đã so "cùng thời điểm tuần trước", biểu đồ cũng phải nói tuần này chưa xong. Các công cụ phân tích phổ biến đều vẽ kỳ đang chạy khác đi (nhạt hoặc nét đứt).
184
+ - **Mới một kỳ thì chưa phải biểu đồ**, như biểu đồ đường: một cột đứng giữa khung trống là một cột đen không so với gì. Thay vùng vẽ bằng khối chữ, giữ chiều cao: số `text-2xl font-semibold tabular-nums` + "Việc xong tuần này. Từ tuần sau sẽ thấy xu hướng".
185
+
186
+ **Biểu đồ cột nhóm** (hai, ba kỳ cạnh nhau):
187
+
188
+ - Cột trong một nhóm cách nhau `gap-1.5`, nhóm cách nhau rộng hơn hẳn (`gap-6` trở lên), để mắt gom đúng nhóm.
189
+ - **Thời gian đi từ trái sang phải**, như trục của biểu đồ đường: kỳ cũ bên trái, **kỳ mới nhất bên phải và đậm nhất**. Chú giải xếp cùng thứ tự. Để 2026 đứng trái 2024 thì nhóm tăng trưởng nhìn như đi xuống.
190
+ - Tối đa ba chuỗi. Nhiều hơn thì tách biểu đồ hoặc chuyển bảng.
191
+ - **Bằng 0 khác chưa có số**, như biểu đồ đường: bằng 0 thì không có cột, ghi `0` sát đáy; chưa có số (kênh chưa mở năm đó) thì ghi `—` sát đáy, đừng ghi `0`.
192
+ - Tên nhóm dài `truncate` kèm `title`, một dòng. Màn hẹp không đủ chỗ thì cụm cột cuộn ngang **trong khung** (`overflow-x-auto`), trang không cuộn (`R1`).
193
+ - Đơn vị ghi một lần ở mô tả hoặc tiêu đề card ("triệu đồng"), không lặp trên từng cột.
194
+
195
+ **Donut** (tỷ lệ trên tổng, tối đa bốn phần):
196
+
197
+ - Vòng bên trái, bảng chú giải bên phải (màn hẹp: vòng trên, bảng dưới). Giữa vòng là tổng `text-2xl font-semibold tabular-nums` + nhãn `text-sm text-muted`.
198
+ - **Xếp lớn xuống nhỏ**, bắt đầu từ 12 giờ đi theo chiều kim đồng hồ, phần lớn nhất đậm nhất theo thang ở trên. Giữa các phần một khe trắng mảnh.
199
+ - Bảng chú giải là danh sách chia dòng: chấm · tên (`min-w-0 flex-1 truncate` + `title`) · số (`font-medium tabular-nums`) · phần trăm (`text-muted tabular-nums`, cột rộng cố định). Số và phần trăm không bao giờ bị đẩy xuống dòng.
200
+ - Phần dưới 1% vẫn vẽ một vệt tối thiểu để thấy được. **Phần trăm cùng số chữ số lẻ trong một biểu đồ**: có phần dưới 1% thì cả bảng một chữ số lẻ, không thì số nguyên. Tổng các dòng làm tròn phải ra 100.
201
+ - **Tổng bằng 0**: chỉ còn rãnh `bg-foreground/5`, giữa vòng ghi `0`, cột phần trăm để trống (không `0%`, như `0/0`).
202
+ - Quá bốn phần thì dùng danh sách thanh tiến độ ở dưới, xếp lớn dần.
203
+
204
+ **Thanh tiến độ trong danh sách:**
205
+
206
+ ```html
207
+ <li class="py-3 first:pt-0">
208
+ <div class="flex items-baseline justify-between gap-3">
209
+ <p class="min-w-0 flex-1 truncate text-sm font-medium text-foreground">Thiết kế giao diện</p>
210
+ <p class="shrink-0 text-sm font-medium text-foreground">92%</p>
211
+ </div>
212
+ <!-- Rãnh là lớp phủ bg-foreground/5, không bg-background: nền tối bg-background tối hơn card, rãnh thành
213
+ vệt đen nặng (M21). Bản sáng hai cách cùng một màu. -->
214
+ <div class="mt-2 h-2 rounded-full bg-foreground/5">
215
+ <div class="h-2 rounded-full bg-primary" style="width: 92%"></div>
216
+ </div>
217
+ </li>
218
+ ```
219
+
220
+ Rãnh nền dùng `--background`, không dùng `--border`. Thanh `h-2`, bo `full`.
221
+ Con số phần trăm ghi ở đầu dòng, không đặt bên trong thanh.
222
+
223
+ **Thanh trong danh sách luôn `bg-primary`**, kể cả khi mục đó có chuyện cần chú ý
224
+ (dự án có việc quá hạn). Thanh nói đúng một điều là đã xong bao nhiêu; chuyện cần
225
+ chú ý nói bằng chữ ở dòng phụ, và **chỉ cụm đó** `text-amber-700`, phần còn lại
226
+ xám: `112 / 120 việc · <span class="text-amber-700">2 việc quá hạn</span>`, cùng
227
+ cách tô với hạn trong `list-row.md`. Tô cả thanh hổ phách thì dự án 93% đọc ra
228
+ "tiến độ có vấn đề", vài thanh cam thành thứ nặng nhất màn.
229
+ Màu theo trạng thái (dưới đây) chỉ cho **thanh đứng riêng**, nơi màu nói về chính
230
+ đại lượng thanh đo (dung lượng gần đầy). Mục xong 100% thì thanh emerald như
231
+ thanh đứng riêng.
232
+
233
+ **Thanh tiến độ đứng riêng** (dung lượng, checklist): hàng trên là
234
+ nhãn trái + số phải, thanh ở giữa, một dòng phụ `text-xs text-muted` bên dưới.
235
+
236
+ - `role="progressbar"` + `aria-valuenow`, `aria-valuemin`, `aria-valuemax`, `aria-label` bằng đúng nhãn. Trình đọc màn hình không thấy độ dài thanh.
237
+ - **Màu theo trạng thái, đúng bảng trạng thái của app**: đang chạy `bg-primary`; **xong** `bg-emerald-600` (checklist đủ); cần chú ý `bg-amber-500`; hỏng hay đầy `bg-red-500`. Ngưỡng đổi màu (vd. dung lượng từ 80%) là của người dùng, truyền qua prop `tone`. Dòng phụ đổi màu theo (`amber-700`, `red-600`) và nói bằng chữ, vì màu thanh một mình không đủ.
238
+ - **Xong mà thanh vẫn đen đầy** thì trông y hệt "đang chạy tới 99%". Thanh đứng riêng (dung lượng, checklist) xong là emerald.
239
+ - **Tải tệp** (một hay nhiều tệp) không dùng khuôn này: theo `file-upload.md`. **Hai độ dày là cố ý, đừng gộp**: thanh ở đây (`h-2`) là **con số chính** của khối, người ta đọc nó để so; thanh tải tệp (`h-1`) chỉ là **trạng thái tạm** của một dòng mà nội dung chính là tên tệp, xong là biến mất. Demo "tiến độ tải file" trong bộ progress bar cũng dựng theo khuôn tải tệp. Thanh mảnh `h-1`, chỉ tệp đang tải mới có thanh; xong và hỏng thì bỏ thanh, bỏ số %, chỉ còn một dòng chữ; Thử lại là nút chữ sau lý do.
240
+ - **Không có gì để đếm** (checklist 0 việc): không ghi `0/0`, ô số để trống, thanh rỗng, dòng phụ "Chưa có việc nào". `0/0` đọc như lỗi chia cho 0.
241
+ - **Một kiểu viết số cho cả app**: `12,4 / 20 GB` và `4 / 6 việc`, gạch chéo có dấu cách hai bên. Chỗ có cách chỗ không là lệch. Số `tabular-nums`.
242
+
243
+ ⚠️ **Luật cũ đã bỏ, đừng hồi sinh:** tải hỏng giữ thanh đỏ dừng giữa chừng kèm số %, nút viền "Thử lại" cuối hàng, cột hành động `w-20`; tải xong giữ thanh xanh lá đầy + `100%`. Lên danh sách nhiều tệp thì thành một dàn sọc đen đỏ xanh, và tệp hỏng không còn ✕ để bỏ. Khuôn mới ở `file-upload.md`.
244
+
245
+ **Biểu đồ đường** (xu hướng theo tháng, theo ngày):
246
+
247
+ - Có thư viện thì cấu hình theo bảng ở mục "Vẽ bằng thư viện"; chưa có thì một `<svg>` với `<polyline>` là đủ; chỉ đề xuất thư viện khi có nhu cầu ở mục 3 phía trên.
248
+ - Một đường `stroke-2` màu `--primary`, không tô vùng dưới, không bo cong làm sai giá trị (`monotone` thì được, `basis`/`cardinal` vọt quá điểm thật thì không).
249
+ - **Chỉ một chấm và một con số, ở điểm cuối** (`size-2.5`, số `text-xs font-semibold tabular-nums` cạnh chấm). Rê chuột hay dùng mũi tên thì chấm và số dời sang tháng đó, **không mở thêm tooltip nổi**: cùng một chỗ, chỉ đổi vị trí (`N1`, `N3`). Dựng một ví dụ tĩnh cho trạng thái đang rê (`N2`).
250
+ - **Nhãn trục bị thưa thì số phải kèm mốc**: "13/09 · 70,9 tr đ", không chỉ "70,9 tr đ". 30 ngày mà trục chỉ ghi 02/09, 07/09, 12/09 thì rê vào một điểm ra một con số không biết của ngày nào. Mọi thư viện biểu đồ phổ biến đều ghi ngày ở đầu tooltip; skill không dùng tooltip nổi nhưng không bỏ ngày. Mốc là `text-muted font-normal`, số giữ `font-semibold`. **Mốc chưa trọn kỳ luôn ghi tên kỳ**: "Hôm nay · 44,4 tr đ", "Tuần này · 330,1 tr đ", "Tháng 9 · 1,41 tỷ đ": số của ngày chưa hết đứng một mình ở cuối đường đọc ra "hôm nay tụt". Trục ghi đủ mọi mốc (12 tháng) thì số đứng một mình được, mốc nằm ngay dưới.
251
+ - **Nhãn đặt về phía không có đường.** Mặc định số nằm trên chấm, nhưng điểm thấp hơn điểm kề thì đoạn nối đi lên đúng chỗ đó và chữ đè lên đường (nền `bg-surface` sau chữ cắt đôi đường, trông như đường đứt). Chọn theo hai điểm kề: cả hai thấp hơn hoặc bằng (đỉnh) thì **trên**; cả hai cao hơn (đáy) thì **dưới** (`top: calc(y + 0.625rem)`); một cao một thấp (sườn) thì **trên, lệch về phía điểm kề thấp hơn** (căn mép phải chữ vào chấm khi điểm thấp ở bên trái, mép trái khi ở bên phải). Điểm đầu, điểm cuối chỉ có một điểm kề: kề thấp hơn thì trên, cao hơn thì dưới. Hai điểm chênh nhau dưới 2% chiều cao vùng vẽ thì coi là bằng nhau: không vậy thì điểm chỉ thấp hơn bên cạnh 0,5 tr bị coi là đáy, số rớt xuống dưới đè lên đoạn gần như nằm ngang.
252
+ **Chỗ đó không vừa thì thử chỗ khác, theo thứ tự:** phía theo hai điểm kề → phía đối diện → **ngang cạnh chấm** (căn giữa theo chấm, cách chấm 8px, ưu tiên bên không có điểm kề hoặc bên điểm kề thấp hơn). Lấy chỗ đầu tiên vừa **nằm trọn trong vùng vẽ** (trên đường 0, dưới đỉnh) vừa **không chạm đường**; tính bằng toạ độ mọi đoạn nằm dưới khung chữ đo được, không chỉ hai đoạn nối vào chấm, và không đoán theo số: ở 375px, 30 điểm chỉ có chừng 260px, chữ rộng ~100px trùm qua cả chục đoạn. Cả bốn chỗ đều chạm (đường dày, màn hẹp) thì giữ cách căn của chỗ đầu tiên, đẩy chữ lên hẳn trên phần đường nằm dưới nó (cách 6px), rồi thử xuống hẳn dưới; vẫn không sạch thì lấy chỗ trong vùng vẽ chạm ít đoạn nhất. **Dưới đường 0 là chỗ của hàng nhãn trục**: số rơi xuống đó đọc ra một nhãn trục thứ hai. Ví dụ "Hôm nay · 6,8 tr đ" sát đáy, điểm kề cao hơn: xuống dưới thì lòi khỏi vùng vẽ, sát nhãn trục; đặt ngang bên trái chấm thì vừa, vì đoạn đứt đi gần như thẳng đứng.
253
+ Probe đo cả hai lỗi ("Nhãn số đè lên đường biểu đồ", "Nhãn số lòi ra ngoài vùng vẽ").
254
+ - **Tab vào biểu đồ: số của mốc đang đứng hiện ra**, như rê chuột tới mốc đó; không vòng quanh chấm (`I13`).
255
+ - **Đáy là 0**, đường kẻ đáy `border-border`, nhãn tháng `text-xs text-muted` bên dưới. Tháng bằng 0 thì chạm đáy.
256
+ - **Không có số khác với bằng 0.** Tháng chưa có dữ liệu (chưa tới, chưa ghi nhận) thì **đứt đường** ở đó, không kéo xuống đáy: kéo xuống 0 là nói dối "tháng đó không bán được gì".
257
+ - **Màn hẹp**: 12 nhãn tháng ở 375px sát vào nhau. Dưới `sm` ghi **cách một nhãn, đếm ngược từ nhãn cuối** (nhãn cuối, cuối − 2, cuối − 4…); đường vẫn đủ 12 điểm. Đừng đếm xuôi "nhãn lẻ + nhãn cuối": số điểm chẵn thì nhãn cuối đứng sát nhãn lẻ cuối cùng, "14/09 21/09" dính nhau ở 375px. Cột cũng theo luật này.
258
+ - **Mới một điểm thì chưa phải biểu đồ.** Một chấm lơ lửng giữa khung trống trông như vẽ lỗi. Thay vùng vẽ bằng khối chữ, **giữ nguyên chiều cao**: con số `text-2xl font-semibold tabular-nums` + dòng `text-sm text-muted` "Số của tháng 9. Từ tháng sau sẽ thấy xu hướng" (`N6`). Luật này cho dữ liệu **chưa có** điểm thứ hai (tháng đầu mở bán). Trang báo cáo mà người dùng tự chọn khoảng một ngày thì không rơi vào đây: chia nhỏ mốc xuống theo giờ (xem "Trang báo cáo" trong `../layouts/app.md`). Thay biểu đồ bằng một con số thì số đó trùng ô "Doanh thu" ngay trên (`N3`).
259
+ - **Rỗng**: giữ chiều cao, câu nói vì sao và bao giờ có: "Chưa có doanh thu. Số liệu hiện sau đơn hàng đầu tiên" (`components/empty-state.md`). Bỏ luôn đường đáy: đáy không nhãn đứng một mình trông như đường kẻ lạc.
260
+ - `role="img"` + `aria-label` tóm tắt bằng chữ ("Doanh thu 12 tháng, tăng từ 0,9 tỷ lên 1,46 tỷ"); điểm nhận focus thì mỗi điểm có `aria-label` tháng + số.
261
+
262
+ **Hàng ô số liệu:** một khối chia `divide-x`, không phải mấy card rời. Xem
263
+ `layouts/app.md`.
264
+
265
+ ---
266
+
267
+ ## Ô số liệu ở màn hẹp
268
+
269
+ Đây là chỗ vỡ nhiều nhất, và chỉ lộ ra khi thu cửa sổ xuống 375px.
270
+
271
+ - **Mobile là 2×2**: `grid-cols-2 lg:grid-cols-4`, như hầu hết app trên điện thoại. Một cột thì bốn ô số ngắn (4, 140, 5, 23) xếp dọc cao 417px, chiếm gần hết màn đầu ở 375px, biểu đồ và việc hôm nay bị đẩy khỏi tầm nhìn; 2×2 còn 209px (chủ dự án duyệt). Ở 375px mỗi ô còn **138px cho chữ số** (`text-xl`, ô `p-4`): vừa `1.284.500` (98px), `184,5 tr đ` (91px), `1,28 tỷ đ` (80px); không vừa `1.284.500.000 đ` (161px).
272
+ - **Có số không vừa 138px** (tiền đầy đủ tới hàng tỷ): gợi ý rút gọn theo "Số quá 9 chữ số" bên dưới. Người dùng muốn giữ số đầy đủ thì hàng đó về `grid-cols-1 sm:grid-cols-2 lg:grid-cols-4`, không để số xuống dòng hay tràn.
273
+ - **Số ô lẻ** (3, 5): 2 cột là một ô đứng lẻ ở hàng dưới (`R3`), nên đi thẳng `grid-cols-1 sm:grid-cols-3`. 6 ô thì 2 cột được.
274
+ - Luật cũ (đã bỏ): "Mobile là một cột" cho mọi hàng số, vì sợ số dài; phần lớn hàng số trên màn tổng quan là số đếm ngắn.
275
+ - Ô trong panel trượt: xem mục dưới.
276
+ - **Trong panel trượt hay cột hẹp: lưới 2×2 cố định, một khung, số nhỏ hơn tên.** Panel 448px không phải trang tổng quan: số ở đây là thông tin phụ của một bản ghi, không phải nhân vật chính. Bốn card rời `rounded-2xl p-5` với số `text-3xl` là thứ nặng nhất panel, nặng hơn cả tên khách (các CRM lớn để số của khách ở cỡ chữ thường). Dựng một khung, kẻ chia bằng khe 1px, số `text-lg font-semibold`, không bao giờ lớn hơn cỡ tên ở header panel. Dòng so sánh chỉ còn icon + số (kỳ ghi một lần, xem "Dòng so sánh") nên 2 cột ở 375px vẫn vừa, không phải xếp thành bốn ô dọc dài.
277
+
278
+ ```html
279
+ <p class="mb-2 text-xs text-muted">12 tháng gần nhất, so với 12 tháng trước</p>
280
+ <div class="grid grid-cols-2 gap-px overflow-hidden rounded-xl border border-border bg-border">
281
+ <div class="min-w-0 bg-surface p-4">
282
+ <p class="text-xs font-medium text-muted">Doanh thu</p>
283
+ <p class="mt-1 text-lg font-semibold tabular-nums text-foreground">184,5<span class="ml-1 text-muted">tr đ</span></p>
284
+ <p class="mt-0.5 flex items-center gap-1 text-xs font-medium tabular-nums text-emerald-700">
285
+ <i data-lucide="trending-up" class="size-3.5"></i>12,4%
286
+ </p>
287
+ </div>
288
+ <!-- 3 ô còn lại cùng khuôn -->
289
+ </div>
290
+ ```
291
+ - **Mỗi ô phải có `min-w-0`.** Grid item mặc định không chịu co nhỏ hơn nội dung, thiếu dòng này là cả trang tràn ngang.
292
+ - **Số của các ô cùng hàng luôn thẳng một đường, kể cả khi có nhãn xuống dòng.** Mỗi ô là `grid row-span-2 grid-rows-subgrid content-start` (ba tầng thì `row-span-3`), khung ngoài không cần khai hàng: hàng nhãn cao theo nhãn dài nhất, số cả hàng cùng nằm dưới đó. Không có subgrid thì một nhãn hai dòng đẩy riêng số của ô đó xuống 16px.
293
+ - **Nhãn có kỳ ngắt sau dấu `·`, không ngắt giữa kỳ**: `Đơn đã giao&nbsp;· 12&nbsp;tháng` ra "Đơn đã giao ·" / "12 tháng". Để khoảng trắng thường thì trình duyệt ngắt "· 12" / "tháng"; dán `&nbsp;` sau dấu thì dấu `·` rơi xuống đầu dòng.
294
+ - **Cỡ số giảm một bậc ở mobile**: `text-xl sm:text-2xl`.
295
+ - **Dùng `tabular-nums`** cho mọi con số. Chữ số đều bề ngang thì các ô thẳng cột nhau, và số không nhảy khi đổi giá trị.
296
+
297
+ ```html
298
+ <!-- Kẻ chia bằng khe 1px lộ nền --border (như lưới trong panel): divide-x không kẻ được đường ngang giữa hai hàng của 2×2 -->
299
+ <div class="grid grid-cols-2 gap-px overflow-hidden rounded-2xl border border-border bg-border lg:grid-cols-4">
300
+ <div class="min-w-0 bg-surface p-4 sm:p-5">
301
+ <p class="text-xs font-medium text-muted">Đang làm</p>
302
+ <p class="mt-1 text-xl font-semibold tracking-tight tabular-nums text-foreground sm:text-2xl">1.284.500</p>
303
+ <p class="mt-1 text-xs text-muted">Trải trên 4 dự án</p>
304
+ </div>
305
+ </div>
306
+ ```
307
+
308
+ ### Dòng so sánh và đơn vị tiền
309
+
310
+ ```html
311
+ <div class="min-w-0 p-5">
312
+ <p class="text-xs font-medium text-muted">Doanh thu tháng này</p>
313
+ <p class="mt-1 text-xl font-semibold sm:text-2xl tracking-tight tabular-nums text-foreground">
314
+ 1.284.500.000<span class="ml-1 font-semibold text-muted">đ</span>
315
+ </p>
316
+ <p class="mt-1 flex items-center gap-1 text-xs text-muted">
317
+ <i data-lucide="trending-up" class="size-3.5 text-emerald-700"></i>
318
+ <span class="font-medium tabular-nums text-emerald-700">12,4%</span> so với tháng trước
319
+ </p>
320
+ </div>
321
+ ```
322
+
323
+ - **Hai luật tiền dưới đây cho tiền VND trong copy tiếng Việt.** Tiền tệ khác hoặc copy tiếng Anh thì format theo locale (`T28`).
324
+ - **Đừng format tiền bằng `Intl.NumberFormat('vi-VN', { style: 'currency', currency: 'VND' })`**: nó tự sinh ra `₫`. Format số bằng `Intl.NumberFormat('vi-VN')` rồi tự gắn `đ`. Grep `currency: 'VND'` trước khi đổ lỗi cho chỗ khác.
325
+ - **Đơn vị tiền dùng chữ `đ` thường, không dùng ký hiệu `₫`.** Chữ `₫` có sẵn một vạch dưới trong chính mặt chữ, CSS không bỏ được: ở cỡ lớn trông như link, thu nhỏ thì thành một vệt gạch lí nhí. `đ` **cùng cỡ với số**, `font-semibold`, chỉ đổi sang `text-muted` và cách `ml-1`: màu mờ đã đủ tách đơn vị khỏi giá trị. Dự án đã quen dùng `₫` thì theo dự án.
326
+ - **Số quá 9 chữ số trong ô hẹp**: gợi ý (người dùng quyết) rút gọn `1,28 tỷ đ`, số đầy đủ để trong `title`. Ô số liệu để đọc xu hướng, không để đối soát từng đồng.
327
+ - **Dòng so sánh**: icon `trending-up` / `trending-down` `size-3.5` + phần trăm `font-medium` có màu + phần còn lại `text-muted`. Chỉ icon và con số mang màu, không tô cả câu.
328
+ - **Màu theo tốt/xấu, không theo lên/xuống.** Doanh thu tăng là xanh, nhưng chi phí hay số đơn huỷ tăng là đỏ. Để một prop kiểu `tone="positive" | "negative" | "neutral"` cho người dùng quyết, đừng suy màu từ dấu của con số. Xanh `emerald-700`, đỏ `red-700`: chữ `text-xs` cần 4.5:1, `emerald-600` và `red-500` không đạt.
329
+ - **Không đổi**: icon `minus`, chữ `text-muted`, không màu. **Một ô không có kỳ trước** (các ô khác có): một câu `text-muted` ngắn ("Chưa có kỳ trước"). Hai ca này vẫn **giữ đúng một dòng**, để các ô trong hàng cao bằng nhau. Ngưỡng coi là "không đổi" do người dùng quyết.
330
+ - **Kỳ so sánh ghi MỘT lần cho cả hàng, không lặp ở từng ô.** Bốn ô cùng đuôi "so với 2025" là một ý nói bốn lần (`N3`), và chính cái đuôi đó làm ô hẹp vỡ dòng. Ghi kỳ một lần ở tiêu đề mục hoặc dòng `text-xs text-muted` trên hàng ô ("12 tháng gần nhất, so với 12 tháng trước"), mỗi ô chỉ còn icon + số ("↗ 12,4%", "↗ 0,6 điểm"). Nhãn ô cũng bỏ kỳ: "Doanh thu", không "Doanh thu 12 tháng" khi ô bên cạnh là "Số đơn" trơn, vì đọc ra hai ô hai kỳ khác nhau. **Kỳ so phải cùng loại với kỳ đo**: đo 12 tháng gần nhất thì so với 12 tháng trước đó, không so với "năm 2025".
331
+ - **Cả hàng không có kỳ trước thì cũng nói một lần**, ở đúng chỗ ghi kỳ ("Khách mới, chưa có kỳ trước để so"), các ô bỏ hẳn dòng so sánh. Bốn ô cùng dòng "Chưa có kỳ trước" là cùng lỗi lặp ở trên. Ngoại lệ: trên cùng màn có số khác kỳ đếm cùng thứ (tab "Đơn hàng 42" trọn đời) thì riêng ô đó ghi kỳ trong nhãn, "Đơn đã giao · 12 tháng" (`layouts/app.md`, "Trang chi tiết bản ghi").
332
+ - **Cả hàng rỗng (khách chưa có đơn nào) thì không dựng lưới số 0.** Bốn ô "0 đ", "0 đơn", "—", "—" là bốn khung chỉ để nói một ý "chưa có gì". Thay cả hàng bằng một khung gọn cùng viền và **cùng nền `bg-surface`** với các card (khung trong suốt nằm trên nền trang thì chỉ còn một dòng chữ xám lơ lửng, viền `--border` gần như không thấy), một câu `text-sm text-muted` nói vì sao và bao giờ có: "Chưa có đơn nào. Số liệu hiện sau đơn đầu tiên" (`components/empty-state.md`, `N6`). Có nút tạo đơn ở chỗ khác trên màn thì không lặp nút ở đây. **Trang chi tiết có tab Đơn hàng thì bỏ hẳn hàng số**, không dựng cả khung gọn: tab rỗng đã nói câu đó (`layouts/app.md`, "Trang chi tiết bản ghi").
333
+ - **`—` chỉ khi không tính được** (mẫu số bằng 0: chưa có đơn nào thì chưa có tỷ lệ hoàn). Có 3 đơn, không đơn nào hoàn thì là `0%`, không phải `—`: `—` ở đó đọc ra "thiếu dữ liệu" trong khi số đã rõ.
334
+ - **Ô không có giá trị thì `—` `text-muted font-normal`**, cùng ký hiệu với ô trống trong bảng (`T18`). `—` tô `text-foreground font-semibold` ở `text-2xl` thành một vạch đen dày, đọc như con số chứ không như "trống".
335
+ - **Nhãn đã nói đơn vị thì số không lặp đơn vị.** Nhãn "Số đơn" thì số là `24`, không `24 đơn`; nhãn "Khách hàng" thì `1.204`, không `1.204 khách`. Đơn vị chữ chỉ gắn khi nhãn chưa nói (nhãn "Đơn hàng" có thể `24 đơn`, nhưng thường thừa).
336
+ - **Số rút gọn thì hậu tố đi cùng đơn vị, cùng một span mờ**: `184,5` rồi `tr đ` `text-muted`, không để `tr` đen mà `đ` xám (đọc thành "184,5 tr" là số, "đ" là đơn vị, trong khi "tr" cũng là đơn vị). Chữ rút gọn: `nghìn`, `tr`, `tỷ`.
337
+ - **Dòng so sánh luôn một dòng, `text-xs`**, kể cả ca có số. Đừng để icon và phần trăm ở dòng trên, "so với tháng trước" rớt xuống dòng dưới: đọc thành hai ý rời, và ô cao thêm một dòng. Ô hẹp không đủ chỗ thì rút đuôi câu ("so với T8", "so với kỳ trước"), không xuống dòng. Câu ca không có kỳ trước cũng vậy: "Chưa có số kỳ trước để so" vỡ thành hai dòng, chữ "so" nằm một mình, nên dùng "Chưa có kỳ trước".
338
+ - **`%` dính vào số, không cách**: `2,8%`, `12,4%`, cùng một kiểu ở số chính lẫn dòng so sánh (`N5`). Khác `đ` và `đơn`: đơn vị là chữ thì cách `ml-1`, ký hiệu `%` thì không. Số chính có `%` thì `%` `text-muted` như `đ`.
339
+ - **Số đếm nhỏ thì so bằng chênh lệch, không bằng phần trăm.** Kỳ trước dưới 20 (việc quá hạn, dự án đang chạy) thì ghi "↗ 2", không "↗ 66,7%": từ 3 lên 5 việc quá hạn mà đọc 66,7% thì tưởng cháy nhà. Nhãn ô đã nói đơn vị nên số trơn, không "2 việc". Các ô trong một hàng được phép khác kiểu (ô 140 việc đang mở vẫn ghi %).
340
+ - **Chỉ số đã là tỷ lệ thì so bằng điểm phần trăm, không bằng phần trăm của phần trăm.** Tỷ lệ huỷ từ 2,2% lên 2,8% ghi "↗ 0,6 điểm", không ghi "↗ 27,3%": 27,3% đọc như tỷ lệ huỷ tăng vọt thêm 27 điểm. Cách tính là logic người dùng; skill chỉ để chỗ và nhãn "điểm".
341
+
342
+ ### Sparkline trong ô số liệu
343
+
344
+ ```tsx
345
+ // Cả hàng dùng chung một cờ: có ít nhất một ô vẽ được thì mới giữ chỗ
346
+ const rowHasSparkline = tiles.some((tile) => tile.points.length >= 2);
347
+
348
+ <div className="flex min-w-0 flex-col p-5">
349
+ {/* nhãn, số, dòng so sánh */}
350
+ {tile.points.length >= 2 ? (
351
+ <svg aria-hidden="true" className="mt-auto h-10 w-full pt-4">{/* polyline + chấm cuối */}</svg>
352
+ ) : rowHasSparkline ? (
353
+ // Giữ chỗ chỉ để cao bằng ô bên cạnh. Hàng 2×2 ở mobile vẫn có ô bên cạnh nên giữ;
354
+ // chỉ hàng về một cột ở mobile (số dài, số ô lẻ) mới thêm max-sm:hidden
355
+ <div aria-hidden="true" className={cn("mt-auto h-10 pt-4", isSingleColumnOnMobile && "max-sm:hidden")} />
356
+ ) : null}
357
+ </div>
358
+ ```
359
+
360
+ Không đặt `min-h` cố định cho ô: chiều cao ô phải đến từ nội dung, để cả hàng
361
+ chưa có sparkline thì ô tự ngắn lại (ô giữ `min-h` hoặc chỗ giữ không phụ thuộc cả hàng thì
362
+ desktop vẫn trống).
363
+
364
+ - Một đường `stroke-[1.5]` `--primary`, cao `h-10` đến `h-12`, rộng hết ô, dính đáy ô (`mt-auto`). Chấm `size-1.5` ở điểm cuối. Không trục, không nhãn, không rê chuột: ô là để liếc hình dáng.
365
+ - **Không lấy đáy 0**, khác biểu đồ đường: kéo giãn theo min–max của chuỗi để thấy được dáng. Biểu đồ đường thì có số để đọc, sparkline chỉ có dáng.
366
+ - **Đường luôn một màu `--primary`**, kể cả khi dòng so sánh đỏ. Màu tốt/xấu đã nói ở dòng so sánh; tô đường đỏ nữa là hai tín hiệu cho một ý (`N3`).
367
+ - Mọi ô trong hàng cùng số điểm, cùng khoảng thời gian, sparkline cùng chiều cao.
368
+ - **Chưa đủ hai điểm** (tháng đầu tiên): không vẽ, không chấm lẻ, không đường ngang giả. **Giữ chỗ chỉ khi có ô bên cạnh để khớp** (`N1`):
369
+ - Cùng hàng có ô **có** sparkline: ô thiếu giữ chỗ bằng đúng chiều cao sparkline, để các ô cao bằng nhau.
370
+ - **Hàng về một cột ở mobile** (số dài không rút gọn, số ô lẻ; mặc định là 2×2, xem "Ô số liệu ở màn hẹp"): các ô xếp dọc, không có ô nào bên cạnh, giữ chỗ chỉ là một khoảng trắng lớn dưới mỗi ô. Chỗ giữ `max-sm:hidden`. Hàng 2×2 thì giữ chỗ cả ở mobile, vì ô cạnh bên vẫn cần cao bằng.
371
+ - **Cả hàng đều chưa có** sparkline: bỏ hẳn chỗ giữ ở mọi cỡ màn, ô chỉ cao đến dòng so sánh.
372
+ - `aria-hidden="true"` trên `<svg>`: xu hướng đã nói bằng chữ ở dòng so sánh.
373
+
374
+ Biểu đồ cột nhiều mốc ở màn hẹp thì cho cả cụm cuộn ngang **trong khung riêng**
375
+ bằng `overflow-x-auto` cộng một bề rộng tối thiểu cho cụm, đừng để nó đẩy cả
376
+ trang. Xem luật `R1` trong `../responsive.md`.
@@ -0,0 +1,132 @@
1
+ # Khung chat với trợ lý AI
2
+
3
+ Hỏi đáp với trợ lý trong app: hỏi số liệu, soạn văn bản, tra đơn. Mượn khuôn từ
4
+ file đã có (`N5`): danh sách bước dùng công cụ mượn đường dọc của cây thư mục
5
+ (`tree.md`) và màu vòng của dòng thời gian (`timeline.md`), gợi ý mượn nút viền
6
+ (`button.md`), lỗi mượn "Lỗi tải" (`empty-state.md`).
7
+
8
+ ```html
9
+ <div class="mx-auto flex h-full w-full max-w-3xl flex-col">
10
+ <ol class="flex-1 space-y-8 overflow-y-auto px-4 py-6"><!-- vùng tin cuộn, ô soạn đứng yên -->
11
+ <li class="space-y-6">
12
+ <!-- Tin người dùng: bong bóng bên phải -->
13
+ <p class="ml-auto w-fit max-w-[80%] rounded-3xl bg-surface px-4 py-2.5 text-base whitespace-pre-wrap [overflow-wrap:anywhere]">…</p>
14
+
15
+ <!-- Câu trả lời: không bong bóng, không avatar, thẳng mép trái cột -->
16
+ <div class="max-w-[55ch] space-y-3">
17
+ <!-- Không nền rê nên không px: chữ tự thẳng mép cột, không -ml-2 (button.md, N11) -->
18
+ <button class="flex h-8 cursor-pointer items-center gap-2 rounded-md text-sm text-muted outline-hidden hover:text-foreground" aria-expanded="false">
19
+ <!-- ô đầu size-4: ChevronRight (xoay 90° khi mở), hoặc LoaderCircle khi chưa có bước nào -->
20
+ Đã dùng 3 công cụ
21
+ </button>
22
+ <ul class="ml-[7px] space-y-3 border-l border-border-strong pl-5"><!-- đường dọc ở tâm chevron -->
23
+ <li class="flex gap-2">
24
+ <!-- Check size-4 text-muted | LoaderCircle animate-spin | CircleAlert text-red-600 -->
25
+ <div class="min-w-0">
26
+ <p class="text-sm text-foreground">Tra cứu đơn hàng</p>
27
+ <p class="text-xs text-muted">1.284 đơn đã giao, không tính 37 đơn hoàn</p>
28
+ </div>
29
+ </li>
30
+ </ul>
31
+ <div class="text-base text-pretty">…</div>
32
+ <!-- hàng thao tác: Sao chép, Tạo lại -->
33
+ </div>
34
+ </li>
35
+ </ol>
36
+ <!-- ô soạn, xem mục dưới -->
37
+ </div>
38
+ ```
39
+
40
+ ## Khung chat nằm trong trang, cạnh các card khác ⚑
41
+
42
+ Mẫu trên là khung chat **chiếm cả vùng nội dung**, nằm thẳng trên nền trang. Khung chat
43
+ là **một khối trong lưới** (trang chủ có chat ở giữa, số liệu bên phải) thì cả khung là
44
+ một card như các card cạnh nó (`card.md`): `bg-surface`, viền `border-border`, bo và bóng
45
+ theo card của dự án. Đầu khung, vùng tin, ô soạn cùng nằm trên nền card, không để vùng
46
+ tin trong suốt ra nền trang.
47
+
48
+ - Vùng tin nền card thì bong bóng tin người dùng đổi sang `bg-background` (bong bóng
49
+ `bg-surface` trên nền card là tàng hình). Ô soạn giữ viền `border-border-strong`.
50
+ - Kiểm: nhìn ở 1440px, mép khung chat có rõ như mép các card bên cạnh không. Probe báo
51
+ "Khung khai viền mà viền không thấy" khi nền trong, viền và nền ngoài gần như một màu.
52
+
53
+ Đừng để vùng tin nền trang: viền `#f7f7f8` còn nhạt hơn nền `#f4f4f6`, cả khung như trong suốt.
54
+
55
+ ## Hai phía
56
+
57
+ - **Câu trả lời không có avatar.** Bên nào nói đã rõ bằng bong bóng và căn phải; một vòng robot trước mỗi câu trả lời là lần nói thứ hai, và ăn mất ~56px bề ngang của cột chữ (`N3`). Các trợ lý AI dạng khung chính đều đã bỏ. Hệ thiết kế lớn khuyên khung nhúng hẹp phải tách hai bên bằng thứ khác ngoài căn lề: ở đây là nền bong bóng của tin người dùng, nên vẫn không cần avatar. Chỉ thêm avatar khi cuộc chat có **từ ba bên trở lên** (trợ lý + nhân viên hỗ trợ thật).
58
+ - **Tin người dùng**: `bg-surface` trên nền trang, `rounded-3xl`, `max-w-[80%]` (màn hẹp `max-w-[85%]`), giữ xuống dòng người gõ (`whitespace-pre-wrap`), đường dẫn liền một chuỗi thì bẻ ở bất kỳ đâu (`[overflow-wrap:anywhere]`, `N8`). Không màu nhấn, không bóng.
59
+ - **Câu trả lời `max-w-[55ch]`** (`T11`), `text-pretty`. Mã đơn, mã khách `font-mono` (`T17`), tiền theo đúng khuôn `đ` của app (`N5`).
60
+ - **Tin nhắn một cỡ, phụ trợ nhỏ hơn**: tin nhắn hai phía `text-base` (đây là chữ để đọc dài, như bài viết), mọi thứ phụ trợ (hàng công cụ, bước, gợi ý, "Đã dừng giữa chừng") `text-sm` hoặc `text-xs`. **Quá trình nhẹ hơn kết quả** (đừng để tên bước công cụ `text-base text-foreground`: nặng ngang câu trả lời ngay bên dưới).
61
+
62
+ ## Bước dùng công cụ
63
+
64
+ - **Hàng đầu** là nút mở đóng `text-sm text-muted`, rê vào chữ đậm lên. Ô đầu `size-4`: chevron khi có danh sách bước; **spinner khi chưa có bước nào** ("Đang soạn câu trả lời"). Bước đầu tiên về thì chevron thế đúng ô đó, chữ không xê (`N1`).
65
+ - **Icon bước**: xong là `Check` **xám**, không xanh lá: dùng công cụ là việc thường ngày, như việc lặp trong `timeline.md`. Đang chạy là spinner. Hỏng là `CircleAlert text-red-600`. Chỉ icon có màu, tên bước luôn `text-foreground` (`timeline.md`).
66
+ - **Mô tả bước nói công cụ đã làm gì, không nói trước kết quả**: phạm vi, nguồn, bộ lọc ("Quét 214 đơn của chi nhánh Thủ Đức trong quý 3", "Dùng số liệu quý 2 đã chốt ngày 05/07"). Kết quả là việc của câu trả lời. Sai: bước ghi "12 đại lý có đơn, tổng 409.000.000 đ", ngay dưới câu trả lời mở đầu "Quý 3 có 12 đại lý đặt hàng, tổng 409.000.000 đ" (`N3`).
67
+ - **Mở/đóng trượt bằng `grid-rows` 0fr ↔ 1fr**, phần đang đóng gắn `inert`; không render có điều kiện, không `hidden`, không `<details>` (`I30`, mẫu ở `accordion.md`).
68
+ - **Mặc định thu gọn**, cả lúc đang chạy lẫn lúc xong. Thu gọn mà đang chạy thì hàng đầu là **tên bước đang chạy** + spinner sau chữ ("Đang đọc bảng chi phí quảng cáo ◌"); xong thì "Đã dùng 3 công cụ".
69
+ - **Một việc đang chạy, một spinner.** Mở danh sách ra thì spinner chỉ còn ở hàng bước, hàng đầu đổi về "Đang dùng 2 công cụ" không quay. Sai: mở ra thấy tên bước và spinner hai lần, một ở hàng đầu một ở danh sách (`N3`).
70
+ - **Có bước lỗi mà vẫn trả lời được thì vẫn thu gọn**, hàng đầu "Đã dùng 2 công cụ, 1 lỗi" **cùng màu xám**; đỏ chỉ ở icon bước khi mở ra. Câu trả lời đã nói lỗi và cách xử lý rồi. Sai: tự mở danh sách, "1 lỗi" đỏ ở hàng đầu, icon đỏ, dòng mô tả bước, rồi câu trả lời: bốn lần một ý.
71
+ - Câu trả lời không dùng công cụ nào thì không có hàng này.
72
+
73
+ ## Đang chạy, dừng, lỗi
74
+
75
+ - **Chữ đang chạy ra**: một khối con trỏ nhạt ở cuối (`inline-block h-[1.1em] w-2 rounded-sm bg-foreground/30 align-text-bottom`), không nhấp nháy: chữ đang chạy đã là chuyển động. Chưa có gợi ý, chưa có hàng thao tác.
76
+ - **Nút gửi thành nút dừng**: cùng chỗ, cùng cỡ, cùng `primary`, icon `Square` đặc, `aria-label="Dừng trả lời"` + tooltip. Ô soạn **không khoá**, gõ trước câu sau được.
77
+ - **Người dùng bấm dừng**: giữ nguyên phần đã chạy ra, rồi **hàng thao tác như câu đã xong**, chữ `text-xs text-muted` "Đã dừng giữa chừng" đứng cùng hàng, sau hai icon. Không đỏ: người dùng tự dừng, không có gì hỏng (`M30`). Sai: dừng xong chỉ có dòng chữ, không Tạo lại, không Sao chép: muốn chạy tiếp phải gõ lại cả câu hỏi (`N6`, mỗi bước có lối ra).
78
+ - **Không trả lời được**: hai tầng như "Lỗi tải" (`empty-state.md`): `text-sm font-medium text-red-600` "Trợ lý chưa trả lời được", dưới là lý do `text-muted`, rồi nút viền `RotateCw` "Thử lại". **Căn trái** ở chỗ câu trả lời, không căn giữa như danh sách.
79
+
80
+ ## Hàng thao tác dưới câu trả lời
81
+
82
+ Nút chỉ icon `ghost` `size-8`, có tooltip và `aria-label`: `Copy` "Sao chép", `RotateCw` "Tạo lại". Icon đầu tiên thẳng cột với chữ câu trả lời: hàng `-ml-2`, **số âm giữ có chủ ý** (`N11` bước 4, `button.md` mục `ghost` cách 3), comment ngay trên dòng: `{/* -ml-2 bù px-2 của nút: icon đầu thẳng cột chữ câu trả lời; hàng nằm giữa cột chữ nên không bớt padding được (N11) */}`.
83
+
84
+ - Có ở câu đã chạy xong **và câu bị dừng**; không có ở câu đang chạy và câu lỗi (câu lỗi đã có nút Thử lại). **Câu cuối luôn hiện**; câu cũ hiện khi rê hoặc Tab tới (`opacity-0 group-hover:opacity-100 group-focus-within:opacity-100`), **vẫn giữ chỗ** để rê vào không xô (`N1`). Màn không có hover thì luôn hiện (`I11`).
85
+ - Không thêm like/dislike, chia sẻ, đọc to khi đề không nói. Đây là phần của mỗi câu trả lời (như ✕ của modal), không phải khối tự thêm; lúc giao báo một dòng.
86
+
87
+ ## Gợi ý hỏi tiếp
88
+
89
+ ```html
90
+ <div class="space-y-2">
91
+ <p class="text-sm text-muted">Hỏi tiếp</p>
92
+ <button class="block w-fit max-w-full cursor-pointer rounded-xl border border-border-strong bg-surface px-3 py-2 text-left text-sm text-foreground outline-hidden hover:bg-button-hover">
93
+ Chi nhánh cửa hàng nào giảm nhiều nhất?
94
+ </button>
95
+ </div>
96
+ ```
97
+
98
+ - **Nút viền** (`I1`), chữ `text-foreground`. Sai: nền xám `secondary` + chữ xám trên nền trang xám, ba khối đọc ra là nút bị khoá (`I8`), lại là thứ nặng nhất dưới câu trả lời.
99
+ - **Chỉ dưới câu trả lời cuối, đã xong.** Gửi câu mới thì gợi ý cũ biến mất.
100
+ - **Mỗi gợi ý một dòng, tối đa ~45 ký tự, 2–3 gợi ý.** Xếp dọc, cột gợi ý không rộng quá cột câu trả lời. Sai: gợi ý 85 ký tự vỡ hai dòng và chạy qua mép phải của câu trả lời tới tận mép ô soạn.
101
+ - Bấm gợi ý gọi gì là handler rỗng (`onSuggestionSelect`, `N10`).
102
+
103
+ ## Rỗng: cuộc trò chuyện mới
104
+
105
+ - **Không có câu "Chưa có tin nhắn nào".** Ở đây người dùng là người mở lời, câu báo rỗng không nói họ làm gì tiếp (`N6`). Thay bằng **2–3 gợi ý mở đầu**, cùng khuôn gợi ý hỏi tiếp, nhãn "Thử hỏi", nằm **ngay trên ô soạn**, đúng chỗ gợi ý hỏi tiếp vẫn nằm trong cuộc trò chuyện (`N5`).
106
+ - Không câu chào to, không logo, không hình (`empty-state.md`). Placeholder của ô soạn đã nói hỏi được gì, câu chào nhắc lại là nói hai lần (`N3`).
107
+
108
+ ## Ô soạn
109
+
110
+ ```html
111
+ <form class="mx-4 mb-4 flex items-end gap-2 rounded-3xl border border-border-strong bg-surface p-2 focus-within:border-focus focus-within:ring-2 focus-within:ring-focus">
112
+ <textarea rows="1" class="max-h-60 min-h-10 flex-1 resize-none bg-transparent px-3 py-2 text-base outline-hidden placeholder:text-muted" placeholder="Hỏi về đơn hàng, doanh thu"></textarea>
113
+ <button class="grid size-10 shrink-0 cursor-pointer place-items-center rounded-2xl bg-primary text-primary-foreground outline-hidden hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-30" aria-label="Gửi" disabled>
114
+ <!-- ArrowUp size-4. Đang chạy: Square size-3.5 fill-current, aria-label="Dừng trả lời", không disabled -->
115
+ </button>
116
+ </form>
117
+ ```
118
+
119
+ - **Dính đáy**, rộng bằng cột tin. Vùng tin cuộn phía trên, ô soạn không cuộn theo.
120
+ - **Tự cao theo chữ** từ một dòng tới `max-h-60` (~8 dòng) rồi cuộn trong ô. Nút gửi **bám góc dưới phải** (`items-end`), không trôi lên giữa khi ô cao lên.
121
+ - **Bo lồng nhau** (`M19`): ô `rounded-3xl` (24px) = nút `rounded-2xl` (16px) + `p-2`. Lệch thì nút sát mép ô ~4px, góc nút và góc ô không song song.
122
+ - **Ô trống thì nút gửi khoá `opacity-30`**, không `opacity-50` như mọi nút khoá khác. Ngoại lệ có lý do: đây là nút khoá **đứng thường trực** trên màn, `primary` mờ 50% ra khối xám giữa, thành thứ đậm nhất màn rỗng.
123
+ - Focus giống ô nhập (`F20`): viền `--border-focus` + ring mờ, trên cả khung, không chỉ quanh textarea.
124
+ - Bàn phím (`N9`): Enter gửi, Shift+Enter xuống dòng. Gửi là handler rỗng (`onSend`).
125
+ - Người dùng cuộn lên đọc lại trong lúc chữ đang chạy: hiện một nút tròn `ArrowDown` viền, `size-8`, giữa cột ngay trên ô soạn, bấm là xuống cuối. Có kéo tự động hay không là logic của người dùng (`N10`).
126
+
127
+ ## Trạng thái phải dựng
128
+
129
+ Mỗi cái một ví dụ tĩnh (`N2`): xong có gợi ý · đang chạy chữ · đang dùng công cụ
130
+ chưa có chữ · đang soạn chưa có công cụ · một công cụ lỗi vẫn trả lời · người
131
+ dùng bấm dừng · không trả lời được · cuộc trò chuyện mới · ca biên (tin rất dài,
132
+ đường dẫn liền một chuỗi, ô soạn nhiều dòng).