@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,340 @@
|
|
|
1
|
+
# Chip và IconButton
|
|
2
|
+
|
|
3
|
+
Nguồn: chip và nút icon của một dự án thật.
|
|
4
|
+
|
|
5
|
+
```tsx
|
|
6
|
+
// Chip: bộ lọc, tag chọn được. Luôn kèm aria-pressed={isActive}
|
|
7
|
+
"inline-flex max-w-48 cursor-pointer items-center rounded-full px-3 py-1 text-xs font-medium outline-hidden transition-colors"
|
|
8
|
+
isActive && "bg-primary text-primary-foreground"
|
|
9
|
+
!isActive && "bg-foreground/5 text-foreground/70 hover:bg-foreground/10 hover:text-foreground"
|
|
10
|
+
|
|
11
|
+
// IconButton: hành động phụ trong dòng hoặc header
|
|
12
|
+
"inline-flex size-8 cursor-pointer items-center justify-center rounded-lg text-muted transition-colors"
|
|
13
|
+
"hover:bg-foreground/5 hover:text-foreground" // nằm trong dòng có nền rê: hover:bg-foreground/8
|
|
14
|
+
"outline-hidden"
|
|
15
|
+
"disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-transparent disabled:hover:text-muted"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Chip có hai cỡ, chọn theo vai trò
|
|
19
|
+
|
|
20
|
+
Đây là chỗ hay sai: lấy cỡ chip trong widget đem ra làm bộ lọc chính của cả màn,
|
|
21
|
+
rồi hàng đó trông teo lại.
|
|
22
|
+
|
|
23
|
+
| Vai trò | Cỡ | Đi cạnh cái gì |
|
|
24
|
+
| --- | --- | --- |
|
|
25
|
+
| **Chip phụ trong widget**, tag, nhãn trạng thái | `px-3 py-1 text-xs` (cao ~26px) | Nằm trong card nhỏ, cạnh chữ `text-xs` |
|
|
26
|
+
| **Chip lọc chính của cả màn** | `h-9 px-3.5 text-sm` | Nằm cạnh ô tìm `h-10`, cạnh nút `h-10` |
|
|
27
|
+
|
|
28
|
+
Nguyên tắc chung: **chip đứng cùng hàng với ô nhập hay nút thì phải gần bằng
|
|
29
|
+
chiều cao của chúng**, lệch quá một bậc là hàng đó trông hỏng. Chip cao 26px
|
|
30
|
+
đứng cạnh input cao 40px thì mắt đọc ra cụm chip là thứ yếu, dù nó là bộ lọc
|
|
31
|
+
chính.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
**Vì sao ổn**
|
|
36
|
+
|
|
37
|
+
- **Chip chưa chọn là viên mờ `bg-foreground/5`, chữ `foreground/70`**; rê vào `foreground/10`, chọn rồi tô `bg-primary`. Lớp phủ theo màu chữ nên **mờ như nhau trên cả nền trang xám lẫn card trắng**, không phải chọn token theo nền. Đừng `bg-background`: trên card trắng thành bảy viên xám rõ, hàng lọc chưa ai dùng nặng thứ nhì màn; trên nền trang thì trùng màu nền nên rê vào không thấy gì. Đừng bỏ hẳn nền, chỉ chữ `text-muted`: hàng chip đọc ra như **một hàng tab thứ hai** nằm ngay dưới hàng tab, và `--muted` trên `#f4f4f6` chỉ 3,5:1. Chip phải còn dáng viên thì mới khác tab. Không viền: mười chip mà chip nào cũng có viền thì đọc như hàng rào.
|
|
38
|
+
- Chip nằm **trong popover có nút xác nhận** (popover Lọc) thì đang chọn không tô `bg-primary`, xem "Popover lọc" trong `../layouts/overlay.md`: chip đen đứng cạnh nút Áp dụng đen là hai khối nặng ngang nhau.
|
|
39
|
+
- Nền chip đang chọn là **`bg-primary text-primary-foreground`**, đúng token, không `bg-[#…]`, không `text-white`. Dùng token thì nền tối tự đảo (`--primary` thành gần trắng, chữ thành gần đen); gõ cứng thì sang nền tối thành chữ trắng trên nền trắng. Cũng đừng lấy nhầm `--primary-hover`: chip đang chọn trông nhạt hơn nút chính ngay cạnh.
|
|
40
|
+
- **Nhãn dài thì cắt, không để chip phình.** Chip `max-w-48`, chữ bọc trong `<span class="truncate">`, và `title` mang đủ tên để rê vào vẫn đọc được. Một chip "Hội chợ Triển lãm Quốc tế 2026" rộng gấp bốn chip "VIP" là cả hàng lệch, mắt dồn hết vào cái dài nhất.
|
|
41
|
+
- **Chip lọc là nút bật/tắt, phải có `aria-pressed={isActive}`.** Trạng thái chọn hiện chỉ bằng màu nền, trình đọc màn hình không thấy màu, nên thiếu `aria-pressed` thì chip nào cũng đọc ra "nút" như nhau. Chip chọn một (kiểu tab) thì dùng `role="radio"` + `aria-checked` trong `role="radiogroup"`, không dùng `aria-pressed`.
|
|
42
|
+
- Chip là `rounded-full`, nút là `rounded-xl` (cao dưới 40px thì `rounded-lg`, `F1`). Khác hình để mắt biết ngay cái nào chọn được nhiều cái nào là hành động.
|
|
43
|
+
- IconButton vuông `size-8`, chữ `text-muted` lúc thường, chỉ đen lên khi hover. Icon phụ không được đen bằng nội dung.
|
|
44
|
+
- **Nền rê của IconButton là lớp phủ `bg-foreground/5`, không `bg-background`.** Nút hay nằm trong dòng rê `hover:bg-item-hover` (`list-row.md`): rê vào nút thì nền nút trùng nền dòng, nút mất hẳn. Lớp phủ lấy màu nền phía sau nên đậm hơn nền quanh nó ở mọi chỗ. **Nằm trong dòng có nền rê thì lên `hover:bg-foreground/8`** (`I10`): `/5` chồng lên nền dòng đang rê gần như trùng.
|
|
45
|
+
- Trạng thái disabled phải tắt luôn cả hover (`disabled:hover:bg-transparent`). Thiếu dòng đó thì nút chết vẫn sáng lên khi rê vào, người dùng bấm hoài không hiểu sao.
|
|
46
|
+
- `aria-label` và `title` luôn nhận cùng một chuỗi `label`. Nút chỉ có icon thì bắt buộc.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Chip lọc đang áp dụng (có ×) ⚑
|
|
51
|
+
|
|
52
|
+
Hàng tóm tắt lọc cạnh số kết quả (*8 kết quả · [Gần bệnh viện ×] [Gần cao đẳng ×] Xoá lọc*)
|
|
53
|
+
là **nút bỏ lọc**, không phải nút bật / tắt như chip lọc. Khác vai thì khác hình:
|
|
54
|
+
|
|
55
|
+
```html
|
|
56
|
+
<button type="button" aria-label="Bỏ lọc Gần bệnh viện"
|
|
57
|
+
class="inline-flex h-8 shrink-0 cursor-pointer items-center gap-1 rounded-full border border-border-strong bg-surface pr-2 pl-3 text-sm text-foreground outline-hidden transition-colors hover:bg-button-hover">
|
|
58
|
+
Gần bệnh viện <i data-lucide="x" class="size-3.5 text-muted"></i>
|
|
59
|
+
</button>
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- **Viền, không nền xám, không tô màu nhấn.** Nền `bg-foreground/5` là hình của chip **chưa
|
|
63
|
+
chọn** ở hàng lọc: hàng tóm tắt dùng nền đó thì đọc thành "chưa chọn", ngược nghĩa. Tô
|
|
64
|
+
`bg-primary` thì thành khối màu thứ ba, thứ tư cạnh chip đang chọn phía trên (`N3`). Viền
|
|
65
|
+
`--border-strong` như nút viền (`M14`): đọc ra "bấm được, bấm là bỏ".
|
|
66
|
+
- **Cả chip là nút bỏ lọc**, không chỉ dấu ×; `aria-label` nói rõ bỏ lọc gì.
|
|
67
|
+
- Nhãn ghi đủ nghĩa khi đứng một mình ("Gần bệnh viện", "Dưới 8 triệu"), không chỉ "Bệnh viện".
|
|
68
|
+
- **Chỉ hiện lọc không nhìn thấy từ ngoài**: lọc chọn trong dropdown, popover, panel (khu vực,
|
|
69
|
+
giá thuê, dạng phòng). Lọc là chip hiện sẵn trên trang thì chip đang chọn đã nói rồi, không
|
|
70
|
+
lặp ở hàng tóm tắt ("Hai chỗ một việc", `V1b`; chủ dự án chốt).
|
|
71
|
+
- **"Xoá lọc"** là nút ghost chữ cuối hàng, hiện khi có ít nhất một lọc, **kể cả lọc là chip**
|
|
72
|
+
ở trên: nó bỏ hết một lần. Không có lọc ẩn nào thì hàng chỉ còn số kết quả và "Xoá lọc".
|
|
73
|
+
|
|
74
|
+
## Hàng chip ở màn hẹp
|
|
75
|
+
|
|
76
|
+
Hàng chip lọc **không bao giờ `flex-wrap`**. Bốn chip ở 375px sẽ thành ba cái
|
|
77
|
+
một hàng và một cái rớt xuống đứng một mình, đọc ra như lỗi chứ không như thiết
|
|
78
|
+
kế.
|
|
79
|
+
|
|
80
|
+
```html
|
|
81
|
+
<!-- Khối bọc hàng có lề ngang ít hơn 4px so với các khối khác (card px-5 thì khối này px-4, R6):
|
|
82
|
+
hàng bên trong px-1 đưa chip đầu về thẳng cột. -->
|
|
83
|
+
<div class="scrollbar-clean overflow-x-auto py-0.5">
|
|
84
|
+
<div class="flex items-center gap-2 px-1">
|
|
85
|
+
<button type="button" aria-pressed="true" class="shrink-0 cursor-pointer rounded-full bg-primary px-3 py-1 text-xs font-medium text-primary-foreground outline-hidden">Tất cả</button>
|
|
86
|
+
<button type="button" aria-pressed="false" class="shrink-0 cursor-pointer rounded-full bg-foreground/5 px-3 py-1 text-xs font-medium text-foreground/70 outline-hidden hover:bg-foreground/10 hover:text-foreground">Quá hạn</button>
|
|
87
|
+
<button type="button" aria-pressed="false" title="Hội chợ Triển lãm Quốc tế 2026" class="max-w-48 shrink-0 cursor-pointer rounded-full bg-foreground/5 px-3 py-1 text-xs font-medium text-foreground/70 outline-hidden hover:bg-foreground/10 hover:text-foreground">
|
|
88
|
+
<span class="block truncate">Hội chợ Triển lãm Quốc tế 2026</span>
|
|
89
|
+
</button>
|
|
90
|
+
</div>
|
|
91
|
+
</div>
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`shrink-0` để chip không bị bóp méo, `scrollbar-clean` để không lòi thanh cuộn ra. Ẩn thanh thì ở máy có chuột phải có nút mũi tên ở phía còn chip khuất (`responsive.md`, sau `R10`). Chip **đang lọc** (bấm để gỡ) thì khác: từ `sm` xuống dòng, "Xoá lọc" luôn thấy (`responsive.md`, sau `R6`).
|
|
95
|
+
|
|
96
|
+
Lề thì đặt trên **hàng bên trong**, đừng đặt trên khung cuộn: `<div class="overflow-x-auto"><div
|
|
97
|
+
class="flex gap-2 px-1">`. Padding bên phải của khung cuộn bị bỏ qua khi cuộn tới cuối, nên chip
|
|
98
|
+
cuối sẽ dính sát mép. **Khung cuộn không kéo ra bằng `-mx-1`** (`N11`): khối bọc nó lùi lề ít hơn
|
|
99
|
+
4px, các khối anh em giữ lề đủ (cách của `R6`).
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Thanh tab: bốn variant, chọn theo chỗ đứng
|
|
104
|
+
|
|
105
|
+
Tab và chip trông na ná nhưng là hai thứ khác nhau:
|
|
106
|
+
|
|
107
|
+
| | Tab | Chip lọc |
|
|
108
|
+
| --- | --- | --- |
|
|
109
|
+
| Chọn | **Đúng một**, luôn có một cái đang chọn | Không, một, hoặc nhiều |
|
|
110
|
+
| Ví dụ | Tất cả / Đang giao dịch / Tiềm năng / Ngừng | Nhãn, người phụ trách, khoảng giá |
|
|
111
|
+
| Hình | Theo variant bên dưới | Pill `rounded-full`, đang chọn tô đặc |
|
|
112
|
+
|
|
113
|
+
### Chọn variant
|
|
114
|
+
|
|
115
|
+
| Variant | Hình | Đặt ở đâu |
|
|
116
|
+
| --- | --- | --- |
|
|
117
|
+
| `boxed` (mặc định) | Chữ trơn, tab đang chọn là **ô nền nhạt viền mảnh** | Trên bảng / danh sách, chuyển trạng thái: Tất cả / Chờ xử lý / Đã giao |
|
|
118
|
+
| `underline` | Đường kẻ chạy hết hàng, **vạch 2px** dưới tab đang chọn | Chia nội dung trang chi tiết hoặc khối lớn: Tổng quan / Hoạt động / Tệp. **Điều hướng giữa các trang cài đặt**: Hồ sơ / Thông báo / Bảo mật (`../layouts/app.md`, "Khu cài đặt có nhiều trang") |
|
|
119
|
+
| `solid` | Tab đang chọn là **pill tô màu nhấn**, chữ đảo màu | Chỉ khi người dùng chọn. Không cho khu cài đặt: trang đầy công tắc bật đã tô `--primary`, viên đen ở đầu trang tranh với chúng |
|
|
120
|
+
| `segmented` | **Rãnh chìm** nhạt, tab đang chọn là **ô trắng nổi** như phím bấm | 2–4 lựa chọn ngắn đổi cách xem: Ngày / Tuần / Tháng, Danh sách / Lưới |
|
|
121
|
+
|
|
122
|
+
- **Ngay dưới hàng tab còn một hàng chip lọc thì tab dùng `underline`, không `boxed`.** Chip là viên xám bo tròn; tab `boxed` đang chọn cũng là viên xám bo góc. Hai hàng viên xám chồng nhau thì tab đang chọn đọc ra như một cái chip nữa, không ai thấy đó là trạng thái đang xem. Vạch dưới 2px là ngôn ngữ khác hẳn viên, hai hàng tách nhau ngay (`N5`).
|
|
123
|
+
- **Một trang chỉ một variant cho mỗi vai.** Tab trạng thái trên bảng đã `boxed` thì mọi bảng trong app đều `boxed`.
|
|
124
|
+
- **`solid` không dùng cho tab trạng thái trên bảng.** Pill tô đặc đứng đầu bảng thì kéo mắt mạnh hơn cả dữ liệu, và đọc ra như một nút bấm. Đừng dùng cho khu cài đặt: viên đen đứng trên sáu công tắc bật cũng đen, chữ trong viên thụt `px-3` lệch cột với tiêu đề mục bên dưới. Khu cài đặt dùng `underline`.
|
|
125
|
+
- `segmented` quá 4 lựa chọn, hoặc nhãn dài hơn hai chữ, thì đổi sang `boxed` hoặc `underline`.
|
|
126
|
+
|
|
127
|
+
### Mặc định khi đề không nói: không icon, không số
|
|
128
|
+
|
|
129
|
+
Tab dựng ra khi người dùng không nhắc gì là **chữ trơn**, không icon, không số đếm.
|
|
130
|
+
Chỉ thêm khi:
|
|
131
|
+
|
|
132
|
+
- **Icon:** người dùng yêu cầu, hoặc dự án đã có hàng tab dùng icon (theo cái đã có).
|
|
133
|
+
- **Số đếm:** người dùng yêu cầu ("có số đếm từng tab"), hoặc dữ liệu thật trả về sẵn số. Không bịa số cho có. Ca hay gặp: tab bản ghi con ở trang chi tiết ("Đơn hàng 24", "Tệp 4"); tab dòng chảy (Tin nhắn, Hoạt động) vẫn chữ trơn (`layouts/app.md`).
|
|
134
|
+
|
|
135
|
+
### Icon: khi có thì theo ba luật
|
|
136
|
+
|
|
137
|
+
Icon trước chữ **không bắt buộc**, xem mặc định ở trên. Có thì:
|
|
138
|
+
|
|
139
|
+
- **Có thì mọi tab trong hàng đều có**, không tab có tab không.
|
|
140
|
+
- Icon `size-4 shrink-0`, lấy từ thư viện icon của dự án, **màu ăn theo chữ** (`currentColor`): tab chưa chọn icon xám cùng chữ, tab đang chọn icon đậm cùng chữ. Không tô icon màu riêng.
|
|
141
|
+
- `segmented` chỉ có icon, không chữ, thì mỗi tab phải có `aria-label` và tooltip.
|
|
142
|
+
|
|
143
|
+
### Chung cho mọi variant
|
|
144
|
+
|
|
145
|
+
```tsx
|
|
146
|
+
<div role="tablist" aria-label="Lọc theo trạng thái" className={getTabListClasses(variant)}>
|
|
147
|
+
{views.map((view) => {
|
|
148
|
+
const isSelected = view.value === activeView;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Button
|
|
152
|
+
key={view.value}
|
|
153
|
+
variant="ghost"
|
|
154
|
+
role="tab"
|
|
155
|
+
aria-selected={isSelected}
|
|
156
|
+
tabIndex={isSelected ? 0 : -1}
|
|
157
|
+
onClick={() => onChange(view.value)}
|
|
158
|
+
className={getTabClasses(variant, isSelected)}
|
|
159
|
+
>
|
|
160
|
+
{view.icon && <view.icon className="size-4 shrink-0" />}
|
|
161
|
+
{view.label}
|
|
162
|
+
{view.count !== undefined && <span className="text-xs font-normal tabular-nums text-muted">{view.count}</span>}
|
|
163
|
+
{view.isNew && <span className={getNewBadgeClasses(isSelected)}>Mới</span>}
|
|
164
|
+
</Button>
|
|
165
|
+
);
|
|
166
|
+
})}
|
|
167
|
+
</div>
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
- **Mọi tab cùng `font-medium`**, kể cả tab chưa chọn. Đổi độ đậm lúc chọn làm chữ nở ra và cả hàng xô ngang.
|
|
171
|
+
- Tab chưa chọn chữ `foreground/70`. Hover dùng nền `foreground/5` (riêng `underline` thì chỉ đậm chữ, không nền, xem dưới); focus bàn phím không vòng (`I13`), **không bao giờ là nền xám**. Không dùng `--background` làm nền hover: tab hay nằm thẳng trên nền trang xám, tô `--background` ở đó thì rê vào không thấy gì.
|
|
172
|
+
- Bàn phím theo WAI-ARIA: chỉ tab đang chọn nằm trong vòng Tab, mũi tên trái/phải chuyển và chọn luôn, Home/End về hai đầu.
|
|
173
|
+
- Số đếm là số trơn `text-foreground/70`, không pill, không màu. **Không `text-muted`**: tab `boxed` đang chọn có nền `--secondary`, `--muted` trên đó chỉ 4.04 : 1 (`styles.md`). Trên tab đang chọn (chữ `--foreground`) số vẫn nhạt hơn nhãn một bậc; trên tab chưa chọn thì số và nhãn cùng màu, vậy là đủ. Không có số thì bỏ, đừng dựng số giả.
|
|
174
|
+
- Hàng tab không bao giờ wrap, màn hẹp thì cuộn ngang trong khung `scrollbar-clean` (`R6`). Từ 6 tab trở lên thì gom phần dư vào tab "Thêm" mở dropdown (`R10`).
|
|
175
|
+
|
|
176
|
+
### `boxed`: tab trạng thái trên bảng
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// Khung cuộn py-0.5, khối bọc lề ít hơn 4px (R6, không -mx-1): nền hover tab đầu/cuối không bị cắt. Hàng: gap-1 px-1.
|
|
180
|
+
"h-9 rounded-lg border px-3"
|
|
181
|
+
isSelected && "border-transparent bg-secondary text-foreground"
|
|
182
|
+
!isSelected && "border-transparent text-foreground/70 hover:bg-foreground/5 hover:text-foreground"
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
- **Mọi tab luôn có `border`**, tab chưa chọn là `border-transparent`. Không thì lúc bấm chuyển, tab đang chọn dày thêm 2px và cả hàng xô sang phải.
|
|
186
|
+
- **Tab đang chọn: nền `--secondary`, chữ `--foreground`, viền trong suốt.** Đây là bậc xám duy nhất chìm đủ rõ trên **cả** card trắng lẫn nền trang `#f4f4f6`. Nền tối thì `--secondary` là trắng phủ 8%, sáng lên thay vì chìm, cùng độ chênh (`M21`).
|
|
187
|
+
- **Không dùng `bg-surface` (trắng) và cũng không dùng `bg-surface-hover`.** Hai bậc đó chỉ chênh nền trắng 1-3% thì liếc vào không thấy tab nào đang chọn.
|
|
188
|
+
- **Ô đang chọn phải chênh với nền NẰM DƯỚI nó**, không phải chênh với mấy tab anh em. Cùng một class mà đổi chỗ đặt (card trắng ↔ nền trang xám) là đổi luôn độ rõ, nên chọn bậc xám nào cũng phải thử ở cả hai nền (`N2`).
|
|
189
|
+
- **Không đặt hàng `boxed` vào một khối xám riêng.** Nó nằm thẳng trên nền trang hoặc trên card. Bọc thêm khối xám là thành `segmented` hỏng: rãnh to, đậm, và ô trắng lọt thỏm.
|
|
190
|
+
- **`h-9 rounded-lg`**, cao dưới 40px nên bo 8px (`F1`), không ngoại lệ. Đừng bo 12px: tab 36px bo 12px là quá tròn so với chiều cao. Đứng cạnh ô tìm `h-10` là lệch đúng một bậc, chấp nhận được.
|
|
191
|
+
- Bên phải cùng hàng: ô tìm, nút **Lọc** (mở popover cho các trường khác ngoài trạng thái), nút **Sắp xếp** nếu cần. Đều là nút viền `h-10 rounded-xl` (`I1`), **cao bằng ô tìm `h-10`**: cả hàng công cụ một chiều cao, như bảng cỡ ở đầu file. Nút `h-9` đứng sát ô tìm `h-10` thì đáy lệch 4px, trông như hàng bị hỏng.
|
|
192
|
+
|
|
193
|
+
### `underline`: chia nội dung trang chi tiết
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
// Hàng: đường kẻ chạy hết bề ngang, vẽ bên trong hàng (bóng inset 1px ở đáy), vạch của tab đang chọn đè lên nó.
|
|
197
|
+
"flex min-w-full gap-2 px-2 shadow-[inset_0_-1px_0_var(--border-strong)]"
|
|
198
|
+
// Tab: vạch là ::after nên không đẩy chiều cao. box-content h-10 pb-px: tab cao 41px phủ cả dòng kẻ, vạch bottom-0 đè đúng lên nó.
|
|
199
|
+
"relative box-content h-10 rounded-xl px-2 pb-px after:absolute after:inset-x-2 after:bottom-0 after:h-0.5 after:rounded-full"
|
|
200
|
+
isSelected && "text-foreground after:bg-foreground"
|
|
201
|
+
!isSelected && "text-foreground/70 after:bg-transparent hover:text-foreground"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
- **Hàng tab nằm chung hàng với ô tìm và nút (toolbar trên bảng) thì bỏ đường kẻ hết hàng, chỉ giữ vạch 2px dưới tab đang chọn.** Đường kẻ chạy nửa hàng rồi cụt ở mép ô tìm trông dở dang, và vì khung cuộn rộng hơn cột nội dung 8px mỗi bên (để chữ tab đầu thẳng cột) nên đầu trái của nó còn thò ra ngoài mép card bên dưới 8px. Đường kẻ hết hàng chỉ dùng khi hàng tab đứng riêng một hàng, như trang chi tiết.
|
|
205
|
+
- Vạch màu `--foreground`, không màu nhấn có sắc: nhấn đã có ở nút chính của trang (`M3`).
|
|
206
|
+
- **Vạch không kéo xuống bằng `after:-bottom-px`** đè lên `border-b` của hàng (`N11`). Khung cuộn `overflow-x-auto` cũng cắt theo chiều dọc, nên khi đường kẻ nằm ở khối bọc ngoài khung cuộn (khu cài đặt) thì mép dưới vạch bị cắt, vạch chỉ còn 1px trên đường kẻ. Bản `box-content pb-px` + `bottom-0` nhìn y hệt bản số âm ở hàng tab đứng riêng (trang chi tiết, panel), và ở khu cài đặt thì vạch hiện đủ 2px.
|
|
207
|
+
- Khối bọc hàng tab lùi lề ít hơn 8px (`R6`), khung cuộn không `-mx-2` (`N11`): chữ tab đầu thẳng cột với nội dung bên dưới.
|
|
208
|
+
- Tab đang chọn không tô nền, không đổi nền lúc hover. Vạch là tín hiệu duy nhất.
|
|
209
|
+
- **Không tab nào có nền, kể cả lúc focus.** Tab bàn phím tới thì không đổi gì (`I13`). Tab là `Button variant="ghost"` mà `Button` dự án còn kiểu cũ "focus trông như hover" (nền xám) thì hàng tab dính theo: tab đang focus có nền xám, tab đang rê chuột đậm chữ, **hai tab cùng sáng** và không đọc ra tab nào đang chọn. Sửa ở `Button` dùng chung, không vá riêng từng tab.
|
|
210
|
+
- Hàng tab nằm được cả trên card trắng lẫn trên dải header xám: đường kẻ `--border-strong` đủ nhìn ở cả hai.
|
|
211
|
+
|
|
212
|
+
### `solid`: pill tô đặc (chỉ khi người dùng chọn)
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
"h-9 rounded-lg px-3"
|
|
216
|
+
isSelected && "bg-primary text-primary-foreground hover:bg-primary"
|
|
217
|
+
!isSelected && "text-foreground/70 hover:bg-foreground/5 hover:text-foreground"
|
|
218
|
+
|
|
219
|
+
// Badge "Mới" cạnh nhãn: pill như mọi badge, đảo màu theo tab.
|
|
220
|
+
function getNewBadgeClasses(isSelected: boolean) {
|
|
221
|
+
return cn(
|
|
222
|
+
"rounded-full px-2 py-0.5 text-xs font-medium",
|
|
223
|
+
isSelected && "bg-primary-foreground text-primary",
|
|
224
|
+
!isSelected && "bg-emerald-500/10 text-emerald-700",
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
- Màu tô là `--primary` và `--primary-foreground`, không viết cứng `bg-black text-white`: ở nền tối màu nhấn đảo thành gần trắng.
|
|
230
|
+
- **Chỉ một chỗ tô đặc trên màn.** Trang đã có nút chính tô `--primary` ngay cạnh thì cân nhắc `boxed`, hai khối tô đặc tranh nhau (`M2`).
|
|
231
|
+
- Badge "Mới" là thông tin trạng thái nên dùng màu xanh báo trạng thái (`M4`), trên tab đang chọn thì đảo sang nền `--primary-foreground`. Mỗi hàng tối đa hai badge, nhiều hơn là mất tác dụng.
|
|
232
|
+
- Pill `rounded-full text-xs`, chữ thường "Mới", không in hoa, không `text-[10px]`: chữ hoa 10px thì dấu tiếng Việt dính nhau, và `rounded` 4px là bậc bo thứ năm (`F1`).
|
|
233
|
+
|
|
234
|
+
### `segmented`: chuyển cách xem, dạng phím nổi
|
|
235
|
+
|
|
236
|
+
Rãnh **chìm** vào mặt (bóng trong), ô đang chọn **nổi** lên (bóng ngoài rất mờ + viền tóc). Hai lớp bóng ngược chiều mới ra cảm giác 3D, chỉ ô nổi thôi thì trông như dán giấy.
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
// Rãnh: nền nhạt --background, KHÔNG dùng --secondary (đậm quá, ô trắng lọt thỏm).
|
|
240
|
+
// Bo 12px, cách 4px, ô bên trong bo 8px: hai góc đồng tâm (M19).
|
|
241
|
+
"inline-flex w-fit max-w-full gap-1 rounded-xl bg-background p-1 shadow-(--shadow-segment-track)"
|
|
242
|
+
// Ô
|
|
243
|
+
"h-8 rounded-lg px-3 transition-[color,background-color,box-shadow] duration-150"
|
|
244
|
+
isSelected && "bg-surface text-foreground shadow-(--shadow-segment-thumb)"
|
|
245
|
+
!isSelected && "text-foreground/60 hover:text-foreground"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Hai token bóng khai trong `tokens.css`, có bản nền tối riêng:
|
|
249
|
+
|
|
250
|
+
```css
|
|
251
|
+
:root {
|
|
252
|
+
--shadow-segment-track: inset 0 0 0 1px rgb(0 0 0 / 0.05), inset 0 1px 2px rgb(0 0 0 / 0.04);
|
|
253
|
+
--shadow-segment-thumb: 0 0 0 1px rgb(0 0 0 / 0.04), 0 1px 2px rgb(0 0 0 / 0.06), 0 2px 6px -2px rgb(0 0 0 / 0.08);
|
|
254
|
+
}
|
|
255
|
+
.dark {
|
|
256
|
+
--shadow-segment-track: inset 0 0 0 1px var(--border), inset 0 1px 2px rgb(0 0 0 / 0.4);
|
|
257
|
+
--shadow-segment-thumb: 0 0 0 1px var(--border-strong), inset 0 1px 0 rgb(255 255 255 / 0.06), 0 1px 2px rgb(0 0 0 / 0.5);
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
- **Rãnh nhạt, không đậm.** `--background` trên card trắng là vừa đủ thấy rãnh; `--secondary` thì rãnh thành mảng xám nặng, kéo mắt hơn cả nội dung widget.
|
|
262
|
+
- Ô chưa chọn **không có nền hover**, chỉ đổi màu chữ. Nền hover xám trong rãnh xám thì thành ba sắc xám chồng nhau.
|
|
263
|
+
- Bóng ở đây là **ngoại lệ có tên của `M15`**: ô đang chọn là một phím vật lý, bóng nói "đang nhấn nó". Không nhân rộng sang `boxed`, `underline`, `solid`, hay khối khác trong trang.
|
|
264
|
+
- Nền tối: bóng đen khó thấy (`M23`), nên ô nổi lên nhờ viền `--border-strong` và vệt sáng `inset` trên mép trên.
|
|
265
|
+
- Rãnh đặt trên nền trang xám (không nằm trong card) thì đổi rãnh sang `bg-foreground/5`, để rãnh vẫn chìm hơn nền quanh nó.
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Badge số đếm
|
|
270
|
+
|
|
271
|
+
Số việc chưa đọc, số mục trong nhóm, số thành viên. Xuất hiện ở sidebar, ở tab,
|
|
272
|
+
ở tiêu đề cột.
|
|
273
|
+
|
|
274
|
+
Mặc định là **số trơn, chữ xám**, không khung, không nền. Pill trắng viền mảnh chỉ
|
|
275
|
+
khi người dùng chọn. Năm pill viền cạnh nhau trên một cột là năm khung nhỏ kéo mắt
|
|
276
|
+
(sidebar cũng vậy, `../layouts/app.md`).
|
|
277
|
+
|
|
278
|
+
```html
|
|
279
|
+
<!-- Mặc định: số trơn. Dòng đang chọn thì số lên text-foreground cùng chữ. -->
|
|
280
|
+
<span class="ml-auto shrink-0 text-xs tabular-nums text-muted">4</span>
|
|
281
|
+
|
|
282
|
+
<!-- Chỉ khi người dùng chọn: pill trắng, viền mảnh, chữ xám. -->
|
|
283
|
+
<span class="ml-auto shrink-0 rounded-full border border-border-strong bg-surface px-2 py-0.5 text-xs font-medium tabular-nums text-muted">121</span>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- **Một danh sách chỉ MỘT kiểu.** Người dùng chọn pill thì mọi số đều là pill, kể cả số `4`. Đừng chia kiểu theo ý nghĩa con số (chưa đọc thì pill, số đếm thường thì trơn): đặt cạnh nhau thì chỉ thấy hai hàng lệch style, không ai đọc ra được ý nghĩa.
|
|
287
|
+
- **Luôn căn phải**, cách nhãn bằng `ml-auto` hoặc `justify-between`.
|
|
288
|
+
- **Không tô màu brand, không nền đặc.** Badge brand chữ trắng trông nặng và làm màu nhấn loang khắp sidebar. Số đếm là thông tin, không phải hành động (`M2`, `M4`). Dự án muốn badge màu thì để họ tự đổi, skill không tự đề xuất.
|
|
289
|
+
- **Có pill thì nền pill là `--surface` (trắng)**, không phải `--secondary`. Pill trắng vẫn nổi rõ trên hàng đang rê (`--background`) và hàng đang chọn (`--secondary`), còn pill xám thì tan vào hàng.
|
|
290
|
+
- Chỉ tô màu khi con số là **cảnh báo thật**, kiểu số việc quá hạn: chữ hổ phách, vẫn không nền đặc.
|
|
291
|
+
- `tabular-nums` để các hàng thẳng cột nhau.
|
|
292
|
+
- Số lớn thì rút gọn: `99+`, đừng để `1.284` phá bề rộng sidebar.
|
|
293
|
+
|
|
294
|
+
---
|
|
295
|
+
|
|
296
|
+
## Phân trang
|
|
297
|
+
|
|
298
|
+
Nằm ở đáy khung bảng hoặc danh sách, cách thân bảng bằng `border-t border-border`. Một hàng,
|
|
299
|
+
hai cụm: **số đếm bên trái, mọi control bên phải**.
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
1 tới 10 trong 1.284 đơn hàng Mỗi trang [10 ▾] ‹ 1 2 3 4 5 … 129 ›
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
```tsx
|
|
306
|
+
<div className="flex items-center justify-between gap-4 border-t border-border px-4 py-3">
|
|
307
|
+
<p className="text-sm tabular-nums text-muted">1 tới 10 trong 1.284 đơn hàng</p>
|
|
308
|
+
<div className="flex shrink-0 items-center gap-4">
|
|
309
|
+
{/* "Mỗi trang" + select h-9 */}
|
|
310
|
+
<nav aria-label="Phân trang" className="flex items-center gap-1">{/* ‹ trang › */}</nav>
|
|
311
|
+
</div>
|
|
312
|
+
</div>
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
// Nút số trang: vuông h-9, luôn có border để lúc chuyển trang không xô hàng
|
|
317
|
+
"inline-flex h-9 min-w-9 cursor-pointer items-center justify-center rounded-lg border px-2 text-sm font-medium tabular-nums outline-hidden"
|
|
318
|
+
isCurrent && "border-transparent bg-secondary text-foreground" // + aria-current="page"
|
|
319
|
+
!isCurrent && "border-transparent text-foreground/70 hover:bg-foreground/5 hover:text-foreground"
|
|
320
|
+
// Mũi tên ‹ ›: IconButton h-9 w-9, có aria-label "Trang trước" / "Trang sau"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
- **Không bao giờ wrap, không nhảy chỗ.** Nav luôn ở cụm phải, cùng hàng với số đếm, bất kể có bao nhiêu trang. Chật thì **bớt số trang trước**: bỏ `2 3 4 5`, chỉ còn `‹ 1 … 12 … 129 ›`. Màn hẹp dưới `sm` thì chỉ còn `‹ 12 / 129 ›`. Không đẩy nav xuống dòng hai (nhiều trang thì nav rớt xuống căn trái, ít trang lại nằm phải).
|
|
324
|
+
- **Dưới `sm` số đếm chỉ còn tổng**: "32 khách hàng", phần "1 tới 10 trong" bọc `<span class="hidden sm:inline">`. Nav `‹ 1 / 4 ›` đã nói đang ở đâu (`I16` vẫn đủ: tổng số bên trái, vị trí bên phải). Để nguyên câu thì ở 375px nó chỉ còn ~168px cạnh nav, rớt một chữ xuống dòng hai ("1 tới 10 trong 32 khách / hàng"), chân bảng cao gấp đôi.
|
|
325
|
+
- **Trang đang chọn chép đúng class tab `boxed` đang chọn**: nền `--secondary`, viền trong suốt. **Không dùng nền trắng + viền**: đứng cạnh select `10 ▾` thì nó trông y như ô input, người dùng tưởng là ô gõ số trang để nhảy.
|
|
326
|
+
- **Select "Mỗi trang" nằm trong cụm phải, sát nav**, không đứng ngay sau số đếm. Số đếm dài ra theo trang ("1 tới 10" rồi "1.271 tới 1.280"), đặt select sau nó là select xê dịch mỗi lần chuyển trang.
|
|
327
|
+
- **Cửa sổ trang luôn đủ 7 ô** (tính cả `…`) khi tổng số trang lớn hơn 7. Ở gần hai đầu thì lấp thêm số cho đủ 7:
|
|
328
|
+
|
|
329
|
+
| Đang xem | Hiện |
|
|
330
|
+
| --- | --- |
|
|
331
|
+
| Trang 1 | `1 2 3 4 5 … 129` |
|
|
332
|
+
| Trang 12 | `1 … 11 12 13 … 129` |
|
|
333
|
+
| Trang 129 | `1 … 125 126 127 128 129` |
|
|
334
|
+
|
|
335
|
+
Số ô cố định thì nav rộng cố định, chuyển trang không kéo select xê dịch. `…` là chữ `text-muted`, không bấm được.
|
|
336
|
+
- **Số ô chọn theo bề rộng khung, một lần cho cả bảng**, không chọn riêng từng trang. Desktop đủ chỗ là 7. Chỉ khi đo thấy tràn mới hạ **cả bộ** xuống 5 (`1 2 3 … 129`, `1 … 12 … 129`, `1 … 127 128 129`), rồi mới tới dạng `‹ 12 / 129 ›`. Khung còn trống mà hiện 5 ô là sai.
|
|
337
|
+
- **Mũi tên ở trang đầu/cuối thì `disabled`**, giữ chỗ, không ẩn, để nav không co giãn.
|
|
338
|
+
- **Chỉ vừa một trang: ẩn nav.** Chỉ còn số đếm ("7 thành viên"). Select "Mỗi trang" chỉ giữ khi tổng số lớn hơn lựa chọn nhỏ nhất, không thì ẩn luôn. Hai mũi tên khoá cộng một ô `1` là nhiễu.
|
|
339
|
+
- **Không có dòng nào: ẩn cả footer.** Empty state của bảng đã nói hết, đừng để "0 khách hàng" cùng bốn control chết bên dưới.
|
|
340
|
+
- Số dùng dấu chấm hàng nghìn (`1.284`) và `tabular-nums`. Chuỗi đếm theo `I16`: "51 tới 75 trong 312 đơn hàng".
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Tiêu đề cột sắp xếp
|
|
2
|
+
|
|
3
|
+
Ô tiêu đề cột bảng bấm được để sắp xếp dòng. Bảng quanh nó (tab, lọc, chọn nhiều dòng,
|
|
4
|
+
phân trang) xem "Bảng dữ liệu" trong `../layouts/app.md`. Dự án có `@tanstack/react-table`
|
|
5
|
+
thì để nó giữ trạng thái sắp, skill chỉ lo hình ô (`N10`).
|
|
6
|
+
|
|
7
|
+
## Ba trạng thái
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Khách hàng ⇅ Ngày tạo ⇅ ⇅ Doanh thu chưa sắp: chữ và mũi tên hai chiều text-muted
|
|
11
|
+
Khách hàng ↑ Ngày tạo ⇅ ⇅ Doanh thu tên A → Z: cột đang sắp chữ và mũi tên text-foreground
|
|
12
|
+
Khách hàng ⇅ Ngày tạo ↓ ⇅ Doanh thu mới nhất trên cùng
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
- **Cột nào sắp được cũng có mũi tên, kể cả khi chưa bấm**: `arrow-up-down` → `arrow-up` /
|
|
16
|
+
`arrow-down`, `size-3.5`, cách chữ `gap-1`. Màn chạm không có hover, thiếu mũi tên thì không
|
|
17
|
+
biết cột nào bấm được. Cột không sắp được thì không mũi tên, không phải nút.
|
|
18
|
+
- **Cột đang sắp chỉ đổi màu chữ** `text-muted` → `text-foreground`, giữ cỡ và độ đậm
|
|
19
|
+
(`font-medium`) để hàng tiêu đề không xô (`N1`). Mỗi lúc chỉ một cột đang sắp.
|
|
20
|
+
- **Mũi tên lên là tăng dần** (A → Z, cũ → mới, nhỏ → lớn), xuống là giảm dần.
|
|
21
|
+
- **Cột số căn phải thì mũi tên đứng trước chữ** (`flex-row-reverse`), để chữ chạm mép phải
|
|
22
|
+
thẳng với con số bên dưới. Mũi tên sau chữ thì chữ thụt vào 18px so với số.
|
|
23
|
+
- **`aria-sort="ascending" | "descending"` chỉ trên `<th>` đang sắp**, cột khác không gắn.
|
|
24
|
+
|
|
25
|
+
## Bấm thế nào (mặc định cho người nối logic)
|
|
26
|
+
|
|
27
|
+
Thứ tự vòng bấm là logic, component chỉ gọi `onSort` (`N10`). Khi đề không nói, ghi chú mặc
|
|
28
|
+
định này cạnh handler:
|
|
29
|
+
|
|
30
|
+
- Bấm lần một: **cột chữ tăng dần** (A → Z), **cột số và ngày giảm dần** (lớn nhất, mới nhất
|
|
31
|
+
trước: người bấm "Doanh thu" muốn xem ai cao nhất). Đây cũng là mặc định của TanStack Table.
|
|
32
|
+
- Bấm lần hai đảo chiều, **lần ba về chưa sắp** (thứ tự dữ liệu trả về). Không có lần ba thì
|
|
33
|
+
"chưa sắp xếp" chỉ thấy lúc mới vào trang, không quay lại được.
|
|
34
|
+
- Bấm cột khác: cột cũ về chưa sắp, cột mới bắt đầu lại từ lần một.
|
|
35
|
+
|
|
36
|
+
## Ô và hover
|
|
37
|
+
|
|
38
|
+
- **Nút phủ kín ô, padding ngang nằm trong nút**, bằng đúng padding của ô dữ liệu cùng cột:
|
|
39
|
+
chữ tiêu đề thẳng cột với dữ liệu mà không kéo lề bằng số âm (`N11`). `h-11`, `text-xs
|
|
40
|
+
font-medium`, `whitespace-nowrap`, `cursor-pointer`.
|
|
41
|
+
- **Không nền hover. Rê vào thì chữ và mũi tên đậm lên** (`hover:text-foreground`). Ngoại lệ
|
|
42
|
+
của `I10`, cùng lý do với accordion (`accordion.md`). Đừng tô `--surface-hover` lúc rê:
|
|
43
|
+
nút phủ ô thành một mảng `#f8f8fa` rộng 505px cho một nhãn 12px, và ô cột đầu,
|
|
44
|
+
cột cuối chạm mép card, sát màu nền trang `#f4f4f6`, card như bị khoét một góc. Quy ước chia đôi:
|
|
45
|
+
có hệ thiết kế doanh nghiệp tô nền cả ô, có hệ chỉ hiện mũi tên và đậm chữ; skill
|
|
46
|
+
chọn không nền vì ca ở trên.
|
|
47
|
+
|
|
48
|
+
```tsx
|
|
49
|
+
<th scope="col" aria-sort={direction ?? undefined} className="p-0">
|
|
50
|
+
<Button
|
|
51
|
+
variant="ghost"
|
|
52
|
+
onClick={onSort}
|
|
53
|
+
className={cn(
|
|
54
|
+
"h-11 w-full justify-start gap-1 rounded-none px-4 py-0 text-xs whitespace-nowrap",
|
|
55
|
+
// Không nền hover: ô cột đầu, cột cuối chạm mép card, --surface-hover sát màu nền trang.
|
|
56
|
+
"text-muted hover:bg-transparent hover:text-foreground",
|
|
57
|
+
align === "right" && "flex-row-reverse",
|
|
58
|
+
isSorted && "text-foreground",
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
{label}
|
|
62
|
+
<SortIcon direction={direction} />
|
|
63
|
+
</Button>
|
|
64
|
+
</th>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Màn hẹp
|
|
68
|
+
|
|
69
|
+
- **Dưới `sm` bảng quản lý thành danh sách dòng** (`../layouts/app.md`), hàng tiêu đề mất theo,
|
|
70
|
+
nên **sắp xếp chuyển thành một nút dropdown** cạnh nút Lọc: nút viền `h-10`, nhãn
|
|
71
|
+
"Sắp xếp:" `text-muted` rồi lựa chọn đang dùng ("Sắp xếp: Doanh thu cao nhất"), `ChevronDown`,
|
|
72
|
+
`w-fit`, cùng khuôn nút "Trạng thái:" của bảng. Mở ra là danh sách mỗi mục một **cột + chiều
|
|
73
|
+
nói bằng chữ**: "Tên A → Z", "Tên Z → A", "Mới nhất", "Cũ nhất", "Doanh thu cao nhất", "Doanh
|
|
74
|
+
thu thấp nhất", cộng "Mặc định" cho chưa sắp; mục đang chọn có dấu check (khuôn Select ở
|
|
75
|
+
`choice-controls.md`). Chữ thay mũi tên vì không còn tên cột đứng cạnh để mũi tên bám vào.
|
|
76
|
+
- **Bảng vẫn giữ dạng bảng ở màn hẹp thì mọi cột sắp được phải thấy mà không cuộn.** Bảng
|
|
77
|
+
ba cột `min-w-[28rem]` cuộn ngang trong khung 341px thì cột Doanh thu nằm hẳn ngoài khung:
|
|
78
|
+
sắp "Doanh thu tăng dần" ở 375px mà không thấy cột doanh thu nào.
|
|
79
|
+
Không vừa thì theo gạch trên, đừng để tiêu đề sắp xếp trôi khỏi màn.
|
|
80
|
+
|
|
81
|
+
## Kiểm
|
|
82
|
+
|
|
83
|
+
- Rê lên tiêu đề cột đầu và cột cuối ở 1280px: có mảng nền nào chạm mép card không?
|
|
84
|
+
- Mép trái chữ tiêu đề bằng mép trái chữ dữ liệu cột đó; cột số: mép phải chữ tiêu đề bằng mép
|
|
85
|
+
phải con số (đo bằng `Range`).
|
|
86
|
+
- Ở 375px: còn thấy tiêu đề (hoặc nút "Sắp xếp:") cho từng cột sắp được không?
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Ô nhập nhiều tag (tag input)
|
|
2
|
+
|
|
3
|
+
Gõ một giá trị rồi Enter thành một thẻ: email người nhận, nhãn, người được gán.
|
|
4
|
+
Ô theo `input.md`, thẻ là pill như badge (`M7`, `F1`), câu lỗi theo
|
|
5
|
+
`layouts/form.md`.
|
|
6
|
+
|
|
7
|
+
- **Enter và dấu phẩy đều thành thẻ.** Dán cả danh sách thì tách theo dấu phẩy. Backspace lúc **chưa gõ chữ nào** thì bỏ thẻ cuối.
|
|
8
|
+
- **Giá trị sai khuôn vẫn được thêm thành thẻ**, không chặn lúc gõ. Dán 30 email mà chặn ngay thì người dùng mất dòng và không biết mất dòng nào. Thẻ sai đổi sang tông đỏ kèm icon, sửa bằng cách bỏ thẻ rồi gõ lại.
|
|
9
|
+
- **Lỗi ở thẻ nào thì đỏ ở thẻ đó, viền ô giữ nguyên.** Chỉ lỗi của **cả ô** (chưa thêm được ai) mới viền đỏ cả ô như mọi ô nhập khác. Hai phạm vi lỗi, hai cách hiện, không lẫn (`N3`).
|
|
10
|
+
- **Câu lỗi nói đúng thứ code đang kiểm**, không bịa thêm luật (`N6`): hàm kiểm "có `@` và tên miền có dấu chấm" thì câu phải là "cần có dấu @ và tên miền", **không** phải "phải có đuôi .com" — `@saoviet.vn` là hợp lệ.
|
|
11
|
+
- **Giá trị dài xuống dòng trong thẻ** (`wrap-anywhere`), không `truncate`, không tooltip: email là thứ người dùng phải đọc đủ mới biết gửi đúng ai (`N8`). Thẻ hết chỗ thì xuống dòng và ô cao theo.
|
|
12
|
+
- **Nút bỏ trên mỗi thẻ có `aria-label` kèm giá trị** ("Bỏ ngoc.tran@saoviet.vn"), không chỉ "Bỏ".
|
|
13
|
+
- **Placeholder là câu hướng dẫn** (`T25`: "Nhập email người nhận"), không dùng ví dụ giả `ten@congty.com`. Dòng gợi ý dưới ô nói cách thêm ("Gõ xong bấm Enter để thêm người nhận") — đây là thứ người dùng chưa biết trước khi gõ, không phải chữ thừa (`T20`).
|
|
14
|
+
- **Khoá**: ô chìm xuống nền trang, **thẻ giữ nguyên màu để còn đọc được**, chỉ nút bỏ tắt, và nói lý do ngay dưới ("Chỉ người tạo chiến dịch sửa được danh sách này") (`N6`).
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Dòng thời gian hoạt động (timeline)
|
|
2
|
+
|
|
3
|
+
Lịch sử của một đối tượng: đơn hàng, hồ sơ, ticket. Nằm trong card (`card.md`).
|
|
4
|
+
**Không dùng cho "Hoạt động gần đây" của cả workspace** (nhiều người, nhiều dự án):
|
|
5
|
+
ở đó câu hỏi là ai vừa đụng gì, khuôn là avatar + một câu, xem `layouts/app.md`
|
|
6
|
+
mục Dashboard.
|
|
7
|
+
Vòng và đường nối mượn thanh các bước (`layouts/form.md`), chữ mượn dòng danh
|
|
8
|
+
sách (`list-row.md`). Mượn là chép class, không chỉ chép dáng (`N5`).
|
|
9
|
+
|
|
10
|
+
```html
|
|
11
|
+
<ol class="text-sm">
|
|
12
|
+
<!-- MỚI NHẤT Ở TRÊN. Hàng cuối (cũ nhất) không có đường nối -->
|
|
13
|
+
<li class="relative flex gap-3 pb-6 last:pb-0">
|
|
14
|
+
<span class="absolute top-10 bottom-1 left-[15px] w-0.5 bg-border" aria-hidden="true"></span>
|
|
15
|
+
<span class="grid size-8 shrink-0 place-items-center rounded-full bg-red-50 text-red-600">
|
|
16
|
+
<!-- icon lucide size-4, riêng cho từng loại việc -->
|
|
17
|
+
</span>
|
|
18
|
+
<div class="min-w-0 flex-1 pt-1.5"><!-- dòng đầu 20px nằm giữa vòng 32px -->
|
|
19
|
+
<div class="flex flex-col gap-0.5 sm:flex-row sm:items-baseline sm:justify-between sm:gap-4">
|
|
20
|
+
<p class="font-medium text-foreground">Giao hàng thất bại</p>
|
|
21
|
+
<time class="shrink-0 text-xs whitespace-nowrap text-muted tabular-nums">11:47 · 19/09</time>
|
|
22
|
+
</div>
|
|
23
|
+
<p class="mt-1 max-w-[55ch] text-pretty text-muted">Shipper gọi 3 lần không liên lạc được người nhận…</p>
|
|
24
|
+
<p class="mt-1 text-xs text-muted">Lê Văn Phúc · Vận chuyển Sao Việt</p>
|
|
25
|
+
</div>
|
|
26
|
+
</li>
|
|
27
|
+
</ol>
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- **Mới nhất ở trên.** Người mở lịch sử muốn biết "giờ đang thế nào"; để cũ trên thì với lịch sử dài, việc vừa thất bại nằm tận đáy phải cuộn mới thấy. Người dùng muốn đọc xuôi thì đảo mảng, component không tự sắp xếp.
|
|
31
|
+
- **Vòng `size-8`, icon `size-4`, màu lấy từ bảng badge trạng thái** (`M7`, `N4`): việc thường `bg-zinc-100 text-zinc-600`, thất bại `bg-red-50 text-red-600`, xong hẳn `bg-emerald-50 text-emerald-700`. **Việc lặp lại thường ngày thì xám, kể cả khi nó là "xong"**: trong lịch sử khách hàng, mỗi đơn giao xong là việc bình thường, tô xanh cả 24 đơn thành một cột vòng xanh, mất tác dụng báo hiệu (`N3`). Xanh chỉ cho mốc kết thúc của chính đối tượng (đơn hàng đã giao trong lịch sử của đơn đó). Chỉ tô vòng; **nhãn luôn `text-foreground`**, kể cả việc thất bại (`N3`). Icon riêng cho từng loại việc, màu không đứng một mình.
|
|
32
|
+
- **Dòng đầu nằm giữa tâm vòng**: khối chữ `pt-1.5` (vòng 32px, dòng `text-sm` 20px). Không có thì nhãn bám mép trên vòng, lệch so với thanh các bước.
|
|
33
|
+
- **Hai cỡ chữ**: nhãn và mô tả `text-sm`, giờ và người làm `text-xs`. Không `text-base`.
|
|
34
|
+
- Mô tả `max-w-[55ch] text-pretty` (`T10`, `T11`): card rộng thì câu dài vẫn không chạy quá 75 ký tự và không chui xuống dưới cột giờ. Không `max-w-prose` (65ch ≈ 90 ký tự chữ Việt), lý do ở `T11`.
|
|
35
|
+
- Giờ `tabular-nums whitespace-nowrap` (`T16`), bám mép phải từ `sm`, màn hẹp xuống dưới nhãn. Mã vận đơn, mã đơn trong mô tả bọc `font-mono` (`T17`).
|
|
36
|
+
- Đường nối `w-0.5 bg-border`, cùng độ dày với thanh các bước (`N5`); `w-px` mờ gần như mất trên nền trắng. Hở một khoảng trên dưới, không chạm vòng. Chỉ một việc thì không có đường nối.
|
|
37
|
+
- **Lịch sử dài thì hiện khoảng 10 việc gần nhất, cuối danh sách là nút `ghost` "Xem hoạt động cũ hơn"** (`I7`, không icon), **chữ** trên nút thẳng cột với chữ của các hàng: `ml-8` (vòng 32px + `gap-3` = 44px, trừ `px-3` của nút ghost). Bấm thì nối thêm 10 việc ngay bên dưới, không phân trang, không mở trang khác; đang tải thì spinner đè giữa nút (`button.md`). Hết việc thì bỏ nút: hàng cũ nhất ("Tạo khách hàng", "Tạo đơn") tự là điểm kết. Không dựng cả trăm hàng một lượt, và không cắt cụt ở 5 hàng rồi nhảy thẳng về mốc tạo: người đọc thấy ô số ghi 24 đơn mà lịch sử có một đơn.
|
|
38
|
+
- Hàng không bấm được thì không hover. Bấm được (mở chi tiết) thì theo hover của `list-row.md`.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Cây thư mục (tree)
|
|
2
|
+
|
|
3
|
+
Danh sách lồng nhau mở đóng được: thư mục tài liệu, cây trang, cây danh mục.
|
|
4
|
+
Mượn khuôn **menu con của sidebar** (`layouts/app.md`): cùng chiều cao hàng, cùng
|
|
5
|
+
nền hover, cùng đường dọc, cùng cách báo mục đang chọn (`N5`).
|
|
6
|
+
|
|
7
|
+
```html
|
|
8
|
+
<ul class="space-y-0.5 text-sm"><!-- 2px giữa các hàng, xem list-row.md -->
|
|
9
|
+
<li>
|
|
10
|
+
<button type="button" class="flex h-10 w-full cursor-pointer items-center gap-2 rounded-xl px-2 outline-hidden hover:bg-item-hover" aria-expanded="true">
|
|
11
|
+
<!-- h-10 rounded-xl: cao bằng link sidebar, bo theo F1. ChevronDown size-4 text-muted, xoay -rotate-90 khi đóng. Hàng file: một ô size-4 trống giữ chỗ.
|
|
12
|
+
Hàng đang chọn: bỏ hover, thêm bg-secondary font-medium, aria-current="page" -->
|
|
13
|
+
<!-- FolderOpen / Folder / FileText size-4 shrink-0 -->
|
|
14
|
+
<span class="min-w-0 truncate">Khách hàng doanh nghiệp</span>
|
|
15
|
+
</button>
|
|
16
|
+
<ul class="ml-4 space-y-0.5 border-l border-border-strong pl-2"><!-- đường dọc chạy ở tâm icon cha -->
|
|
17
|
+
…
|
|
18
|
+
</ul>
|
|
19
|
+
</li>
|
|
20
|
+
</ul>
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- **Hàng đang chọn**: nền `bg-secondary` + `font-medium` + **đoạn đường dọc của nó đậm lên `--foreground`**, y như menu con của sidebar. Không tô màu nhấn, không viền. Đoạn đậm là `before:absolute before:inset-y-0 before:w-px before:bg-foreground` trên hàng, lùi sang trái đúng `pl` của danh sách cộng 1px viền (`before:-left-[9px]` với `pl-2`); số âm giữ có chủ ý (`N11` bước 4), comment ngay trên dòng. Cách không âm là mỗi `<li>` tự `border-l` và đoạn đang chọn đổi màu viền, nhưng khe `space-y-0.5` giữa các hàng làm đường dọc đứt từng quãng 2px, còn đổi khe sang `pt-0.5` thì đoạn đậm dài hơn nền hàng 2px.
|
|
24
|
+
- **Rê `hover:bg-item-hover`, đang chọn đậm hơn một bậc `bg-secondary`**, như link sidebar (`I10`; chủ dự án chốt). Cây không có checkbox, nền là dấu chọn duy nhất: rê ra đúng nền đang chọn thì rê qua hàng nào cũng trông như vừa chọn nó. "Rê và đã chọn cùng một nền mờ" chỉ còn cho dòng bảng tick checkbox. Hai hàng cạnh nhau cùng sáng nền (một đang chọn, một đang rê) vẫn xảy ra, nên danh sách **phải có `space-y-0.5`**, nếu không hai hàng dính thành một khối.
|
|
25
|
+
- **Chevron chỉ ở hàng mở được.** Hàng file vẫn chừa đúng một ô `size-4` để chữ thẳng cột với hàng thư mục. Icon thư mục đổi theo trạng thái: `FolderOpen` khi mở, `Folder` khi đóng. Icon file lấy theo đuôi từ **bảng chung trong `file-upload.md`** (cùng dáng tờ giấy, một màu).
|
|
26
|
+
- **Thư mục rỗng có một hàng chữ xám "Thư mục trống"**, `text-muted`, **thẳng mép chữ của hàng con cùng cấp** — không thụt ít hơn, không có icon. Không có hàng này thì mở thư mục ra chẳng thấy gì đổi, người dùng tưởng bấm hụt (`N2`).
|
|
27
|
+
- **Tên dài cắt giữa, giữ vài ký tự cuối của tên cộng đuôi file** ("bao-cao…quy-3.xlsx", không "bao-cao-doanh….xlsx" bốn chấm), dấu `…` dính liền (`T14`). Tooltip tên đầy đủ **chỉ gắn khi tên thật sự bị cắt** (so `scrollWidth` với `clientWidth`) và không che hàng kế (`N8`). **Tooltip tên dài được xuống dòng, không `whitespace-nowrap`**: `max-w-[min(20rem,calc(100vw-1rem))] whitespace-normal wrap-anywhere`, và `side="right"` hết chỗ thì lật xuống dưới hàng. Tooltip `nowrap` với tên 90 ký tự ở 375px rộng 765px, tràn khỏi màn gần 400px, cắt mất đúng nửa tên nó sinh ra để hiện.
|
|
28
|
+
- **Mỗi tầng thụt `ml-4`**, đường dọc `border-l border-border-strong` chạy ở tâm icon của hàng cha. Cột hẹp cỡ sidebar (288px) vẫn phải lồng được bốn tầng; sâu hơn thì thụt ít lại chứ không cho cuộn ngang (`R1`).
|
|
29
|
+
- **Mở/đóng trượt bằng `grid-rows` 0fr ↔ 1fr**, phần đang đóng gắn `inert`; không render có điều kiện, không `hidden`, không `<details>` (`I30`, mẫu ở `accordion.md`).
|
|
30
|
+
- Bàn phím (`N9`): mặc định là **nút mở đóng thường** — cả cây là `<ul>` lồng, hàng mở đóng là `<button aria-expanded>`, đi bằng Tab, Enter/Space mở đóng. Muốn điều hướng bằng mũi tên thì phải dựng **đủ mẫu tree của WAI-ARIA** (`role="tree"`, `treeitem`, `group`, `aria-level`, `aria-selected`, roving tabindex), đừng gắn phím mũi tên lên nút thường: trình đọc màn hình không báo mô hình đó.
|