@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,1369 @@
|
|
|
1
|
+
# Bố cục màn hình trong app
|
|
2
|
+
|
|
3
|
+
> **Chốt loại màn hình trước, mở file này sau.** Có sẵn code mẫu kanban thì rất
|
|
4
|
+
> dễ đọc mọi đề mơ hồ thành kanban. Xem luật `S10` và `S11` trong `../../SKILL.md`.
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
Không có wireframe thì dựng bố cục mặc định (hoặc cái mà điều kiện trong đề
|
|
8
|
+
chọn), báo một dòng lúc giao. Xem câu 4 trong `../../SKILL.md`. Nhịp ở đây là nhịp app:
|
|
9
|
+
`p-5`, `gap-3`, `text-sm`, viền mảnh, bóng gần như không.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Dashboard
|
|
14
|
+
|
|
15
|
+
**A. Hàng số liệu trên, lưới widget dưới** (mặc định)
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
┌─────────────────────────────────────┐
|
|
19
|
+
│ header pill: lời chào [Cài đặt]│
|
|
20
|
+
├───────┬───────┬───────┬─────────────┤
|
|
21
|
+
│ số 1 │ số 2 │ số 3 │ số 4 │ <- 1 khối chia kẻ, KHÔNG 4 card
|
|
22
|
+
├───────┴───────┴───┬─────────────────┤
|
|
23
|
+
│ widget chính │ widget phụ │ <- widget chính col-span-2
|
|
24
|
+
│ (col-span-2) ├─────────────────┤
|
|
25
|
+
│ │ widget phụ │
|
|
26
|
+
└───────────────────┴─────────────────┘
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Không chia đều ba cột. Widget quan trọng nhất chiếm gấp đôi.
|
|
30
|
+
Bốn ô số liệu dùng **một màu duy nhất**, khác nhau ở con số chứ không ở màu.
|
|
31
|
+
|
|
32
|
+
**Các khối thường có trên một màn tổng quan.** Đề để hở thì dựng **bộ mặc định**
|
|
33
|
+
dưới đây, không hỏi (`S5`). Bộ mặc định: **hàng ô số liệu + biểu đồ xu hướng
|
|
34
|
+
(widget chính) + danh sách tiến độ + việc cần làm hôm nay** — bốn khối. Có nhiều
|
|
35
|
+
người cùng làm thì thêm hoạt động gần đây. Lúc giao liệt kê các khối đã dựng.
|
|
36
|
+
|
|
37
|
+
| Khối | Khi nào đáng có |
|
|
38
|
+
| --- | --- |
|
|
39
|
+
| Hàng ô số liệu | Gần như luôn. Bốn ô là vừa, sáu ô là bắt đầu loãng. Mobile 2×2, xem `../components/charts.md` |
|
|
40
|
+
| Biểu đồ xu hướng theo thời gian | Khi có dữ liệu tích luỹ theo tuần hoặc tháng |
|
|
41
|
+
| Danh sách tiến độ theo nhóm | Khi công việc chia được thành dự án hoặc nhóm |
|
|
42
|
+
| Việc cần làm hôm nay | Khi người dùng vào đây để bắt tay làm, không phải để xem báo cáo |
|
|
43
|
+
| Hoạt động gần đây | Khi có nhiều người cùng làm và cần biết ai vừa đụng gì |
|
|
44
|
+
| Bảng chi tiết | Khi màn này thay luôn cả trang danh sách. Có bảng rồi thì bỏ bớt widget |
|
|
45
|
+
|
|
46
|
+
Ba khối là mỏng cho một màn tổng quan. Bốn tới năm là vừa. Đề đòi quá sáu khối
|
|
47
|
+
thì vẫn dựng, lúc giao gợi ý một câu khối nào nên tách sang màn riêng.
|
|
48
|
+
|
|
49
|
+
Xem `../components/charts.md` cho công thức biểu đồ và luật màu.
|
|
50
|
+
|
|
51
|
+
**Chi tiết từng khối**:
|
|
52
|
+
|
|
53
|
+
- **Widget chính đếm theo kỳ thì là biểu đồ cột, không phải đường.** "Việc xong mỗi tuần", "đơn mỗi ngày" là số đếm rời của từng kỳ; cột có số trên đầu đọc được cả 8 tuần một lượt, đường chỉ ghi số ở điểm cuối, muốn biết tuần 24/08 bao nhiêu phải rê chuột từng điểm. Chọn loại theo bảng "Cột hay đường" ở `../components/charts.md`.
|
|
54
|
+
- **Danh sách tiến độ: thanh luôn `bg-primary`, chỉ cụm "2 việc quá hạn" tô hổ phách.** Quá hạn là một chuyện khác với phần trăm xong; tô cả thanh 93% màu hổ phách đọc ra "tiến độ đang có vấn đề", và hai trên bốn thanh cam thành thứ nặng nhất màn. Tô cả dòng phụ "112 / 120 việc · 2 việc quá hạn" cũng sai: khối việc hôm nay ngay bên cạnh chỉ tô cụm "Quá hạn 2 ngày", hai khối một màn hai cách (`N5`). Chi tiết ở "Thanh tiến độ trong danh sách", `../components/charts.md`.
|
|
55
|
+
- **Hoạt động gần đây là luồng tin của cả workspace, không phải dòng thời gian của một bản ghi.** Đừng bê `../components/timeline.md` (vòng icon theo loại việc, đường nối, nhãn loại việc đậm, người làm dòng cuối cỡ nhỏ): câu hỏi ở đây là "**ai** vừa đụng **gì**", mà khuôn timeline đưa loại việc lên chữ đậm, tên việc xám, người làm xuống dòng mờ nhất. Mỗi mục bốn dòng, sáu mục cao 820px. Khuôn, như các app quản lý dự án phổ biến:
|
|
56
|
+
|
|
57
|
+
```
|
|
58
|
+
(TK) Tuấn Khang hoàn thành Cập nhật ảnh đội ngũ trên trang giới thiệu
|
|
59
|
+
Website Evondev Studio · 09:37
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- Avatar người làm `size-8` bên trái (`avatar.md`), không vòng icon loại việc, không đường nối (các mục không phải các bước của một thứ).
|
|
63
|
+
- Một câu `text-sm text-muted line-clamp-2`: **tên người** `font-medium text-foreground`, động từ xám, **tên đối tượng** `text-foreground` và là link sang đối tượng đó (`N8`; cả khối không link nào là trang cụt, như "Bản ghi khác nhắc tới trên trang là link" ở dưới).
|
|
64
|
+
- Dòng phụ `text-xs text-muted`: dự án · giờ. Hôm nay ghi giờ, hôm qua ghi "Hôm qua", cũ hơn ghi ngày. Dự án dài `truncate`, giờ `shrink-0`.
|
|
65
|
+
- **5 mục**, header có "Xem tất cả" như các khối danh sách khác trên màn (`card.md`). Trang cao 1512px còn ~1150px, hai cột kết thúc gần ngang nhau.
|
|
66
|
+
- **Hai cột lưới phải kết thúc gần ngang nhau.** Khối việc hôm nay `self-start` (đúng, không kéo card trắng rỗng), nhưng cột phải dài gấp đôi thì dưới cột trái là một mảng xám 450px. Chữa bằng cách **cắt số hàng của khối dài** (luồng hoạt động 5 mục, việc hôm nay tối đa 8 rồi "Xem tất cả"), không kéo khối ngắn, không đổi thứ tự khối.
|
|
67
|
+
- **Workspace mới (chưa có dự án nào): khung "Các bước bắt đầu" thay cả lưới.** Đừng dựng đủ năm khối rồi cho mỗi khối một câu "Chưa có…": năm khung cùng nói một ý (`N3`), khung biểu đồ cao 340px chỉ chứa một dòng chữ, và cả màn **không có lối đi tiếp** nào (`N6`). Khung theo mục **Các bước bắt đầu** ngay dưới; app không có checklist thì một khối chào: tiêu đề `text-base font-semibold` ("Bắt đầu với dự án đầu tiên"), một câu vì sao, nút `primary` "+ Tạo dự án" (`I2`: lối đi tiếp duy nhất của màn rỗng). Nằm trong một card trắng như mọi khối khác, không nền trong suốt. Từ lúc có một dự án thì lưới trở lại **dưới** khung các bước (khung còn tới khi xong hết hoặc bị ẩn), khối nào chưa có số thì theo ca rỗng của khối đó (biểu đồ một điểm, việc hôm nay trống).
|
|
68
|
+
- **Khối không có dữ liệu thì bỏ "Xem tất cả"**, nút dẫn sang một danh sách rỗng là thừa. "Hôm nay không có việc nào đến hạn" chỉ đúng khi có việc mà không việc nào đến hạn hôm nay; chưa có việc nào thì câu là "Chưa có việc nào được giao cho bạn".
|
|
69
|
+
- **Câu rỗng của các khối cùng hàng cùng căn một kiểu.** Khung biểu đồ căn câu giữa theo chiều dọc, khung tiến độ bên cạnh để câu sát đầu: cùng hàng hai vị trí (`N5`). Cả hai căn giữa khung.
|
|
70
|
+
|
|
71
|
+
**B. Cột trái điều hướng, nội dung phải** (khi có từ 5 mục điều hướng trở lên)
|
|
72
|
+
|
|
73
|
+
Xem mục **Khung app có sidebar** bên dưới cho công thức đầy đủ.
|
|
74
|
+
|
|
75
|
+
### Các bước bắt đầu (onboarding)
|
|
76
|
+
|
|
77
|
+
Checklist cho người mới vào app: tạo dự án, mời người, giao việc đầu tiên… Khuôn theo cách các bộ thiết kế lớn dựng "setup guide":
|
|
78
|
+
mỗi bước một ô tick tròn, bước mở ra xem được, chỉ bước đầu tiên mở sẵn, có nút ẩn cả khung,
|
|
79
|
+
dòng "1 / 5 bước".
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
┌──────────────────────────────────────────────────┐
|
|
83
|
+
│ Các bước bắt đầu 1 / 5 bước ✕ │
|
|
84
|
+
│ ████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ │
|
|
85
|
+
├──────────────────────────────────────────────────┤
|
|
86
|
+
│ (✓) Tạo workspace ⌄ │
|
|
87
|
+
├──────────────────────────────────────────────────┤
|
|
88
|
+
│ ( ) Tạo dự án đầu tiên ⌃ │
|
|
89
|
+
│ Dự án gom việc, tài liệu và người làm… │
|
|
90
|
+
│ [ Tạo dự án ] │
|
|
91
|
+
├──────────────────────────────────────────────────┤
|
|
92
|
+
│ ( ) Mời thành viên ⌄ │
|
|
93
|
+
├──────────────────────────────────────────────────┤
|
|
94
|
+
│ ( ) Giao việc đầu tiên ⌄ │
|
|
95
|
+
└──────────────────────────────────────────────────┘
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- **Một chỗ, mặc định là đầu trang Tổng quan.** Người mới vào app là rơi vào tổng quan, nên
|
|
99
|
+
khung nằm ở đó: workspace chưa có gì thì thay cả lưới, có rồi thì nằm trên lưới, **cách lưới
|
|
100
|
+
`mt-6` (24px), không dùng khe `gap-3` của lưới**. Dòng chú thích `text-xs` trên hàng số
|
|
101
|
+
(`mb-2`, "Tuần đầu, chưa có tuần trước để so") mà cách khung trên 12px, cách hàng số 8px thì chênh
|
|
102
|
+
4px, đọc như chú thích của khung các bước; 24px so với 8px thì rõ nó đi
|
|
103
|
+
với hàng số. Đừng dựng
|
|
104
|
+
thêm một trang "Chào mừng" riêng khi tổng quan đã có khối chào: bấm "Vào tổng quan" xong lại
|
|
105
|
+
gặp "Bắt đầu với dự án đầu tiên" + nút "Tạo dự án", hai màn nói một việc (`N3`), header
|
|
106
|
+
trang chào còn ghi "Tổng quan" cạnh nút "Vào tổng quan". Trang riêng chỉ
|
|
107
|
+
khi sản phẩm có mục "Bắt đầu" hẳn trong sidebar, và lúc đó tổng quan không có khối chào nữa.
|
|
108
|
+
Không cần tiêu đề "Chào mừng tới …": tên workspace đã ở đầu sidebar (`form.md`, không bịa câu chào).
|
|
109
|
+
- **Khung**: một card, các bước chia đường kẻ (`F3`, không mỗi bước một card). Hàng đầu: tên
|
|
110
|
+
"Các bước bắt đầu" `text-sm font-medium`, "1 / 5 bước" bên phải, thanh tiến độ dưới
|
|
111
|
+
(`../components/charts.md`), nút ẩn `X` ghost `size-8` ở góc phải, **thẳng cột với chevron
|
|
112
|
+
của các dòng** dưới.
|
|
113
|
+
- **Vòng bên trái là ô tick, không đánh số.** Chưa xong: vòng `size-5` viền đứt
|
|
114
|
+
`border-[1.5px] border-dashed border-foreground/40`. Xong: `size-5 bg-primary` + `Check` trắng
|
|
115
|
+
`size-3`. Số thứ tự đọc như bước bắt buộc làm lần lượt; người dùng làm bước 3 và 5 trước thì
|
|
116
|
+
màn thành "bước 2 là bước tiếp theo" giữa hai bước đã xong. Đừng mượn vòng số `size-8` từ
|
|
117
|
+
thanh các bước của form nhiều bước. Thứ tự trong danh sách đã là gợi ý.
|
|
118
|
+
- **Mỗi bước là một mục accordion, chỉ bước tiếp theo mở sẵn.** Nút tiêu đề rộng hết hàng
|
|
119
|
+
theo `../components/accordion.md`: vòng, tên `text-sm font-medium`, `ChevronDown` bên phải,
|
|
120
|
+
dòng `px-4 py-3 sm:px-5` (cao 44px, vừa vùng bấm ở màn cảm ứng), vòng thẳng tâm dòng tên.
|
|
121
|
+
Bước tiếp theo = bước chưa xong đầu tiên làm được ngay. Mở ra: phần thân `pb-4`, thụt trái bằng
|
|
122
|
+
`px` của dòng + vòng 20px + `gap-3`; một câu vì sao `text-sm text-muted` **không `mt`**, rồi nút
|
|
123
|
+
`mt-4` **dưới câu, thẳng mép trái với tên**, ở mọi bề rộng (không đẩy sang phải rồi xếp lại ở màn
|
|
124
|
+
hẹp). Đo được: tên → câu 12px, câu → nút 16px, nút → mép dưới 16px; câu bám vào tên của nó.
|
|
125
|
+
Đừng thêm `mt-1` cho câu: nó chồng lên đệm dưới của nút tiêu đề, tên → câu 20px mà câu → nút
|
|
126
|
+
13px, câu trôi xuống bám vào nút (FAQ ở `accordion.md` cũng không có `mt` ở câu trả lời).
|
|
127
|
+
Đừng mở hết các bước: năm câu mô tả + bốn nút xếp một cột phải, tên bước lặp lại trên nút
|
|
128
|
+
("Mời thành viên" / "Mời thành viên"), card cao 830px ở 1280 và 1540px ở 375; accordion còn
|
|
129
|
+
390px và 830px, một nút đặc.
|
|
130
|
+
- **Nút**: bước tiếp theo `primary`, là nút đặc duy nhất của khung (`I3`); bước khác người dùng
|
|
131
|
+
tự mở thì nút viền. Không có nút "Vào tổng quan": khung đã nằm trên tổng quan.
|
|
132
|
+
- **Bước khoá** (phải xong bước khác trước): vòng như bước chưa xong, mở ra là câu lý do
|
|
133
|
+
nói bước cần làm trước ("Tạo dự án trước: mỗi việc phải nằm trong một dự án"), **không nút mờ**.
|
|
134
|
+
Nút `disabled` 50% trên nền trắng gần như tan mất, đọc như một khung rỗng.
|
|
135
|
+
- **Bước xong**: tên `text-muted`, **không gạch ngang** (gạch ngang là của danh sách việc người
|
|
136
|
+
dùng tự tick; bước ở đây app tự đánh dấu), đóng sẵn, vẫn mở ra được.
|
|
137
|
+
- **Ẩn**: bấm `X` là ẩn luôn, không hộp xác nhận; toast "Đã ẩn các bước bắt đầu" có "Hoàn tác"
|
|
138
|
+
(`../system.md`, xoá khôi phục được).
|
|
139
|
+
- **Xong hết**: thanh xanh lá, danh sách bước **thu hết**, khung còn hàng đầu và một câu "Xong hết
|
|
140
|
+
các bước. Workspace đã sẵn sàng cho cả nhóm." cùng nút `X`. Đừng in lại năm dòng đã xong: năm vòng
|
|
141
|
+
đen xếp cột thành thứ nặng nhất màn mà không còn việc gì để làm. Cách này là
|
|
142
|
+
lựa chọn của skill, các bộ thiết kế lớn chưa chốt ca xong hết.
|
|
143
|
+
- **Trạng thái cần có ở trang `/states`**: vừa vào (xong một bước), làm không theo thứ tự, bước khoá
|
|
144
|
+
đã mở khoá, xong hết, và đã ẩn (tổng quan không còn khung).
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## Khung app có sidebar
|
|
149
|
+
|
|
150
|
+
```
|
|
151
|
+
nền trắng --surface nền xám --background
|
|
152
|
+
┌──────────────┐░┌──────────────────────────────────────┐
|
|
153
|
+
│ ◐ Tổ chức │░│ Trang / Mục hiện tại [chuông][avatar] │
|
|
154
|
+
├──────────────┤░├──────────────────────────────────────┤ <- một đường kẻ ngang border-border, chạy liền qua cả hai cột
|
|
155
|
+
│ [tìm ⌘K] │░│ │
|
|
156
|
+
│ │░│ │
|
|
157
|
+
│ ⌂ Trang chủ │░│ │
|
|
158
|
+
│ ✉ Hộp thư 20│░│ <- số đếm căn phải, số trơn │
|
|
159
|
+
│ ☑ Việc │░│ │
|
|
160
|
+
│ │░│ <- giữa các nhóm chỉ khoảng trắng │
|
|
161
|
+
│ CÔNG VIỆC ⌄ │░│ <- nhãn nhóm IN HOA, bấm để thu gọn │
|
|
162
|
+
│ ▤ Dự án 4 │░│ │
|
|
163
|
+
│ ▦ Tài liệu │░│ │
|
|
164
|
+
│ │░│ │
|
|
165
|
+
│ KINH DOANH › │░│ <- nhóm đang thu gọn │
|
|
166
|
+
│ │░│ │
|
|
167
|
+
│ ⚙ Cài đặt │░│ │
|
|
168
|
+
│ ◐ Tên người ⇕│░│ <- ghim đáy (bỏ nếu avatar ở header) │
|
|
169
|
+
└──────────────┘░└──────────────────────────────────────┘
|
|
170
|
+
↑ KHÔNG có đường kẻ dọc: trắng cạnh xám đã là ranh giới
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
- **Sidebar nền trắng `--surface`, KHÔNG `border-r`** khi vùng nội dung là nền trang xám `--background`: trắng cạnh xám đã là ranh giới, thêm đường kẻ là hai tín hiệu cho một ý (`N3`; chủ dự án bỏ). Chỉ kẻ `border-r border-border-strong` khi vùng nội dung cũng trắng. **Đừng để sidebar trong suốt** ăn theo `--background` của trang: sidebar xám trùng nền trang thì cả màn thành một mảng xám, không còn ranh giới nào.
|
|
174
|
+
- **Rộng `w-60` tới `w-64`**, cố định, `shrink-0`.
|
|
175
|
+
- **Rê là `hover:bg-item-hover`, đang chọn đậm hơn một bậc `bg-secondary` + `font-medium`.** Hai nền phải khác nhau: rê ra đúng nền đang chọn thì rê qua mục nào cũng trông như vừa chọn nó, không còn biết mình đang ở trang nào (`I10`; chủ dự án chốt). "Rê và đã chọn cùng một nền mờ" chỉ còn cho dòng bảng tick checkbox, vì ở đó checkbox đã là dấu chọn. **Không tô màu nhấn**, không viền.
|
|
176
|
+
- **Mỗi link cao 40px** (`h-10`, `px-3`), bo `rounded-xl` 12px theo luật bo-theo-chiều-cao `F1`. Link 36px trông chật, nền hover lọt thỏm; 40px thì hàng thoáng và bấm trúng dễ hơn.
|
|
177
|
+
- **Icon và chữ đi cùng nhau.** Lúc thường cả hai `text-foreground/70`: dịu hơn chữ chính nhưng **không mờ tới `--muted`**, xám `--muted` trên nền trắng là đọc không ra tên mục. Hover hay đang chọn thì **cả icon lẫn chữ** lên `text-foreground`. Đặt màu trên phần tử `<a>`, icon dùng `currentColor`, đừng gán màu riêng cho icon, nếu không hover chỉ sáng mỗi chữ.
|
|
178
|
+
- **Nhãn nhóm IN HOA, chữ XÁM**: `text-xs font-medium uppercase tracking-wide text-muted`, hover mới lên `text-foreground`. IN HOA đã đủ tách nhãn khỏi link, nên nhãn phải **nhạt hơn** mục con, không đậm hơn: nhãn đen `--foreground` cộng IN HOA thì nặng nhất cột, lấn cả mục đang chọn. Viết thường thì nhãn nhóm trông y như một mục nav nhạt màu, mắt không tách được đâu là tiêu đề, đâu là link. Chữ trong dữ liệu vẫn viết thường ("Công việc"), IN HOA bằng CSS, để screen reader không đánh vần từng chữ.
|
|
179
|
+
- **Giữa các nhóm KHÔNG kẻ đường chia**, tách bằng khoảng trắng `mt-4` và nhãn nhóm. Nhãn IN HOA xám + chevron đã đủ báo "nhóm mới bắt đầu" kể cả khi sidebar cuộn; thêm đường kẻ là ba tín hiệu cho một ý (`N3`). Các app quản lý lớn đều không kẻ. Đừng kẻ `border-t` trên mỗi nhóm: ba đường kẻ chạy ngang cột thành thứ nặng nhất sidebar (chủ dự án bỏ: "đường line hơi đậm").
|
|
180
|
+
- **Sidebar chỉ còn một đường kẻ `border-border`**: dưới đầu sidebar (tên workspace), cùng màu đường kẻ dưới header và vạch chia trong menu, popover (`overlay.md`). Cả khung app một màu đường kẻ: đầu sidebar `#eaeaea` mà vạch trong menu tài khoản `#f7f7f8` thì nhìn ra hai màu kẻ cho cùng một việc (chủ dự án chốt; đừng dùng lại `--border-strong` với lý do `--border` "gần như tàng hình"). Đường kẻ khung chỉ là nét gợi, khối tách nhau bằng nền trắng cạnh xám.
|
|
181
|
+
- **Đường kẻ dưới đầu sidebar chạy HẾT bề ngang, mép chạm mép.** Đặt nó trên phần tử đầu sidebar, không trong vùng có padding ngang. Đừng vá bằng `-mx-3`: đổi padding một chỗ là đường lệch. **Vùng nav cuộn được thì thanh cuộn không được giữ chỗ** (`scrollbar-gutter: auto`, thanh cuộn tự ẩn theo `I18`): giữ gutter 4px là nền rê của mọi mục trong vùng cuộn hụt 4px ở mép phải so với Cài đặt và hàng profile nằm ngoài vùng cuộn.
|
|
182
|
+
- **Sidebar nhiều link thì nhóm thu gọn được.** Từ **3 nhóm có nhãn trở lên**, hoặc tổng số mục đủ để sidebar phải cuộn: nhãn nhóm thành một **nút rộng hết hàng** (`I29`), chevron ở mép phải (`ChevronDown` `size-4`, xoay `-rotate-90` khi đóng), có `aria-expanded`. Hover nhãn là nền `--background` như mục nav.
|
|
183
|
+
- **Nút nhãn cùng khuôn với mục con**: cùng `h-10 px-3`, cùng `rounded-xl`. Đặt chiều cao bằng `h-10` chứ không bằng `py`, vì chữ `text-xs` của nhãn thấp hơn chữ `text-sm` của link, dùng `py` là nút nhãn lùn hơn hàng con. Mép trái chữ nhãn thẳng mép icon con, chevron thẳng mép phải badge.
|
|
184
|
+
- **Mục con không thụt vào, không đường dọc.** Nhóm ở đây là *phân khu*, các mục con ngang hàng nhau và đã có icon riêng. Thụt vào cộng đường dọc là ngôn ngữ của **cây lồng nhau**, và nó ăn mất 16-20px của cột vốn đã hẹp, chữ dài bị cắt sớm hơn.
|
|
185
|
+
- Chỉ thụt + đường dọc khi đó là **menu con của một link** (Dự án ▸ Dự án A, Dự án B): mục con **không icon**, thụt để chữ thẳng mép chữ của link cha, đường dọc `border-l border-border-strong` chạy ở tâm icon cha. Mục con đang chọn thì đoạn đường dọc của nó đậm lên `--foreground`.
|
|
186
|
+
- **Mở/đóng có animation trượt**, không chớp giật: `grid` với `grid-rows-[1fr]` ↔ `grid-rows-[0fr]`, con bọc `overflow-hidden min-h-0`, `transition-[grid-template-rows] duration-200 ease-out`. Chevron xoay cùng `duration-200`. Không đo chiều cao bằng JS. Thêm `motion-reduce:transition-none`. Nhóm đang đóng gắn `inert` để Tab không lọt vào link đã ẩn.
|
|
187
|
+
- Nhóm đầu không nhãn (Tổng quan, Hộp thư) thì luôn mở, không thu gọn.
|
|
188
|
+
- Mặc định **mở hết**. Nhóm chứa trang đang xem thì **không được đóng lúc tải trang**, nếu không người ta không thấy mình đang ở đâu.
|
|
189
|
+
- **Thanh cuộn của sidebar tự ẩn** theo `I18`: đứng yên không thấy, rê vào hoặc đang cuộn mới hiện, 4px. Thanh cuộn xám đứng yên chạy dọc sidebar trắng là thứ nặng nhất trên cột, nặng hơn cả chữ.
|
|
190
|
+
- **Số đếm căn phải, là số trơn** `text-xs tabular-nums text-muted`, không pill, không viền. Năm pill viền cạnh nhau trên một cột là năm khung nhỏ kéo mắt. Mục đang chọn thì số lên `text-foreground` cùng chữ. **Không badge màu brand**, xem `../components/small-controls.md`.
|
|
191
|
+
- **Các link cách nhau `gap-1` (4px)**, không `gap-0.5`: 2px thì nền rê của hai mục kề nhau gần
|
|
192
|
+
như dính, cột đọc thành một khối (chủ dự án thấy).
|
|
193
|
+
- **Dự án đã có badge màu brand trong sidebar** ("Mới" nền đỏ, vai màu, `review.md`) thì giữ,
|
|
194
|
+
nhưng **mục đang chọn tô nền màu nhấn thì badge của nó đảo**: nền `--primary-foreground`,
|
|
195
|
+
chữ `--primary`. Badge đỏ nằm trên nền đỏ thì tan mất, và cột có hai ba khối đỏ đặc tranh
|
|
196
|
+
nhau (`N3`). Wireframe vẽ luôn ca này (`design-process.md`, `U3`).
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
<Link
|
|
200
|
+
href={item.href}
|
|
201
|
+
className={cn(
|
|
202
|
+
"flex h-10 w-full cursor-pointer items-center gap-2.5 rounded-xl px-3 text-sm text-foreground/70 outline-hidden transition-colors",
|
|
203
|
+
!isActive && "hover:bg-item-hover hover:text-foreground",
|
|
204
|
+
isActive && "bg-secondary font-medium text-foreground",
|
|
205
|
+
)}
|
|
206
|
+
>
|
|
207
|
+
<Inbox className="size-4 shrink-0" />
|
|
208
|
+
{item.label}
|
|
209
|
+
{item.unread > 0 && (
|
|
210
|
+
<span className={cn("ml-auto shrink-0 text-xs tabular-nums text-muted", isActive && "text-foreground")}>
|
|
211
|
+
{item.unread > 99 ? "99+" : item.unread}
|
|
212
|
+
</span>
|
|
213
|
+
)}
|
|
214
|
+
</Link>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Nhóm thu gọn được: nút nhãn cùng khuôn mục con, trượt bằng `grid-rows`.
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
// Vùng nav: `flex flex-col py-3 px-3`. Nhóm tách nhau bằng `mt-4`, KHÔNG kẻ đường chia.
|
|
221
|
+
<div className="mt-4 first:mt-0">
|
|
222
|
+
<button
|
|
223
|
+
type="button"
|
|
224
|
+
aria-expanded={isOpen}
|
|
225
|
+
aria-controls={groupId}
|
|
226
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
227
|
+
className="flex h-10 w-full cursor-pointer items-center rounded-xl px-3 text-xs font-medium uppercase tracking-wide text-muted outline-hidden transition-colors hover:bg-item-hover hover:text-foreground"
|
|
228
|
+
>
|
|
229
|
+
{group.label}
|
|
230
|
+
<ChevronDown
|
|
231
|
+
className={cn(
|
|
232
|
+
"ml-auto size-4 shrink-0 transition-transform duration-200 motion-reduce:transition-none",
|
|
233
|
+
!isOpen && "-rotate-90",
|
|
234
|
+
)}
|
|
235
|
+
/>
|
|
236
|
+
</button>
|
|
237
|
+
|
|
238
|
+
<div
|
|
239
|
+
id={groupId}
|
|
240
|
+
inert={!isOpen}
|
|
241
|
+
className={cn(
|
|
242
|
+
"grid transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
|
|
243
|
+
isOpen && "grid-rows-[1fr]",
|
|
244
|
+
!isOpen && "grid-rows-[0fr]",
|
|
245
|
+
)}
|
|
246
|
+
>
|
|
247
|
+
<div className="flex min-h-0 flex-col gap-1 overflow-hidden">
|
|
248
|
+
{group.items.map((item) => <SidebarNavLink key={item.href} item={item} />)}
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
`h-10` = đúng chiều cao link con, nên nhãn và link cùng khuôn. Nút nhãn dùng
|
|
255
|
+
`<button>` thuần ở đây cho gọn ví dụ; dự án có `Button` dùng chung thì dùng nó.
|
|
256
|
+
|
|
257
|
+
- **Khối tài khoản ghim đáy, nằm TRONG một khung**, xem mục **Chân sidebar** bên dưới.
|
|
258
|
+
- **Ô tìm ở đầu sidebar** có gợi ý phím tắt `/` hoặc `⌘K` ở mép phải.
|
|
259
|
+
- **Tên mục bị cắt thì rê vào hiện đủ tên**, cả lúc sidebar đang mở: "Báo cáo tài chính theo q…" phải có tooltip "Báo cáo tài chính theo quý". Dùng lại tooltip của chế độ thu gọn, chỉ bật khi chữ **thật sự bị cắt** (đo theo `T14`: bề rộng chữ bằng `Range`, không bằng `scrollWidth`), mục ngắn không bật. Đừng chỉ bật tooltip lúc thu gọn: lúc mở thì tên dài cụt hẳn, không có cách nào đọc được.
|
|
260
|
+
- **Dưới `lg`, sidebar là panel trượt từ TRÁI, cùng khuôn với panel trượt ở `overlay.md`, chỉ đổi phía**: khung `bg-surface shadow-modal`, nền tối thêm `border-r border-border` (`M23`); lớp phủ `bg-black/15` (không `/30`, đó là của modal), vào **500ms** / ra **350ms** `cubic-bezier(0.32,0.72,0,1)`, lớp phủ cùng nhịp; chỉ `translate`, không `scale`, không `opacity` trên panel. **Không nút ✕ hiện ra**: đóng bằng bấm lớp phủ (dải tối bên phải luôn lộ ít nhất 56px), bấm một link, Escape, vuốt sang trái. Menu điều hướng trượt của các app đều vậy; ✕ chen vào hàng logo thì trông lạc, nhất là khi nó đứng sát tên thay vì sát mép. Vẫn có một nút đóng `sr-only` đứng đầu panel cho trình đọc màn hình. Panel khác (xem bản ghi, lọc) vẫn có ✕ theo `overlay.md`: chúng chứa việc đang làm, không phải chỗ đi qua. Mọi khối trượt từ mép trong app dùng **một** lớp phủ và **một** đường cong (`N5`). Đừng tự chọn `bg-black/30` + 200ms `ease-out` khi đề chỉ ghi "trượt từ trái". Bóng cũng vậy: đừng để panel phẳng không bóng, ở phong cách nổi thì nó thấp hơn cả card phía sau.
|
|
261
|
+
|
|
262
|
+
- **Thanh điều hướng dưới thay ☰ khi app có từ 5 mục chính trở xuống** và người dùng chuyển mục
|
|
263
|
+
liên tục trên điện thoại (app dùng hằng ngày, app người dùng cuối). App quản trị nhiều nhóm menu,
|
|
264
|
+
ít mở trên điện thoại thì giữ ☰. Dưới `lg`:
|
|
265
|
+
- `fixed inset-x-0 bottom-0` nền `--surface`, viền trên `border-border`, cao `h-16` cộng
|
|
266
|
+
`pb-[env(safe-area-inset-bottom)]` (vạch home của iPhone), trang chừa `pb-20` để mục cuối
|
|
267
|
+
không bị che.
|
|
268
|
+
- 4–5 mục chia đều (`grid grid-cols-4`/`5`), mỗi mục icon lucide `size-5` trên chữ `text-xs`,
|
|
269
|
+
cả ô là vùng bấm. Đang chọn: icon và chữ `text-primary` (dự án gần đen thì `text-foreground
|
|
270
|
+
font-medium`), không nền, không vạch; mục khác `text-muted`. Link có `aria-current="page"`.
|
|
271
|
+
- Mục thứ 6 trở đi gom vào mục cuối **"Thêm"** mở sheet từ đáy, không nhồi 6 mục chữ bị cắt.
|
|
272
|
+
- Số đếm (việc chờ duyệt) là chấm hay badge nhỏ ở góc icon, không đẩy chữ.
|
|
273
|
+
- Có thanh dưới thì header mobile bỏ ☰; khối tài khoản vào "Thêm" hay avatar ở header.
|
|
274
|
+
- **Màn rộng: nội dung bám sidebar, lấp bằng thêm cột.** Trang lưới hay danh sách trong khung
|
|
275
|
+
có sidebar không `mx-auto` giữa vùng nội dung: ở 1920px trở lên nó để một khoảng trống giữa
|
|
276
|
+
sidebar và nội dung, cả trang trông như trôi (`mx-auto max-w-300` hở
|
|
277
|
+
235px mỗi bên ở 1920). Dư bề ngang thì **thêm cột**: lưới card lên `2xl:grid-cols-4`, danh
|
|
278
|
+
sách card ngang thành hai cột khi vùng nội dung từ khoảng 1600px. Đừng kéo dài card: card
|
|
279
|
+
ngang rộng 1800px thì nửa phải trống. Cần trần thì đặt rộng (`max-w-[1600px]` trở lên) và vẫn
|
|
280
|
+
căn trái. Trang chữ, form, cài đặt giữ cột hẹp như mẫu của từng trang.
|
|
281
|
+
**Mọi khối trong cột chung một mép phải.** Ô tìm, khối lọc, hàng kết quả, lưới: trần (nếu có)
|
|
282
|
+
đặt ở khung bọc cả cột, không đặt riêng từng khối. Bó riêng ô tìm và khối lọc
|
|
283
|
+
(`max-w-3xl`) trong khi lưới bên dưới trải hết thì cạnh khối lọc trống nửa màn, mép phải so
|
|
284
|
+
le giữa các khối. Ô tìm dài ở 1920 vẫn đỡ hơn khoảng trống đó; muốn ô ngắn lại thì đặt nó
|
|
285
|
+
cùng hàng với control khác (nút Tìm, sắp xếp), đừng co cả khối. Đừng bó `max-w-3xl` cho ô tìm và khối lọc chỉ vì "ô tìm dài 1550px":
|
|
286
|
+
nửa phải trống, lưới bên dưới vẫn chạy hết mép.
|
|
287
|
+
|
|
288
|
+
### Thu gọn sidebar (từ `lg` trở lên)
|
|
289
|
+
|
|
290
|
+
```
|
|
291
|
+
Mở Thu gọn
|
|
292
|
+
┌──────────────────┐ ┌──────┐
|
|
293
|
+
│ ◐ Evondev Studio │ │ ◐ │
|
|
294
|
+
├──────────────────┤ ├──────┤
|
|
295
|
+
│ ⌂ Tổng quan │ │ ⌂ │
|
|
296
|
+
│ ✉ Hộp thư 99+ │ │ ✉• │ <- chấm chỉ cho số "cần xử lý"
|
|
297
|
+
│ ▦ Lịch │ │ ▦ │
|
|
298
|
+
│ │ │ │
|
|
299
|
+
│ CÔNG VIỆC ⌄ │ │ ─ │ <- nhãn mờ đi TẠI CHỖ, thay bằng gạch ngắn
|
|
300
|
+
│ ▣ Dự án 4 │ │ ▣ │ <- nhóm đang mở: icon vẫn hiện, đứng yên
|
|
301
|
+
│ ☰ Việc của tôi 12│ │ ☰ │
|
|
302
|
+
│ │ │ │
|
|
303
|
+
│ KINH DOANH › │ │ ─ │ <- nhóm đang đóng: vẫn đóng, chỉ còn gạch
|
|
304
|
+
│ │ │ │
|
|
305
|
+
│ ⚙ Cài đặt │ │ ⚙ │
|
|
306
|
+
│ ◐ Tên người ⇕ │ │ ◐ │ <- chỉ avatar, vẫn mở menu
|
|
307
|
+
└──────────────────┘ └──────┘
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
Mặc định là **thu về dải icon**, không ẩn hẳn. Ẩn hẳn (bề rộng về 0) chỉ làm khi
|
|
311
|
+
người dùng yêu cầu.
|
|
312
|
+
|
|
313
|
+
- **Mục nào đang hiện lúc mở thì lúc thu vẫn hiện, thành icon.** Nhóm đang mở giữ nguyên icon các mục con; nhóm đang đóng thì vẫn đóng. Như chế độ `collapsible="icon"` của sidebar shadcn. Đừng chỉ giữ nhóm đầu rồi ẩn mọi nhóm có nhãn: mở thấy 13 mục, thu còn 3, người dùng tưởng mất mục, và trang đang xem nằm trong nhóm bị ẩn thì dải icon không có mục nào đang chọn (chủ dự án bỏ).
|
|
314
|
+
- **Chỉ nhãn nhóm và chevron mờ đi tại chỗ** (`opacity-0` + `inert`), hàng nhãn vẫn giữ chiều cao. Khoảng trống nhãn để lại chính là chỗ tách nhóm trong dải icon, và mọi icon **đứng yên đúng vị trí** lúc thu và lúc mở (luật icon đứng yên bên dưới). Gỡ hàng nhãn ra thì icon bên dưới nhảy lên.
|
|
315
|
+
- **Hàng nhãn lúc thu có một gạch ngắn** thay cho chữ: `w-4 h-px bg-border-strong`, nằm giữa hàng theo chiều dọc, **thẳng tâm icon** (hàng nhãn `px-3` nên gạch tự rơi vào 24–40px, tâm 32px, không `justify-center`). Như số đếm, gạch và chữ là **hai bản chuyển bằng opacity**: chữ + chevron `opacity-0`, gạch `opacity-100`, cả hai luôn trong DOM; gạch `aria-hidden`. Chỉ khoảng trống thì một nhóm đang đóng thành một lỗ trống giữa dải icon: đóng Kinh doanh rồi thu gọn, giữa Tài liệu và Thành viên trống 156px, gần gấp ba khoảng thường, không ai biết ở đó có một nhóm (chủ dự án duyệt gạch ngắn). Có gạch thì mỗi khoảng trống đọc ra là ranh giới nhóm, hai gạch liền nhau là có nhóm đang đóng ở giữa.
|
|
316
|
+
- **Vùng nav cuộn được cả lúc thu** (dải icon có thể dài hơn màn). Lúc thu thì **ẩn hẳn thanh cuộn** (`[scrollbar-width:none]`), vẫn cuộn bằng chuột, phím, cảm ứng: thanh cuộn 4px giữ chỗ làm ô icon 40px còn 36px và lệch khỏi tâm.
|
|
317
|
+
- **Đổi trang thì cuộn mục đang chọn vào tầm nhìn** (`scrollIntoView({ block: "nearest" })`): màn thấp, mục ở gần đáy bị mép dưới cắt mất nửa, không biết mình đang ở đâu.
|
|
318
|
+
- **Mép vùng nav mờ dần khi còn mục bị khuất** (cả lúc mở lẫn lúc thu, vì thanh cuộn tự ẩn hoặc ẩn hẳn): `mask-image` gradient **32px** ở mép trên khi đã cuộn khỏi đầu, ở mép dưới khi còn mục phía dưới, không cuộn được thì không mờ. Tính hai cờ từ `scrollTop`, `scrollHeight`, `clientHeight` lúc cuộn và khi đổi kích thước (`ResizeObserver`), đưa vào biến CSS để mask đổi ngay: `[mask-image:linear-gradient(to_bottom,transparent,#000_var(--fade-top),#000_calc(100%-var(--fade-bottom)),transparent)]`, `--fade-top`/`--fade-bottom` là `0px` hoặc `32px`. **Dài 32px, gần bằng một hàng `h-10`**, không 16px: tự cuộn xong thường còn một hàng bị cắt ló ở mép, 16px thì phần ló vẫn đậm 70–80%, thành mấy vệt vụn (đáy icon, dấu chấm của "ị") dính ngay dưới hàng Tìm kiếm như rác. Mục đang chọn tự cuộn vào thì dừng **ngoài** dải mờ: `scroll-my-8` trên link, khớp độ dài mờ. Mask chứ không đè một lớp gradient trắng: đè lớp màu thì hover và nền mục đang chọn ở mép bị phủ trắng lệch màu. Không mờ thì màn thấp 600px sau khi tự cuộn tới Thành viên: Lịch dính ngay dưới icon Tìm, Tổng quan và Hộp thư khuất phía trên, Phòng ban khuất phía dưới, không có dấu gì báo còn mục.
|
|
319
|
+
- **Chấm ở góc icon chỉ cho số "cần xử lý"** (chưa đọc, chờ duyệt, quá hạn). Số đếm tổng như "Dự án 4", "Thành viên 18" lúc thu thì bỏ, không chấm: mười chấm trên một cột là mười tín hiệu vô nghĩa. Số vẫn nằm trong tooltip.
|
|
320
|
+
- **Chân sidebar giữ lại**: Cài đặt thành icon, hàng profile thành **chỉ avatar**, bấm vẫn mở menu tài khoản như cũ.
|
|
321
|
+
- **Lúc thu, rê vào avatar không tô nền ô vuông**, mà hiện vòng quanh chính avatar: `ring-2 ring-foreground/10` (menu đang mở thì giữ vòng). Avatar là hình tròn có nền màu riêng; tô thêm một ô xám bo góc quanh nó là tròn trong vuông, hai nền nhạt lồng nhau, trông như một cục mờ. Cùng lý do với ngoại lệ ảnh ở `I15`. Lúc mở thì hàng có chữ, tô nền cả hàng như link là đúng.
|
|
322
|
+
- **Mỗi icon có tooltip** là tên mục, hiện bên phải. Link giữ `aria-label` bằng tên mục vì chữ đã ẩn.
|
|
323
|
+
- **Dải rộng `w-16` (64px)**. Link vẫn là link cũ, `h-10 w-full px-3 rounded-xl`, chỉ bị bề rộng sidebar bóp lại còn 40px, thành ô vuông. Hover và đang chọn như link thường.
|
|
324
|
+
- **Icon đứng YÊN một chỗ ở cả hai trạng thái. Không bao giờ `justify-center`.** Thu gọn mà căn giữa, mở ra lại căn trái, thì lúc bấm mở icon và logo nhảy từ giữa sang trái rồi chữ mới bật ra, cả sidebar như "bung từ giữa", khựng. Cách làm: mọi thứ **căn trái**, và padding tính sao cho tâm icon rơi đúng **32px** (giữa dải 64px) ngay khi căn trái:
|
|
325
|
+
|
|
326
|
+
| Phần tử | Tính | Tâm |
|
|
327
|
+
| --- | --- | --- |
|
|
328
|
+
| Icon link `size-4` | nav `px-3` 12 + link `px-3` 12 + nửa icon 8 | 32 |
|
|
329
|
+
| Logo `size-8` ở đầu sidebar | header `px-4` 16 + nửa logo 16 | 32 |
|
|
330
|
+
| Avatar `size-8` trong hàng profile | chân `px-3` 12 + hàng `px-1` 4 + nửa avatar 16 | 32 |
|
|
331
|
+
|
|
332
|
+
Hàng profile **cao `h-10` như link**, không `h-12`: thu gọn về dải 40px thì `h-12` thành ô 40×48 đứng dọc, lệch khỏi mọi ô icon vuông 40×40 bên trên. Lúc mở và lúc thu dùng **cùng** các padding này, không đổi padding theo trạng thái.
|
|
333
|
+
- **Lúc thu, số đếm "cần xử lý" thành một chấm** (số đếm tổng thì bỏ, xem trên) `size-1.5 rounded-full bg-foreground/50` ở góc trên phải icon — **xám, không màu nhấn**: chấm lúc thu là bản thu nhỏ của số "99+" lúc mở, số đó xám `text-muted` thì chấm cũng xám, hai trạng thái cùng độ đậm (`N5`); sidebar không có badge màu brand (`I15`). Muốn chưa đọc nổi hơn thì đổi **cả hai trạng thái cùng lúc**, không riêng chấm (`absolute left-6 top-2`, neo theo icon). Số trơn cỡ nhỏ đè góc icon thì không đọc được; số thật nằm trong tooltip ("Hộp thư · 99+") và trong `aria-label` của link.
|
|
334
|
+
- **Đường kẻ dưới đầu sidebar vẫn chạy hết bề ngang dải.**
|
|
335
|
+
- **Chuyển động: chỉ bề rộng chạy, bố cục bên trong không đổi.** `<aside>` `overflow-hidden`, `transition-[width] duration-200 ease-out motion-reduce:transition-none`, `w-64` ↔ `w-16`.
|
|
336
|
+
- **Chữ luôn nằm trong DOM**, `whitespace-nowrap`, bị mép sidebar **cắt dần** khi thu và **lộ dần** khi mở, như kéo rèm. Không `hidden`, không render có điều kiện: gỡ chữ ra rồi gắn lại là nó bật "phựt" một cái, và bố cục tính lại làm icon xê dịch.
|
|
337
|
+
- Chữ, tên workspace, badge cạnh chữ, nhãn nhóm thêm `transition-opacity duration-150`, thu thì `opacity-0`. Mờ đi cùng lúc bị cắt thì không thấy nửa chữ lơ lửng ở mép.
|
|
338
|
+
- **Số đếm có HAI bản, chuyển bằng opacity**: số trơn cạnh chữ (`ml-auto`) mờ đi, chấm đè góc icon (`absolute`) hiện lên. Đừng di chuyển một badge từ chỗ này sang chỗ kia, nó sẽ bay chéo qua sidebar. **Cả hai bản `aria-hidden`**, số đọc cho trình đọc màn hình nằm trong một `<span className="sr-only">, 20 chưa đọc</span>` duy nhất: `opacity-0` không gỡ chữ khỏi cây truy cập, để nguyên thì nó đọc "Hộp thư 99+ 99+"; lúc thu, `aria-label` của link cũng phải kèm số.
|
|
339
|
+
- **Nhãn nhóm mờ đi tại chỗ** (`opacity-0` + `inert` trên nút nhãn), không gỡ ra. Gỡ ra thì chiều cao nav đổi, icon và thanh cuộn nhảy.
|
|
340
|
+
- Có nhớ trạng thái thu/mở hay không, có phím tắt hay không là việc của người dùng. Nếu đề có phím tắt thì ghi nó trong tooltip của nút toggle.
|
|
341
|
+
- **Nút toggle ở đầu header vùng nội dung**, icon `PanelLeftClose` khi đang mở, `PanelLeftOpen` khi đang thu. Nút ghost `size-10 rounded-xl`, có `aria-label` và `aria-expanded`. `outline-hidden`, không vòng focus (`I13`). Viền xám dày hiện ra sau khi bấm là outline mặc định của trình duyệt lọt ra (thiếu `outline-hidden`), không phải thiết kế.
|
|
342
|
+
|
|
343
|
+
```tsx
|
|
344
|
+
// MỘT link cho cả hai trạng thái. Không justify-center, không đổi padding:
|
|
345
|
+
// icon đứng yên, chỉ chữ bị mép sidebar cắt dần.
|
|
346
|
+
<Link
|
|
347
|
+
to={item.href}
|
|
348
|
+
aria-label={isCollapsed ? getSidebarLinkLabel(item) : undefined} // "Hộp thư, 20 chưa đọc"
|
|
349
|
+
className={cn(
|
|
350
|
+
"relative flex h-10 w-full items-center gap-2.5 rounded-xl px-3 text-sm whitespace-nowrap text-foreground/70 outline-hidden",
|
|
351
|
+
!isActive && "hover:bg-item-hover hover:text-foreground",
|
|
352
|
+
isActive && "bg-secondary font-medium text-foreground",
|
|
353
|
+
)}
|
|
354
|
+
>
|
|
355
|
+
<item.icon className="size-4 shrink-0" aria-hidden />
|
|
356
|
+
<span className={cn("min-w-0 flex-1 truncate transition-opacity duration-150", isCollapsed && "opacity-0")}>
|
|
357
|
+
{item.label}
|
|
358
|
+
</span>
|
|
359
|
+
|
|
360
|
+
{hasCount && (
|
|
361
|
+
<>
|
|
362
|
+
{/* Lúc mở: số trơn. */}
|
|
363
|
+
<span aria-hidden className={cn("shrink-0 text-xs tabular-nums text-muted transition-opacity duration-150", isActive && "text-foreground", isCollapsed && "opacity-0")}>
|
|
364
|
+
{formatSidebarCount(item.count)}
|
|
365
|
+
</span>
|
|
366
|
+
{/* Lúc thu: một chấm ở góc icon, CHỈ cho số cần xử lý (chưa đọc, chờ duyệt). Số trong tooltip và aria-label. */}
|
|
367
|
+
{item.isActionable && (
|
|
368
|
+
<span aria-hidden className={cn("absolute left-6 top-2 size-1.5 rounded-full bg-foreground/50 transition-opacity duration-150", !isCollapsed && "opacity-0")} />
|
|
369
|
+
)}
|
|
370
|
+
</>
|
|
371
|
+
)}
|
|
372
|
+
</Link>
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Chân sidebar
|
|
376
|
+
|
|
377
|
+
```
|
|
378
|
+
│ ⚙ Cài đặt │ <- mục nav thường, cùng style các mục trên
|
|
379
|
+
│ ╭──────────────────────╮ │
|
|
380
|
+
│ │ ◐ Trần Nguyễn A… ⇕ │ │ <- một hàng không viền, cả hàng là nút, bấm ra menu
|
|
381
|
+
│ ╰──────────────────────╯ │
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
- **Profile là một hàng bấm được, không khung viền**: `h-10 w-full rounded-xl px-1 hover:bg-item-hover`, như `NavUser` của sidebar shadcn. Avatar `size-8` (theo `avatar.md` nhưng **bỏ viền của avatar**), tên `text-sm font-medium truncate`, icon **`ChevronsUpDown`** `size-4 text-muted` ở mép phải. Đừng bọc khung viền quanh avatar vốn đã có viền: hai đường viền lồng nhau, avatar dính sát mép khung, tên bị cắt sớm, khung thành cục nặng nhất đáy sidebar (chủ dự án bỏ: "footer profile bị xấu").
|
|
385
|
+
- **Dấu hiệu bấm được là icon `ChevronsUpDown` + nền khi rê**, không phải khung. Avatar và tên trôi tự do, không ai biết bấm được, là do **không có icon nào**; có icon mở menu thì hết.
|
|
386
|
+
- **Tâm avatar thẳng tâm icon các link** khi sidebar thu gọn: hàng `px-1` + avatar `size-8` ra tâm 20px, đúng bằng link `px-3` + icon `size-4`.
|
|
387
|
+
- **Cả hàng là một `<button>`**, là trigger của dropdown/popover (`I29`). Đừng làm riêng nút nhỏ ở góc: bấm vào tên mà không có gì xảy ra là người ta tưởng app bị đơ.
|
|
388
|
+
- **Trong hàng chỉ có avatar + tên**, `truncate`. Email đưa lên **đầu menu**, một dòng, **cắt phần trước `@`, giữ nguyên tên miền** theo "Cắt email" ở `overlay.md`. Email là thứ để biết mình đang ở tài khoản nào (`N8`); cắt ở cuối "tran.nguyen.anh.tuan.khang@evond…" là mất đúng phần tên miền cần đọc. Đừng cho xuống dòng (`[overflow-wrap:anywhere]`): trình duyệt bẻ giữa tên miền "…@ev / ondev…", và dòng thứ hai trông như một mục riêng.
|
|
389
|
+
- **Tài khoản chỉ có một lối vào**: app có avatar trên header thì không có hàng profile ở đây, và ngược lại (`overlay.md`, "Menu tài khoản").
|
|
390
|
+
- **Menu mở lên trên** (`side="top"`, `align="start"`), **rộng đúng bằng hàng profile** (`w-(--radix-dropdown-menu-trigger-width)`), không lòi qua mép sidebar sang vùng nội dung, portal ra `body` (`I22`). Mục trong menu **cao 40px, bo 12px**, khung `rounded-2xl p-1`, xem `overlay.md`. Trong menu: email ở đầu (`text-xs text-muted`), rồi Hồ sơ, Giao diện, Cài đặt; **Đăng xuất ở cuối**, cách bằng đường chia, lúc thường trung tính, **rê vào thì đỏ** `rose` (`I4`).
|
|
391
|
+
- **Cài đặt ở trên khung là mục nav thường**, cùng style với các mục ở đầu sidebar (`foreground/70`, hover mờ). Đừng cho nó xám `--muted` hay tách riêng bằng đường kẻ.
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
<DropdownMenu>
|
|
395
|
+
<DropdownMenuTrigger asChild>
|
|
396
|
+
<Button
|
|
397
|
+
variant="ghost"
|
|
398
|
+
className="flex h-10 w-full cursor-pointer items-center gap-2.5 overflow-hidden whitespace-nowrap rounded-xl px-1 text-left hover:bg-item-hover"
|
|
399
|
+
>
|
|
400
|
+
<Avatar name={user.name} src={user.avatarUrl} className="size-8" />
|
|
401
|
+
<span className="min-w-0 flex-1 truncate text-sm font-medium text-foreground">
|
|
402
|
+
{user.name}
|
|
403
|
+
</span>
|
|
404
|
+
<ChevronsUpDown className="size-4 shrink-0 text-muted" />
|
|
405
|
+
</Button>
|
|
406
|
+
</DropdownMenuTrigger>
|
|
407
|
+
<DropdownMenuContent side="top" align="start" className="w-(--radix-dropdown-menu-trigger-width) rounded-2xl p-1">
|
|
408
|
+
{/* Mỗi DropdownMenuItem: h-10 rounded-xl px-3, như link sidebar. */}
|
|
409
|
+
{/* Email một dòng, cắt phần trước @, giữ tên miền: khuôn "Cắt email" ở overlay.md. */}
|
|
410
|
+
<DropdownMenuLabel className="px-3 py-2 font-normal"><AccountEmail email={user.email} /></DropdownMenuLabel>
|
|
411
|
+
<DropdownMenuSeparator />
|
|
412
|
+
{/* Hồ sơ, Giao diện, Cài đặt… */}
|
|
413
|
+
<DropdownMenuSeparator />
|
|
414
|
+
{/* Đăng xuất */}
|
|
415
|
+
</DropdownMenuContent>
|
|
416
|
+
</DropdownMenu>
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Vùng nội dung có **thanh tiêu đề riêng** ở trên: đường dẫn ở trái, nhóm nút ở
|
|
420
|
+
phải. **Thanh nền `--surface` (trắng như sidebar)**, không trong suốt trên nền trang: đầu sidebar
|
|
421
|
+
và thanh header thành một dải trắng liền trên cùng, nội dung xám nằm dưới. Các app đều để header
|
|
422
|
+
nền trắng; thanh trong suốt trông như chưa xong, và thanh dính đỉnh thì nội dung cuộn lên lộ ra
|
|
423
|
+
sau chữ. Thanh tách với nội dung bằng đường kẻ ngang `border-border` (luật chốt 18, `locked-rules.md`), **cùng độ cao `h-16` và cùng màu với đường dưới đầu sidebar** để thành một đường liền chạy ngang cả màn. Đường ngang này **không kéo theo đường kẻ dọc** cho sidebar: kẻ dọc chỉ có khi vùng nội dung cũng trắng (xem đầu mục).
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
┌──────┐░┌─────────────────────────────┐
|
|
427
|
+
│ w-60 │░│ header h-16 │
|
|
428
|
+
├──────┤░├─────────────────────────────┤ <- hai đoạn cùng một đường
|
|
429
|
+
│ nav │░│ nội dung │
|
|
430
|
+
└──────┘░└─────────────────────────────┘
|
|
431
|
+
↑ không kẻ dọc khi nội dung nền xám
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
### Nhóm nút bên phải thanh header
|
|
435
|
+
|
|
436
|
+
```
|
|
437
|
+
Phòng trọ ♡ 🔔 Quản lý tin Đăng nhập [+ Đăng tin]
|
|
438
|
+
└ chỉ icon ┘ └──── ghost có chữ ────┘ └ nút đặc duy nhất
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
- **Một nút đặc**, là việc chính của cả sản phẩm (Đăng tin, Tạo mới), đặt **cuối hàng**. Còn
|
|
442
|
+
lại là nút ghost. Màu nút đặc theo vai màu của dự án (`review.md`), nhưng **cùng chiều cao,
|
|
443
|
+
cùng bo, cùng cỡ chữ** với nút ghost cạnh nó (`h-9`, `rounded-lg`, `text-sm font-medium`),
|
|
444
|
+
không bóng màu (`M15`), không chữ `font-extrabold`.
|
|
445
|
+
- **Mục chỉ cần nhận ra, không cần đọc thì chỉ icon**: đã lưu (tim), thông báo (chuông, có
|
|
446
|
+
chấm hay số khi có tin mới), giỏ. Chữ nằm ở `aria-label` và tooltip. Các trang rao vặt và
|
|
447
|
+
đặt phòng lớn đều để tim và chuông chỉ icon. Mục có chữ tối đa khoảng ba.
|
|
448
|
+
- **Chữ nút ghost không nặng hơn tên trang** bên trái: `font-medium`, màu chữ thường hoặc
|
|
449
|
+
`--muted`. Năm mục cùng `font-semibold` màu chữ chính là năm thứ tranh với tên trang.
|
|
450
|
+
- **Khoảng giữa các nút `gap-2`** (8px): padding của nút ghost đã là khoảng thở, `gap-2` tách
|
|
451
|
+
nền rê của hai nút cạnh nhau cho khỏi dính. `gap-4` cộng padding thì hàng trải ra nửa header,
|
|
452
|
+
đọc như menu trang giới thiệu. Đừng hạ xuống `gap-1` (4px): bốn nút sát nhau trông như một
|
|
453
|
+
cục (chủ dự án nâng).
|
|
454
|
+
- **Mục đã có trong sidebar thì không lặp trên header** (luật "Hai chỗ một việc", `V1b`).
|
|
455
|
+
Bỏ bên nào là quyết định của người dùng: đưa lên bảng, không chọn sẵn.
|
|
456
|
+
- **Liên hệ, hỗ trợ, tải app** không đứng cùng hàng với việc chính: để cuối sidebar hoặc
|
|
457
|
+
trong menu tài khoản.
|
|
458
|
+
- Đăng nhập khi chưa có tài khoản là một nút ghost ("Đăng nhập"); đăng ký nằm trong màn đăng
|
|
459
|
+
nhập. Dự án đã tô link đăng nhập bằng màu riêng thì giữ màu (vai màu), vẫn theo cỡ chung.
|
|
460
|
+
|
|
461
|
+
Wireframe đã vẽ năm mục cùng cỡ, một nút đặc thì đừng giữ header cũ của dự án: sáu mục icon +
|
|
462
|
+
chữ `font-semibold` cách nhau 32px, nút đặc bo tròn hẳn cao 30px chữ 13px `font-extrabold` có
|
|
463
|
+
bóng màu đứng giữa các nút bo 12px cao 34px chữ 14px. Probe đo "hàng nút trên header không
|
|
464
|
+
đồng cỡ".
|
|
465
|
+
|
|
466
|
+
### Đầu trang trong vùng nội dung
|
|
467
|
+
|
|
468
|
+
```
|
|
469
|
+
Khách hàng › Khách hàng doanh nghiệp <- chỉ các cấp CHA, là link
|
|
470
|
+
Công ty TNHH Minh Phát [⤓ Xuất file] [+ Tạo đơn hàng]
|
|
471
|
+
Khách hàng từ 3/2024, 18 đơn hàng, doanh thu 1.284.500.000 đ
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
```tsx
|
|
475
|
+
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
476
|
+
{/* flex-1: khối chữ lấy hết chỗ còn lại, không co theo dòng dài nhất */}
|
|
477
|
+
<div className="min-w-0 flex-1">
|
|
478
|
+
<Breadcrumb items={parents} />{/* ../components/breadcrumb.md; hàng h-8 đã chừa khoảng, tên không mt */}
|
|
479
|
+
<h1 className="text-xl font-semibold text-balance">{title}</h1>
|
|
480
|
+
<p className="mt-1 max-w-[55ch] text-sm text-pretty text-muted">{description}</p>
|
|
481
|
+
</div>
|
|
482
|
+
<div className="flex shrink-0 gap-2">{actions}</div>
|
|
483
|
+
</header>
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
- **Đường dẫn chỉ ghi các cấp cha, không ghi trang đang đứng.** Tên trang nằm ngay dưới, ghi lại là lặp ("Cài đặt › Thành viên" rồi "Thành viên"). Ghi một chữ khác tên trang còn tệ hơn: "Khách hàng › Hồ sơ" trên đầu "Công ty TNHH Minh Phát", người đọc không biết mình đang ở đâu. Hình, đường dài, màn hẹp: `../components/breadcrumb.md`.
|
|
487
|
+
- **Đường dẫn đặt ở MỘT chỗ.** App đã có đường dẫn trên thanh header `h-16` thì đầu trang không lặp lại, chỉ còn tên, mô tả, nút.
|
|
488
|
+
- **Một trang đúng một `<h1>`, và tên trang chỉ ghi MỘT chỗ.** Hai `<h1>` thì trình đọc màn hình không biết trang này tên gì ("Việc của tôi" trên header và "Tạo công việc mới" cùng là `<h1>`); không `<h1>` nào thì cũng vậy (bỏ đầu trang để khỏi lặp "Khách hàng", mất luôn `<h1>`). Chia theo loại trang:
|
|
489
|
+
- **Trang không có đầu trang riêng** (danh sách, bảng quản lý, tổng quan, kanban): tên trên thanh header `h-16` **chính là `<h1>`**, giữ nguyên cỡ chữ của thanh (`text-base font-semibold`, cỡ chữ không đổi theo thẻ; 700 chỉ cho trang trình diễn, `T2`). Vùng nội dung không lặp lại tên; nút chính ("+ Thêm khách hàng") nằm cuối hàng công cụ cạnh ô tìm.
|
|
490
|
+
- **Hàng công cụ không vừa một hàng mà tách hai thì nút chính lên cuối hàng TRÊN** (`ml-auto`,
|
|
491
|
+
`shrink-0`), thẳng mép phải vùng nội dung; ô tìm, lọc xuống hàng dưới. Đừng để hàng dưới
|
|
492
|
+
`flex` trơn nối nút chính sau ô lọc: nút lơ lửng giữa hàng, không thẳng với khối nào. Dưới `sm` giữ xếp dọc, nút chính rộng hết ở cuối: đưa lên hàng trên thì chữ ngày gãy dòng.
|
|
493
|
+
- **Có điều hướng ngày** (lịch, lịch hẹn, báo cáo theo ngày) thì cụm `‹ [Hôm nay] ›` theo đúng
|
|
494
|
+
mục "Lịch công việc" bên dưới: ‹ › là `ghost` chỉ icon, chỉ "Hôm nay" có viền. Ba ô viền
|
|
495
|
+
cạnh nhau là ba khối nặng cho một việc phụ.
|
|
496
|
+
- **Trang có đầu trang riêng** (chi tiết bản ghi, form tạo, trang có mô tả hay nút riêng cho bản ghi): `<h1>` là tên trong đầu trang; thanh header chỉ ghi **cấp cha** ("Khách hàng" là link), bằng `<p>`/`<nav>`, không ghi lại tên trang.
|
|
497
|
+
- Khung app nhận tên trang từ route rồi tự chọn thẻ: có đầu trang riêng thì `<p>`, không thì `<h1>`. Đừng để mỗi trang tự nhớ.
|
|
498
|
+
- **Tên trang `text-xl`**, trang chi tiết của một bản ghi (khách hàng, đơn, dự án) thì `text-lg` (`budgets.md`; `T9` chỉ cho nội dung lặp lại như bài viết, sản phẩm). Không `text-2xl`, `text-3xl`: đó là cỡ hero (`budgets.md`). `text-balance` để tên dài xuống dòng đều.
|
|
499
|
+
- **Khối chữ `min-w-0 flex-1`.** Thiếu `flex-1` thì khối co theo dòng dài nhất (thường là đường dẫn), mô tả bị ép xuống dòng ở nửa khung dù bên phải còn trống (sửa `max-w` không ăn vì bề rộng đã bị flex bóp trước).
|
|
500
|
+
- **Tên trang `font-semibold`, không `tracking-tight`** ở cỡ `lg`/`xl`. Tên trang là chữ đậm nhất vùng nội dung; nhạt hơn tiêu đề khối bên dưới là đảo thứ bậc.
|
|
501
|
+
- **Mô tả `max-w-[55ch] text-pretty`**: đủ rộng để một câu ngắn nằm một dòng, câu dài vẫn dưới 75 ký tự mỗi dòng (`T11`). Đừng dùng `max-w-2xl`: ở `text-sm` thực tế ~99 ký tự.
|
|
502
|
+
- **Nút bên phải, bám mép trên** (`sm:items-start`), `shrink-0`. Tối đa một nút `primary` (hành động chính của trang, `I3`), còn lại nút viền có icon (`I1`). Từ nút thứ ba thì gom vào nút `MoreHorizontal`.
|
|
503
|
+
- **Màn hẹp**: nút xuống dưới chữ, căn trái, giữ trên một hàng, không để hai nút trên một nút dưới (`../responsive.md`).
|
|
504
|
+
- Không có mô tả, không có nút thì đầu trang chỉ còn tên, không chừa chỗ trống.
|
|
505
|
+
|
|
506
|
+
---
|
|
507
|
+
|
|
508
|
+
## Trang báo cáo (doanh thu, phân tích)
|
|
509
|
+
|
|
510
|
+
Trang để **đọc số theo một khoảng ngày**, khác màn tổng quan (vào để bắt tay làm).
|
|
511
|
+
|
|
512
|
+
```
|
|
513
|
+
thanh header: ☰ Báo cáo doanh thu 🔔 (T)
|
|
514
|
+
┌──────────────────────────────┐
|
|
515
|
+
│ 29/08/2026 – 27/09/2026 📅 │ <- ô khoảng ngày, căn trái, mở đầu vùng nội dung
|
|
516
|
+
└──────────────────────────────┘
|
|
517
|
+
So với 30 ngày liền trước, 30/07/2026 – 28/08/2026 <- kỳ so, ghi MỘT lần
|
|
518
|
+
┌ Doanh thu ─┬ Số đơn ─┬ Giá trị đơn TB ─┬ Tỷ lệ hoàn tiền ┐
|
|
519
|
+
│ 1,62 tỷ đ │ 2.571 │ 632.000 đ │ 2,1% │
|
|
520
|
+
│ ↗ 4,4% │ ↗ 4,6% │ — Không đổi │ ↘ 0,1 điểm │
|
|
521
|
+
└────────────┴─────────┴─────────────────┴─────────────────┘
|
|
522
|
+
┌ Doanh thu theo ngày ──────────────────────────────────────┐
|
|
523
|
+
│ 60 tr ─────────────────────────────────────────────────── │ <- lưới ngang + nhãn mức
|
|
524
|
+
│ 40 tr ───────╱╲──────╱╲─────── 13/09 · 70,9 tr đ ──────── │
|
|
525
|
+
│ 20 tr ─────────────────────────────────────────────────── │
|
|
526
|
+
│ 0 ─────────────────────────────────────────────────────── │
|
|
527
|
+
│ 02/09 07/09 12/09 17/09 22/09 27/09 │
|
|
528
|
+
└───────────────────────────────────────────────────────────┘
|
|
529
|
+
┌ Doanh thu theo kênh ────────┐ ┌ Sản phẩm bán chạy ────────┐
|
|
530
|
+
│ Website 636,4 tr đ 39% │ │ Tai nghe… 123 đơn 232 tr│
|
|
531
|
+
│ ▓▓▓▓▓▓▓▓░░░░░░░░ │ │ … │
|
|
532
|
+
└─────────────────────────────┘ └────────────────────────────┘
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
- **Ô khoảng ngày là bộ lọc của cả trang**, mở đầu vùng nội dung, căn trái, `sm:w-72`. Tên trang đã nằm trên thanh header thì không dựng thêm đầu trang. Ô theo "Khoảng ngày" trong `../components/choice-controls.md`; trường nhìn lùi nên **ngày sau hôm nay khoá** (gợi ý mặc định, danh sách ngày khoá là của người dùng) và mở ra với tháng hiện tại bên phải.
|
|
536
|
+
- **Kỳ so ghi một lần** ở dòng `text-xs text-muted` ngay trên hàng ô số (`../components/charts.md`, "Dòng so sánh"), không lặp trong từng ô. Chưa có kỳ trước (tháng mở bán đầu tiên) thì dòng đó ghi "Chưa có kỳ trước để so" và các ô bỏ dòng so sánh.
|
|
537
|
+
- **Kỳ so theo kiểu khoảng**, như các công cụ phân tích phổ biến:
|
|
538
|
+
- Khoảng trượt ("7 ngày qua", "30 ngày qua", khoảng tự chọn): **cùng số ngày liền trước**. "So với 30 ngày liền trước, 30/07 – 28/08/2026".
|
|
539
|
+
- Khoảng theo lịch ("Tháng này", "Quý này", "Năm nay", cả một tháng): **cùng kỳ của đơn vị trước**, tính tới cùng ngày. Tháng này tới 26/09 thì so 01/08 – 26/08 ("So với cùng kỳ tháng trước"); năm nay thì so 01/01 – 26/09/2025 ("So với cùng kỳ năm trước"). Lùi đúng số ngày ra những khoảng không ai nghĩ tới: "Từ đầu năm" ghi "So với 269 ngày liền trước, 07/04/2025 – 31/12/2025", "Cả tháng 9" ghi "so với 26 ngày liền trước, 06/08 – 31/08".
|
|
540
|
+
Chọn kỳ nào là logic của người dùng (`N10`); skill lo câu chữ và mặc định khi đề để hở.
|
|
541
|
+
- **Mỗi mốc của biểu đồ theo độ dài khoảng**, như trang doanh thu của các cổng thanh toán lớn: **1 ngày → theo giờ** (24 mốc), tới ~31 ngày → ngày, tới ~92 ngày → tuần, dài hơn → tháng. Tên card đổi theo: "Doanh thu theo giờ / ngày / tuần / tháng". Khoảng một ngày mà thay biểu đồ bằng khối chữ "87,6 tr đ · Chọn từ hai ngày trở lên để thấy xu hướng" là lặp đúng số ở ô "Doanh thu" ngay trên (`N3`) và bắt người dùng đổi khoảng mới thấy gì. Khối chữ "một điểm" chỉ dùng khi dữ liệu thật sự mới có một mốc.
|
|
542
|
+
- **Biểu đồ chính có lưới ngang và nhãn mức** (ngoại lệ trong "Bỏ bớt đi" của `../components/charts.md`), số ở điểm đang xem **kèm mốc** ("13/09 · 70,9 tr đ", "Hôm nay · 44,4 tr đ"), đặt về phía không có đường, và Tab vào thì số của mốc đó hiện ra. Kỳ chưa trọn (hôm nay, tuần này) nét đứt.
|
|
543
|
+
- **Hai khối phân tích dưới biểu đồ**, hai cột từ `lg`, một cột ở màn hẹp: phần chia theo nhóm (kênh, khu vực) là danh sách thanh xếp lớn dần, mỗi dòng số + phần trăm (`../components/charts.md`, "Thanh tiến độ trong danh sách"); top mục (sản phẩm, khách hàng) là danh sách dòng tên + dòng phụ bên trái, số bên phải `tabular-nums`, 5 dòng.
|
|
544
|
+
- **Trạng thái**: đang tải giữ khung từng khối với skeleton đúng chiều cao (ô khoảng ngày vẫn dùng được); tải hỏng một khung `ListError` thay phần dưới ô khoảng ngày; khoảng không có đơn nào thì một khung câu "Không có đơn nào từ … tới …" + link về khoảng mặc định. Khoảng ở tương lai không có ca rỗng riêng: ngày sau hôm nay đã khoá trong lịch.
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## Trang chi tiết bản ghi
|
|
549
|
+
|
|
550
|
+
```
|
|
551
|
+
thanh header: ☰ Khách hàng 🔔 (T)
|
|
552
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
553
|
+
│ (N) Nguyễn Minh Anh <h1> text-lg [✉ Email] [+ Tạo đơn] ⋯│
|
|
554
|
+
│ ● Đang giao dịch Lumen Studio │
|
|
555
|
+
│ │
|
|
556
|
+
│ 12 tháng gần nhất, so với 12 tháng trước ┌ Liên hệ ─────────┐ │
|
|
557
|
+
│ ┌ Doanh thu ┬ Đơn đã giao ┬ Giá trị ┬ Hoàn ┐ │ Email ✉ ⧉ │ │
|
|
558
|
+
│ └───────────┴─────────────┴─────────┴──────┘ │ Điện thoại ☎ ⧉ │ │
|
|
559
|
+
│ ┌ Đơn hàng 24 · Tin nhắn · Tệp 4 · Hoạt động ┐ └──────────────────┘ │
|
|
560
|
+
│ │ DH-10412 20/09 3 SP ● Đã giao 12,6 tr │ ┌ Phân loại ───────┐ │
|
|
561
|
+
│ │ ... │ │ Nhãn, phụ trách │ │
|
|
562
|
+
│ │ Xem tất cả 24 đơn → │ └──────────────────┘ │
|
|
563
|
+
│ └───────────────────────────────────────────┘ │
|
|
564
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
565
|
+
khung nội dung từ 70rem (@container, không phải xl): cột chính minmax(0,1fr) + cột phải 22rem;
|
|
566
|
+
hẹp hơn một cột: số liệu → hai card (khung từ 40rem thì đứng cạnh nhau) → khối tab
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
Đầu trang theo mục trên (`<h1>` `text-lg`, thanh header chỉ ghi cấp cha). Hàng số liệu theo
|
|
570
|
+
`../components/charts.md`, card Liên hệ / Phân loại theo `../components/description-list.md`
|
|
571
|
+
(xếp chồng, cột phải hẹp), dòng thời gian theo `../components/timeline.md`.
|
|
572
|
+
|
|
573
|
+
- **Cột phải chỉ mở khi cột chính còn ≥ ~744px: khung nội dung (không phải màn) từ `@[70rem]`.** Khung
|
|
574
|
+
trang là `@container`, lưới `@[70rem]:grid-cols-[minmax(0,1fr)_22rem]`. Mở theo `xl:` thì ở 1280px, sidebar
|
|
575
|
+
mở, khung 992px trừ cột phải còn 616px cho bốn ô số và bảng: "14/10 · 09:00" gãy đôi trong ô số, nhãn ô
|
|
576
|
+
hai dòng, cột Dịch vụ 122px xuống hai ba dòng ở cả 11 dòng. Một cột ở 1280px: ô số một dòng, bảng
|
|
577
|
+
không dòng nào gãy, hai card đứng cạnh nhau. 1366px (khung 1078px) vẫn một cột; 1440px mở cột phải
|
|
578
|
+
(cột chính 776px).
|
|
579
|
+
- **Card cột phải xếp theo việc chính của trang, không chép thứ tự Liên hệ → Phân loại của khuôn khách
|
|
580
|
+
hàng.** Card phục vụ việc chính đứng đầu: hồ sơ bệnh nhân bác sĩ mở trước khi khám thì **Y tế** (bệnh
|
|
581
|
+
nền, thuốc đang dùng, lưu ý) trước **Liên hệ**; trang khách của bán hàng thì Liên hệ trước. Một cột
|
|
582
|
+
thì card đầu đứng trái / trên, nên thứ tự này quyết cái gì hiện ngay dưới hàng số ở điện thoại. Chép
|
|
583
|
+
thứ tự thì "Sợ tiêm, giải thích từng bước trước khi gây tê" nằm cuối card Y tế, dưới cả địa chỉ
|
|
584
|
+
và người thân; ở 375px phải cuộn qua hết card Liên hệ mới tới.
|
|
585
|
+
|
|
586
|
+
- **Trang chi tiết không phải panel xem nhanh phóng to.** Panel để liếc một khách giữa danh sách; trang để làm việc với khách đó, và việc chính là xem, mở **các bản ghi con**. Mượn khuôn của panel (hàng tên, ô số, danh sách mô tả) được, bê nguyên bộ tab của panel thì không (`../principles.md`, "Dựng một thứ chưa có mẫu").
|
|
587
|
+
- **Tab đầu tiên là bản ghi con chính**: khách hàng → Đơn hàng, dự án → Công việc, công ty → Người liên hệ. Tiếp theo mới tới Tin nhắn, Tệp, Hoạt động. Chép tab Tin nhắn / Tệp / Hoạt động của panel thì ô số ghi 24 đơn, nút chính là "Tạo đơn", mà muốn xem đơn phải lội tab Hoạt động, nơi 24 đơn chỉ là 24 dòng "Đơn đã giao" lẫn với "Gắn nhãn VIP", không lọc, không mở được đơn nào.
|
|
588
|
+
- **Tab bản ghi con là bảng gọn**, không phải dòng thời gian: Mã đơn (`font-mono`, link sang đơn), Ngày đặt, Số sản phẩm, Trạng thái (badge `M7`), Tổng tiền căn phải `tabular-nums`. Mới nhất lên đầu, 10 dòng, cuối bảng là link "Xem tất cả 24 đơn" sang danh sách đơn đã lọc sẵn theo khách này, **căn trái thẳng mép chữ cột đầu**, cùng phía với "Xem hoạt động cũ hơn" ở tab Hoạt động: đổi tab mà lối "xem thêm" nhảy từ mép phải sang mép trái là mắt phải đi tìm lại. Không phân trang trong tab. Dưới `sm` thành danh sách dòng như bảng quản lý (mục "Bảng dữ liệu").
|
|
589
|
+
- **Tab bản ghi con có số đếm, tab dòng chảy thì không**: "Đơn hàng 24", "Tệp 4", còn "Tin nhắn", "Hoạt động" để chữ trơn. Dữ liệu có sẵn số nên đây là ca được thêm số của `../components/small-controls.md`; khách mới nhìn hàng tab là biết tab nào rỗng, khỏi bấm từng tab. Số tin nhắn, số hoạt động thì lớn dần mãi, đọc không ra gì.
|
|
590
|
+
- **Hai số cùng đếm một thứ mà khác kỳ thì ô số ghi kỳ ngay trong nhãn.** Số trên tab đếm từ trước tới nay (bảng liệt kê đủ), hàng số liệu tính 12 tháng như panel (`N5`). Ô nào đếm cùng thứ với tab thì nhãn là "Đơn đã giao · 12 tháng", các ô khác giữ nhãn trơn, dòng kỳ trên hàng vẫn ghi một lần. Thiếu kỳ trong nhãn thì "Đơn đã giao 24" và "Đơn hàng 42" cách nhau 100px, dòng "12 tháng gần nhất" ở trên hàng không đủ gỡ, người đọc tưởng số sai. Không đổi hàng số sang trọn đời: panel và trang của cùng một khách sẽ ra hai bộ số.
|
|
591
|
+
- **Bản ghi khác nhắc tới trên trang là link**: mã đơn trong dòng hoạt động, mã đơn trong bảng, tên tệp. Cả vùng nội dung không có link nào là trang cụt: thấy "Đơn DH-10412" mà không mở được.
|
|
592
|
+
- **Hành động gắn với một giá trị thì nằm cạnh giá trị đó**, không vào menu ⋯ đầu trang. Email là link `mailto:`, số điện thoại là link `tel:`, rê vào hàng thì hiện icon button sao chép (`description-list.md`). Menu ⋯ chỉ còn việc với cả bản ghi: Sửa thông tin, rồi Xoá sau đường chia (`I11`). Đừng để "Gọi điện" và "Sao chép email" trong menu ⋯ ở góc trên: số điện thoại và email ngay bên dưới thành chữ chết.
|
|
593
|
+
- **Khách chưa có đơn nào thì bỏ hẳn hàng số liệu.** Tab Đơn hàng rỗng đã nói "Chưa có đơn nào", nút "Tạo đơn" đã ở đầu trang; giữ thêm khung "Chưa có đơn nào. Số liệu hiện sau đơn đầu tiên" là hai khối cùng nói một ý (`N3`). Khung gọn đó chỉ dành cho panel, nơi không có tab Đơn hàng (`../components/charts.md`).
|
|
594
|
+
- **Không tìm thấy bản ghi** (id sai, đã xoá) là một ca 404 trong khung app: dựng đúng khối căn giữa của "Trang lỗi" bên dưới (dòng "404" mờ, `<h1>` "Không tìm thấy khách hàng", một câu vì sao), nhưng **nút đặc là "Về danh sách khách hàng"**, không phải "Về trang tổng quan": người mở một khách hỏng gần như luôn muốn tìm khách khác (`N6`). Lối phụ "Quay lại trang trước" như 404. Thanh header vẫn ghi cấp cha. Không dựng đầu trang căn trái kèm một link chữ trơn: link cao 20px là chỗ bấm dưới 32px trên điện thoại, và một trang lỗi trong khung app mà hai kiểu là hai khuôn cho một việc.
|
|
595
|
+
|
|
596
|
+
---
|
|
597
|
+
|
|
598
|
+
## Bảng kanban
|
|
599
|
+
|
|
600
|
+
```
|
|
601
|
+
○ Cần làm 4 ◉ Đang làm 3 ⋯ Chờ duyệt 2 ✓ Xong 3
|
|
602
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
603
|
+
│ tiêu đề việc │ │ tiêu đề việc │ │ tiêu đề việc │ │ tiêu đề việc │
|
|
604
|
+
│ dự án · hạn │ │ dự án · hạn │ │ dự án · hạn │ │ dự án · xong │
|
|
605
|
+
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
606
|
+
┌──────────────┐ ┌──────────────┐
|
|
607
|
+
│ ... │ │ ... │
|
|
608
|
+
└──────────────┘ └──────────────┘
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
- **Cột không tô màu riêng.** Đầu cột là icon trạng thái + tên + số đếm, icon và màu icon lấy đúng bảng trạng thái ở `M7` (`circle`, `circle-dot`, `circle-ellipsis`, `circle-check`), cùng hình với hàng nhóm của view danh sách (`D2`). Màu chỉ ở icon `size-4`; nền cột, viền cột, chữ tên cột không màu. Bốn cột bốn nền màu là dấu hiệu chưa quyết định được cái nào quan trọng, và nó phá luật một màu nhấn.
|
|
612
|
+
- **Thẻ ở cột Xong không thêm ✓ trước tiêu đề.** Đầu cột đã có `circle-check`, ✓ trên từng thẻ là nói một ý hai lần (`M6`), lại đẩy tiêu đề lệch cột so với thẻ các cột khác. Cột Xong khác ở dòng phụ: "xong 18/09" thay cho hạn chót.
|
|
613
|
+
- **Thẻ là card thật**, đây là ngoại lệ hợp lệ của luật `F3`: thẻ kanban là vật kéo thả được, không phải một dòng trong danh sách.
|
|
614
|
+
- **Thẻ `p-4`, gap giữa thẻ `gap-3`, gap giữa cột `gap-4`.** Xem `budgets.md`. Đừng hạ xuống `p-3`, chật.
|
|
615
|
+
- **Tiêu đề việc không `truncate`**, cho xuống tối đa hai dòng rồi mới cắt. Thẻ hẹp mà cắt một dòng thì đọc không ra việc gì.
|
|
616
|
+
- **Cột `w-[248px] shrink-0 grow max-w-[320px]`, hàng bên trong `flex w-max min-w-full`.** Bốn cột vừa khít ở 1366px trở lên khi sidebar mở, rộng hơn thì cột giãn đều tới 320px; hẹp hơn thì cột giữ 248px và cuộn ngang. Cột cứng `w-[280px]` cần 1216px, mà khung nội dung ở 1440px (sidebar `w-64`) chỉ có 1184px: cột Xong hụt 8px ở mép phải, ở 1366px hụt 32px, đúng hai bề rộng laptop hay gặp nhất, trông như lỗi chứ không như "còn nữa". `w-max` giữ lề phải khi cuộn, `min-w-full` cho hàng đủ rộng để cột giãn. Sidebar hoặc lề trang khác thì tính lại: 4 × bề rộng cột + 3 khe + 2 lề ≤ bề rộng khung ở 1366px.
|
|
617
|
+
- **Màn hẹp thì cuộn ngang trong khung**, không wrap thành hai hàng. Xem luật `R6` trong `../responsive.md`. Lề đặt trên hàng bên trong (`flex gap-4 px-3`), không đặt trên khung cuộn, nếu không cột cuối dính sát mép. **Khung cuộn của board không `scrollbar-clean`**: dùng thanh tự ẩn (`I18`) và mép mờ (`R10`). `scrollbar-clean` chỉ dành cho hàng chip, hàng tab (`rules-state.md`); board ẩn hẳn thanh thì người dùng chuột không có bánh xe ngang (Windows) chỉ còn Shift + lăn để thấy cột bị khuất.
|
|
618
|
+
- **Nút ⋯ trên thẻ: hiện khi rê hoặc Tab vào thẻ**, như các app board lớn: 16 thẻ là 16 dấu ⋯ đứng yên, nhiễu hơn cả tên việc. `opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 aria-expanded:opacity-100 [@media(hover:none)]:opacity-100` trên chính nút, thẻ là `group` (chỗ vẫn giữ, avatar không nhảy). Khác bảng (`I11`): bảng dò theo cột nên nút luôn hiện, thẻ thì không có cột.
|
|
619
|
+
- **Nút ⋯ nằm ở hàng cuối thẻ, ngay trước avatar**, không ở góc trên phải. Hàng cuối `flex h-8 items-center justify-between`: ưu tiên bên trái, bên phải một cụm `flex items-center gap-1` gồm nút ⋯ `size-8` rồi avatar `size-6`. Hàng cuối chỉ có ưu tiên (chữ ngắn) nên dư chỗ; góc trên phải là chỗ của tên việc. Hai cách đã bỏ (cột 248px): nút ở góc trên, tiêu đề `pr-8` giữ chỗ thì 5/15 tên bị cắt "…", còn cho 3 dòng thì ra chữ mồ côi ở dòng 3 kèm một khoảng trống 32px bên phải dòng 1–2; nút đè lên chữ không giữ chỗ (kiểu nút sửa hiện lên khi rê) thì nền nút cắt đôi một chữ ("doanh thu c⋯"). Ở hàng cuối: 1/15 tên bị cắt (tên thật sự dài), thẻ chỉ cao thêm 8px. Không kéo `-my-*` để giữ hàng `h-6`: Button có `max-w-full`, margin âm làm khối bọc co và bóp nút (nút còn 24×32, `N11`).
|
|
620
|
+
- **Đầu cột có nút `plus` thêm việc vào cột đó**: icon button ghost `size-8` ở cuối hàng đầu cột (`ml-auto`), `aria-label="Thêm việc vào Cần làm"`, mở form tạo với trạng thái của cột. Board nào cũng có lối thêm ngay tại cột; chỉ có nút ở đầu trang thì thêm xong còn phải kéo thẻ sang cột đúng.
|
|
621
|
+
- **Cột rỗng vẫn phải chiếm chỗ**, xem `../components/empty-state.md`.
|
|
622
|
+
- Quá năm cột thì hỏi xem có nên gộp bớt trạng thái không.
|
|
623
|
+
|
|
624
|
+
### Kéo thả thẻ
|
|
625
|
+
|
|
626
|
+
Kéo thẻ sang cột khác là đổi trạng thái. Dựng đủ ba đường vào, không chỉ chuột:
|
|
627
|
+
|
|
628
|
+
- **Chuột: nhích quá 4px mới tính là kéo**, không thì cú bấm run tay thành kéo, và bấm thẻ để mở chi tiết không được.
|
|
629
|
+
- **Tay: giữ yên 250ms mới nhấc**, ngón trôi quá 8px trong lúc chờ là đang cuộn, bỏ kéo. Vuốt ngay phải là cuộn board như thường. Thẻ `select-none [-webkit-touch-callout:none]`, không thì giữ lâu ra menu chép chữ của iOS.
|
|
630
|
+
- **Bàn phím: thẻ vào được bằng Tab**, `aria-roledescription="thẻ kéo thả được"`, `aria-describedby` trỏ tới câu hướng dẫn `sr-only`: Space nhấc, ← → đổi cột, Space hoặc Enter thả, Esc huỷ. Mỗi bước đọc qua một vùng `aria-live="polite"`: "Đã nhấc … ở cột Cần làm", "Cột Đang làm", "Đã chuyển … sang Đang làm", "Đã huỷ kéo, … vẫn ở Cần làm". Thả xong tiêu điểm vẫn ở thẻ.
|
|
631
|
+
- **Thẻ đang cầm là một bản sao nổi**: portal ra `body`, `fixed`, bay theo con trỏ bằng `transform` gắn thẳng vào DOM (không render lại cả board mỗi lần nhích), giữ đúng điểm đã nắm trên thẻ. Viền `--border-strong` + `shadow-lg` vì nó là lớp nổi (`M15`). Không nghiêng, không phóng to (`F22`). Bản sao `inert`, thẻ thật vẫn ở trong cột cho trình đọc màn hình. Nhấc bằng phím thì chính thẻ đó mang viền + bóng này, cùng một ý "đang cầm".
|
|
632
|
+
- **Chỗ thả là khung viền đứt `border-foreground/40`, cao đúng bằng thẻ**, đứng đúng vị trí thẻ sẽ nằm theo khoá sắp xếp của cột (cột xếp theo hạn chót thì khung nằm giữa 28/09 và 05/10, không nằm dưới con trỏ). Đậm hơn khung cột rỗng (`foreground/15`): cột rỗng là "chỗ trống", khung này là "rơi vào đây" (`N2`). Số đếm ở đầu hai cột đổi ngay lúc kéo.
|
|
633
|
+
- **Cả dải dọc của cột là vùng thả**, tính theo tọa độ ngang, không riêng phần có thẻ: cột ngắn thì thả vào khoảng trống bên dưới vẫn được.
|
|
634
|
+
- **Kéo tới mép khung thì board tự cuộn ngang**, nhanh dần khi càng sát mép. Không có thì cột bị khuất không bao giờ thả tới được ở 375px.
|
|
635
|
+
- **Nhấc bằng phím mà đổi sang cột đang khuất thì cuộn cả cột đích vào khung**, không chỉ cuộn cho có. Khung cuộn `scroll-px-8` (bằng bề rộng mép mờ `R10`), rồi **chỉ cuộn ngang** khung board: so mép `<section>` của cột với mép khung trừ `scroll-padding`, `scroller.scrollBy({ left })` phần hụt; sau đó `card.scrollIntoView({ block: "nearest", inline: "nearest" })` cho chiều dọc. Đừng `scrollIntoView` trên cả cột: cột cao hơn màn thì trình duyệt canh đỉnh cột, trang nhảy dọc mỗi lần bấm mũi tên. Chỉ cuộn cho có thì ở 1280px, sang cột Xong board dừng ở 40 trên 64px, thẻ đang cầm mất mép phải, nằm dưới mép mờ.
|
|
636
|
+
- **Thả xong bản sao bay về chỗ mới** 200ms, `cubic-bezier(0.32, 0.72, 0, 1)` như panel trượt (`overlay.md`), `motion-reduce` thì đặt thẳng. Kéo sang cột Xong thì dòng phụ đổi sang "xong dd/mm" ngay.
|
|
637
|
+
|
|
638
|
+
**Code mẫu đã duyệt: `app-kanban.html`.** Chép cấu trúc từ đó, đừng dịch lại từ
|
|
639
|
+
mấy gạch đầu dòng trên. File đó đã qua vòng tra tấn 375px, cuộn ngang có lề hai
|
|
640
|
+
đầu, thẻ `p-4`, chip lọc `h-9`, header `h-16` nền trắng như khung app. Ba chip vừa 375px
|
|
641
|
+
nên hàng chip không cuộn; nhiều chip hơn thì theo `R6`, `R10`. File tĩnh, không có menu ⋯,
|
|
642
|
+
kéo thả và mép mờ của board: ba thứ đó theo các gạch đầu dòng trên và mục "Kéo thả thẻ".
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## Trang lịch (lịch tháng)
|
|
647
|
+
|
|
648
|
+
Lịch việc theo hạn chót (hoặc lịch hẹn): lưới tháng đủ khi khung rộng, lịch gọn + danh
|
|
649
|
+
sách việc của ngày đang chọn khi khung hẹp.
|
|
650
|
+
|
|
651
|
+
```
|
|
652
|
+
Tháng 9, 2026 ⌄ [Hôm nay] ‹ › [+ Thêm việc]
|
|
653
|
+
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┐
|
|
654
|
+
│ T2 │ T3 │ T4 │ T5 │ T6 │ T7 │ CN │
|
|
655
|
+
├──────┼──────┼──────┼──────┼──────┼──────┼──────┤
|
|
656
|
+
│ 31 │ 1 │ 2 │ … │ │ (26) │ 27 │ <- lưới: hôm nay vòng đặc
|
|
657
|
+
│ ○ Họp│ │ ✓ Gửi│ │ │ ◉ Viế│ ⋯ Bản│
|
|
658
|
+
│ │ │ │ │ │ ○ Đặt│ │
|
|
659
|
+
│ │ │ │ │ │ 2 việc khác │
|
|
660
|
+
```
|
|
661
|
+
|
|
662
|
+
- **Hàng công cụ: "Hôm nay" liền ‹ ›, một cụm điều hướng.** Tên tháng (nút mở lưới 12 tháng)
|
|
663
|
+
bên trái, rồi `[Hôm nay] ‹ ›`, nút chính "Thêm việc" `ml-auto` ở cuối. Bộ lịch phổ biến
|
|
664
|
+
nhất đặt `today prev,next` liền nhau làm mặc định, các app lịch lớn cũng vậy: cả ba đều là
|
|
665
|
+
"đi tới ngày khác". Để ‹ › cạnh tên tháng còn "Hôm nay" dạt sang phải cạnh nút chính là tách
|
|
666
|
+
một việc làm hai chỗ, và "Hôm nay" viền đứng sát nút đặc trông như cặp hành động. **‹ › là nút `ghost` chỉ icon** `size-10`, không viền; chỉ "Hôm nay" là nút
|
|
667
|
+
viền: ba ô viền cạnh nhau là ba khối nặng ngang nhau cho một việc phụ. "Hôm nay"
|
|
668
|
+
`whitespace-nowrap shrink-0` (375px: tên tháng + cụm vừa 343px).
|
|
669
|
+
- **Chọn khuôn theo bề rộng khung lịch, không theo viewport.** Ô lưới cần **≥ 128px** mới đọc
|
|
670
|
+
được tên việc (~12 ký tự sau icon). Sidebar mở: 1280px ô 139px ("Họp tổng
|
|
671
|
+
kết s…"), 1024px ô 102px và 768px ô 103px chỉ còn một chữ ("Chuẩn …", "Viết tài …"): lưới
|
|
672
|
+
đủ mà không đọc được việc nào là trang hỏng việc chính. Khung lịch là `@container`: từ
|
|
673
|
+
`@4xl` (896px, = 7 × 128) lưới tháng đủ; hẹp hơn là khuôn gọn, và từ `@2xl` (672px) lịch gọn
|
|
674
|
+
(`w-80`) với danh sách ngày đứng cạnh nhau thay vì chồng dọc. Đừng `useMediaQuery` theo
|
|
675
|
+
viewport: sidebar mở hay thu đổi bề rộng khung 200px+ mà viewport không đổi.
|
|
676
|
+
- **Ô lưới cao cố định, tính ra chứ không đoán.** Cao = `p` hai đầu + hàng số + N dòng việc +
|
|
677
|
+
khe; ghi phép tính vào comment. Mỗi `<li>` bọc nút dòng việc phải `flex` (hoặc nút `flex`):
|
|
678
|
+
nút `inline-flex` trong khối thường dư khe baseline 1px mỗi dòng, ô ba việc cao 122–123px
|
|
679
|
+
thay vì 120px, lưới co giãn theo tháng (`min-h-30` mà hàng 4, 5 cao
|
|
680
|
+
hơn). Ngày quá N việc: hiện N−1 việc + "K việc khác" (chữ "khác", không "+K": dấu cộng dưới
|
|
681
|
+
tên việc đọc thành "thêm việc"), bấm ra khung liệt kê đủ việc ngày đó.
|
|
682
|
+
- **Tên thứ, số ngày, icon dòng việc thẳng một mép.** Số ngày bọc vòng `size-7` căn giữa thì
|
|
683
|
+
chữ số trôi theo số chữ số: "1" lệch 5px, "31" lệch 1px so với tên thứ và icon (tên thứ
|
|
684
|
+
12px, số 11–17px, icon 12px). Chọn một: số và tên thứ **cùng căn giữa** cột (dòng việc
|
|
685
|
+
vẫn căn trái), hoặc cùng căn trái: số `px-1.5` không vòng, chữ số đúng mép tên thứ; hôm
|
|
686
|
+
nay thêm `min-w-7 justify-center` + nền, mép trái vòng trùng mép nền dòng việc. Hôm nay có
|
|
687
|
+
hai chữ số thì chữ vẫn đúng mép; một chữ số ("5") thì chữ nằm giữa vòng, lệch
|
|
688
|
+
3px. Chấp nhận: vòng méo thành viên thuốc để giữ 3px thì xấu hơn.
|
|
689
|
+
- **Hôm nay và ngày đang chọn: vòng đặc chỉ cho một thứ.** Lưới tháng đủ không có ngày đang
|
|
690
|
+
chọn, nên hôm nay là vòng đặc `bg-primary`. **Lịch gọn có ngày đang chọn** (danh sách bên
|
|
691
|
+
dưới là của ngày đó), nên theo đúng ô chọn ngày (`../components/choice-controls.md`): **ngày
|
|
692
|
+
đang chọn là vòng đặc**; hôm nay là vòng viền `inset-ring-1 inset-ring-foreground` +
|
|
693
|
+
`font-semibold` (chấm dưới số đã là "có việc", không dùng chấm cho hôm nay như ô chọn ngày).
|
|
694
|
+
Hôm nay cũng là ngày đang chọn thì chỉ vòng đặc. Đừng để ngày chọn là nền ô vuông xám còn
|
|
695
|
+
hôm nay vẫn vòng đen: chọn 28 mà 26 vẫn là khối đậm nhất lịch, mắt đọc danh sách bên dưới
|
|
696
|
+
thành việc của 26. Hai ô chọn ngày của hệ thống thiết kế lớn cũng vậy:
|
|
697
|
+
hôm nay vòng viền, đang chọn vòng đặc.
|
|
698
|
+
- **Lịch gọn: mọi trạng thái vẽ trên vòng quanh số, không trên cả ô.** Khác ô chọn ngày
|
|
699
|
+
(`choice-controls.md` tô cả ô `rounded-xl`): ở đây chấm "có việc" nằm dưới số, ngoài vòng, như
|
|
700
|
+
lịch điện thoại. Nút ngày `h-12 w-full` chỉ là vùng bấm, `hover:bg-transparent` và không ring;
|
|
701
|
+
số là `group-hover:bg-foreground/5` (ngày chưa chọn), Tab tới không vòng (`I13`). Thiếu thì rê ra nền ô vuông 46×48 cạnh
|
|
702
|
+
vòng chọn 32px, bấm chuột xong ô vừa chọn giữ nền vuông chồng lên vòng đen: hai hình cho một ô ngày.
|
|
703
|
+
- **Dòng việc trong ô**: `h-6` icon trạng thái `size-3.5` (bảng `M7`) + tên `text-xs truncate`
|
|
704
|
+
+ `title` đủ tên; không nền màu, không viền (30 việc mỗi việc một khối màu là lịch loang).
|
|
705
|
+
Xong: chữ `muted`. Quá hạn: chữ hổ phách như hạn chót quá hạn ở danh sách, không đỏ.
|
|
706
|
+
- **Tháng không có việc nào: không cần khối rỗng**, lưới trống là đủ rõ, như các app lịch.
|
|
707
|
+
Lỗi tải: thanh lỗi trên lưới, lưới vẫn giữ để còn đổi tháng. Đang tải: thanh chờ trong ô.
|
|
708
|
+
- **Màn hẹp, nút thêm nằm ở đầu danh sách ngày đang chọn** (nút viền "+ Thêm" trong slot
|
|
709
|
+
action của card), mở form với hạn chót là ngày đó.
|
|
710
|
+
|
|
711
|
+
### Lưới giờ trong ngày (cột theo người, ghế, phòng)
|
|
712
|
+
|
|
713
|
+
Lịch hẹn trong ngày của phòng khám, salon, phòng họp: giờ là trục dọc, mỗi bác sĩ (ghế, phòng)
|
|
714
|
+
một cột, ô hẹn cao theo thời lượng.
|
|
715
|
+
|
|
716
|
+
- **Hàng tên cột dính đầu khung khi cuộn dọc** (`sticky top-0` trong khung lưới), cột nhãn giờ
|
|
717
|
+
dính trái khi cuộn ngang. Lưới 08:00–17:00 cao hơn một màn: hàng tên trôi đi thì cuộn tới
|
|
718
|
+
buổi chiều là không biết cột nào của ai.
|
|
719
|
+
- **Mở ra cuộn sẵn tới giờ hiện tại**, vạch "bây giờ" nằm khoảng một phần ba từ trên. Vạch mang
|
|
720
|
+
`data-now`: probe thấy vạch trong phần đang hiện của khung thì hiểu là cuộn có chủ ý, không báo
|
|
721
|
+
"trang tự cuộn".
|
|
722
|
+
- **Vạch "bây giờ" vẽ trên ô hẹn** (`z-index` cao hơn ô), mảnh 1px, chấm tròn ở đầu; chấp nhận vạch
|
|
723
|
+
cắt ngang chữ trong ô. Vạch nằm dưới ô thì đúng lúc đông lịch nhất nó chỉ lộ ở khe giữa các ô:
|
|
724
|
+
vạch hiện 2% bề ngang, không còn là mốc để đọc ai trễ, ai
|
|
725
|
+
sắp tới. Probe đo phần thấy được của `[data-now]`.
|
|
726
|
+
- **Số đếm trạng thái cần xử lý bấm được.** "Trễ 1" trong hàng đếm là nút: bấm thì cuộn tới ô đó
|
|
727
|
+
và mở chi tiết; màn hẹp chỉ hiện một cột thì chuyển sang cột của người đó trước. Ô trễ nằm ở cột
|
|
728
|
+
đang ẩn mà số đếm không bấm được thì lễ tân cầm điện thoại không thấy ai trễ.
|
|
729
|
+
- **Trạng thái suy từ giờ phải có mặt**: quá giờ hẹn mà chưa đến là "Trễ N phút", hổ phách
|
|
730
|
+
như việc quá hạn (`M4`), đứng đầu hàng đếm trạng thái; quá lâu thì lễ tân tự chuyển "Không
|
|
731
|
+
đến". Dữ liệu chỉ lưu "Đã xác nhận", nên phải tính ra, và wireframe phải có ít nhất một ca
|
|
732
|
+
(`design-process.md`, U3). Không ca nào trễ thì việc chính của lễ tân vắng mặt.
|
|
733
|
+
- **Nút đổi trạng thái trên ô hay dòng là động từ** ("Tiếp đón", "Bắt đầu khám", "Hoàn tất"),
|
|
734
|
+
không phải tên trạng thái đích (`rules-state.md`, bảng nút).
|
|
735
|
+
- **Ô ngắn 30 phút chỉ giữ ba dòng**: giờ + trạng thái, tên, dịch vụ. Ghi chú để panel hay hồ sơ.
|
|
736
|
+
- **Hiện hết các cột khi khung đủ, tính theo số cột, không theo một mốc cố định.** Cột cần **≥ 160px**
|
|
737
|
+
(giờ + "Trễ 25 phút" một dòng, tên đọc được). Khung lưới ≥ cột giờ + N × 160 thì hiện đủ N cột; không đủ
|
|
738
|
+
mới về một cột + hàng tab chọn người. Tính trong code (`ResizeObserver` trên khung, hoặc `@container` với
|
|
739
|
+
mốc ghi ra từ phép tính N × 160 + cột giờ, kèm comment), không khoá `@3xl` cho mọi phòng khám: bốn bác sĩ
|
|
740
|
+
cần 700px, sáu bác sĩ cần 1020px. Khung 720px mà mốc `@3xl` (768px)
|
|
741
|
+
thì chỉ hiện một cột rộng 650px, hai người trễ nằm ở hai cột đang ẩn; bốn cột 166px đọc đủ tên, trạng thái,
|
|
742
|
+
"Trễ 25 phút". Cột hẹp dưới ~200px thì tên đầu cột dùng tên ngắn ("BS. Khoa", như hàng tab) thay vì cắt
|
|
743
|
+
"BS. Nguyễn …".
|
|
744
|
+
|
|
745
|
+
---
|
|
746
|
+
|
|
747
|
+
## Danh sách có bộ lọc
|
|
748
|
+
|
|
749
|
+
```
|
|
750
|
+
┌─────────────────────────────────────┐
|
|
751
|
+
│ tab [tab] tab [tìm ] [+ Thêm]│ <- chọn một: tab; chọn nhiều: chip
|
|
752
|
+
│ │ tên trang là <h1> trên thanh header, không lặp ở đây
|
|
753
|
+
├─────────────────────────────────────┤
|
|
754
|
+
│ ⬤ nội dung dòng giá trị ⋯ ⋯ │ <- hành động phụ ẩn, hiện khi hover
|
|
755
|
+
│ ⬤ nội dung dòng giá trị │
|
|
756
|
+
│ ⬤ nội dung dòng giá trị │
|
|
757
|
+
└─────────────────────────────────────┘
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
Xem `../components/list-row.md` cho công thức từng dòng. Danh sách là **một khối
|
|
761
|
+
chia đường kẻ**, không phải mỗi dòng một card.
|
|
762
|
+
|
|
763
|
+
**Danh sách + chi tiết (hai cột, bấm mục trái mở chi tiết phải).** Mục bên trái chỉ mang
|
|
764
|
+
thứ để **chọn**: một ảnh nhỏ, dòng giá hay trạng thái, tên, **một** dòng phụ. Tối đa ba dòng
|
|
765
|
+
chữ. Mọi thứ khác (địa chỉ đầy đủ, xác thực, ngày đăng, thông số) đã có ở cột chi tiết, lặp
|
|
766
|
+
lại ở trái chỉ làm cột chật (năm dòng mỗi mục đọc như một bức tường chữ). Dòng thụt vào, mục đang mở và mục đang rê khác nền (`I10`, đoạn "Mục đang mở"). Ảnh ở cột chi tiết
|
|
767
|
+
có trần cao (`max-h-[420px]`, `object-cover`): dữ liệu chỉ có một ảnh thì không để nó phủ
|
|
768
|
+
hết khung rộng 1400px.
|
|
769
|
+
|
|
770
|
+
**Đếm cột trước khi thêm cột lọc.** Sidebar app + cột lọc + danh sách + chi tiết là bốn cột:
|
|
771
|
+
ở 1440px vùng nội dung chỉ còn khoảng 1190px, danh sách bị ép còn khoảng 320px, tiêu đề
|
|
772
|
+
cụt sau hai ba chữ. Ba cột nội dung chỉ khi vùng nội dung từ khoảng
|
|
773
|
+
1600px; hẹp hơn thì lọc thành nút "Bộ lọc" mở panel, như bản 1024px.
|
|
774
|
+
|
|
775
|
+
**Cột lọc bên trái dính theo khi cuộn, không cuộn riêng.** `sticky top-*` chỉ khi cột thấp
|
|
776
|
+
hơn màn. Cột dài hơn màn thì để nó cuộn theo trang, đừng bó `max-h-[calc(100vh-…)]
|
|
777
|
+
overflow-y-auto`: thanh cuộn riêng hiện thường trực, dài gần hết cột, dính sát viền khung.
|
|
778
|
+
Nhóm dài (khu vực, tỉnh) thì hiện năm sáu mục kèm "Xem thêm N" dạng
|
|
779
|
+
link, đừng cho cả cột thành khung cuộn.
|
|
780
|
+
|
|
781
|
+
---
|
|
782
|
+
|
|
783
|
+
## Bảng dữ liệu
|
|
784
|
+
|
|
785
|
+
> **Kiểm thư viện trước.** Dự án có `@tanstack/react-table`, `ag-grid`, danh
|
|
786
|
+
> sách ảo (`@tanstack/react-virtual`, `react-window`, `react-virtuoso`) thì dùng
|
|
787
|
+
> nó cho phần sắp xếp, chọn dòng, cuộn ảo; skill chỉ lo hình. Chưa có thì dựng
|
|
788
|
+
> theo mục này, lúc giao đề xuất một dòng nếu bảng cần cuộn nhiều nghìn dòng.
|
|
789
|
+
|
|
790
|
+
Bảng quản lý (khách hàng, đơn hàng, thành viên…) có tìm, lọc, phân trang, chọn
|
|
791
|
+
nhiều dòng. Bộ mặc định, dựng đủ không hỏi:
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
(tên trang "Khách hàng" là <h1> trên thanh header, không lặp ở đây)
|
|
795
|
+
[Tất cả 32] Đang giao dịch 18 Tiềm năng 9 Ngừng 5 [tìm…] [Lọc] [+ Thêm khách hàng]
|
|
796
|
+
┌──────────────────────────────────────────────────────────────────┐
|
|
797
|
+
│ ☐ Khách hàng ↕ Công ty Trạng thái Doanh thu ↕ ⋯ │
|
|
798
|
+
├──────────────────────────────────────────────────────────────────┤
|
|
799
|
+
│ ☐ ⬤ Tên Công ty (● Đang GD) 184.500.000 đ ⋯ │
|
|
800
|
+
│ ☐ ⬤ Tên Công ty (● Tiềm năng) 0 đ ⋯ │
|
|
801
|
+
├──────────────────────────────────────────────────────────────────┤
|
|
802
|
+
│ 1 tới 10 trong 32 khách hàng Mỗi trang [10▾] ‹ 1 2 3 4 › │
|
|
803
|
+
└──────────────────────────────────────────────────────────────────┘
|
|
804
|
+
|
|
805
|
+
Khi có dòng được chọn, hàng tab + tìm + nút thêm được THAY bằng:
|
|
806
|
+
[Đã chọn 3 · Bỏ chọn] [Xoá 3 dòng]
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
- **Tab trạng thái** ở trên bảng theo "Thanh tab" trong `../components/small-controls.md` — mở file đó lấy variant và class, đừng chép lại ở đây. Bảng này thường có thêm hàng chip lọc ngay dưới hàng tab, và khi đó tab dùng `underline`. "Bộ lọc" trong đề không chỉ là hàng tab: các trường khác (công ty, người phụ trách, khoảng ngày) vào nút **Lọc** mở popover, dựng theo "Popover lọc" trong `overlay.md`.
|
|
810
|
+
- **Dưới `sm`, tab trạng thái không vừa một hàng thì thành một nút dropdown**, không cuộn ngang: nút viền `h-10` ghi **nhãn "Trạng thái:" (`text-muted`) rồi trạng thái đang chọn kèm số** ("Trạng thái: Tất cả · 32") và `ChevronDown`; thiếu nhãn thì nút "Tất cả · 32" đứng một mình trông như ô nhập hay nút lạ, không biết đang lọc theo gì (chủ dự án chốt). Nút mở ra là danh sách đủ các trạng thái, mỗi mục kèm số, mục đang chọn có dấu check (khuôn Select ở `../components/choice-controls.md`, danh sách mở ra theo Dropdown ở `overlay.md`). Đây là bước 3 của `R10`: hàng tab cuộn ngang làm tab cuối nằm **hẳn** ngoài khung ("Ngừng giao dịch 6" bắt đầu ở 370px trong khung 367px), mép mờ không có gì để mờ, người dùng tưởng chỉ có ba trạng thái. Rút chữ ("Đang GD") thì mất nghĩa. Nút **căn trái, rộng theo nội dung** (`w-fit`), đứng riêng một hàng: nó là bộ lọc, đọc từ trái như hàng chip bên dưới và ô tìm bên trên; căn phải thì một nút lẻ trôi giữa khoảng trống, tách khỏi hàng chip nó đi cùng. Không kéo rộng hết hàng: trông như ô nhập. Hàng chip vẫn cuộn ngang, có mép mờ (`R10`; vạch chỉ vị trí chỉ khi người dùng chọn): chip là lọc thêm, thấy một phần là đủ biết còn.
|
|
811
|
+
- **Hover dòng `hover:bg-surface-hover`**, không `hover:bg-item-hover` (`I10`). Dòng chạm hai mép khung trắng mà tô màu nền trang là trông như thủng.
|
|
812
|
+
- **Cột trạng thái là badge màu** theo `M7`, không chấm xám + chữ đen. **Trạng thái mà gần hết các dòng giống nhau thì không làm cột**: dòng thường không có dấu, chỉ dòng ngoại lệ có badge cạnh tên (thành viên "Đang hoạt động" / lời mời "Chờ chấp nhận", xem "Trang thành viên và phân quyền").
|
|
813
|
+
- **Nút gỡ lọc ghi "Xoá lọc", không ghi "Bỏ chọn".** Khi đang chọn dòng, thanh trên cùng đã có "Bỏ chọn" (bỏ tick dòng); cuối hàng chip mà cũng "Bỏ chọn" thì một màn có hai nút cùng chữ khác việc (`N6`). **"Xoá lọc" cuối hàng chip chỉ hiện khi có chip đang chọn** (bấm là gỡ hết: chip, từ khoá, tab về Tất cả). Chỉ có từ khoá thì không hiện: ô tìm đã có nút `X` riêng, khối rỗng đã có "Xoá tìm kiếm"; thêm nút này là **ba nút cùng gỡ một từ khoá** trên một màn, và ở 375px nó chiếm một phần ba hàng chip.
|
|
814
|
+
- **Cột chữ tự co giãn, đừng khoá `max-w` khi bảng còn dư chỗ.** Cột tên và cột công ty để co theo bảng, `truncate` chỉ bật khi thật sự hết chỗ. Khoá cứng thì ra cảnh tên bị cắt "Tôn Nữ Thị Phương Thảo N…" trong khi giữa bảng còn một mảng trắng.
|
|
815
|
+
- **Đếm cột trước khi dựng**: khung còn ~970px ở 1280px khi sidebar mở, quá ~6 cột là bắt đầu chật. Thử theo thứ tự: gộp cột (email xuống dưới tên), ẩn cột ít dùng sau nút "Hiển thị cột", rồi mới cho cuộn ngang trong khung với cột đầu ghim `sticky left-0` (`R9`). Bảng 7–9 cột cuộn ngang ở các sản phẩm lớn vẫn có, cuộn không sai; sai là cuộn khi chưa thử gộp.
|
|
816
|
+
- **Cột tiền là đúng ca cần `tabular-nums`** (`T16`). Font không có bảng `tnum` thì class chỉ là chữ chết, các mốc nghìn không thẳng cột: báo người dùng một dòng lúc giao, đổi font là việc của họ (`N10`).
|
|
817
|
+
- **Hành động dòng** theo `I11`: 1–2 cái thì icon button luôn hiện ở cột cuối; từ 3 cái hoặc có xoá thì một nút `MoreHorizontal` ra dropdown. Cột cuối hẹp `w-12`, căn phải, không tiêu đề (có `<span class="sr-only">Thao tác</span>`).
|
|
818
|
+
- **Chọn nhiều dòng:** checkbox đầu dòng, checkbox tiêu đề có ba trạng thái (không / một phần / tất cả trong trang). Không có dòng nào (rỗng, rỗng do lọc, đang tải, lỗi) thì **ẩn checkbox tiêu đề**, giữ chỗ để cột không xê dịch. Có dòng được chọn thì **thanh hành động hàng loạt thay chỗ** hàng tab, cùng chiều cao để bảng không nhảy. Xoá hàng loạt luôn qua hộp xác nhận (`../layouts/overlay.md`), nói rõ số dòng.
|
|
819
|
+
- **Mỗi ô một dòng.** Tên công ty dài thì `truncate` (`min-w-0`) và `title` đầy đủ, bề rộng do bảng chia chứ không khoá `max-w`, không cho xuống ba dòng: một dòng cao gấp ba làm cả bảng mất nhịp. Ô hai tầng (tên + email) là ngoại lệ duy nhất, và mọi dòng đều hai tầng như nhau.
|
|
820
|
+
- **Giá trị trống thống nhất một kiểu**: `—` màu `text-muted`. Không chỗ "Chưa có", chỗ "Khách lẻ", chỗ để trống.
|
|
821
|
+
- **Số căn phải, `tabular-nums`**, tiêu đề cột số cũng căn phải. Cột số, ngày có sắp xếp thì tiêu đề là nút có icon mũi tên, dựng theo `../components/sortable-header.md` (không nền hover, dưới `sm` thành nút "Sắp xếp:").
|
|
822
|
+
- **Dòng tiêu đề bảng** `text-xs font-medium text-muted`, nền `--surface`, chia với thân bằng `--border`.
|
|
823
|
+
- Phân trang có tổng số và vị trí đang xem (`I16`), dựng theo "Phân trang" trong `../components/small-controls.md`: một trang thì ẩn nav, không có dòng thì ẩn cả footer. Màn hẹp xem gạch dưới.
|
|
824
|
+
- **Dưới `sm`, bảng quản lý thành danh sách dòng, không cuộn ngang.** Mỗi dòng theo `../components/list-row.md`: checkbox · avatar · tên (`font-medium truncate`) trên email (`text-xs text-muted truncate`) · nút ⋯ ở mép phải; hàng dưới cùng thụt thẳng mép chữ tên là **badge trạng thái** bên trái, **số chính** (doanh thu) căn phải `tabular-nums`. Cột phụ (công ty, ngày tạo) không hiện, xem ở trang/drawer chi tiết. Hàng tab, chip, thanh hàng loạt, phân trang giữ nguyên. Bảng 6 cột ở 375px mà cuộn ngang thì cột tên (271px trong khung 341px) trôi mất ngay nhịp cuộn đầu, còn lại "Công ty —, Đang giao dịch" không biết của ai; ghim cột tên cũng chỉ chừa ~70px để cuộn. Các app quản lý lớn trên điện thoại đều đổi sang dòng.
|
|
825
|
+
- **Khung vừa thì ẩn cột phụ, không cuộn.** Card bảng là `@container`; dưới `@4xl` (56rem, ~896px) ẩn đúng các cột mà danh sách dòng dưới `sm` cũng bỏ (công ty, ngày tạo): `hidden @4xl:table-cell` trên cả `<th>` lẫn `<td>`. Khung 718px (768px, hoặc 1024px khi sidebar mở) thì bảng 7 cột 960px cuộn ngang, nút ⋯ của mọi dòng nằm ngoài khung tới khi cuộn; ẩn hai cột phụ là vừa khít, cột tên rộng từ 208 lên 264px. Dữ liệu cột phụ vẫn xem được ở drawer / trang chi tiết.
|
|
826
|
+
- **Từ `sm` tới hết bề rộng mà bảng vẫn phải cuộn ngang thì bắt buộc ghim cột đầu** `sticky left-0 bg-surface` (dòng hover/đang chọn thì ô ghim đổi nền theo), cột ghim không quá ~40% khung, mép phải cột ghim có mép mờ theo `R10` khi đang cuộn.
|
|
827
|
+
|
|
828
|
+
|
|
829
|
+
### Bảng nhóm theo trạng thái (danh sách công việc)
|
|
830
|
+
|
|
831
|
+
```
|
|
832
|
+
[Danh sách] Kanban [+ Thêm việc]
|
|
833
|
+
┌──────────────────────────────────────────────────────────────────────┐
|
|
834
|
+
│ Công việc Ưu tiên Người phụ trách Hạn chót ↑ │
|
|
835
|
+
├──────────────────────────────────────────────────────────────────────┤
|
|
836
|
+
│ ⌄ ○ Cần làm 5 │ <- hàng nhóm: nút rộng hết hàng
|
|
837
|
+
├──────────────────────────────────────────────────────────────────────┤
|
|
838
|
+
│ Tiêu đề việc ! Khẩn cấp ⬤ Trần Nguyễn Anh Tuấn Quá hạn 3 ngày ⋯ │
|
|
839
|
+
│ Dự án │
|
|
840
|
+
│ Tiêu đề việc ▂▄ Cao ⬤ Đỗ Khánh Linh Hôm nay ⋯ │
|
|
841
|
+
│ Tiêu đề việc ▂ Thấp — — ⋯ │
|
|
842
|
+
├──────────────────────────────────────────────────────────────────────┤
|
|
843
|
+
│ › ◉ Đang làm 4 │ <- đang thu
|
|
844
|
+
│ ⌄ ⋯ Chờ duyệt 0 │
|
|
845
|
+
│ Chưa có việc nào ở nhóm này │
|
|
846
|
+
└──────────────────────────────────────────────────────────────────────┘
|
|
847
|
+
```
|
|
848
|
+
|
|
849
|
+
- **Hàng nhóm là icon trạng thái + tên + số, không pill** (`D2`, `M7`). Ba nhóm pill xám một nhóm pill xanh thì liếc không biết mình đang ở nhóm nào. Tên `text-sm font-medium`, số `text-muted`. Cả hàng là một `<button aria-expanded>` rộng hết hàng (`I29`), chevron `size-4` đầu hàng, xoay `-rotate-90` khi thu.
|
|
850
|
+
- **Trong nhóm phải xếp theo một khoá, và khoá đó hiện ở tiêu đề cột.** Mặc định hạn chót tăng dần: quá hạn lên đầu, hôm nay, rồi ngày xa, không có hạn xuống cuối. Nhóm Xong xếp ngày xong giảm dần. Tiêu đề cột đang sắp có mũi tên (`arrow-up` `size-3.5`, `../components/sortable-header.md`). Để thứ tự dữ liệu mẫu thì ra "Hôm nay" nằm dưới "28/09" và dưới một việc không hạn, người đọc tưởng bảng xếp theo thứ gì đó mà dò không ra.
|
|
851
|
+
- **Mọi cột ngắn co theo nội dung dài nhất, cột tiêu đề nhận phần dư.** Ưu tiên, người phụ trách, hạn chót, cột ⋯: `<th class="w-px">` và ô `whitespace-nowrap`; ô tiêu đề `w-full`. Chỉ `w-px` mà quên `nowrap` ở một cột thì cột đó bị bóp tới chữ đầu tiên ("Trung", "Trần N…"). Tên tiếng Việt cắt đuôi là mất **tên gọi**, phần duy nhất phân biệt người này với người kia: "Trần Nguyễn Anh Tuấ…", "Nguyễn Thị Phương T…" trong khi cột công việc bên trái dư cả nửa bảng (`N8`). Chỉ cắt khi cả bảng hết chỗ, lúc đó rút cột tiêu đề trước: tiêu đề `min-w-0` + `truncate` (hoặc `line-clamp-1`) bên trong ô, không để tràn đè sang cột bên.
|
|
852
|
+
- **Khuôn đổi theo bề rộng khung bảng (`@container` trên card), không khoá `min-w` cho bảng rồi cuộn ngang.** Ba bậc:
|
|
853
|
+
- **Khung từ `@4xl` (56rem, ~896px):** đủ cột, người phụ trách avatar + tên.
|
|
854
|
+
- **Từ `@2xl` (42rem) tới dưới `@4xl`:** cột người phụ trách **chỉ còn avatar**, tên vào `title` và `sr-only` cạnh avatar (`whitespace-nowrap @max-4xl:sr-only`, không `sr-only … @4xl:not-sr-only`: `not-sr-only` trả `white-space` về `normal`, tên bốn chữ lại xuống ba dòng, `tailwind-v4-traps.md` W11), tiêu đề cột rút thành "Phụ trách". Cột này là cột ngốn chỗ nhất (~260px với tên bốn chữ), bỏ tên trả lại ~200px cho tiêu đề việc. Ở 1024px khi sidebar mở (khung 718px) tiêu đề việc từ ~190px lên 253px, không cuộn.
|
|
855
|
+
- **Dưới `@2xl`: danh sách dòng**, cùng tinh thần bảng quản lý dưới `sm` (mục Bảng dữ liệu). Giữ hàng nhóm, ẩn `<thead>` (vẫn là `<table>`, các `<tr>` hiển thị `grid`). Mỗi việc: tên `text-sm font-medium` **`line-clamp-2 text-pretty`** cùng hàng với nút ⋯ (nút canh đầu tên, không canh giữa cả dòng); dự án `text-xs text-muted truncate`; hàng cuối là ưu tiên (icon + chữ) rồi hạn chót, `text-xs`, và avatar người phụ trách **thẳng cột với nút ⋯**. Tên việc hai dòng là ngoại lệ của "mỗi ô một dòng": ở dòng hẹp, tên là thứ duy nhất nhận ra việc, một dòng ở 375px chỉ còn ~20 ký tự ("Đặt lịch phỏng vấn hai ứn…"). Thiếu `text-pretty` thì dòng hai trơ một chữ ("CRM", "mới", "nhánh"; `T10`).
|
|
856
|
+
**Mảnh trống thì bỏ hẳn, không ghi `—`.** Không còn tiêu đề cột, `—` không biết là của cột nào: hàng cuối ra "Thấp — —". Không có hạn thì không hiện gì ở chỗ hạn (đặt hạn ở trang chi tiết), chưa giao ai thì không avatar.
|
|
857
|
+
Đừng khoá bảng `min-w-[52rem]` rồi cuộn ngang trong khung: ở 375px cuộn một nhịp là mất cả tên nhóm lẫn tên việc; ở 768 và 1024px nút ⋯ nằm hẳn ngoài khung, "Quá hạn 3 ng" bị mép card cắt ngang, còn cột đang sắp xếp thì đứt đôi. `probe.mjs` báo "bảng cuộn ngang mà cột đầu trôi theo".
|
|
858
|
+
- **Thu nhóm trong `<table>`: mỗi nhóm một `<tbody>`, thu thì ẩn `<tr>`, không khối trượt.** Hàng nhóm là `<tr>` đầu `<tbody>`, trong đó `<th scope="rowgroup" colSpan={số cột}>` chứa nút `aria-expanded`; thu thì các `<tr>` dòng việc `hidden`. Khối trượt `grid-rows` của sidebar (`I29`, mục thu nhóm sidebar ở trên) **không** dùng được trong bảng: `<div>` bọc quanh `<tr>` là HTML sai, trình duyệt đẩy nó ra khỏi bảng hoặc tính lại bề rộng cột theo khối đó, cột giữa bị bóp và tên việc dài tràn đè sang cột bên. Bảng không trượt chiều cao; muốn có chuyển động thì chỉ xoay chevron.
|
|
859
|
+
- **Ô trống một kiểu `—` ở mọi cột, kể cả hạn chót.** Người phụ trách trống ghi `—` mà hạn chót trống ghi "Đặt hạn" là hai kiểu trống trên một hàng; "Đặt hạn" xám còn trông như một giá trị. Ô sửa được tại chỗ thì cả ô là nút mở date picker / chọn người (dấu bấm được chỉ hiện lúc rê vì đây là ô phụ; ô là việc chính của trang thì dấu luôn hiện, xem "Trang thành viên và phân quyền"): lúc thường `—`, rê vào hoặc Tab tới thì nền `bg-foreground/8 rounded-lg` và đổi thành icon `calendar-plus` + "Đặt hạn". **Không `bg-surface-hover`**: đó cũng là nền của cả dòng lúc rê, ô nằm trên dòng đang rê thì nền ô trùng nền dòng, không nổi lên. `foreground/8` chồng lên nền dòng đang rê vẫn đậm hơn rõ một bậc (`I10`; `/5` gần trùng nền dòng). Ô có giá trị ("Hôm nay", "28/09") cùng công thức; lúc lịch đang mở giữ nền này (`aria-expanded:bg-foreground/8`). Ô `px-2` (các ô khác `px-4`), nút ô `px-2`: chữ thẳng cột với tiêu đề cột mà không kéo nút ra bằng `-mx-2` (`N11`, `F13`). Máy không có chuột thì `—` vẫn bấm được. Lịch mở từ ô theo mục "mở từ một ô trong bảng" trong `../components/choice-controls.md`: bấm ngày là lưu, có "Xoá hạn" khi ô đang có hạn.
|
|
860
|
+
- **Nhóm rỗng** mở ra là một dòng `text-sm text-muted` "Chưa có việc nào ở nhóm này", thụt thẳng cột tiêu đề (`../components/empty-state.md`). Cả bảng rỗng thì giữ hàng tiêu đề cột, một dòng chữ mờ căn giữa.
|
|
861
|
+
- **Khung chờ có cả hàng nhóm.** Dòng đầu của bảng thật là hàng nhóm; khung chờ bắt đầu thẳng bằng dòng việc thì lúc dữ liệu về cả bảng tụt xuống một hàng (`I19`). Khung chờ: một hàng nhóm (chevron + thanh `w-24`), rồi các dòng việc. **Cột co theo nội dung thì ô chờ dùng đúng class bề rộng của ô thật** (`min-w-32` ưu tiên, `min-w-40` hạn chót…), và thanh chờ ở cột tên người dài cỡ một tên bốn chữ (avatar + thanh `w-44`). Cột co theo nội dung dài nhất nên thanh ngắn là cột hẹp lại, dữ liệu về thì cả ba cột nhảy ngang (thanh tên `w-24` thì cột người phụ trách 176px lúc chờ, 262px lúc có dữ liệu, cột Ưu tiên nhảy 86px).
|
|
862
|
+
- **Nút "Thêm việc" nằm cuối hàng chuyển view**, căn phải, cả hai view dùng chung (`primary`, icon `plus`). Trang không có nút nào dẫn tới form tạo việc là trang chỉ xem được, không làm được.
|
|
863
|
+
- **Tên view: "Danh sách" / "Kanban", không "Bảng".** Theo `S10` "bảng" là table, mà view danh sách ở đây chính là table: ghi "Bảng" cho view kanban là một chữ hai nghĩa trên cùng một màn. Chuyển view là `segmented` trong "Thanh tab" (`../components/small-controls.md`), mỗi view kèm icon `list` / `square-kanban` được.
|
|
864
|
+
- Hành động dòng, hover, badge ưu tiên, màu hạn chót theo các mục trên và `../components/list-row.md`. Menu dòng đang mở thì dòng giữ nền hover.
|
|
865
|
+
|
|
866
|
+
---
|
|
867
|
+
|
|
868
|
+
## Danh sách rỗng
|
|
869
|
+
|
|
870
|
+
Một dòng chữ mờ, `py-6`, không hình, không nút. Xem `../components/empty-state.md`.
|
|
871
|
+
|
|
872
|
+
Chỉ dựng empty state có hình và CTA khi đó là màn hình chính của cả app và người
|
|
873
|
+
dùng lần đầu vào chưa có gì để làm.
|
|
874
|
+
|
|
875
|
+
---
|
|
876
|
+
|
|
877
|
+
## Trang lỗi (404, 403, 500, bảo trì)
|
|
878
|
+
|
|
879
|
+
Bốn trang, **hai chỗ đặt** theo việc khung app còn
|
|
880
|
+
dựng được không:
|
|
881
|
+
|
|
882
|
+
| Trang | Đặt ở đâu | Việc chính (nút đặc) | Lối phụ |
|
|
883
|
+
| --- | --- | --- | --- |
|
|
884
|
+
| 404 trong app (đã đăng nhập) | **trong khung app**, sidebar và header giữ nguyên | Về trang tổng quan | Quay lại trang trước (chỉ khi có trang trước) |
|
|
885
|
+
| Bản ghi không tìm thấy (khách, đơn… id sai hoặc đã xoá) | trong khung app, cùng khối với 404 | Về danh sách của loại bản ghi đó ("Về danh sách khách hàng") | Quay lại trang trước (chỉ khi có trang trước) |
|
|
886
|
+
| 403 | trong khung app | Gửi yêu cầu cấp quyền | Về trang tổng quan; dòng cuối "Bạn đang đăng nhập bằng … · Đổi tài khoản" |
|
|
887
|
+
| 500 của một trang (khung vẫn chạy) | trong khung app | Tải lại trang | Về trang tổng quan; dòng cuối mã lỗi `font-mono` + nút sao chép |
|
|
888
|
+
| 404 khi chưa đăng nhập, 500 làm sập cả app, bảo trì | **đứng riêng**: cùng khối như trong khung, thêm logo căn giữa ở trên; **không card** | 404: về trang chủ / đăng nhập. 500: Tải lại trang. Bảo trì: không nút đặc, "Tải lại trang" là nút viền | — |
|
|
889
|
+
|
|
890
|
+
- **Người đã đăng nhập gặp lỗi thì vẫn ở trong app.** Bỏ khung app đi là bỏ luôn đường ra: muốn
|
|
891
|
+
sang trang khác phải bấm đúng một nút trên card. Các hướng dẫn thiết kế trang 404 đều giữ
|
|
892
|
+
thanh điều hướng của site, và router lồng nhau dựng trang not-found **bên trong** layout cha.
|
|
893
|
+
Header ghi tên lỗi ("Không tìm thấy trang"), không ghi tên trang cũ, không đường dẫn.
|
|
894
|
+
- **Route bắt mọi đường dẫn trong khung app phải có trang.** `{ path: "*" }` không `element`
|
|
895
|
+
thì khung dựng vùng nội dung trống, header vẫn ghi "Tổng quan": người dùng tưởng trang đang
|
|
896
|
+
tải. Lưới router bắt ở cấp gốc chỉ phủ đường dẫn
|
|
897
|
+
ngoài khung.
|
|
898
|
+
- **Khối trong khung: không card, căn giữa, chữ căn giữa** như khối lỗi tải
|
|
899
|
+
(`../components/empty-state.md`): `mx-auto max-w-md pt-16 pb-16 text-center sm:pt-24`, nằm
|
|
900
|
+
thẳng trên nền trang. Nút `h-11 md:h-10`, dưới `sm` rộng hết và xếp dọc (nút đặc trên), từ
|
|
901
|
+
`sm` co theo chữ, đứng ngang, căn giữa, nút đặc trước.
|
|
902
|
+
- **Đứng riêng cũng là khối đó, không card.** Trang `min-h-screen bg-background px-4 pt-24
|
|
903
|
+
sm:pt-40`, logo (`ProductBrand`, `../components/logo.md`) căn giữa ở trên, tiêu đề cách logo `mt-8`; chữ, nút, khoảng
|
|
904
|
+
cách giống hệt bản trong khung. Trang lỗi không phải form: mượn card màn xác thực
|
|
905
|
+
(nút `h-12` rộng hết card) thì trang 404 chỉ có một thanh đen 400px là thứ nặng nhất màn, và đặt
|
|
906
|
+
cạnh bản trong khung thì một loại trang ra hai khuôn khác hẳn nhau (`N5`; chủ dự án thấy "xấu xấu"). Các bộ component trang 404 phổ biến cũng là trang trơn,
|
|
907
|
+
không card. `M29` (một card giữa trang trống) chỉ còn cho màn xác thực và onboarding một khối.
|
|
908
|
+
- **404 có dòng mã "404" nhỏ mờ trên tiêu đề**: `text-sm font-medium text-muted tabular-nums`,
|
|
909
|
+
tiêu đề `mt-1`. Các bộ component trang 404 phổ biến đều có dòng này; "404" là chữ người dùng
|
|
910
|
+
nhận ra và gõ đi tìm. 403, 500, bảo trì không có (500 đã có mã lỗi riêng ở dòng cuối, hai mã
|
|
911
|
+
là thừa).
|
|
912
|
+
- **Tiêu đề `text-xl font-semibold`** (`T2`), không `font-bold`. Không icon to, không hình minh
|
|
913
|
+
hoạ, không đỏ: người dùng không có gì phải sửa (`M30`).
|
|
914
|
+
- **Câu dẫn nói vì sao và làm gì tiếp**, một hai câu. 404: "Đường dẫn có thể bị gõ sai, hoặc
|
|
915
|
+
trang đã được chuyển sang chỗ khác." 403: nêu tên trang (`font-medium text-foreground`) và ai
|
|
916
|
+
được vào. 500: "Lỗi nằm ở phía hệ thống, không phải do bạn." Bảo trì: giờ mở lại, viết theo
|
|
917
|
+
kiểu câu văn (`T16b`: "lúc 23:30 hôm nay", không "23:30 · 26/09/2026").
|
|
918
|
+
- **403: xin quyền và tài khoản đang dùng.** Vào nhầm tài khoản là lý do hay gặp nhất, nên dòng
|
|
919
|
+
cuối nói email đang đăng nhập và có link "Đổi tài khoản", như trang "cần quyền truy cập" của
|
|
920
|
+
bộ văn phòng trực tuyến lớn. Email giữa câu thì dấu chấm đi theo `EmailText` `suffix`, và cả
|
|
921
|
+
email là một khối: vừa một dòng thì xuống dòng nguyên cụm, không bẻ đôi sau `@`
|
|
922
|
+
(`../components/description-list.md`). **"Đổi tài khoản" đứng một dòng riêng** dưới câu
|
|
923
|
+
(`mx-auto mt-1 flex h-8 w-fit items-center px-1.5`), không nối sau email: nối sau thì dòng email + link rộng 448px, rộng
|
|
924
|
+
nhất khối (câu dẫn 439px, tiêu đề 310px), chân nặng hơn đầu (đo ở
|
|
925
|
+
1280px). Tách dòng thì chân còn ~270px.
|
|
926
|
+
- **403 đã gửi: đổi cả tiêu đề và câu dẫn, không nhét dòng chữ xanh vào chỗ nút.** Tiêu đề "Đã
|
|
927
|
+
gửi yêu cầu", câu dẫn "Bạn sẽ nhận email khi **<tên>** chấp nhận yêu cầu.", hàng nút chỉ còn
|
|
928
|
+
"Về trang tổng quan" (vẫn nút viền, giữ kiểu theo vai); dòng tài khoản giữ nguyên. Tiêu đề
|
|
929
|
+
`tabIndex={-1}`, gửi xong chuyển tiêu điểm vào đó (`I31`). Bộ văn phòng trực tuyến lớn cũng
|
|
930
|
+
đổi cả màn thành "Request sent" + câu hẹn email. Đừng để ô nút ẩn giữ chỗ với dòng "✓ Đã gửi
|
|
931
|
+
yêu cầu" nằm giữa ô đó: chữ ngắn hơn nút, cả hàng lệch tâm 16px sang phải, trông như thiếu một
|
|
932
|
+
nút.
|
|
933
|
+
- **"Quay lại trang trước" chỉ khi có trang trước trong app.** Mở thẳng bằng link (tab mới) thì
|
|
934
|
+
nút này đưa người dùng ra khỏi app hoặc không làm gì; ẩn đi. React Router: `location.key ===
|
|
935
|
+
"default"` là trang đầu của phiên.
|
|
936
|
+
|
|
937
|
+
---
|
|
938
|
+
|
|
939
|
+
## Trang cài đặt
|
|
940
|
+
|
|
941
|
+
**A. Một cột, chia mục có tiêu đề** (mặc định, dưới 15 tuỳ chọn)
|
|
942
|
+
|
|
943
|
+
```
|
|
944
|
+
Tài khoản
|
|
945
|
+
┌─────────────────────────────────────┐
|
|
946
|
+
│ Tên hiển thị [ô nhập ] │
|
|
947
|
+
│ ─────────────────────────────────── │
|
|
948
|
+
│ Email name@mail.com │
|
|
949
|
+
└─────────────────────────────────────┘
|
|
950
|
+
|
|
951
|
+
Giao diện
|
|
952
|
+
┌─────────────────────────────────────┐
|
|
953
|
+
│ Chế độ tối [ ○]│
|
|
954
|
+
└─────────────────────────────────────┘
|
|
955
|
+
|
|
956
|
+
Vùng nguy hiểm
|
|
957
|
+
┌─────────────────────────────────────┐
|
|
958
|
+
│ Xoá tài khoản [Xoá] │
|
|
959
|
+
└─────────────────────────────────────┘
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
- Mỗi mục là **một khối chia kẻ**, không phải mỗi tuỳ chọn một card.
|
|
963
|
+
- Nhãn trái, điều khiển phải, cùng một hàng.
|
|
964
|
+
- **Nhãn thẳng hàng với ô, không với cả khối bên phải.** Hàng có chữ gợi ý hoặc lỗi dưới ô thì khối phải cao hơn ô; nhãn căn giữa cả hàng là tụt xuống lưng chừng giữa ô và dòng chữ. Grid mặc định kéo ô nhãn cao bằng cả hàng (`stretch`), nên `min-h` + `items-center` trên ô nhãn không đủ, phải có `sm:items-start` trên hàng (thiếu thì nhãn "Múi giờ" và nhãn của ô đang báo lỗi lệch xuống 11px so với ô):
|
|
965
|
+
|
|
966
|
+
```html
|
|
967
|
+
<div class="grid gap-2 px-4 py-4 sm:grid-cols-[10rem_minmax(0,1fr)] sm:items-start sm:gap-6 sm:px-5">
|
|
968
|
+
<!-- min-h bằng chiều cao ô: nhãn một dòng nằm đúng tâm ô -->
|
|
969
|
+
<div class="flex min-w-0 flex-wrap items-center gap-x-2 sm:min-h-11 md:min-h-10">
|
|
970
|
+
<label for="timezone" class="text-sm font-medium">Múi giờ</label>
|
|
971
|
+
<!-- dấu "Đã lưu" ở đây -->
|
|
972
|
+
</div>
|
|
973
|
+
<div class="flex min-w-0 flex-col gap-2">
|
|
974
|
+
<!-- ô, rồi chữ gợi ý hoặc lỗi -->
|
|
975
|
+
</div>
|
|
976
|
+
</div>
|
|
977
|
+
```
|
|
978
|
+
|
|
979
|
+
Hàng **chữ chỉ đọc + nút** (Email, Mật khẩu) cũng `sm:items-start`: nhãn và nút nằm ở dải cao bằng ô đầu hàng, chữ giá trị `sm:py-2.5` để dòng đầu thẳng nhãn. Căn giữa thì giá trị dài bốn dòng (email xuống dòng + dòng "Đang chờ xác nhận…") làm nhãn "Email" trôi xuống giữa dòng 2 và 3. Chỉ hàng ảnh đại diện căn giữa (`sm:items-center`): avatar cao 64px, không có dòng đầu nào để thẳng theo.
|
|
980
|
+
|
|
981
|
+
**Mục chỉ có chữ chỉ đọc (không ô, không nút) thì hàng không mượn dải cao của ô**: `min-h-11` và `sm:py-2.5` có để nhãn thẳng tâm ô, không có ô thì hàng một dòng chữ cao 72px, trống nửa hàng. Thường là dấu hiệu mấy hàng đó nên gộp thành dòng phụ của hàng trên ("Gia hạn tiếp theo", "Dùng gói này từ", xem "Trang thanh toán").
|
|
982
|
+
- Không viết chữ giải thích dưới mọi dòng. Chỉ giải thích thứ thật sự khó đoán.
|
|
983
|
+
- Vùng nguy hiểm tách xuống cuối cùng.
|
|
984
|
+
- **Mặc định dựng kiểu không có nút "Lưu thay đổi" tổng**: mỗi dòng chừa chỗ cho một dấu "Đã lưu" nhỏ cạnh điều khiển (hình ba trạng thái ở `../components/loading.md`). Lưu lúc nào, gọi gì là việc của người dùng, skill chỉ để handler rỗng (`onChange`). **Toggle, select áp ngay; ô chữ thì lưu khi rời ô, hoặc có nút Lưu riêng của đúng khối đó** (mỗi card cài đặt có ô chữ thì có nút Lưu ở chân card). Cái cần tránh là **một nút Lưu tổng cho cả trang** trong khi có toggle tự áp: người dùng không biết bật xong có phải bấm Lưu không. Nút Lưu của khối khoá khi chưa có gì đổi.
|
|
985
|
+
|
|
986
|
+
- **Hàng cài đặt có mô tả thì câu lỗi và câu lý do khoá thay chỗ mô tả, giữ cỡ `text-sm` của mô tả.** `text-xs` chỉ dành cho dòng nằm dưới ô trong form, cạnh chữ gợi ý cũng `text-xs`. Trong khung chia kẻ, mô tả các hàng đều `text-sm`; câu "Bật Email ở trên để nhận bản tin." `text-xs` đứng giữa chúng thì hàng khoá trông như chữ chú thích lạc vào, và hàng cao thấp đổi theo lúc bật tắt Email.
|
|
987
|
+
- **Câu mô tả của công tắc mở khu phải đọc được khi khu đang đóng.** "Không gửi… trong khung giờ này" khi hai ô giờ còn giấu thì "khung giờ này" trỏ vào thứ chưa thấy. Viết cho lúc tắt: "Không gửi email và thông báo trình duyệt vào khung giờ bạn chọn."
|
|
988
|
+
|
|
989
|
+
### Khu cài đặt có nhiều trang
|
|
990
|
+
|
|
991
|
+
App có từ hai trang cài đặt trở lên (Hồ sơ, Thông báo, Bảo mật…) thì chúng là **một khu** có
|
|
992
|
+
điều hướng riêng, không phải mấy route rời chỉ gõ được bằng tay.
|
|
993
|
+
|
|
994
|
+
```
|
|
995
|
+
header h-16: Cài đặt <- là <h1>, tên khu
|
|
996
|
+
Hồ sơ Thông báo Bảo mật <- tab underline, mỗi tab một route
|
|
997
|
+
─────── ━━━━━━━━━ ────────────
|
|
998
|
+
Kênh nhận <- vào thẳng mục đầu, không đầu trang riêng
|
|
999
|
+
┌──────────────────────────────┐
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
- **Route gốc không bao giờ trống**: `/settings` chuyển thẳng (replace, không thêm lịch sử) tới trang con đầu. Mục "Cài đặt" ở sidebar sáng ở mọi trang con (`aria-current` theo tiền tố route).
|
|
1003
|
+
- **Dưới 6 trang: hàng tab `underline` trên cùng vùng nội dung**, rộng bằng cột nội dung (`max-w-2xl`), chữ tab đầu thẳng cột với tiêu đề mục bên dưới. Tab là `<Link>` có `aria-current="page"`, không `role="tablist"`: mỗi tab là một trang, nút Back phải quay về tab trước. Không dùng `solid`: trang cài đặt đầy công tắc bật đã tô `--primary`, thêm viên đen ở đầu trang là hai loại khối đen tranh nhau, và chữ trong viên thụt `px-3` lệch cột với nội dung.
|
|
1004
|
+
- **Từ 6 trang trở lên: cột nav dọc bên trái** (kiểu B), `w-48 shrink-0`, mục cùng khuôn link sidebar: `h-10 rounded-xl px-3 text-sm text-foreground/70 outline-hidden`, rê `hover:bg-item-hover hover:text-foreground`, đang chọn một bậc đậm hơn `bg-secondary font-medium text-foreground`, có `aria-current="page"`. Cột đặt trong khung trắng `bg-surface rounded-2xl p-2`: đứng thẳng trên nền trang xám thì nền rê `--background` trùng nền trang, rê không thấy gì (`I10`). Dưới `lg` cột đó thành hàng tab `underline` cuộn ngang ở trên (`../responsive.md`).
|
|
1005
|
+
- **Tên khu ở thanh header là `<h1>`, trang con không có đầu trang riêng.** Tab đang sáng đã nói đang ở trang nào; thêm tiêu đề "Thông báo" + một câu mô tả dưới hàng tab là ghi tên trang hai lần (luật "một trang đúng một `<h1>`" ở "Đầu trang trong vùng nội dung"). Trang trạng thái (`/settings/…/states`) giữ đường dẫn cha như cũ.
|
|
1006
|
+
- **Hồ sơ cá nhân là tab đầu của khu**, không phải route riêng ngoài khu. Mục "Hồ sơ" trong menu tài khoản dẫn tới đúng tab đó.
|
|
1007
|
+
- **Đường kẻ dưới hàng tab rộng đúng bằng cột nội dung**, không rộng bằng khung cuộn của hàng tab (khung này rộng hơn cột 8px mỗi bên để chữ tab đầu thẳng cột, `small-controls.md`): kẻ ở khung cuộn thì đường kẻ thò ra 8px hai bên so với mép card bên dưới. Vẽ bằng `before:absolute before:inset-x-2 before:bottom-0 before:h-px before:bg-border-strong` trên khối bọc hàng tab (`relative`), số dương, không `-mx` (`N11`). Dùng `before:` chứ không `after:`: `::after` là con cuối nên vẽ đè lên vạch tab đang chọn. Tab theo mẫu `underline` (`box-content h-10 pb-px`, vạch `bottom-0`) thì khối bọc cao 41px, đường kẻ nằm đúng dòng cuối, vạch đè lên. Cột nội dung có `max-w-*` thì cộng thêm 16px cho phần hàng tab lấn ra hai bên (`max-w-2xl` thành `max-w-[43rem]`), các khối khác trong cột `mx-2` (ở 375 và 768px vị trí mọi phần tử trùng bản `-mx-2`). Màu `border-border-strong`: đường này nằm trên nền trang xám, `--border` (`#f7f7f8`) ở đó còn sáng hơn nền nên mất hẳn. Đừng tự pha `border-foreground/10` (trên nền trang ra `#e1e1e3`, đậm hơn hẳn). Vạch 2px của tab đang chọn vẫn đè lên đường kẻ này.
|
|
1008
|
+
- Màn hẹp: hàng tab cuộn ngang theo `small-controls.md` ("Hàng chip ở màn hẹp"), không xuống dòng, không đổi thành select.
|
|
1009
|
+
|
|
1010
|
+
**B. Tab dọc bên trái** (từ 15 tuỳ chọn trên một trang, hoặc từ 6 trang cài đặt trở lên): xem mục "Khu cài đặt có nhiều trang" ngay trên.
|
|
1011
|
+
|
|
1012
|
+
### Trang tuỳ chọn thông báo
|
|
1013
|
+
|
|
1014
|
+
Là trang cài đặt kiểu A. Bộ mục mặc định: **Kênh nhận** (trình duyệt, email, email tổng hợp),
|
|
1015
|
+
**Báo cho bạn khi** (mỗi loại việc một công tắc, cuối là mốc nhắc hạn chót), **Không làm phiền**
|
|
1016
|
+
(công tắc mở khung giờ trượt ngay dưới, cùng hàng).
|
|
1017
|
+
|
|
1018
|
+
- **Nói rõ công tắc loại việc có áp cho chuông trong app hay không, ở MỘT chỗ.** Mục Kênh nhận ghi "Chuông luôn nhận đủ" mà mục bên dưới ghi "Áp dụng cho mọi kênh đang bật" thì tắt "Có bình luận mới" xong người dùng không biết chuông còn báo không. Mặc định: chuông nhận đủ, công tắc loại việc chỉ áp cho kênh gửi ra ngoài, và câu mô tả mục ghi đúng vậy: "Áp dụng cho trình duyệt và email."
|
|
1019
|
+
- Nhiều loại việc × nhiều kênh mà cần chọn riêng từng ô (ví dụ bình luận chỉ qua email) thì mới dựng bảng lưới việc × kênh bằng checkbox. Quy ước chia đôi: sản phẩm có nhiều loại sự kiện và từ hai kênh gửi ra ngoài (email, đẩy) thì dùng lưới, sản phẩm ít sự kiện dùng danh sách công tắc. Mặc định của skill là ba mục trên, vì app dashboard thường ít sự kiện; dự án có trên ~6 loại việc và hai kênh gửi ra ngoài thì đề xuất lưới lúc giao.
|
|
1020
|
+
- Hàng có ô chọn cùng khuôn hàng công tắc (nhãn trái, ô phải `sm:w-48`); dưới `sm` ô xuống dưới chữ, rộng hết hàng.
|
|
1021
|
+
- Trình duyệt đang chặn quyền thông báo: công tắc khoá ở trạng thái tắt, câu lý do nói chỗ mở lại (biểu tượng ổ khoá cạnh địa chỉ trang). Email tắt thì email tổng hợp khoá theo, lý do "Bật Email ở trên để nhận bản tin."
|
|
1022
|
+
|
|
1023
|
+
---
|
|
1024
|
+
|
|
1025
|
+
## Trang hồ sơ cá nhân
|
|
1026
|
+
|
|
1027
|
+
Là trang cài đặt kiểu A ở trên, không phải khuôn riêng. Bộ mục mặc định:
|
|
1028
|
+
|
|
1029
|
+
| Mục | Hàng | Lưu |
|
|
1030
|
+
| --- | --- | --- |
|
|
1031
|
+
| Thông tin cá nhân | Ảnh đại diện, Họ và tên, Chức danh, Số điện thoại | Ô chữ: nút Lưu ở chân card. Ảnh: áp ngay khi tải xong |
|
|
1032
|
+
| Đăng nhập | Email, Mật khẩu: chữ chỉ đọc + nút viền "Đổi email", "Đổi mật khẩu" | Luồng riêng (modal) |
|
|
1033
|
+
| Tuỳ chọn | Ngôn ngữ, Múi giờ | Áp ngay, dấu "Đã lưu" cạnh nhãn |
|
|
1034
|
+
| Vùng nguy hiểm | Xoá tài khoản | Hộp xác nhận (`overlay.md`) |
|
|
1035
|
+
|
|
1036
|
+
- **Avatar trên trang là cùng component, cùng seed màu với avatar ở header / chân sidebar** (`../components/avatar.md`). Lệch thì header nền chàm, trang hồ sơ nền hổ phách, cùng chữ "T" của cùng một người: nhìn hai chỗ tưởng hai tài khoản.
|
|
1037
|
+
- **Avatar và tên ở header vẽ theo giá trị đã lưu, không theo ô đang gõ.** Xoá trống ô họ tên thì avatar vẫn là "T", không thành "?". Lưu xong mới đổi, header và trang đổi cùng lúc.
|
|
1038
|
+
- **Hàng ảnh đại diện có bốn trạng thái:**
|
|
1039
|
+
- Chưa có ảnh: chữ cái đầu, nút "Tải ảnh lên", dòng gợi ý `text-xs text-muted` "JPG hoặc PNG, tối đa 2 MB".
|
|
1040
|
+
- Có ảnh: "Đổi ảnh" và "Xoá ảnh" **đều là nút `outline`**. Xoá ảnh **không đỏ, không hộp xác nhận**: bấm là xoá ngay, avatar về chữ cái đầu, kèm toast "Đã xoá ảnh đại diện" có nút **Hoàn tác** (`D3`: xoá mà lấy lại được thì xoá ngay + hoàn tác). Có hoàn tác thì không mất gì, nên không phải việc phá huỷ (`rose` của `I4` dành cho thứ mất hẳn: xoá tài khoản, xoá dự án). Hai nút cùng `outline`, không để Xoá ảnh `secondary` nền xám: nó thành nút nặng hơn Đổi ảnh, trong khi đổi ảnh mới là việc người ta hay làm. **Không dùng `ghost`**: chữ `text-muted` đứng cạnh nút viền đọc ra là nút đang khoá, và lúc khoá thật (đang tải ảnh) thì gần như không khác gì (đã dính: "Xoá ảnh" `#828282` cùng xám với dòng gợi ý bên dưới).
|
|
1041
|
+
- Đang tải: avatar mờ `opacity-50` với spinner giữa, nút "Tải ảnh lên" khoá.
|
|
1042
|
+
- Lỗi: chữ đỏ **thay chỗ** dòng gợi ý, không thêm dòng. Nói số thật và cách sửa: "Ảnh nặng 4,8 MB, chọn ảnh dưới 2 MB".
|
|
1043
|
+
- **Ảnh áp ngay khi tải xong**, như select, không tính vào nút Lưu của card. Người dùng chọn ảnh xong thấy ảnh mới trên avatar là nghĩ đã xong; bắt bấm Lưu nữa thì rời trang là mất ảnh.
|
|
1044
|
+
- **Email và mật khẩu không sửa tại chỗ.** Đổi email phải xác nhận địa chỉ mới, đổi mật khẩu phải nhập mật khẩu cũ, nên hàng chỉ đọc + nút mở luồng riêng. Email đang chờ xác nhận thì dưới email hiện một dòng `text-sm text-muted`: "Đang chờ xác nhận **moi@…**", email cũ vẫn là email đăng nhập tới lúc xác nhận. **"Gửi lại" và "Huỷ" đứng một hàng riêng ngay dưới** (`flex gap-4 mt-1`), không nối sau email bằng dấu `·`: là nút chữ `font-medium text-foreground hover:underline underline-offset-2` (như "Thử lại" ở `../components/file-upload.md`), vùng bấm nới bằng `relative before:absolute before:-inset-x-1.5 before:-inset-y-2` (34px, số âm giữ theo `N11` như nút sao chép ở `description-list.md`). Nối vào câu thì ở 375px email dài đẩy "· Huỷ" xuống đầu dòng mới, dấu `·` mồ côi, và hai nút chữ cao 18px nằm giữa câu, ngón tay khó trúng.
|
|
1045
|
+
- Mật khẩu ghi mốc đổi gần nhất `text-muted` ("Đổi lần cuối 12/06/2026"), không ghi chuỗi `••••••••`: chấm tròn không nói gì mà trông như ô nhập được.
|
|
1046
|
+
- **Trang trạng thái** (dựng tĩnh cạnh nhau) đủ các ca: chưa sửa (Lưu khoá), vừa sửa, đang lưu, vừa lưu, lỗi nhập, ảnh quá dung lượng, **có ảnh**, **đang tải ảnh**, **email chờ xác nhận**, tên và chức danh rất dài, vừa đổi múi giờ, hộp xác nhận xoá tài khoản. Hộp xác nhận xem thêm ở 375px: email dài phải xuống dòng ở sau `@` (`../components/description-list.md`).
|
|
1047
|
+
|
|
1048
|
+
---
|
|
1049
|
+
|
|
1050
|
+
## Trang bảo mật
|
|
1051
|
+
|
|
1052
|
+
Là trang cài đặt kiểu A ở trên. Bộ mục mặc định:
|
|
1053
|
+
|
|
1054
|
+
| Mục | Hàng | Lưu |
|
|
1055
|
+
| --- | --- | --- |
|
|
1056
|
+
| Xác thực hai lớp | Dòng trạng thái + nút mở luồng; đã bật thì thêm hàng Phương thức, Mã dự phòng | Luồng riêng (modal), không công tắc |
|
|
1057
|
+
| Phiên đăng nhập | Mỗi thiết bị một dòng, phiên đang dùng đứng đầu; chân khung có nút đăng xuất hàng loạt | Làm ngay, toast báo xong |
|
|
1058
|
+
|
|
1059
|
+
Mật khẩu đã có ở trang hồ sơ (mục Đăng nhập) thì không lặp lại ở đây. App không có
|
|
1060
|
+
trang hồ sơ thì hàng Mật khẩu đứng đầu trang này, dựng y như bên hồ sơ.
|
|
1061
|
+
|
|
1062
|
+
```
|
|
1063
|
+
Xác thực hai lớp
|
|
1064
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1065
|
+
│ Chưa bật [Bật xác thực hai lớp] │ <- chưa bật
|
|
1066
|
+
│ Ngoài mật khẩu, nhập thêm mã 6 số từ ứng dụng xác thực. │
|
|
1067
|
+
└──────────────────────────────────────────────────────────┘
|
|
1068
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1069
|
+
│ ✓ Đã bật từ 12/06/2026 [Tắt] │ <- đã bật
|
|
1070
|
+
│ ──────────────────────────────────────────────────────── │
|
|
1071
|
+
│ Ứng dụng xác thực Google Authenticator [Đổi] │
|
|
1072
|
+
│ ──────────────────────────────────────────────────────── │
|
|
1073
|
+
│ Mã dự phòng Còn 8 / 10 mã [Tạo mã mới] │
|
|
1074
|
+
└──────────────────────────────────────────────────────────┘
|
|
1075
|
+
|
|
1076
|
+
Phiên đăng nhập
|
|
1077
|
+
Thấy thiết bị lạ thì đăng xuất thiết bị đó rồi đổi mật khẩu.
|
|
1078
|
+
┌──────────────────────────────────────────────────────────┐
|
|
1079
|
+
│ [▭] Chrome trên macOS Thiết bị này │
|
|
1080
|
+
│ Hà Nội, Việt Nam │
|
|
1081
|
+
│ ──────────────────────────────────────────────────────── │
|
|
1082
|
+
│ [▯] Safari trên iPhone [Đăng xuất] │
|
|
1083
|
+
│ Hà Nội, Việt Nam · 2 giờ trước │
|
|
1084
|
+
│ ──────────────────────────────────────────────────────── │
|
|
1085
|
+
│ [Đăng xuất 4 thiết bị khác] │ <- cùng cỡ, cùng dạng nút dòng
|
|
1086
|
+
└──────────────────────────────────────────────────────────┘
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
- **Xác thực hai lớp không phải công tắc** (`../components/choice-controls.md`, "Hàng cài
|
|
1090
|
+
đặt"). Bật phải qua ba bước: quét mã QR (hoặc chép khoá), nhập mã 6 số để thử, lưu mã
|
|
1091
|
+
dự phòng. Tắt phải nhập lại mật khẩu. Nên là dòng trạng thái + nút mở luồng.
|
|
1092
|
+
Đã dính khi dựng thành công tắc: gạt là hiện "Đã lưu" trong khi chưa quét mã nào, và lúc đã bật thì không còn
|
|
1093
|
+
chỗ cho phương thức và mã dự phòng.
|
|
1094
|
+
- Chưa bật: chữ "Chưa bật" `text-sm font-medium` + câu hệ quả `text-muted`, nút **`primary`**
|
|
1095
|
+
"Bật xác thực hai lớp" bên phải (dưới `sm` xuống dưới chữ, căn trái). Lý do nền nhấn
|
|
1096
|
+
(`I2`): đây là việc trang muốn người dùng làm, và là nút chính duy nhất của trang. Nút viền
|
|
1097
|
+
đứng ngang hàng bốn nút "Đăng xuất" thì việc nên làm nhất trang không nổi hơn
|
|
1098
|
+
việc gì (chủ dự án: "bật xác thực hai lớp tôi nghĩ nền đen"). Không tô chữ "Chưa bật"
|
|
1099
|
+
vàng hay đỏ: nút đặc đã đủ kéo mắt, trang cài đặt không phải chỗ doạ người dùng (`M7`).
|
|
1100
|
+
- Đã bật: dòng đầu icon `check` `text-emerald-600` + "Đã bật từ 12/06/2026", nút
|
|
1101
|
+
`outline` "Tắt" (không `rose`: tắt không mất dữ liệu, bật lại được, `I4`). Dưới là hai
|
|
1102
|
+
hàng kiểu A: Ứng dụng xác thực (nút "Đổi"), Mã dự phòng ghi số mã còn lại (nút "Tạo mã
|
|
1103
|
+
mới"). Còn từ 2 mã trở xuống thì câu phụ `text-muted` "Sắp hết mã, tạo mã mới rồi cất
|
|
1104
|
+
ở chỗ an toàn." Mất điện thoại mà không có mã dự phòng là mất tài khoản, nên hàng này
|
|
1105
|
+
không giấu vào luồng khác.
|
|
1106
|
+
- Luồng bật, luồng tắt: skill để handler rỗng (`onEnable`, `onDisable`), trang chỉ đổi
|
|
1107
|
+
hình theo trạng thái máy chủ trả về (`N10`).
|
|
1108
|
+
- **Dòng phiên** theo `../components/list-row.md`: icon loại thiết bị trong ô `size-10
|
|
1109
|
+
rounded-lg bg-background`, tên thiết bị `text-sm font-medium truncate`, dòng phụ `text-xs
|
|
1110
|
+
text-muted` "vị trí · lần hoạt động cuối", nút `outline` "Đăng xuất" cỡ nút form (`h-11 md:h-10 rounded-xl`, như nút "Bật xác thực hai lớp"),
|
|
1111
|
+
**rê vào / Tab tới thì đỏ** (dạng "nút lặp lại trên từng dòng" của `I4`), `aria-label` có
|
|
1112
|
+
tên thiết bị. Phiên đang dùng đứng đầu, nhãn `text-xs text-muted` "Thiết bị
|
|
1113
|
+
này" cạnh tên, **không nút** (tự đăng xuất đi qua menu tài khoản). Dưới `sm` nút xuống
|
|
1114
|
+
dưới chữ, căn trái.
|
|
1115
|
+
- **Đăng xuất một thiết bị làm ngay**, không hỏi, toast "Đã đăng xuất thiết bị" với tên
|
|
1116
|
+
thiết bị ở dòng dưới. Không có Hoàn tác: phiên đã thu hồi thì không gọi về được.
|
|
1117
|
+
- **Đăng xuất hàng loạt: nút nguy hiểm đứng riêng** (nền `rose-500/10`, chữ `rose-700`,
|
|
1118
|
+
`I4`), **cùng cỡ nút dòng** (`h-11 md:h-10 rounded-xl`), ở chân khung, chỉ hiện khi còn thiết bị
|
|
1119
|
+
khác. Đỏ vì nó đá mọi thiết bị khác ra cùng lúc và không gọi lại được, kể cả máy của
|
|
1120
|
+
chính mình đang dùng dở; đăng xuất ở skill này là việc nguy hiểm (`I4`, chủ dự án chốt).
|
|
1121
|
+
**Hỏi lại trước** (`D3`: nhiều thiết bị một lúc) bằng hộp xác nhận đỏ như hộp xoá (icon
|
|
1122
|
+
`log-out` nền `rose-500/10`, nút xác nhận `rose`).
|
|
1123
|
+
Đừng dùng lại lý lẽ "không mất dữ liệu" (chủ dự án đã bác) để đổi sang nút viền trung tính:
|
|
1124
|
+
cả trang không còn gì nói "cẩn thận" nữa. Nút `rose` cao hơn nút dòng thì hai cỡ nút chồng nhau trong một khung: giữ
|
|
1125
|
+
cùng cỡ nút dòng, giữ màu `rose`.
|
|
1126
|
+
- **Cả trang một cỡ nút** (`h-11 md:h-10`), không `h-8` cho nút trong dòng phiên. Ở 1280px
|
|
1127
|
+
dòng không cao thêm (ô icon `size-10` đã cao 40px, nút cao bằng ô icon); ở 375px mỗi dòng
|
|
1128
|
+
cao thêm 12px nhưng nút đạt 44px, cỡ bấm tối thiểu khuyến nghị cho màn cảm ứng (nút 32px
|
|
1129
|
+
thì hụt). Nút dòng và nút hàng loạt `h-8` đứng dưới nút "Bật xác thực
|
|
1130
|
+
hai lớp" `h-10` là một trang hai cỡ nút (chủ dự án: "để h-10 luôn cho đồng bộ").
|
|
1131
|
+
- **Gỡ dòng xong thì chuyển focus** (`I31`): đăng xuất một thiết bị thì focus sang nút
|
|
1132
|
+
"Đăng xuất" của dòng kế (hết dòng thì dòng trên); đăng xuất hàng loạt xong thì focus lên
|
|
1133
|
+
tiêu đề mục "Phiên đăng nhập" (`tabIndex={-1}`). Không chuyển thì focus rơi
|
|
1134
|
+
về `<body>`, trình đọc màn hình mất chỗ.
|
|
1135
|
+
- **Khung chờ**: dòng đầu là phiên đang dùng nên **không có khối nút**, các dòng sau có
|
|
1136
|
+
(`I19`: khung chờ đúng hình).
|
|
1137
|
+
- **Trang trạng thái** đủ các ca: xác thực hai lớp chưa bật, đã bật, đã bật mà sắp hết mã
|
|
1138
|
+
dự phòng; danh sách đang tải, lỗi tải, đang đăng xuất một dòng (spinner trong nút, nút
|
|
1139
|
+
giữ cỡ), chỉ còn thiết bị này (không chân khung), tên thiết bị rất dài; hộp xác nhận
|
|
1140
|
+
đăng xuất hàng loạt; hai toast.
|
|
1141
|
+
|
|
1142
|
+
## Trang khoá API
|
|
1143
|
+
|
|
1144
|
+
Là trang cài đặt kiểu A, **một mục** "Khoá của workspace": câu mô tả nói khoá dùng ở đâu và
|
|
1145
|
+
phải giữ thế nào, nút `primary` "Tạo khoá API" ngang hàng tiêu đề mục (việc chính duy nhất
|
|
1146
|
+
của trang, `I2`; dưới `sm` xuống dưới câu mô tả, căn trái). Không đầu trang riêng (xem "Khu
|
|
1147
|
+
cài đặt có nhiều trang").
|
|
1148
|
+
|
|
1149
|
+
```
|
|
1150
|
+
Khoá của workspace [+ Tạo khoá API]
|
|
1151
|
+
Dùng để gọi API từ máy chủ của bạn. Giữ khoá như mật khẩu…
|
|
1152
|
+
┌─────────────────────────────────────────────────────────────────────────┐
|
|
1153
|
+
│ Máy chủ production ⋯ │
|
|
1154
|
+
│ evd_live_…a3f9 · Toàn quyền · Dùng 2 phút trước · Không hết hạn │
|
|
1155
|
+
│ ─────────────────────────────────────────────────────────────────────── │
|
|
1156
|
+
│ Đồng bộ đơn hàng sang phần mềm kế toán cho chi nhánh… ⋯ │
|
|
1157
|
+
│ evd_live_…7c1e · Chỉ đọc · Dùng 3 ngày trước · Hết hạn sau 3 ngày │ <- hổ phách
|
|
1158
|
+
│ ─────────────────────────────────────────────────────────────────────── │
|
|
1159
|
+
│ Script báo cáo cũ (Đã hết hạn) ⋯ │ <- cuối danh sách
|
|
1160
|
+
│ evd_live_…5e6f · Chỉ đọc · Dùng 11/09 │
|
|
1161
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
1162
|
+
```
|
|
1163
|
+
|
|
1164
|
+
- **Dòng khoá** theo `../components/list-row.md`, **không ô icon đầu dòng**: mọi dòng cùng
|
|
1165
|
+
một icon chìa khoá là một ý nói năm lần (`N3`). Tên `text-sm font-medium truncate` (có
|
|
1166
|
+
`title`). Dòng phụ `text-xs text-muted`: **chuỗi khoá đã che** `font-mono` (`T17`) gồm phần
|
|
1167
|
+
đầu và bốn ký tự cuối (`evd_live_…a3f9`: phần đầu nói khoá thật hay khoá thử, đuôi để đối
|
|
1168
|
+
chiếu), quyền, lần dùng cuối (`<time>` có giờ đủ trong `title`, `T16b`; chưa dùng thì
|
|
1169
|
+
"Chưa dùng lần nào"), hạn dùng. Ngày theo `T16b`: năm nay thì bỏ năm ("Dùng 07/09", "Hết
|
|
1170
|
+
hạn 23/12"), khác năm mới ghi đủ. Đừng để "Dùng 07/09/2026" đứng cạnh "Hết hạn
|
|
1171
|
+
23/12" trong cùng một dòng.
|
|
1172
|
+
- **Hạn dùng ba mức** như `list-row.md`: còn xa `text-muted` "Hết hạn 23/12"; còn từ 7 ngày
|
|
1173
|
+
trở xuống ghi số ngày, `text-amber-700` "Hết hạn sau 3 ngày" (`M7`, đó là thứ phải làm);
|
|
1174
|
+
đã hết hạn thì badge trung tính "Đã hết hạn" cạnh tên và **bỏ mảnh hạn dùng** khỏi dòng phụ
|
|
1175
|
+
(không nói hai lần). Khoá hết hạn xếp xuống cuối, còn lại mới tạo đứng đầu.
|
|
1176
|
+
- **Thao tác của dòng là nút `⋯`** (`I11`: có xoá thì gom vào ba chấm), cỡ `size-10`,
|
|
1177
|
+
`aria-label` có tên khoá. Menu: **"Đổi tên"**, đường chia, **"Thu hồi khoá"** (mục menu
|
|
1178
|
+
nguy hiểm, đỏ lúc rê, `I4`). Khoá đã hết hạn: "Xoá" thay "Thu hồi khoá" (thu hồi một khoá đã
|
|
1179
|
+
chết là chữ sai việc, `N6`). Ba nền tảng lớn đều gom thao tác của dòng
|
|
1180
|
+
khoá vào nút ba chấm, hai trong đó có mục sửa tên. Vì có từ hai việc nên khác dòng phiên
|
|
1181
|
+
đăng nhập (chỉ một việc, nút "Đăng xuất" hiện thẳng).
|
|
1182
|
+
Đừng mượn khuôn dòng phiên (nút viền "Thu hồi" `h-11` trên mọi dòng): ở 375px nút rơi xuống
|
|
1183
|
+
dưới chữ, mỗi dòng cao 143px, năm khoá dài hơn một màn; với `⋯` nút ở lại bên phải, dòng còn
|
|
1184
|
+
87px.
|
|
1185
|
+
- **Đổi tên** mở hộp một ô (modal có form, `I20`), điền sẵn tên hiện tại, con trỏ nằm sẵn
|
|
1186
|
+
trong ô, nút "Lưu" / "Huỷ". Lỗi trùng tên so với các khoá **khác** (so với chính nó thì không
|
|
1187
|
+
tính). Đổi xong đóng hộp, toast "Đã đổi tên khoá" với tên mới ở dòng dưới, focus về nút `⋯`
|
|
1188
|
+
của dòng đó. Không hỏi lại: tên chỉ để người trong workspace nhận ra khoá, ứng dụng đang dùng
|
|
1189
|
+
khoá không bị ảnh hưởng. Lưu gì, gọi gì khi bấm Lưu là việc của người dùng (`N10`): skill để
|
|
1190
|
+
handler `onRename` rỗng.
|
|
1191
|
+
- **Dòng phụ ở màn hẹp chia hai dòng theo nghĩa**, không để trình duyệt tự ngắt: "chuỗi khoá
|
|
1192
|
+
· quyền" / "lần dùng · hạn dùng". Từ `sm` nối thành một dòng. Ngắt tự do thì dấu `·` rớt
|
|
1193
|
+
lên đầu dòng sau ("· Chưa dùng lần nào"). Cách làm ở
|
|
1194
|
+
`list-row.md`, "Dòng phụ nhiều mảnh".
|
|
1195
|
+
- **Thu hồi hỏi lại** bằng hộp xác nhận đỏ như hộp xoá (`I4` câu 2, kết thúc thứ đang chạy; `D3`: ứng dụng đang dùng khoá hỏng
|
|
1196
|
+
ngay, không gọi lại được), icon `key-round`, tên khoá in đậm đầu câu, nút "Thu hồi khoá".
|
|
1197
|
+
**Xoá khoá đã hết hạn làm ngay**, không hỏi: khoá đó đã không gọi được gì. Cả hai xong thì
|
|
1198
|
+
toast, tên khoá ở dòng dưới; không Hoàn tác. Gỡ dòng thì chuyển focus (`I31`): sang nút `⋯`
|
|
1199
|
+
của dòng kế, hết thì dòng trên, không còn dòng nào thì tiêu đề mục.
|
|
1200
|
+
- **Hộp tạo khoá** (modal có form, `I20`: bấm ra ngoài không đóng): ba trường.
|
|
1201
|
+
- "Tên khoá", gợi ý "Đặt theo nơi dùng khoá, để sau này biết khoá nào thu hồi được." Con
|
|
1202
|
+
trỏ nằm sẵn ở ô. Trùng tên khoá đang có thì báo lỗi dưới ô.
|
|
1203
|
+
- "Quyền": card chọn (`choice-controls.md`), vì hai lựa chọn có hệ quả khác hẳn nhau; **sẵn
|
|
1204
|
+
lựa chọn hẹp nhất** ("Chỉ đọc"). Tài liệu tạo khoá của hai nền tảng lớn
|
|
1205
|
+
đều dặn chọn quyền tối thiểu cần dùng.
|
|
1206
|
+
- "Hạn dùng": select 30 ngày / 90 ngày / 1 năm / Không hết hạn, **có sẵn một hạn**, không sẵn
|
|
1207
|
+
"Không hết hạn". Chữ gợi ý dưới ô ghi ngày hết hạn thật ("Hết hạn ngày 25/12/2026."), không
|
|
1208
|
+
bắt người dùng tự cộng.
|
|
1209
|
+
- **Tạo xong thì cùng hộp chuyển sang bước hiện khoá**, không đóng hộp này mở hộp khác (một
|
|
1210
|
+
nhịp chớp, focus đi hai lần). Tiêu đề "Sao chép khoá API", câu mô tả nói đây là lần duy nhất
|
|
1211
|
+
thấy khoá đầy đủ. Khoá nằm trong khối `bg-background rounded-xl px-4 py-3 font-mono
|
|
1212
|
+
break-all select-all` (bấm là bôi đen cả khoá; không cắt "…" vì người dùng cần đối chiếu
|
|
1213
|
+
khoá đã dán). Dưới khối là nút `primary` "Sao chép khoá" (`I2`: việc duy nhất của bước này) có icon `copy`, nhận focus khi bước
|
|
1214
|
+
này hiện; chép xong icon thành `check` 1,5 giây, chữ giữ nguyên (`N1`), kèm `role="status"`
|
|
1215
|
+
"Đã sao chép". Chân hộp chỉ còn "Xong" (`secondary`). Không đặt nút sao chép cạnh khối khoá
|
|
1216
|
+
trên một hàng: ở 640 và 1280px khoá bị ép xuống hai dòng và khối đen to
|
|
1217
|
+
bằng khối khoá đứng cạnh nó.
|
|
1218
|
+
- **Trang trạng thái** đủ các ca: đang tải (khung chờ đúng hình dòng, chỗ nút là ô `size-10`),
|
|
1219
|
+
lỗi tải, chưa có khoá (một dòng chữ, nút tạo đã ở đầu mục), tên khoá rất dài, khoá sắp hết
|
|
1220
|
+
hạn, khoá đã hết hạn, menu `⋯` đang mở; hộp xác nhận thu hồi; hộp tạo trống, lỗi thiếu tên,
|
|
1221
|
+
lỗi trùng tên, đang tạo; bước hiện khoá, vừa sao chép; hai toast.
|
|
1222
|
+
|
|
1223
|
+
---
|
|
1224
|
+
|
|
1225
|
+
## Trang thanh toán
|
|
1226
|
+
|
|
1227
|
+
Là trang cài đặt kiểu A, bốn mục theo thứ tự: **Gói đang dùng**, **Phương thức thanh toán**,
|
|
1228
|
+
**Lịch sử hoá đơn**, **Vùng nguy hiểm** (huỷ gói, chỉ ở gói trả phí chưa huỷ). App xuất hoá đơn cho công ty thì thêm mục **Thông tin xuất hoá đơn** (email
|
|
1229
|
+
nhận hoá đơn, tên công ty, mã số thuế; nút "Sửa" mở hộp một form) giữa phương thức và lịch sử.
|
|
1230
|
+
Năm trang thanh toán của các sản phẩm lớn đều có mục phương thức thanh toán
|
|
1231
|
+
(thẻ che số, nút đổi) và lịch sử hoá đơn tải được; ba có email hoặc thông tin xuất hoá đơn; các
|
|
1232
|
+
trang đều báo lần trừ tiền lỗi ngay trên trang, kèm lối trả lại hoặc đổi thẻ.
|
|
1233
|
+
|
|
1234
|
+
```
|
|
1235
|
+
[!] Chưa trừ được 1.290.000 đ kỳ 12/09 [Cập nhật thẻ] <- chỉ khi trừ lỗi
|
|
1236
|
+
Thẻ Visa •••• 4242 bị từ chối. Cập nhật thẻ trước 19/09 để giữ gói Pro.
|
|
1237
|
+
|
|
1238
|
+
Gói đang dùng
|
|
1239
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
1240
|
+
│ Pro [Đổi gói] │ <- tên 16px 600
|
|
1241
|
+
│ 1.290.000 đ mỗi tháng │
|
|
1242
|
+
│ Gia hạn ngày 12/10/2026 │ <- bỏ khi đang trừ lỗi
|
|
1243
|
+
└──────────────────────────────────────────────────────────────┘
|
|
1244
|
+
Phương thức thanh toán
|
|
1245
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
1246
|
+
│ Visa •••• 4242 [Đổi thẻ] │
|
|
1247
|
+
│ Hết hạn 08/2027 │
|
|
1248
|
+
└──────────────────────────────────────────────────────────────┘
|
|
1249
|
+
Lịch sử hoá đơn
|
|
1250
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
1251
|
+
│ Ngày Mã hoá đơn Trạng thái Số tiền │
|
|
1252
|
+
│ 12/09 HD-2026-0008 • Đã thanh toán 1.290.000 đ ⤓ │
|
|
1253
|
+
└──────────────────────────────────────────────────────────────┘
|
|
1254
|
+
Vùng nguy hiểm
|
|
1255
|
+
┌──────────────────────────────────────────────────────────────┐
|
|
1256
|
+
│ Huỷ gói Pro: bạn vẫn dùng tới hết 12/10/2026, [Huỷ gói] │ <- nền đỏ mờ (I4)
|
|
1257
|
+
│ sau đó cả workspace về gói Miễn phí. │
|
|
1258
|
+
└──────────────────────────────────────────────────────────────┘
|
|
1259
|
+
```
|
|
1260
|
+
|
|
1261
|
+
- **Gói là một hàng, không tách thành các hàng nhãn–ngày.** Tên gói `text-base font-semibold`,
|
|
1262
|
+
thứ nặng nhất khối. Dưới tên là hai dòng `text-sm text-muted`: giá (số `text-foreground`) kèm
|
|
1263
|
+
chu kỳ ("mỗi tháng", "mỗi năm"), rồi "Gia hạn ngày 12/10/2026" (mốc đứng riêng thì ghi đủ năm,
|
|
1264
|
+
`T16b`). Giá và ngày là **hai dòng riêng**, không nối bằng `·`: ở 375px dòng nối gãy ngay giữa
|
|
1265
|
+
"Gia hạn ngày" và "12/10/2026". **Không hàng "Dùng gói này từ"**:
|
|
1266
|
+
không ai vào trang này để làm gì với ngày đó, kỳ đầu đã nằm ở cuối lịch sử hoá đơn.
|
|
1267
|
+
Tách gói, "Gia hạn tiếp theo", "Dùng gói này từ" thành ba hàng thì cao 77, 73, 72px
|
|
1268
|
+
(hàng ngày mượn dải cao của ô nhập), tên gói 14px 500 giống hệt hai nhãn ngày, nhìn không ra
|
|
1269
|
+
đâu là tên gói. Một hàng thì khối còn khoảng 80px, tên gói đọc ra ngay.
|
|
1270
|
+
- **Nút của mục gói theo loại gói:**
|
|
1271
|
+
- Trả phí: chỉ "Đổi gói" `outline`, là link sang bảng giá (`pricing.md`).
|
|
1272
|
+
- **"Huỷ gói" nằm ở Vùng nguy hiểm cuối trang, không cạnh "Đổi gói"** (luật kiểu A: vùng nguy
|
|
1273
|
+
hiểm tách xuống cuối cùng, như "Xoá workspace"). Một hàng như hàng vùng nguy hiểm của trang
|
|
1274
|
+
workspace: câu hậu quả `text-sm text-muted` trái ("Huỷ gói Pro: bạn vẫn dùng tới hết
|
|
1275
|
+
12/10/2026, sau đó cả workspace về gói Miễn phí."), nút "Huỷ gói" phải, **nút nguy hiểm đứng
|
|
1276
|
+
riêng** (`I4`: nền `rose-500/10`, chữ `rose-700`). Tiêu đề mục "Vùng nguy hiểm", không đặt
|
|
1277
|
+
"Huỷ gói" (tiêu đề và nút cùng chữ). Vẫn thấy ngay khi cuộn, một lần bấm là mở hộp xác nhận:
|
|
1278
|
+
không giấu vào trang khác hay sau nhiều bước. Huỷ gói cùng họ với huỷ tài khoản, rời nhóm,
|
|
1279
|
+
đăng xuất (`I4`, câu 2: kết thúc thứ đang chạy).
|
|
1280
|
+
Nút đỏ đứng cạnh "Đổi gói" ngay đầu trang là khối nặng nhất trang, trên một tài khoản
|
|
1281
|
+
đang ổn mắt rơi vào nút huỷ trước tiên. Dời xuống cuối thì khối gói chỉ còn "Đổi gói", nút
|
|
1282
|
+
huỷ vẫn đỏ, vẫn một lần bấm.
|
|
1283
|
+
- **Hộp xác nhận huỷ gói đỏ như hộp xoá** (`I4`, `overlay.md`): icon `calendar-x` nền `rose-500/10`
|
|
1284
|
+
glyph `rose-700`, nút xác nhận "Huỷ gói" nền `rose-500/10` chữ `rose-700`, nút "Giữ gói"
|
|
1285
|
+
`--secondary` nhận focus khi mở. Câu hậu quả nói ngày kết thúc thật ("Bạn vẫn dùng gói Pro tới
|
|
1286
|
+
hết 12/10/2026, sau đó về gói Miễn phí"). Không dùng khuôn "hộp xác nhận cho việc không mất
|
|
1287
|
+
dữ liệu" (icon xám, nút `primary` đen).
|
|
1288
|
+
Đừng dùng lại lý lẽ "Huỷ gói không đỏ, vì không mất gì" (nút viền trung tính, hộp icon xám,
|
|
1289
|
+
nút xác nhận đen): cùng lý lẽ "không mất dữ liệu" mà chủ dự án đã bác ở đăng xuất hàng loạt.
|
|
1290
|
+
- Miễn phí: dòng dưới tên chỉ "Miễn phí", không dòng gia hạn, một nút `primary` "Nâng cấp gói"
|
|
1291
|
+
(`I2`: việc trang muốn người dùng làm). Mục phương thức thanh toán ẩn khi chưa có thẻ.
|
|
1292
|
+
- Đã huỷ, còn hạn: dòng ngày đổi thành `text-amber-700` "Kết thúc ngày 12/10/2026, sau đó về
|
|
1293
|
+
gói Miễn phí"; "Tiếp tục gói" (`outline`) đứng cạnh "Đổi gói" trong khối gói, **Vùng nguy
|
|
1294
|
+
hiểm ẩn** (không còn gì để huỷ).
|
|
1295
|
+
- **Đang trừ lỗi: bỏ dòng "Gia hạn ngày …"** khỏi khối gói. Kỳ này chưa trả thì ngày gia hạn
|
|
1296
|
+
kế không còn đúng, để nguyên là nói ngược banner ngay trên (`S6`). Không thêm badge "Quá hạn"
|
|
1297
|
+
cạnh tên: banner đã nói (`N3`).
|
|
1298
|
+
**Câu hậu quả ở Vùng nguy hiểm và hộp xác nhận huỷ cũng không dùng ngày đó**: "bạn vẫn dùng
|
|
1299
|
+
tới hết 12/10/2026" là hứa một kỳ chưa trả. Huỷ lúc đang trừ lỗi thì hậu quả thật (về gói
|
|
1300
|
+
Miễn phí ngay, hay còn hạn tới đâu) là logic của người dùng (`N10`); câu lấy từ dữ liệu, mẫu
|
|
1301
|
+
mặc định "Huỷ gói Pro: workspace về gói Miễn phí ngay." Khối gói đã bỏ ngày gia hạn mà cuối
|
|
1302
|
+
trang vẫn "bạn vẫn dùng tới hết 12/10/2026" là hai câu ngược nhau.
|
|
1303
|
+
- Dưới `sm` các nút xuống dưới chữ, căn trái, như mọi hàng cài đặt.
|
|
1304
|
+
- **Phương thức thanh toán là một hàng**: "Visa •••• 4242" `text-sm font-medium` (tên hãng bằng
|
|
1305
|
+
chữ, bốn số cuối; không vẽ logo hãng thẻ), dòng dưới `text-sm text-muted` "Hết hạn 08/2027",
|
|
1306
|
+
nút `outline` "Đổi thẻ". Thẻ hết hạn trước kỳ gia hạn tới thì dòng dưới `text-amber-700` "Hết
|
|
1307
|
+
hạn 10/2026, trước kỳ gia hạn 12/10"; đã hết hạn thì `text-red-600` "Đã hết hạn 08/2026".
|
|
1308
|
+
Luồng đổi thẻ là của người dùng (`N10`), skill để handler rỗng.
|
|
1309
|
+
**Nút đứng một mình trên hàng không vì thế mà thành `primary`** (`I1`, `I2`): "Đổi thẻ" là
|
|
1310
|
+
việc thỉnh thoảng mới làm, cùng loại "Đổi email", "Đổi mật khẩu" ở trang hồ sơ, nên là nút
|
|
1311
|
+
viền. Cả trang chỉ một nút `primary` là "Nâng cấp gói" ở gói miễn phí (`I3`). Nút nào đứng
|
|
1312
|
+
một mình cũng tô đen thì trang trả phí có ba khối đen ("Đổi gói", "Đổi thẻ", "Cập nhật thẻ")
|
|
1313
|
+
và không khối nào còn nổi.
|
|
1314
|
+
- **Trừ tiền thất bại: banner tông lỗi đầu trang, trên mục gói** (`../components/banner.md`,
|
|
1315
|
+
không ✕, `role="alert"`). Tiêu đề nói số tiền và kỳ, mô tả nói vì sao và hạn chót, **một** nút
|
|
1316
|
+
`outline` "Cập nhật thẻ" mở đúng luồng của "Đổi thẻ". Dòng hoá đơn đó vẫn badge đỏ "Thất bại".
|
|
1317
|
+
**Nút đứng một mình trong banner vẫn là `outline` nền trắng, không `primary`** (luật của
|
|
1318
|
+
`banner.md`): nền đỏ nhạt và icon đỏ đã kéo mắt tới khối, nút trắng là mảng sáng nhất trong khối
|
|
1319
|
+
nên vẫn thấy ngay. Nút đen đặt trong khối đỏ là hai tín hiệu mạnh nhất trang chồng lên nhau.
|
|
1320
|
+
Hai hệ thiết kế lớn chia đôi, một hệ chỉ cho nút ghost trong thông báo nằm
|
|
1321
|
+
trong trang, một hệ cho chọn nút chính hay nút phụ tuỳ độ nhấn. Không có số đông đòi nút đặc,
|
|
1322
|
+
nên giữ luật của `banner.md` (`I2`: màu nhấn phải có lý do, ở đây tông đỏ đã làm việc đó).
|
|
1323
|
+
Đừng để ca trừ lỗi chỉ có badge đỏ ở dòng hoá đơn: mục gói vẫn ghi "Gia hạn tiếp
|
|
1324
|
+
theo" như chưa có gì, và cả trang không có chỗ nào đổi thẻ để sửa.
|
|
1325
|
+
- **Lịch sử hoá đơn**: bảng năm cột Ngày, Mã hoá đơn (`font-mono text-muted`), Trạng thái
|
|
1326
|
+
(badge), Số tiền (bám phải), nút tải. Mới nhất ở trên. **Không cột "Gói"**: mọi dòng cùng một
|
|
1327
|
+
gói là một ý nói tám lần (`N3`). Dòng không bấm được nên không hover; nút tải chỉ icon
|
|
1328
|
+
`download`, `ghost`, **luôn hiện** (`I11`: một việc, không nguy hiểm), tooltip "Tải PDF",
|
|
1329
|
+
`aria-label` có mã hoá đơn. Ngày theo `T16b` (năm nay bỏ năm, `title` đủ ngày).
|
|
1330
|
+
**Bảng hay danh sách chọn theo bề rộng khung (`@container`)**, không theo viewport: có cột nav
|
|
1331
|
+
dọc thì ở 1024px cột nội dung chỉ còn ~490px. Hẹp thì mỗi hoá đơn một dòng hai tầng: ngày +
|
|
1332
|
+
mã, rồi badge trái số tiền phải; nút tải `size-10` ở mép phải.
|
|
1333
|
+
- **Trang trạng thái** đủ các ca: đang tải (khung chờ đúng hình từng mục), lỗi tải từng mục, gói
|
|
1334
|
+
miễn phí (chưa có hoá đơn), trả theo năm (hoá đơn năm trước ghi đủ năm), trừ tiền thất bại
|
|
1335
|
+
(banner + badge), thẻ sắp hết hạn, thẻ đã hết hạn, gói đã huỷ còn hạn, hộp xác nhận huỷ gói.
|
|
1336
|
+
|
|
1337
|
+
---
|
|
1338
|
+
|
|
1339
|
+
## Trang thành viên và phân quyền
|
|
1340
|
+
|
|
1341
|
+
Là bảng quản lý ở mục "Bảng dữ liệu" trên, khác ở những chỗ dưới đây. Hai việc
|
|
1342
|
+
chính của trang là **mời** và **đổi vai trò**, nên cả hai phải thấy được ngay khi
|
|
1343
|
+
nhìn, không nằm sau lúc rê chuột hay trong menu ⋯.
|
|
1344
|
+
|
|
1345
|
+
```
|
|
1346
|
+
[Tất cả 18] Đang hoạt động 14 Chờ chấp nhận 4 [tìm…] [Vai trò ▾] [+ Mời thành viên]
|
|
1347
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
1348
|
+
│ ☐ Thành viên Vai trò Ngày tham gia │
|
|
1349
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
1350
|
+
│ ⬤ Trần Nguyễn Anh Tuấn Khang Bạn Chủ sở hữu 04/03/2024 │ <- khoá: chữ trơn, không ⌄, không ⋯
|
|
1351
|
+
│ tran.khang@evondev-studio.com │
|
|
1352
|
+
│ ☐ ⬤ Lê Minh Anh Quản trị viên ⌄ 17/06/2024 ⋯ │
|
|
1353
|
+
│ minhanh.le@evondev-studio.com │
|
|
1354
|
+
│ ☐ ⬤ hoang.long.pham@gmail.com (Chờ chấp nhận) Thành viên ⌄ — ⋯ │ <- ngoại lệ mới có badge
|
|
1355
|
+
│ Đã mời 24/09 │
|
|
1356
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
1357
|
+
```
|
|
1358
|
+
|
|
1359
|
+
- **Ô vai trò đổi được thì luôn có `ChevronDown` `size-3.5 text-muted` sau chữ**, không chỉ hiện nền lúc rê. Chỉ có nền lúc rê thì ô "Quản trị viên" đứng yên trông y hệt ô "Chủ sở hữu" không đổi được, và việc đề bài đòi ("đổi vai trò") không ai tìm ra (phải rê đúng vào chữ mới biết bấm được). Các app quản lý thành viên phổ biến đều để mũi tên luôn hiện ở cột vai trò. Dòng khoá (chủ sở hữu, chính mình) là chữ trơn không mũi tên: **có mũi tên hay không chính là tín hiệu đổi được hay không**, không cần thêm icon khoá. Ô `px-2`, nút ô `px-2` (như ô hạn chót, không `-mx-2`), nền rê `bg-foreground/8`, giữ nền lúc danh sách mở (`aria-expanded:bg-foreground/8`) như ô sửa tại chỗ ở bảng công việc (`I10`: nút trong dòng đang rê dùng `/8`, `/5` gần trùng nền dòng).
|
|
1360
|
+
- **Mũi tên thẳng một cột**: nút rộng bằng **nhãn dài nhất** trong danh sách vai trò, mũi tên `ml-auto` bám mép phải nút. Để nút co theo chữ thì "Chỉ xem ⌄", "Thành viên ⌄", "Quản trị viên ⌄" mỗi dòng mũi tên một chỗ, cột trông lệch (lệch 12px giữa hai vai trò). Không cần đo bằng JS: trong nút đặt một `grid`, mọi nhãn chồng lên cùng một ô (`col-start-1 row-start-1`), nhãn đang chọn hiện, các nhãn còn lại `invisible` + `aria-hidden`; ô grid tự rộng bằng nhãn dài nhất. Không khoá `w-36` cứng: đổi tên vai trò hay đổi sang tiếng Anh là lệch lại.
|
|
1361
|
+
- Khác bảng công việc (mục "Bảng nhóm theo trạng thái"): ở đó mỗi dòng ba bốn ô sửa được, mũi tên khắp nơi là nhiễu, nên dấu bấm được chỉ hiện lúc rê. Luật chung: **ô sửa tại chỗ là việc chính của trang thì dấu bấm được luôn hiện; ô sửa tại chỗ phụ thì hiện lúc rê.**
|
|
1362
|
+
- Danh sách mở ra theo Select ở `../components/choice-controls.md`: mỗi vai trò một dòng tên + một câu nói làm được gì, vai trò đang có có dấu check. Chọn là lưu và đóng, toast "Đã đổi vai trò" kèm **Hoàn tác** (`D3`), không hộp xác nhận: đổi vai trò đổi lại được. Chủ sở hữu không nằm trong danh sách (chuyển quyền sở hữu là luồng riêng có xác nhận).
|
|
1363
|
+
- **Không có cột Trạng thái.** "Đang hoạt động" là trạng thái thường của thành viên; một cột badge xanh lặp trên 14/18 dòng là một câu nói 14 lần (`N3`). **Chỉ dòng ngoại lệ mới có dấu**: lời mời chưa chấp nhận có badge xám "Chờ chấp nhận" ngay sau email ở tầng trên (chỗ của nhãn "Bạn"), tầng dưới "Đã mời 24/09", ngày tham gia `—`. Thành viên bị tạm ngưng (nếu dự án có) cũng là ngoại lệ, cùng cách. Tab trạng thái vẫn giữ để lọc. Bỏ cột thì dưới `sm` mỗi dòng còn hai tầng (tên + email bên trái, vai trò bên phải) thay vì ba tầng vì badge chiếm riêng một hàng.
|
|
1364
|
+
- **Lọc vai trò là một nút dropdown "Vai trò" cạnh ô tìm, không phải hàng chip.** Menu có checkbox, chọn được nhiều; đang lọc một vai trò thì nút ghi tên vai trò đó ("Quản trị viên"), nhiều vai trò thì "Vai trò · 2", `ChevronDown` cuối nút. Nút này là ô Select: không hover, lúc mở viền `border-focus` + `ring-2`, nền giữ trắng (`../components/button.md`). Hàng chip dưới hàng tab dành cho bảng mà lọc là cách đi chính (khách hàng theo nhãn). Ở đây vai trò đã hiện ở cột, lọc theo vai trò là việc ít làm, mà hàng chip chiếm nguyên một hàng trên bảng: hai hàng lọc cho 18 người. Các app quản lý thành viên phổ biến đều lọc vai trò bằng một dropdown. Dưới `sm` nút này đứng cùng hàng với nút "Trạng thái:".
|
|
1365
|
+
- **Email đứng ở tầng trên (lời mời chưa có tên) thì cắt phần trước `@`, giữ tên miền** (`AccountEmail` ở `overlay.md`, `N8`): với lời mời, tên miền nói người này trong công ty hay ngoài công ty. Cắt cuối thì "nguyen.thi.thuy.duong.ketoan.chinhanh.hcm@co…" mất hẳn tên miền. Email ở tầng dưới của thành viên đã có tên thì cắt cuối như thường: cả bảng chung một tên miền, phần phân biệt là phần trước `@`.
|
|
1366
|
+
- **Menu ⋯ chỉ còn việc không nằm ở ô nào**: thành viên có "Xoá khỏi workspace"; lời mời có "Gửi lại lời mời", "Thu hồi lời mời". Đổi vai trò không lặp trong menu (đã ở ô). Dòng khoá không có checkbox, không có ⋯.
|
|
1367
|
+
- **Thanh hàng loạt có "Đổi vai trò" (dropdown) cạnh "Xoá N thành viên"**: đổi vai trò cho cả nhóm người mới vào là việc hay làm hơn xoá hàng loạt. Đổi xong một toast gộp kèm Hoàn tác.
|
|
1368
|
+
- **Modal mời nhận nhiều email một lần**: ô email là tag input (`../components/tag-input.md`), dán cả danh sách, một vai trò cho cả lô, nút ghi số ("Gửi 3 lời mời"). Ô một email thì mời năm người phải mở modal năm lần. Các app quản lý thành viên phổ biến đều cho mời nhiều người một lần. Chọn vai trò trong modal dùng cùng danh sách có câu mô tả như ở ô bảng.
|
|
1369
|
+
- **Email trùng nói đúng ca, đỏ ở đúng thẻ đó:** đã là thành viên thì "Đã là thành viên"; đã mời mà chưa chấp nhận thì "Đã mời 24/09, chưa chấp nhận". Không gộp hai ca thành "Email này đã có trong workspace": người đang chờ chấp nhận chưa ở trong workspace, và việc người mời cần làm là gửi lại lời mời chứ không phải bỏ cuộc.
|