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