@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,430 @@
|
|
|
1
|
+
# Bố cục form và xác thực
|
|
2
|
+
|
|
3
|
+
Không có wireframe thì dựng bố cục mặc định (hoặc cái mà điều kiện trong đề
|
|
4
|
+
chọn), rồi báo một dòng lúc giao. Xem câu 4 trong `../../SKILL.md`.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Đăng nhập, đăng ký
|
|
9
|
+
|
|
10
|
+
**A. Một cột giữa màn** (mặc định, hợp mọi trường hợp)
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
┌─────────────────┐
|
|
14
|
+
│ logo │
|
|
15
|
+
│ Tiêu đề │
|
|
16
|
+
│ (câu dẫn) │
|
|
17
|
+
│ │
|
|
18
|
+
│ nhãn │
|
|
19
|
+
│ [ô nhập ] │
|
|
20
|
+
│ nhãn quên mk? │
|
|
21
|
+
│ [ô nhập ] │
|
|
22
|
+
│ (chỗ câu lỗi) │
|
|
23
|
+
│ [ NÚT ] │
|
|
24
|
+
│ ─── hoặc ─── │
|
|
25
|
+
│ [ nút Google ] │
|
|
26
|
+
│ chưa có tk? Tạo │
|
|
27
|
+
└─────────────────┘
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Khối rộng `max-w-md`. **Không viền** — đây là card đứng một mình, xem `M29`.
|
|
31
|
+
Không căn giữa chữ trong form, chỉ căn giữa cả khối.
|
|
32
|
+
|
|
33
|
+
- **Logo sản phẩm luôn có**, đặt trên tiêu đề, dùng đúng dấu hiệu đang nằm ở sidebar
|
|
34
|
+
(dự án có component logo thì import, chưa có thì dựng theo `../components/logo.md`).
|
|
35
|
+
Màn xác thực là chỗ duy nhất người dùng chưa vào app; thiếu logo thì trang đọc như
|
|
36
|
+
form quản trị không biết của ai, và người dùng không chắc mình đang đăng nhập đúng
|
|
37
|
+
chỗ. Logo ở đây là cấu trúc, không phải "thêm logo" mà `S3` cấm.
|
|
38
|
+
- **Câu dẫn chỉ khi có điều để nói** (vd "Dùng thử 14 ngày, không cần thẻ" khi đề cho
|
|
39
|
+
biết). Không bịa câu chào kiểu "Chào mừng bạn quay lại!" cho đủ khuôn.
|
|
40
|
+
|
|
41
|
+
**B. Hai cột, form trái ảnh phải** (hợp khi muốn chèn lời chứng thực hoặc ảnh sản phẩm)
|
|
42
|
+
|
|
43
|
+
```
|
|
44
|
+
┌──────────────┬──────────────┐
|
|
45
|
+
│ form như A │ ảnh / trích │
|
|
46
|
+
│ │ dẫn khách │
|
|
47
|
+
└──────────────┴──────────────┘
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Mặc định cho màn xác thực — dựng luôn, báo một dòng
|
|
53
|
+
|
|
54
|
+
**Đừng hỏi bốn câu.** Người dùng gõ "dựng màn đăng nhập" là muốn thấy màn đăng
|
|
55
|
+
nhập, không muốn làm một bài khảo sát. Dựng theo mặc định dưới đây, rồi **nói
|
|
56
|
+
một dòng** cho họ biết mình đã chọn gì.
|
|
57
|
+
|
|
58
|
+
| Thứ | Mặc định | Vì sao |
|
|
59
|
+
| --- | --- | --- |
|
|
60
|
+
| **"Quên mật khẩu?"** | **Có** | Thiếu nó thì người quên mật khẩu không còn đường nào vào. Đây là phần tử duy nhất mà thiếu là hỏng chức năng, không phải hỏng thẩm mỹ |
|
|
61
|
+
| **Ghi nhớ đăng nhập** | **Không** | Nó đổi thời hạn phiên ở **backend**, không phải chỉ là cái checkbox. Vẽ ra mà backend không làm gì là lừa người dùng — sai nhiều hơn là thiếu |
|
|
62
|
+
| **Đăng nhập mạng xã hội** | **Có, một nút Google** ("Đăng nhập bằng Google" / "Đăng ký bằng Google") | Gần như mọi app có màn đăng nhập đều có nút này, người dùng tìm nó trước cả ô email. Thiếu là trang trông như bản dựng dở. Handler để rỗng, báo lúc giao là cần nối nhà cung cấp. Sản phẩm cho lập trình viên thì thêm GitHub. Cả đăng nhập lẫn đăng ký đều có, và cùng bộ nhà cung cấp |
|
|
63
|
+
| **Placeholder** | **Có**, ngoại lệ của `T25` | Màn đứng một mình, cả trang chỉ có vài ô: ô trống trơn trông như chưa dựng xong. Chữ ở mục dưới |
|
|
64
|
+
| **Con trỏ lúc mở màn** | **Nằm sẵn ở ô đầu** (`autoFocus`) | Cả màn chỉ có một việc là gõ. Áp cho **mọi** màn trong luồng: đăng nhập, đăng ký, từng bước quên mật khẩu, OTP. Bước này có bước kia không thì sang bước mới người dùng phải chạm lại vào ô |
|
|
65
|
+
| **"Nhập lại mật khẩu"** | **Không** | Ô mật khẩu đã có nút mắt để xem lại. Hầu hết app đã bỏ ô gõ lại; thêm nó là thêm một ô cho mỗi người đăng ký để phòng lỗi mà nút mắt đã phòng rồi |
|
|
66
|
+
| **Chiều cao ô và nút** | **`h-12`** | Ngoại lệ duy nhất của `h-11 md:h-10` (`budgets.md`): form đứng một mình giữa trang, cả màn chỉ có nó, ô to hơn một bậc là hợp. Form trong app, modal, cài đặt thì không |
|
|
67
|
+
|
|
68
|
+
**Dòng báo, đặt chung với dòng báo bố cục lúc giao:**
|
|
69
|
+
|
|
70
|
+
> Mình dựng kèm "Quên mật khẩu?" và nút Google (nút chưa nối, cần cấu hình đăng nhập
|
|
71
|
+
> Google ở backend); chưa có ghi nhớ đăng nhập vì nó cần backend đổi thời hạn phiên.
|
|
72
|
+
> Muốn thêm nhà cung cấp khác hay bỏ Google thì nói.
|
|
73
|
+
|
|
74
|
+
### Placeholder trên màn xác thực
|
|
75
|
+
|
|
76
|
+
Câu hướng dẫn ngắn, không dùng email mẫu (`ten@congty.com` bị đọc nhầm thành chữ đã
|
|
77
|
+
điền sẵn, `T25`), không dùng `••••••` (`T26`):
|
|
78
|
+
|
|
79
|
+
| Ô | Placeholder |
|
|
80
|
+
| --- | --- |
|
|
81
|
+
| Họ và tên | `Nhập họ và tên` |
|
|
82
|
+
| Email | `Nhập email` |
|
|
83
|
+
| Mật khẩu (đăng nhập) | `Nhập mật khẩu` |
|
|
84
|
+
| Mật khẩu (đăng ký) | `Tạo mật khẩu`, dòng gợi ý "Ít nhất 8 ký tự" vẫn nằm **dưới ô** vì nó phải còn đó lúc đang gõ |
|
|
85
|
+
|
|
86
|
+
Câu lỗi vẫn không được trùng placeholder: "Chưa nhập email", không phải "Nhập email".
|
|
87
|
+
Ngoại lệ này chỉ cho màn xác thực đứng một mình. Form trong app, modal, cài đặt vẫn
|
|
88
|
+
theo `T25`.
|
|
89
|
+
|
|
90
|
+
Một dòng, không phải một bảng câu hỏi. Người dùng **không nhắc gì** thì coi như
|
|
91
|
+
đồng ý, dựng theo mặc định, đi tiếp.
|
|
92
|
+
|
|
93
|
+
**Chưa nối backend thì gửi hợp lệ vẫn phải đi tiếp.** Bấm "Đăng ký" với form đúng mà
|
|
94
|
+
không có gì xảy ra thì người duyệt tưởng nút hỏng. Giả lập: nút
|
|
95
|
+
quay spinner ~1 giây, rồi sang bước kế tiếp nếu đã dựng (đăng nhập → trang đầu của app, đăng ký → nhập OTP → trang đầu của app, quên mật
|
|
96
|
+
khẩu → nhập mã, mật khẩu mới → màn xong). Hàm gọi API vẫn để trống, có comment chỗ nối.
|
|
97
|
+
|
|
98
|
+
**Bước sau hiện đúng thứ người dùng vừa gõ.** Gõ `an@congty.vn` ở trang đăng ký thì trang
|
|
99
|
+
OTP ghi "Mã … vừa được gửi tới an@congty.vn", không phải email mẫu của trang OTP (người duyệt
|
|
100
|
+
tưởng mã gửi nhầm địa chỉ). Chuyền qua state của router; email mẫu chỉ dùng khi mở thẳng trang OTP bằng link, và
|
|
101
|
+
ở trang `/states`. Cùng luật `S6`: dữ liệu giả của một luồng phải khớp nhau giữa các bước.
|
|
102
|
+
|
|
103
|
+
**Họ nói muốn thêm thì thêm ngay, đừng hỏi lại.** "Thêm remember me" là đủ rõ —
|
|
104
|
+
dựng luôn, không hỏi "bro muốn tích sẵn không, đặt ở đâu". Mặc định phần dưới
|
|
105
|
+
đã trả lời hết mấy câu đó rồi.
|
|
106
|
+
|
|
107
|
+
### Ghi nhớ đăng nhập — khi họ yêu cầu
|
|
108
|
+
|
|
109
|
+
- **Không tích sẵn.** Tích sẵn là quyết định hộ người dùng về bảo mật.
|
|
110
|
+
- Nhãn **"Ghi nhớ đăng nhập"**, đừng dịch thẳng "Nhớ tôi" — tiếng Việt đọc ra như máy dịch (`T24`).
|
|
111
|
+
- Đặt cùng hàng với "Quên mật khẩu?": checkbox trái, link phải.
|
|
112
|
+
- Nói một câu lúc giao: cái này cần backend đổi thời hạn phiên, không chỉ là checkbox.
|
|
113
|
+
- App có dữ liệu nhạy cảm hoặc hay dùng trên máy chung — ngân hàng, hồ sơ sức khoẻ, quản trị nội bộ — thì **nói một câu khuyên bỏ**, rồi vẫn làm theo ý họ.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## "Quên mật khẩu?"
|
|
118
|
+
|
|
119
|
+
**Nằm cùng hàng với nhãn "Mật khẩu", căn phải.** Không nằm dưới ô nhập, không
|
|
120
|
+
nằm dưới nút submit.
|
|
121
|
+
|
|
122
|
+
```html
|
|
123
|
+
<div class="relative flex flex-col gap-2">
|
|
124
|
+
<label for="password" class="w-fit cursor-pointer text-sm font-medium">Mật khẩu</label>
|
|
125
|
+
<div class="relative"><!-- ô mật khẩu + nút mắt --></div>
|
|
126
|
+
<!-- Đứng SAU ô trong DOM, absolute lên hàng nhãn -->
|
|
127
|
+
<a href="/quen-mat-khau" class="absolute top-0 right-0 text-sm text-foreground outline-hidden hover:underline">Quên mật khẩu?</a>
|
|
128
|
+
</div>
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
**Thứ tự Tab: email → mật khẩu → nút mắt → "Quên mật khẩu?" → Đăng nhập.** Link đặt
|
|
132
|
+
trong hàng nhãn bằng `flex justify-between` thì nó đứng trước ô mật khẩu trong DOM: gõ
|
|
133
|
+
email xong bấm Tab là rơi vào link, bấm Enter theo thói quen là rời trang đăng nhập. Nên link nằm sau ô trong DOM, `absolute top-0 right-0` để vẫn hiện ở
|
|
134
|
+
hàng nhãn; vị trí trên màn không đổi một pixel. Đừng chữa bằng `tabindex="-1"`: người
|
|
135
|
+
dùng bàn phím mất luôn lối vào link.
|
|
136
|
+
|
|
137
|
+
**Vì sao không để dưới ô nhập** — dòng đó đã có chủ: gợi ý lúc thường, câu lỗi
|
|
138
|
+
khi sai. Và "gõ sai mật khẩu" chính là lúc link này cần rõ nhất, nên hai thứ đạt
|
|
139
|
+
đỉnh cùng lúc ở cùng một chỗ. Hàng nhãn thì luôn chỉ có một dòng, không bao giờ
|
|
140
|
+
đụng.
|
|
141
|
+
|
|
142
|
+
**Vì sao không để dưới nút submit** — ở đó nó lẫn vào khu "hoặc / đăng nhập bằng
|
|
143
|
+
Google / chưa có tài khoản?", thành một link tình cờ nằm giữa một đống link.
|
|
144
|
+
|
|
145
|
+
Ba thông số:
|
|
146
|
+
|
|
147
|
+
| | Lấy gì | Vì sao |
|
|
148
|
+
| --- | --- | --- |
|
|
149
|
+
| Cỡ | `text-sm`, **bằng nhãn** | Cùng hàng mà lệch cỡ thì trông như canh hụt. Nhỏ hơn cũng làm vùng chạm mobile hẹp lại |
|
|
150
|
+
| Màu | `--foreground`, **không làm mờ** | Chữ nhạt đọc ra là đã bị khoá (`I8`). Đây là lối thoát duy nhất của người không vào được tài khoản — làm nó trông disabled là chặn đúng người đang cần |
|
|
151
|
+
| Độ đậm | `font-normal` (nhãn là `font-medium`) | Phân cấp bằng **một** thứ thôi. `M13`: thứ bậc đến từ cỡ chữ, độ đậm, màu chữ — dùng cả ba cùng lúc là thừa |
|
|
152
|
+
|
|
153
|
+
**Là link, không phải nút.** Trong form mà thành nút thì nó cạnh tranh với nút đăng
|
|
154
|
+
nhập nằm ngay dưới. Có `cursor-pointer`, gạch chân khi hover. Khác "Xem tất cả" của
|
|
155
|
+
dashboard (`I7`, cũng là link) ở màu: ở đây `--foreground` không làm mờ, vì là lối
|
|
156
|
+
thoát duy nhất của người không vào được tài khoản.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Luồng quên mật khẩu
|
|
161
|
+
|
|
162
|
+
Ba bước và một màn xong, cùng một card, cùng khuôn với màn đăng nhập: logo, ô `h-12`,
|
|
163
|
+
placeholder. Gửi mã 6 số hay gửi link trong email là chuyện backend; mặc định dựng mã
|
|
164
|
+
vì cả luồng nằm trong một tab, báo một dòng lúc giao.
|
|
165
|
+
|
|
166
|
+
1. **Nhập email.** Tiêu đề "Quên mật khẩu", câu dẫn nói sẽ nhận được gì ("Nhập email
|
|
167
|
+
đã đăng ký, mã đặt lại mật khẩu sẽ được gửi tới đó"). Đến từ trang đăng nhập mà ô
|
|
168
|
+
email đã gõ thì điền sẵn. Nút "Gửi mã". Dưới card là link **"Quay lại đăng nhập"**,
|
|
169
|
+
cách hầu hết app gọi lối ra này.
|
|
170
|
+
2. **Nhập mã**, theo `components/otp-input.md`. Câu dẫn **không xác nhận email có tài
|
|
171
|
+
khoản hay không**: "Nếu email này đã đăng ký, mã gồm 6 số đã được gửi tới …". Báo
|
|
172
|
+
"Email chưa đăng ký" là cho người lạ dò xem ai có tài khoản.
|
|
173
|
+
3. **Mật khẩu mới.** Câu dẫn nói đang đổi cho tài khoản nào: "Cho tài khoản
|
|
174
|
+
**an@congty.vn**" (email in bằng `EmailText`). Một ô "Mật khẩu mới" có nút mắt, gợi ý
|
|
175
|
+
"Ít nhất 8 ký tự" dưới ô, **không có ô nhập lại** (cùng lý do với trang đăng ký, bảng
|
|
176
|
+
mặc định ở trên). Nút "Lưu mật khẩu". Form có ô `username` ẩn mang email đó để trình
|
|
177
|
+
quản lý mật khẩu lưu đúng tài khoản (`I28`).
|
|
178
|
+
- **Phiên đặt lại hết hạn thì thay cả form** bằng câu báo và một nút `primary`
|
|
179
|
+
"Gửi mã mới" (`I3`: lối ra duy nhất) (gửi lại tới email cũ, sang bước 2). Câu báo nói luôn điều người
|
|
180
|
+
dùng lo: "Mật khẩu cũ chưa bị đổi". Đừng để ô mật khẩu và nút "Lưu mật khẩu" nằm
|
|
181
|
+
dưới khối lỗi: bấm Lưu lần nữa vẫn hỏng, và khối lỗi có nút riêng thì màn có hai
|
|
182
|
+
nút tranh nhau làm việc chính (`N5`).
|
|
183
|
+
4. **Xong.** "Đã đổi mật khẩu", nút "Đăng nhập" về trang đăng nhập với email điền sẵn.
|
|
184
|
+
Backend cho đăng nhập luôn sau khi đổi thì bỏ màn này, vào thẳng app; báo lúc giao.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## Nút đăng nhập mạng xã hội
|
|
189
|
+
|
|
190
|
+
Số lượng quyết định bố cục:
|
|
191
|
+
|
|
192
|
+
| Số nút | Bố cục |
|
|
193
|
+
| --- | --- |
|
|
194
|
+
| 1–2 | Xếp dọc, full width, có chữ: `Đăng nhập bằng Google`. Nút viền (`variant="outline"`), logo gốc trái chữ, cao bằng ô nhập |
|
|
195
|
+
| **3 trở lên** | **A** bên dưới. Báo một dòng lúc giao: muốn xếp dọc đủ chữ thì nói |
|
|
196
|
+
|
|
197
|
+
**Đường chia "hoặc"** giữa nút chính và nút mạng xã hội:
|
|
198
|
+
|
|
199
|
+
```html
|
|
200
|
+
<div class="my-6 flex items-center gap-3">
|
|
201
|
+
<span class="h-px flex-1 bg-border-strong"></span>
|
|
202
|
+
<span class="text-xs text-muted">hoặc</span>
|
|
203
|
+
<span class="h-px flex-1 bg-border-strong"></span>
|
|
204
|
+
</div>
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Kẻ bằng **`--border-strong`**, không `--border`. `--border` (`#f7f7f8`) là đường tóc giữa các
|
|
208
|
+
hàng danh sách, nơi có chữ hai bên đỡ mắt; đứng một mình trên card trắng thì nó chỉ
|
|
209
|
+
1.06 : 1, hai vạch biến mất, còn trơ chữ "hoặc" lơ lửng giữa hai nút.
|
|
210
|
+
|
|
211
|
+
Xếp dọc 3–4 nút full width thì phần mạng xã hội **dài hơn cả form thật**, và
|
|
212
|
+
người dùng phải cuộn qua một dãy nút giống hệt nhau mới thấy ô email. Thứ chính
|
|
213
|
+
của màn bị đẩy lên trên thành thứ phụ.
|
|
214
|
+
|
|
215
|
+
**A. Chia cột, chỉ icon** (mặc định khi có từ 3 nút)
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
│ [ Đăng nhập ] │
|
|
219
|
+
│ ─── hoặc ─── │
|
|
220
|
+
│ [ G ] [ GH ] [ X ] │
|
|
221
|
+
└────────────────────┘
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- Nút vuông, cao bằng ô nhập, chia đều `grid-cols-3`.
|
|
225
|
+
- **Bắt buộc có `aria-label`** — không có chữ thì trình đọc màn hình chỉ thấy một cái nút trống.
|
|
226
|
+
- Logo giữ màu gốc theo `F16`, đừng tô xám cho "đồng bộ".
|
|
227
|
+
- Quá 4 nút thì không xếp một hàng nữa: giữ 2–3 cái dùng nhiều nhất, phần còn lại bỏ hẳn.
|
|
228
|
+
|
|
229
|
+
**B. Vẫn xếp dọc, đủ chữ** (chỉ khi người dùng yêu cầu)
|
|
230
|
+
|
|
231
|
+
Chữ đầy đủ đọc rõ hơn icon trần, đổi lại tốn chiều dọc. Chọn B thì nói rõ
|
|
232
|
+
đánh đổi đó lúc giao.
|
|
233
|
+
|
|
234
|
+
**Nhớ đâu là cái chính.** Ô email và nút đăng nhập là nhân vật chính của màn,
|
|
235
|
+
khối mạng xã hội là đường tắt. Khối tắt mà chiếm nhiều chỗ hơn đường chính thì
|
|
236
|
+
bố cục đã sai, dù từng nút đều đẹp.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Đánh dấu trường bắt buộc
|
|
241
|
+
|
|
242
|
+
Người dùng phải biết trường nào bắt buộc **trước khi** bấm gửi, không phải sau
|
|
243
|
+
khi bị báo lỗi. Chọn một trong hai cách theo tỉ lệ, đừng dùng cả hai:
|
|
244
|
+
|
|
245
|
+
| Tình huống | Cách đánh dấu |
|
|
246
|
+
| --- | --- |
|
|
247
|
+
| Đa số trường bắt buộc | Ghi `Không bắt buộc` bên cạnh nhãn của số ít trường tuỳ chọn |
|
|
248
|
+
| Đa số trường tuỳ chọn | Ghi dấu `*` sau nhãn của trường bắt buộc, kèm một dòng chú thích ở đầu form |
|
|
249
|
+
|
|
250
|
+
Dấu `*` **tô đỏ**, cùng sắc với câu lỗi (`text-red-600 dark:text-red-400`), ngoại lệ có tên ở `M9`,
|
|
251
|
+
`aria-hidden="true"`; ô vẫn mang `required` để trình đọc màn hình đọc "bắt buộc".
|
|
252
|
+
Đây là quy ước số đông: người dùng đã quen "* đỏ = phải điền", để xám thì họ phải
|
|
253
|
+
dừng lại tự hỏi dấu đó nghĩa là gì.
|
|
254
|
+
|
|
255
|
+
Dấu `*` trong dòng chú thích ("Mục có dấu * là bắt buộc") cũng đỏ y như ở nhãn: chú thích giải nghĩa một ký hiệu thì phải in đúng ký hiệu đó, xám ở đây đỏ ở kia là hai dấu khác nhau.
|
|
256
|
+
|
|
257
|
+
```html
|
|
258
|
+
<label for="full-name" class="text-sm font-medium">
|
|
259
|
+
Họ và tên <span aria-hidden="true" class="text-red-600 dark:text-red-400">*</span>
|
|
260
|
+
</label>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## Form nhiều trường
|
|
266
|
+
|
|
267
|
+
**A. Một cột dọc** (mặc định, dưới 8 trường)
|
|
268
|
+
|
|
269
|
+
Nhãn nằm trên ô nhập, không nằm cạnh. Trường liên quan nhau thì gom nhóm, cách
|
|
270
|
+
nhóm khác bằng khoảng trắng lớn hơn, không bằng đường kẻ.
|
|
271
|
+
|
|
272
|
+
**B. Chia mục có tiêu đề** (từ 8 trường trở lên)
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
Thông tin cá nhân
|
|
276
|
+
[ô] [ô]
|
|
277
|
+
[ô ]
|
|
278
|
+
|
|
279
|
+
Địa chỉ
|
|
280
|
+
[ô ]
|
|
281
|
+
[ô] [ô] [ô]
|
|
282
|
+
|
|
283
|
+
[Huỷ] [LƯU]
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Nút hành động nằm cuối, căn phải, primary bên phải cùng.
|
|
287
|
+
|
|
288
|
+
**Ô tuỳ chọn ít người đụng tới** (mã, tuỳ chọn kỹ thuật, đều có mặc định): gom vào một
|
|
289
|
+
khu thu gọn "Cài đặt nâng cao" ở cuối các ô, dựng là dòng chữ có chevron, không khung, ô
|
|
290
|
+
bên trong thẳng cột với ô ngoài. Mẫu ở "Khu thu gọn trong form" trong
|
|
291
|
+
`../components/accordion.md`.
|
|
292
|
+
|
|
293
|
+
**C. Nhiều bước** (khi **các bước phụ thuộc nhau** hoặc là luồng làm một lần: onboarding, thanh toán, đăng ký hồ sơ. Form dài mà các phần độc lập thì dùng kiểu B có mục, như trang cài đặt — số trường nhiều không phải lý do chia bước)
|
|
294
|
+
|
|
295
|
+
Thanh bước ở trên, mỗi bước một màn, nút "Quay lại" và "Tiếp" ở đáy. Không dùng
|
|
296
|
+
nhiều bước cho form ngắn, nó chỉ làm chậm.
|
|
297
|
+
|
|
298
|
+
**Thanh các bước:**
|
|
299
|
+
|
|
300
|
+
```
|
|
301
|
+
(✓)━━━━━━━━━━━━━━━━(2)─────────────────(3)
|
|
302
|
+
Thông tin công ty **Người liên hệ** Xác nhận
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
| Trạng thái | Vòng `size-8 rounded-full` | Nhãn | Đường nối phía sau |
|
|
306
|
+
| --- | --- | --- | --- |
|
|
307
|
+
| Đã xong | nền `primary`, icon `check` `size-4` `primary-foreground` | `text-foreground` | `h-0.5 bg-primary` |
|
|
308
|
+
| Đang làm | `border-2 border-foreground bg-surface`, số `font-semibold` | `font-semibold text-foreground` | `h-0.5 bg-secondary` |
|
|
309
|
+
| Chưa tới | nền `bg-secondary`, số `text-muted` | `text-muted` | `h-0.5 bg-secondary` |
|
|
310
|
+
| Có lỗi | nền `bg-red-600`, chữ `!` `text-sm font-bold text-white` (ký tự, không icon) | `text-red-700`, câu lỗi `text-sm text-red-600` dưới nhãn | như trạng thái của nó |
|
|
311
|
+
|
|
312
|
+
- **Phần "chưa tới" dùng `--secondary`, không `--background`, không `--border`.** Thanh các bước thường đứng trên nền trang, ngoài card. Vòng `bg-background` trùng đúng màu nền trang nên tan mất, chỉ còn con số "3" lơ lửng cuối đường nối; `--border` (#f7f7f8) còn **sáng hơn** nền trang (#f4f4f6), đường nối và đoạn thanh mảnh chưa tới gần như vô hình. `--secondary` (#e7e8ec) đọc được cả trên nền trang lẫn trên card trắng.
|
|
313
|
+
- **Vòng lỗi là vòng đặc đỏ với dấu `!`, không phải vòng viền đỏ bọc icon `circle-alert`.** Icon đó tự có một vòng tròn, đặt vào vòng viền thành hai vòng lồng nhau, nhìn rối và nhỏ xíu. Vòng đặc cùng khuôn với bước đã xong (đặc + ký hiệu), chỉ đổi màu và ký hiệu.
|
|
314
|
+
- Dựng bằng `<ol>`, bước đang làm có `aria-current="step"`. Các bước `flex-1`, **riêng bước cuối `flex-none`** (không đường nối, không `pr`): hàng trải đúng từ mép trái tới mép phải của card bên dưới. Để bước cuối `flex-1` thì cột cuối chỉ có vòng và một chữ ngắn, hở một mảng bên phải, cả thanh lệch trái so với card.
|
|
315
|
+
- **Thanh ngang chỉ có nhãn, không mô tả dưới nhãn**, khi mỗi bước đã có tiêu đề và câu dẫn trong card (mặc định, xem "Khung một bước" dưới). Mô tả ở thanh cộng câu dẫn trong card là hai câu cho một ý, cách nhau 100px (`N3`). Thanh ghi "Có thể để trống, mời sau", câu dẫn ghi "Chưa cần thì để trống, mời sau ở trang Thành viên", nhãn ô ghi "Không bắt buộc": ba lần "không bắt buộc" trên một màn. Mô tả dưới nhãn chỉ dùng khi bên cạnh không có tiêu đề bước riêng (thường là thanh dọc bên trái form dài); khi đó `text-sm text-muted text-pretty`, cho xuống dòng, không `truncate`. Thiếu `text-pretty` là trơ một chữ ở dòng cuối.
|
|
316
|
+
- **Bước đã xong bấm được để quay lại** (vòng + nhãn là một nút, `cursor-pointer`, hover nhãn gạch chân). Bước chưa tới không bấm được. Có cho nhảy cóc tới bước chưa tới hay không là logic, người dùng quyết. **Đang gửi ở bước cuối thì các bước đã xong `disabled`** như nút "Quay lại": trông bấm được (rê vào gạch chân) mà bấm không đi đâu là nói sai.
|
|
317
|
+
- **Màn hẹp dưới `sm` thu gọn**, không cố nhét ba cột: một dòng "Bước 2 / 3" `text-sm font-medium tabular-nums` + một thanh mảnh `h-1` chia đoạn theo số bước. **Tên bước không ghi ở dòng này khi card ngay dưới đã có tiêu đề bước**: "Bước 1 / 3 · Thông tin workspace" rồi tiêu đề card "Thông tin workspace" cách 50px là lặp, cùng lý do đường dẫn không ghi trang đang đứng (`layouts/app.md`). Đoạn bước đã xong `bg-primary`, **đoạn bước đang làm `bg-primary/30`** (nửa đậm), chưa tới `bg-secondary`. Ba tầng như ba kiểu vòng ở màn rộng. Không tô đậm đủ đoạn đang làm: đứng ở bước cuối sẽ trông y như đã xong hết. Cũng không để đoạn đang làm xám như chưa tới: chữ ghi "Bước 2 / 3" mà thanh chỉ sáng một đoạn, đọc như thanh bị thiếu. **Ca có ghi tên bước** ("Bước 2 / 3 · Người liên hệ", khi bên dưới không có tiêu đề bước riêng): **tên được xuống dòng**, `text-pretty`, không `truncate`: tên bước là thứ người dùng cần đọc (`N8`), và đổi bước là đổi cả màn nên dòng chữ cao thêm một dòng không tính là nhảy (`N1`). Cụm "Bước 2 / 3 ·" `whitespace-nowrap` để không bị bẻ đôi. Mô tả ẩn. Không để các cột bước wrap thành hai hàng (`R6`). Có bước lỗi thì đoạn của bước đó đỏ, và **thêm một dòng `text-xs text-red-600` dưới thanh nói bước nào sai** ("Bước 1 còn thiếu mã số thuế"), bấm được để quay lại. Chỉ có đoạn đỏ mà không có chữ thì màn hẹp không biết sai ở đâu.
|
|
318
|
+
- Không quá 5 bước. Hơn nữa là form đang cần gộp bước lại.
|
|
319
|
+
|
|
320
|
+
**Khung một bước:**
|
|
321
|
+
|
|
322
|
+
```
|
|
323
|
+
Tạo workspace mới <- tên trang, một lần
|
|
324
|
+
(1)━━━━━━━━━━(2)──────────(3) <- thanh các bước, chỉ nhãn
|
|
325
|
+
┌──────────────────────────────────┐
|
|
326
|
+
│ Thông tin workspace │ <- h2 = nhãn bước, text-base font-semibold
|
|
327
|
+
│ Một câu dẫn nói điều chưa ai nói │ <- text-sm text-muted
|
|
328
|
+
│ [ô] [ô] │
|
|
329
|
+
│ ──────────────────────────────── │
|
|
330
|
+
│ [Huỷ | Quay lại] [Tiếp] │
|
|
331
|
+
└──────────────────────────────────┘
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
- **Mỗi thông tin nói một lần.** Tên bước: thanh các bước (bản đồ cả luồng, như mục đang chọn ở sidebar) và tiêu đề card. Câu dẫn chỉ nói thứ chưa nằm ở chỗ khác: vì sao cần, dùng vào đâu, mời sau ở đâu. **"Không bắt buộc" chỉ ở cạnh nhãn ô** (mục "Đánh dấu trường bắt buộc"), không nhắc lại ở câu dẫn hay ở thanh.
|
|
335
|
+
- **Tiêu điểm**: mở trang thì con trỏ nằm sẵn ở ô đầu của bước 1 (`autoFocus`, cùng lý do màn đăng nhập ở trên). Sang bước khác thì đưa tiêu điểm lên `h2` (`tabIndex={-1}`, `outline-hidden`): trình đọc màn hình đọc tên bước mới, và ở màn hẹp trang về đầu bước thay vì đứng ở hàng nút dưới đáy.
|
|
336
|
+
- **Hàng nút đáy**: lối lùi bên trái, nút chính bên phải, `border-t border-border pt-5 mt-8`, cao bằng ô (`h-11 md:h-10`). Bước 1 lối lùi là "Huỷ" (rời luồng), các bước sau "Quay lại". Nút chính "Tiếp", bước cuối là động từ của cả luồng ("Tạo workspace", không "Hoàn tất"). Bấm "Tiếp" chỉ kiểm bước đang đứng; sai thì đứng lại, focus ô sai đầu tiên. Lùi về không mất gì đã điền.
|
|
337
|
+
- **Chặn bấm đúp ở nút chính**: nút cuối hiện đúng chỗ nút "Tiếp", bấm đúp "Tiếp" ở bước áp chót thì cú thứ hai gửi luôn khi người dùng chưa kịp đọc bước xác nhận. `onClick` bỏ qua `event.detail > 1` (Enter có `detail = 0`, không bị chặn).
|
|
338
|
+
- **Bước xác nhận chia nhóm theo bước, mỗi nhóm một link "Sửa"** ở mép phải tiêu đề nhóm, bấm về đúng bước đó. Đây là cách trang thanh toán nào cũng làm. Ở màn hẹp thanh các bước chỉ là dòng chữ và thanh mảnh, không bấm được, nên thiếu "Sửa" thì muốn sửa tên từ bước 3 phải bấm "Quay lại" hai lần qua bước 2. Nhóm là khối nhãn–giá trị (`../components/description-list.md`), tiêu đề nhóm **`text-sm font-semibold`**, link "Sửa" là link chữ `text-sm text-foreground/70` như `I7`, rê vào `text-foreground` + gạch chân, không nút viền. **Tiêu đề nhóm phải khác kiểu chữ giá trị**: giá trị trong khối nhãn–giá trị đã là `text-sm font-medium text-foreground`, tiêu đề nhóm cũng `font-medium` thì trùng hệt, ở màn hẹp (nhãn trên, giá trị dưới) "Thông tin workspace" đọc như một giá trị nữa. Không đổi sang chữ nhỏ xám: tiêu đề nhóm nhạt hơn nhãn con bên dưới là ngược thứ bậc. Không thêm đường kẻ giữa nhóm, khoảng trắng `gap-6` đủ tách. Sửa xong bấm "Tiếp" đi lại tuần tự như thường, không nhảy thẳng về bước xác nhận (nhảy thẳng là logic, người dùng quyết).
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
## Trạng thái lỗi
|
|
343
|
+
|
|
344
|
+
Lỗi hiện **dưới ô nhập**, không hiện trong placeholder, không hiện ở tooltip.
|
|
345
|
+
|
|
346
|
+
```
|
|
347
|
+
nhãn
|
|
348
|
+
[ô nhập ] <- viền đỏ đặc; quầng đỏ mờ chỉ khi đang focus
|
|
349
|
+
Email này đã có người dùng <- chữ đỏ, text-xs, ngay dưới ô
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
- Viền `red-500` đặc, ring `red-500/10` rất mờ. Không tô nền đỏ cả ô.
|
|
353
|
+
- Câu lỗi nói **cách sửa**, không nói "không hợp lệ". "Email này đã có người dùng" chứ không phải "Email không hợp lệ".
|
|
354
|
+
- **Câu lỗi không được trùng chữ với placeholder hay nhãn.** Trùng là dấu hiệu nó không mang thêm thông tin nào — xem mục dưới.
|
|
355
|
+
- **Ô tự điền theo ô khác** (đường dẫn theo tên, mã theo tên sản phẩm): ô nguồn trống thì **chỉ ô nguồn báo lỗi**, ô phụ thuộc để yên, vì gõ ô nguồn là ô kia tự có. Ô phụ thuộc chỉ báo lỗi khi người dùng đã tự sửa nó, hoặc khi ô nguồn có chữ mà giá trị sinh ra vẫn sai (quá ngắn, trùng). Báo cả hai thì bấm Tiếp khi trống ra hai dòng đỏ "Chưa nhập tên workspace" và "Chưa nhập đường dẫn", mà chỉ cần gõ tên là hết cả hai (`N3`).
|
|
356
|
+
- Gợi ý thời điểm (người dùng quyết): hiện lỗi sau khi rời ô hoặc bấm gửi, đừng hiện ngay ký tự đầu tiên. Skill chỉ lo lỗi **trông ra sao**, dựng nó như một trạng thái tĩnh của ô.
|
|
357
|
+
- **Sửa xong một ô thì câu lỗi mất, nhưng chỗ của nó ở lại** tới lần bấm gửi sau (`min-h-4` trên dòng dưới ô, bằng một dòng `text-xs`). Rút câu lỗi đi ngay thì mọi ô bên dưới nhảy lên 16px đúng lúc người dùng đang đưa chuột xuống ô kế tiếp (`N1`). Ô nào có sẵn dòng gợi ý thì không cần: gợi ý quay về đúng chỗ câu lỗi vừa rời.
|
|
358
|
+
- **Mặc định: chỉ lỗi tại chỗ, không banner tóm tắt.** Bấm gửi mà có lỗi thì cuộn tới và **focus ô lỗi đầu tiên**; mỗi ô sai viền đỏ + một câu dưới ô. Form trong sản phẩm (SaaS, công cụ làm việc) hầu hết làm vậy; hệ thiết kế cho dịch vụ công thì thêm hộp tóm tắt lỗi đầu trang có link tới từng ô, vì form của họ dài và người điền lần đầu, skill chỉ giữ ca đó cho form rất dài (ca 2 bên dưới). Banner liệt kê lỗi trên một form thường chỉ đọc lại đúng mấy câu đã nằm dưới từng ô: hai tín hiệu cho một ý (`N3`), và cả màn đỏ rực (chủ dự án: "thực tế có ai làm mục đỏ ở trên đâu"). Luật cũ "form dài hơn một màn thì có banner" sai, vì ở 375px form nào cũng dài hơn một màn.
|
|
359
|
+
- **Banner chỉ dùng cho hai ca:**
|
|
360
|
+
1. **Lỗi không gắn với ô nào**: mất mạng, hết phiên, không có quyền, trùng dữ liệu phía máy chủ. Banner một dòng nói chuyện gì và làm gì tiếp, không liệt kê.
|
|
361
|
+
2. **Form rất dài, chia nhiều mục có tiêu đề** (từ khoảng 12 trường, hoặc phải cuộn qua nhiều mục): lỗi ở mục cuối không thể thấy khi đang đứng ở đầu. Lúc đó banner liệt kê từng lỗi kèm link nhảy tới đúng ô, và **vẫn giữ** lỗi tại chỗ.
|
|
362
|
+
|
|
363
|
+
```html
|
|
364
|
+
<div role="alert" class="mb-6 rounded-xl border border-red-200 bg-red-50 p-4">
|
|
365
|
+
<p class="text-sm font-medium text-red-700">Chưa gửi được, còn 3 chỗ cần sửa</p>
|
|
366
|
+
<ul class="mt-2 space-y-1 text-sm text-red-600">
|
|
367
|
+
<li><a href="#tieu-de" class="underline underline-offset-2 outline-hidden">Tiêu đề</a>, chưa điền</li>
|
|
368
|
+
</ul>
|
|
369
|
+
</div>
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Đây là chỗ duy nhất được tô nền đỏ. Ô nhập thì không bao giờ.
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Gợi ý và câu lỗi là hai thứ khác nhau
|
|
377
|
+
|
|
378
|
+
Một field có **ba** chỗ chứa chữ, mỗi chỗ một việc. Lẫn lộn chúng là lỗi hay gặp
|
|
379
|
+
nhất ở form, và nhìn ảnh chụp rất khó nhận ra vì "trông vẫn đủ chữ".
|
|
380
|
+
|
|
381
|
+
| Chỗ | Việc | Màu | Khi nào hiện |
|
|
382
|
+
| --- | --- | --- | --- |
|
|
383
|
+
| **Nhãn** | Ô này là gì | `--foreground` | Luôn |
|
|
384
|
+
| **Gợi ý** | Thứ người dùng chưa biết trước khi gõ | `--muted`, `text-xs` | Luôn |
|
|
385
|
+
| **Câu lỗi** | Vừa gõ sai cái gì, sửa thế nào | đỏ, `text-xs` | Chỉ khi sai |
|
|
386
|
+
|
|
387
|
+
**Dưới ô chỉ có MỘT dòng.** Có lỗi thì câu lỗi **thay chỗ** gợi ý, không đẩy gợi
|
|
388
|
+
ý xuống thành hai dòng chồng nhau.
|
|
389
|
+
|
|
390
|
+
### Ô có giới hạn ký tự
|
|
391
|
+
|
|
392
|
+
Gợi ý đã ghi "Tối đa 120 ký tự" thì ô phải cho thấy mình đang ở đâu so với mức đó.
|
|
393
|
+
Chỉ ghi mà không đếm thì người gõ câu 132 ký tự không biết mình vượt, tới lúc gửi mới
|
|
394
|
+
bị chặn.
|
|
395
|
+
Cách của các hệ thiết kế lớn:
|
|
396
|
+
|
|
397
|
+
- **Bộ đếm cùng dòng với gợi ý, căn phải**: `flex justify-between gap-3`, gợi ý trái, bộ
|
|
398
|
+
đếm `shrink-0 text-xs text-muted tabular-nums` phải, ghi "98/120". Vẫn một dòng dưới ô.
|
|
399
|
+
- **Chỉ hiện khi đã gõ tới khoảng 80% giới hạn.** Dưới đó chỉ có gợi ý; đếm từ ký tự đầu
|
|
400
|
+
là một con số nhảy liên tục ngay cạnh chỗ người ta đang nghĩ câu chữ.
|
|
401
|
+
- **Vượt thì bộ đếm đỏ** ("132/120", chữ đỏ như câu lỗi), gợi ý giữ nguyên. Bấm gửi mà
|
|
402
|
+
vẫn vượt thì câu lỗi thay gợi ý và nói cách sửa: "Dài hơn 120 ký tự, bớt 12 ký tự".
|
|
403
|
+
- **Không `maxlength`.** Chặn cứng thì dán một câu dài bị cắt giữa chữ mà không ai báo,
|
|
404
|
+
người dùng mất phần đuôi. Cho gõ và dán quá, rồi báo.
|
|
405
|
+
- Bộ đếm nằm trong `aria-describedby` của ô, cập nhật trong vùng `aria-live="polite"`.
|
|
406
|
+
|
|
407
|
+
### Ba câu hỏi trước khi viết một dòng chữ đỏ
|
|
408
|
+
|
|
409
|
+
1. **Câu này có trùng chữ với placeholder hoặc nhãn không?** Trùng thì bỏ. Placeholder ghi "Nhập mật khẩu của bạn" mà chữ đỏ dưới ô cũng ghi "Nhập mật khẩu của bạn" thì người dùng đọc hai lần cùng một câu, và vẫn không biết mình sai ở đâu.
|
|
410
|
+
2. **Nó nói người dùng LÀM GÌ tiếp, hay chỉ nói ô đang trống?** Mắt đã thấy ô trống rồi.
|
|
411
|
+
3. **Nó có phải lỗi không, hay là gợi ý bị tô nhầm màu đỏ?** "Nhập email bạn dùng để đăng nhập" là gợi ý — nó đúng cả khi người dùng chưa làm gì sai. Gợi ý thì xám và hiện sẵn, đừng đợi có lỗi mới đỏ lên.
|
|
412
|
+
|
|
413
|
+
### Ô trống thì viết gì
|
|
414
|
+
|
|
415
|
+
| Ô | Sai | Đúng |
|
|
416
|
+
| --- | --- | --- |
|
|
417
|
+
| Email | `Nhập email của bạn` *(trùng placeholder)* | `Chưa nhập email` |
|
|
418
|
+
| Mật khẩu | `Nhập mật khẩu của bạn` *(trùng placeholder)* | `Chưa nhập mật khẩu` |
|
|
419
|
+
| Email sai định dạng | `Email không hợp lệ` | `Email phải có dấu @` |
|
|
420
|
+
| Mật khẩu ngắn | `Mật khẩu không hợp lệ` | `Mật khẩu cần ít nhất 8 ký tự` |
|
|
421
|
+
| Sai thông tin đăng nhập | `Đăng nhập thất bại` | `Email hoặc mật khẩu chưa đúng` |
|
|
422
|
+
|
|
423
|
+
Dòng cuối là ca riêng: nói rõ sai cái nào **là lỗ hổng bảo mật** — người ngoài dò
|
|
424
|
+
được email nào có tài khoản. Nên ở đúng ca này thì mơ hồ là cố ý, và câu lỗi đặt
|
|
425
|
+
ở chỗ câu lỗi trên nút Đăng nhập (xem khung wireframe đăng nhập ở đầu file) chứ không dưới một ô cụ thể.
|
|
426
|
+
Sau lỗi này: **giữ email, xoá ô mật khẩu và đưa con trỏ vào đó**, khối lỗi `role="alert"`
|
|
427
|
+
để trình đọc màn hình đọc ngay. Người dùng gần như luôn gõ lại mật khẩu chứ không sửa
|
|
428
|
+
từng ký tự, và ô còn chấm tròn cũ thì họ phải bôi đen xoá trước (cách Google, GitHub,
|
|
429
|
+
Microsoft đều làm). Không tô đỏ ô nào, vì không biết ô nào sai. Gõ lại vào ô thì khối
|
|
430
|
+
lỗi vẫn đứng yên tới lần gửi sau, không biến mất giữa lúc gõ làm nút nhảy lên (`N1`).
|