@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,369 @@
|
|
|
1
|
+
# Checkbox, radio, công tắc, select
|
|
2
|
+
|
|
3
|
+
Bốn ô **chọn** (khác ô gõ ở `input.md`). Dự án đã có component thì dùng của họ,
|
|
4
|
+
chỉ chỉnh cho khớp cỡ, màu và focus dưới đây.
|
|
5
|
+
|
|
6
|
+
Chủ dự án chốt. Mọi con số dưới là **mặc định**: dự án có nhịp riêng
|
|
7
|
+
(form dày, bảng dày) thì đổi cỡ theo dự án, nhưng giữ tỉ lệ và giữ đủ trạng thái.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Cỡ
|
|
12
|
+
|
|
13
|
+
| | Mặc định | Nhỏ | Nhỏ dùng khi |
|
|
14
|
+
| --- | --- | --- | --- |
|
|
15
|
+
| Checkbox | `size-5` 20px, bo `rounded-md` 6px, tick `size-3.5` | `size-4` 16px, bo `rounded` 4px, tick `size-3` | Trong bảng, trong menu, dòng `text-xs` |
|
|
16
|
+
| Radio | `size-5` 20px | `size-4` 16px | Như trên |
|
|
17
|
+
| Công tắc | track `h-6 w-11` (24×44), núm `size-5`, chạy `translate-x-5` | track `h-5 w-9` (20×36), núm `size-4`, chạy `translate-x-4` | Dòng dày, card nhỏ |
|
|
18
|
+
| Select | `h-11 md:h-10`, y như ô nhập | theo ô nhập của dự án | |
|
|
19
|
+
|
|
20
|
+
Công thức công tắc: `rộng track = 2 × núm + 4px` (đệm `p-0.5` hai bên), khoảng
|
|
21
|
+
chạy = cỡ núm. Đổi cỡ thì giữ công thức đó, núm không chạm mép track.
|
|
22
|
+
|
|
23
|
+
16px là cỡ **nhỏ**, không phải mặc định: cạnh chữ `text-sm` thì ô 16px trông
|
|
24
|
+
teo, và vùng bấm quá nhỏ trên điện thoại. Vùng bấm thật là cả nhãn (`I26`), nên
|
|
25
|
+
luôn bọc ô trong `<label>`.
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Viền ô chưa chọn
|
|
30
|
+
|
|
31
|
+
Ô chưa chọn dùng **`border-[1.5px] border-border-strong`**, cùng token với viền ô
|
|
32
|
+
nhập (`M14`), để cả form một độ đậm viền. Rê vào thì viền đậm lên, **chỉ khi chưa chọn**:
|
|
33
|
+
`not-checked:hover:border-foreground` (checkbox thêm `not-indeterminate:`). Viết trơn
|
|
34
|
+
`hover:border-foreground` thì trong CSS của Tailwind v4 `hover:` đứng sau `checked:`, nên rê vào ô
|
|
35
|
+
đã chọn là viền màu chữ đè viền màu nhấn: radio đang chọn đổi sang xám, checkbox đỏ viền đen.
|
|
36
|
+
Token của skill có màu nhấn gần đen trùng màu chữ nên không thấy; dự án màu nhấn đỏ thì lộ ngay.
|
|
37
|
+
|
|
38
|
+
Viền này chỉ ~1.2:1 với nền trắng, chưa đạt WCAG 1.4.11. Đã thử `--muted` (4.95:1),
|
|
39
|
+
chủ dự án thấy **đậm và xấu**, trả về. Đánh đổi có chủ ý như viền
|
|
40
|
+
ô nhập, xem `P3` trong `styles.md`. Bù lại: radio luôn đi kèm nhãn, nhóm radio
|
|
41
|
+
luôn có sẵn một lựa chọn đã tô đặc, nên người dùng vẫn đọc ra đây là nhóm lựa
|
|
42
|
+
chọn.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## Kiểu, chọn theo chỗ đứng
|
|
47
|
+
|
|
48
|
+
**Ô** có hai kiểu tô:
|
|
49
|
+
|
|
50
|
+
| Kiểu | Checkbox đã chọn | Radio đã chọn | Hợp khi |
|
|
51
|
+
| --- | --- | --- | --- |
|
|
52
|
+
| `filled` (mặc định) | nền nhấn, tick `--primary-foreground` | nền nhấn đặc, chấm trắng ở giữa | Mọi chỗ |
|
|
53
|
+
| `outline` | viền nhấn, nền trắng, tick màu nhấn | vòng nhấn, khe trắng, chấm nhấn | Trang đã nhiều khối tô đặc, cần nhẹ tay (`M2`) |
|
|
54
|
+
|
|
55
|
+
Một app chọn **một** kiểu tô, dùng khắp nơi (`D1`).
|
|
56
|
+
|
|
57
|
+
**Bố cục** có bốn loại:
|
|
58
|
+
|
|
59
|
+
| Bố cục | Hình | Dùng khi |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| Hàng | ô + nhãn một dòng | Lựa chọn tự giải thích: "Nhớ đăng nhập" |
|
|
62
|
+
| Hàng có mô tả | ô + nhãn đậm + một câu phụ `text-muted` | Lựa chọn cần một câu hệ quả |
|
|
63
|
+
| Card | cả khung viền là vùng bấm, ô ở góc trái trên | 2–4 lựa chọn **quan trọng**, mỗi cái có hệ quả khác nhau: quyền đăng bài, gói cước, phương thức giao hàng |
|
|
64
|
+
| Card có icon | như card, thêm icon lucide trong ô vuông `size-10 rounded-lg bg-background` bên trái chữ; card có nền rê thì nền rê không được là `bg-background` (`I32`) | Như card, khi các lựa chọn khác nhau về **loại**: giao tiêu chuẩn / giao nhanh / nhận tại cửa hàng |
|
|
65
|
+
|
|
66
|
+
Công tắc có thêm bố cục **hàng cài đặt**: nhãn + mô tả bên trái, công tắc căn
|
|
67
|
+
phải, xem cuối file.
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## Checkbox
|
|
72
|
+
|
|
73
|
+
```html
|
|
74
|
+
<label class="inline-flex w-fit cursor-pointer items-center gap-3 text-sm">
|
|
75
|
+
<span class="relative inline-flex shrink-0">
|
|
76
|
+
<input type="checkbox" class="peer size-5 cursor-pointer appearance-none rounded-md border-[1.5px] border-border-strong bg-surface outline-hidden transition-colors not-checked:not-indeterminate:hover:border-foreground checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary disabled:cursor-not-allowed disabled:opacity-50" />
|
|
77
|
+
<i data-lucide="check" class="pointer-events-none absolute inset-0 m-auto size-3.5 stroke-[3] text-primary-foreground opacity-0 peer-checked:opacity-100"></i>
|
|
78
|
+
<i data-lucide="minus" class="pointer-events-none absolute inset-0 m-auto size-3.5 stroke-[3] text-primary-foreground opacity-0 peer-indeterminate:opacity-100"></i>
|
|
79
|
+
</span>
|
|
80
|
+
Nhận email thông báo
|
|
81
|
+
</label>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- **Dựng trên `<input type="checkbox">` thật** + `appearance-none`, không dựng bằng `<div>`. Phím Space, form submit, trình đọc màn hình đều có sẵn.
|
|
85
|
+
- `outline`: đổi `checked:bg-primary` thành `checked:bg-surface`, icon `text-primary`.
|
|
86
|
+
- **Ba trạng thái** (checkbox tiêu đề bảng: không / một phần / tất cả) đặt bằng JS `input.indeterminate = true`, không có thuộc tính HTML. Icon `minus` thay cho `check`.
|
|
87
|
+
- Lỗi (bắt buộc tick mà chưa tick): `border-red-500`, câu lỗi dưới nhãn theo `input.md`.
|
|
88
|
+
- Tick "Đồng ý điều khoản" **không bao giờ tick sẵn**.
|
|
89
|
+
|
|
90
|
+
## Radio
|
|
91
|
+
|
|
92
|
+
```html
|
|
93
|
+
<!-- filled: viền dày 6px màu nhấn, lõi trắng 8px chính là chấm -->
|
|
94
|
+
<input type="radio" name="shipping" class="size-5 cursor-pointer appearance-none rounded-full border-[1.5px] border-border-strong bg-surface outline-hidden transition-[border-color,border-width] not-checked:hover:border-foreground checked:border-[6px] checked:border-primary disabled:cursor-not-allowed disabled:opacity-50" />
|
|
95
|
+
|
|
96
|
+
<!-- outline: nền chỉ tô phần lõi (bg-clip-content), padding 3px là khe trắng -->
|
|
97
|
+
<input type="radio" name="shipping" class="size-5 cursor-pointer appearance-none rounded-full border-[1.5px] border-border-strong bg-clip-content p-[3px] outline-hidden not-checked:hover:border-foreground checked:border-2 checked:border-primary checked:bg-primary disabled:cursor-not-allowed disabled:opacity-50" />
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Cả hai không cần phần tử phụ, không cần icon. Cỡ nhỏ `size-4`: `filled` dùng
|
|
101
|
+
`checked:border-[5px]`, `outline` dùng `p-[2px]`.
|
|
102
|
+
|
|
103
|
+
- **Nhóm radio luôn có sẵn một lựa chọn** (thường là cái phổ biến nhất). Radio không bỏ chọn được, nên nhóm trắng từ đầu là người dùng không quay lại được trạng thái "chưa chọn". Ngoại lệ: chọn sai gây hậu quả tiền bạc (gói trả phí) thì để trống và bắt chọn.
|
|
104
|
+
- Bọc nhóm trong `<fieldset>` + `<legend>` là câu hỏi. Không có legend thì trình đọc màn hình đọc "Giao tiêu chuẩn, nút radio" mà không biết đang hỏi gì.
|
|
105
|
+
- Từ 5 lựa chọn trở lên thì dùng select.
|
|
106
|
+
- **Xếp nhóm: một hàng ngang hoặc một cột dọc, không lưới hai cột.** Từ `sm` nhãn ngắn vừa một hàng thì `flex gap-6`; dưới `sm` thì dọc. Lưới 2×2 đọc thành chữ Z, thang có thứ tự như mức ưu tiên ra "Thấp, Trung bình / Cao, Khẩn cấp", mắt phải nhảy về đầu hàng mới biết "Cao" đứng sau "Trung bình" (`R6`).
|
|
107
|
+
- **Dưới `sm` mỗi lựa chọn là một dòng cao 44px bấm được cả dòng**: `<label>` bọc cả ô lẫn chữ, `flex w-fit min-h-11 items-center gap-3` (vẫn đúng `I26`: rộng bằng ô + chữ, không kéo hết hàng). Ô 20px với nhãn cao 20px thì vùng bấm chỉ 20px, hai hàng cách 12px thì ngón tay chạm khe giữa là trượt (probe báo 20×20 ở cùng form).
|
|
108
|
+
|
|
109
|
+
## Card chọn
|
|
110
|
+
|
|
111
|
+
Đang chọn: **viền `--border-focus` + ring `--ring-focus`**, y như ô nhập đang
|
|
112
|
+
focus. Chỉ viền mà không ring thì hai card cạnh nhau khó thấy cái nào đang
|
|
113
|
+
chọn; chỉ tô nền thì cả card đổi màu, quá nặng.
|
|
114
|
+
|
|
115
|
+
```html
|
|
116
|
+
<fieldset class="space-y-3">
|
|
117
|
+
<legend class="mb-3 text-sm font-medium">Ai được đăng bài</legend>
|
|
118
|
+
|
|
119
|
+
<label class="flex cursor-pointer items-start gap-3 rounded-xl border border-border-strong bg-surface p-4 transition-colors hover:bg-surface-hover has-checked:border-focus has-checked:ring-2 has-checked:ring-focus">
|
|
120
|
+
<input type="radio" name="post" checked class="mt-0.5 size-5 shrink-0 cursor-pointer appearance-none rounded-full border-[1.5px] border-border-strong bg-surface outline-hidden checked:border-[6px] checked:border-primary" />
|
|
121
|
+
<span class="min-w-0">
|
|
122
|
+
<span class="block text-sm font-medium">Mọi thành viên</span>
|
|
123
|
+
<span class="mt-1 block text-sm text-muted">Ai đã tham gia cộng đồng đều đăng được bài.</span>
|
|
124
|
+
</span>
|
|
125
|
+
</label>
|
|
126
|
+
<!-- card thứ hai y hệt, không có `checked` -->
|
|
127
|
+
</fieldset>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
- **Tab tới không thêm gì** (`I13`): không vòng quanh card, không vòng quanh ô tròn.
|
|
131
|
+
- `has-checked:` là Tailwind v4. Tailwind v3.4 viết `has-[:checked]:`.
|
|
132
|
+
- `mt-0.5` để ô 20px thẳng hàng với dòng đầu `text-sm`, không căn giữa cả card.
|
|
133
|
+
- Card chưa chọn viền `--border-strong` như ô nhập.
|
|
134
|
+
- Checkbox dạng card dùng y hệt, đổi `type="checkbox"`.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Công tắc
|
|
139
|
+
|
|
140
|
+
Công tắc = **có hiệu lực ngay**, không chờ nút Lưu (`layouts/app.md`, trang cài
|
|
141
|
+
đặt). Cần bấm Lưu mới có hiệu lực thì dùng checkbox.
|
|
142
|
+
|
|
143
|
+
```html
|
|
144
|
+
<button type="button" role="switch" aria-checked="false" aria-labelledby="notify-label"
|
|
145
|
+
class="group relative inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full bg-muted/40 p-0.5 outline-hidden before:absolute before:-inset-2 transition-colors hover:bg-muted/60 aria-checked:bg-primary aria-checked:hover:bg-primary-hover disabled:cursor-not-allowed disabled:opacity-50">
|
|
146
|
+
<span class="size-5 rounded-full bg-surface shadow-sm transition-transform group-aria-checked:translate-x-5 group-aria-checked:bg-primary-foreground motion-reduce:transition-none"></span>
|
|
147
|
+
</button>
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
- `role="switch"` + `aria-checked`, JS đảo `aria-checked` khi bấm. Có nhãn qua `aria-labelledby`.
|
|
151
|
+
- **Vùng bấm nới ra 40px mà hình giữ 24px** (`N9`): `relative before:absolute before:-inset-2` (số âm buộc phải giữ theo `N11`, như nút sao chép ở `description-list.md`). Track 44×24 trơn trên màn chạm thấp hơn mức 32px; to track lên `h-8` thì công tắc nặng hơn chữ nhãn cạnh nó. Hai hàng cài đặt `py-4` cách nhau đủ xa nên vùng 40px không chồng nhau.
|
|
152
|
+
- Track tắt `bg-muted/40`, không dùng `--background-hover`: track xám quá nhạt trên card trắng thì trông như công tắc bị khoá (`I8`).
|
|
153
|
+
- **Núm trượt là ngoại lệ của `I12`** (chỉ đổi màu khi chuyển trạng thái): vị trí núm là thông tin, nhảy cụp một cái thì mắt không kịp thấy đã đổi. Kèm `motion-reduce:transition-none`.
|
|
154
|
+
- `shadow-sm` trên núm là ngoại lệ có tên của `M15`, như ô nổi của tab segmented. Không đổ bóng track.
|
|
155
|
+
- Không ghi chữ "Bật / Tắt" trong track. Cần chữ thì đặt cạnh, và chữ đó mô tả **việc**, không mô tả trạng thái.
|
|
156
|
+
|
|
157
|
+
**Hàng cài đặt:**
|
|
158
|
+
|
|
159
|
+
```html
|
|
160
|
+
<div class="flex items-center justify-between gap-4 py-4">
|
|
161
|
+
<div class="min-w-0">
|
|
162
|
+
<p id="notify-label" class="text-sm font-medium">Thông báo qua email</p>
|
|
163
|
+
<p class="mt-1 text-sm text-muted">Gửi khi có người nhắc tới bạn.</p>
|
|
164
|
+
</div>
|
|
165
|
+
<!-- công tắc ở trên -->
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
- Chỉ một dòng dưới nhãn: có lỗi thì câu lỗi `text-red-600` thay chỗ mô tả, khoá thì câu lý do (không mờ theo ô) thay chỗ mô tả. Cả hai **cùng `text-sm` với mô tả**, để hàng không co lại và không lạc cỡ giữa các hàng bên cạnh.
|
|
170
|
+
|
|
171
|
+
- **Công tắc chỉ cho việc gạt là xong**, đổi lại được ngay, không qua bước nào nữa.
|
|
172
|
+
Bật lên mà phải quét mã, nhập mật khẩu, thanh toán hay xác nhận thì là **nút mở
|
|
173
|
+
luồng**, cạnh một dòng trạng thái ("Chưa bật" / "Bật từ 12/06/2026"). Công tắc mở
|
|
174
|
+
modal thì bấm Huỷ giữa chừng làm công tắc bật lên rồi bật về, và dấu "Đã lưu" hiện
|
|
175
|
+
lúc chưa có gì được lưu. Xem "Trang bảo mật" ở `../layouts/app.md`.
|
|
176
|
+
|
|
177
|
+
---
|
|
178
|
+
|
|
179
|
+
## Select
|
|
180
|
+
|
|
181
|
+
**Nút mở trông y như ô nhập**: cùng cao, cùng viền, cùng bo. Đang mở thì viền + ring
|
|
182
|
+
như ô nhập đang focus, vì người dùng vẫn đang "ở trong" ô đó. **Focus thì `focus-visible:`,
|
|
183
|
+
không `focus:`**: chọn bằng chuột xong, focus trả về nút, `focus:` làm nút giữ nguyên dáng
|
|
184
|
+
đang mở sau khi danh sách đã đóng (`rules-state.md`, dưới bảng focus).
|
|
185
|
+
Ô chọn ngày, ô chọn giờ cùng luật.
|
|
186
|
+
|
|
187
|
+
```html
|
|
188
|
+
<button type="button" role="combobox" aria-haspopup="listbox" aria-expanded="false"
|
|
189
|
+
class="group flex h-11 w-full md:h-10 cursor-pointer items-center justify-between gap-2 rounded-xl border border-border-strong bg-surface px-4 text-left text-base outline-hidden transition-colors md:text-sm focus-visible:border-focus aria-expanded:border-focus aria-expanded:ring-2 aria-expanded:ring-focus">
|
|
190
|
+
<span class="truncate">An Giang</span>
|
|
191
|
+
<i data-lucide="chevron-down" class="size-4 shrink-0 text-muted transition-transform group-aria-expanded:rotate-180"></i>
|
|
192
|
+
</button>
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
- Chưa chọn thì chữ là placeholder `text-muted`, viết theo `T25`: "Chọn tỉnh, thành phố".
|
|
196
|
+
- Radix / shadcn: thay `aria-expanded:` bằng `data-[state=open]:`.
|
|
197
|
+
- **Nút lọc dạng dropdown ngoài form** (co theo chữ, `w-fit`) cũng mượn đúng trạng thái mở này: viền `border-focus` + `ring-2`, nền giữ trắng. **Không có hover**, như ô Select trong form: mũi tên + `cursor-pointer` đã đủ báo bấm được (`button.md`). Đừng mượn hover của nút viền: đã thử tô nền xám (tan vào nền trang, kể cả `--button-hover` `#f1f1f3` trên nền trang `#f4f4f6` chỉ đậm hơn 3 mức) rồi viền đậm lên, chủ dự án chốt bỏ hẳn.
|
|
198
|
+
- Lỗi: y như ô nhập, `border-red-500`, quầng `ring-red-500/10` chỉ khi đang focus.
|
|
199
|
+
|
|
200
|
+
**Danh sách mở ra** theo khung dropdown ở `layouts/overlay.md` (`rounded-2xl`,
|
|
201
|
+
`p-1`, mục `h-10 rounded-xl`, portal ra `body` theo `I22`):
|
|
202
|
+
|
|
203
|
+
- Rộng **bằng nút mở** (Radix: `w-(--radix-select-trigger-width)`), cách nút `mt-2`. Nội dung bên trong (ô tìm, danh sách) rộng hết khung, **không chặn `max-w`** trên nó: khung đã rộng bằng nút mà nội dung dừng ở `24rem` thì bên phải còn một dải trống, thanh cuộn nằm lọt giữa (probe báo "Lớp nổi có dải trống").
|
|
204
|
+
- Cao tối đa **`max-h-76`** (304px), cuộn bên trong: lộ 7 mục rưỡi, mục thứ 8 bị cắt ngang để báo còn nữa (`I18`). `max-h-72` cũ cắt đúng sát ranh giới mục thứ 8 (chỉ lộ 4px), nhìn như danh sách hết ở mục 7. Mở ra thì cuộn sẵn tới mục đang chọn và chớp thanh cuộn một lần (`flashScrollbar`, `I18`). Danh sách chiếm cả khung `rounded-2xl` nên cả hai đầu rãnh chạm góc bo: `[&::-webkit-scrollbar-track]:my-4`, khe `p-1 pr-0 [scrollbar-gutter:stable]` (`I18`).
|
|
205
|
+
- **Mục có mô tả** (vai trò, gói, quyền): hai tầng, tên `text-sm font-medium` + một câu `text-sm text-muted` nói **quyền làm được gì**, mục cao theo nội dung (`py-2.5`), không ép `h-10`. Ô đã đóng chỉ hiện tên, không hiện mô tả. Chọn quyền mà không có câu này thì người mời phải đoán "Thành viên" khác "Chỉ xem" chỗ nào.
|
|
206
|
+
- **Mục đang chọn**: chữ `font-medium` + icon `check` `size-4` căn phải. Nền xám `bg-item-hover` là của **mục đang sáng** (chuột hoặc phím mũi tên, `data-[highlighted]`), không phải của mục đang chọn; mở ra thì mục đang chọn sáng trước.
|
|
207
|
+
- Trên 8 mục: **ô tìm ở đầu danh sách**, icon `search` trái, placeholder "Tìm tỉnh, thành phố", đường chia bên dưới tràn hai mép (`F25`). Gợi ý (người dùng quyết): gõ là lọc ngay, không bấm Enter; lọc ở máy hay gọi server thì để handler rỗng. Không ra kết quả thì một dòng `text-muted` căn giữa: *Không tìm thấy tỉnh, thành phố nào*. **Không nhắc lại từ khoá**: nó nằm ngay ô tìm phía trên (`N3`), và từ khoá dài bị cắt giữa chữ thành "của phò…".
|
|
208
|
+
- **`<select>` gốc chỉ cho màn cảm ứng**: dưới 8 mục, không cần tìm, và component chỉ hiện trên mobile (bản mobile riêng, hay app chỉ chạy trên điện thoại) thì `<select>` gốc là đủ: `appearance-none` + icon `chevron-down` đặt `absolute` bên phải, `pr-10`. Điện thoại mở bánh xe chọn gốc, dễ bấm hơn mọi danh sách tự dựng. **Màn desktop thì luôn dựng danh sách như trên**: tô ô ngoài chỉ đổi được lúc đóng, bấm vào vẫn bung menu xám của hệ điều hành, lạc giữa app đã có kiểu. Một ô dùng chung cho cả hai khổ thì dựng danh sách.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Ô chọn giờ
|
|
213
|
+
|
|
214
|
+
Nút mở y như select (`h-11 md:h-10`, icon `clock` bên phải, giá trị `tabular-nums`),
|
|
215
|
+
bấm ra một popover có các cột cuộn.
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
┌──────────────────────────────┐
|
|
219
|
+
│ Giờ Phút Giây │ <- nhãn cột text-xs, cột đang đứng: text-foreground
|
|
220
|
+
│ 06 28 58 │
|
|
221
|
+
│ 07 29 59 │
|
|
222
|
+
│ ┌──────────────────────────┐ │
|
|
223
|
+
│ │ 08 30 00 │ │ <- MỘT dải nền chạy ngang cả ba cột
|
|
224
|
+
│ └──────────────────────────┘ │
|
|
225
|
+
│ 09 31 01 │
|
|
226
|
+
│ 10 32 02 │
|
|
227
|
+
└──────────────────────────────┘
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
- **Giá trị đang chọn là MỘT dải ngang xuyên ba cột**, nền `bg-item-hover` bo `rounded-xl`, số trong dải `font-semibold text-foreground`. Không tô từng ô bằng khối `primary` đặc: ba khối đen trong một popover nhỏ là thứ nặng nhất màn hình, và mắt đọc ra ba lựa chọn rời chứ không đọc ra một mốc "08:30:00". Cùng tinh thần select: đang chọn là chữ đậm, không phải nền màu nhấn.
|
|
231
|
+
- **Mọi cột đều cuộn vòng, kể cả cột giây**, không có đầu cuối. Khác số đông có chủ ý: bộ chọn giờ của các bộ component web thường không vòng và đẩy số đang chọn lên đầu cột; skill giữ dải chọn cố định ở giữa nên cần vòng để số nào cũng tới được dải. Phía trên `00` là `23` (cột giờ) hay `59` (cột phút, giây), phía dưới `59` là lại `00`, như mặt đồng hồ. Nhờ vậy số đang chọn của mọi cột luôn nằm giữa, cùng một hàng, và vùng cuộn lúc nào cũng đầy số. **Không chèn đệm trống trên dưới** để đẩy `00` xuống giữa: nửa trên popover trống trơn, trông như chưa tải xong.
|
|
232
|
+
- **Dải chọn đứng yên ở giữa là cố ý** (kiểu bánh xe): số nằm trong dải là số được chọn. Nhưng **không chỉ cuộn mới chọn được**. Đủ ba đường: **bấm vào một số** thì số đó trượt vào dải và được chọn (`cursor-pointer`, hover chữ lên `text-foreground`); cuộn chuột hoặc kéo; phím mũi tên. Chỉ cho cuộn thì trên desktop rất khổ: bàn di chuột cuộn một phát trượt qua năm sáu số.
|
|
233
|
+
- Gợi ý (người dùng quyết): cho **gõ thẳng vào ô** theo từng đoạn `HH`, `mm` như `<input type="time">`. Người quen bàn phím gõ `0830` nhanh hơn mọi bánh xe, popover chỉ là đường phụ.
|
|
234
|
+
- **Cuộn vòng phải cuộn liền tay được.** Cách dựng: lặp danh sách nhiều vòng (khoảng 10 vòng: 240 ô giờ, 600 ô phút, vẫn nhẹ), mở ra đứng ở vòng giữa, và **chỉ kéo về vòng giữa khi đã dừng cuộn** (`scrollend`, hoặc 150ms không có sự kiện `scroll`), đổi `scrollTop` đúng một bội số chiều dài vòng nên mắt không thấy nhảy. Đừng chỉ lặp 3 vòng rồi kéo về giữa lúc đang cuộn: đổi `scrollTop` giữa đà cuộn là trình duyệt cắt đà, cuộn tới một điểm thì khựng lại, phải thả tay cuộn lại. Đây là lỗi của cách dựng component, skill lo, không đẩy cho người dùng.
|
|
235
|
+
- **Chưa chọn thì ô vẫn hiện placeholder** ("Chọn giờ gửi"). Mở popover thì bánh xe dừng sẵn ở một mốc gợi ý (giờ hiện tại làm tròn, hay mốc người dùng truyền vào, họ quyết), nhưng giá trị chỉ ghi vào ô khi người dùng cuộn, bấm, hoặc Enter. Mở ra mà ô tự điền `00:00:00` là đang chọn hộ.
|
|
236
|
+
- **Không số nào bị cắt nửa**: cao vùng cuộn đúng một số lẻ ô (5 ô `h-10` = `h-50`), `scroll-snap-type: y mandatory` + `snap-center` mỗi ô. Mép trên dưới làm mờ dần bằng `mask-image` (`linear-gradient` trong suốt ở hai đầu) để báo "còn nữa", thay cho chữ bị xén ngang.
|
|
237
|
+
- **Cột đang đứng (bàn phím)**: nhãn cột lên `text-foreground`, các cột khác `text-muted`. Không vòng viền quanh ô (`I13`).
|
|
238
|
+
- **Mặc định chỉ giờ và phút** (`HH:mm`). Cột giây chỉ thêm khi đề cần tới giây, người dùng quyết.
|
|
239
|
+
- Màn cảm ứng: gợi ý (người dùng quyết) dùng `<input type="time">` gốc, điện thoại mở bánh xe chọn giờ sẵn, dễ bấm hơn cột tự dựng. Cùng lý do với select dưới 8 mục.
|
|
240
|
+
- **Kiểu khác, chỉ dựng khi đề yêu cầu: cột danh sách** (kiểu Ant Design). Không có dải chung; mỗi cột tô nền `bg-item-hover` cho đúng ô đang chọn, chữ `font-semibold`; ô đang chọn cuộn lên hàng đầu cột; cột không cuộn vòng, cuối cột có đệm để `59` lên được hàng đầu. Hợp desktop dày thông tin, nhưng mất cái lợi đọc giờ thành một hàng. Mặc định vẫn là bánh xe ở trên.
|
|
241
|
+
- Lỗi và khoá theo đúng ô nhập: lỗi viền `red-500` + ring đỏ mờ + câu lỗi dưới ô nói mốc cần so ("phải sau giờ bắt đầu 08:30"); khoá theo bảng dưới.
|
|
242
|
+
|
|
243
|
+
## Ô chọn ngày
|
|
244
|
+
|
|
245
|
+
> **Kiểm thư viện trước** (bảng thư viện chuyên dụng trong `SKILL.md`). Dự án có
|
|
246
|
+
> `react-day-picker` (shadcn `Calendar`), `@mantine/dates`, `react-datepicker`…
|
|
247
|
+
> thì dùng nó, chỉnh cho ra hình dưới đây. Chưa có thì dựng theo mục này; chỉ
|
|
248
|
+
> đề xuất thư viện khi cần thứ tự dựng sẽ tốn (nhiều múi giờ, lịch âm, nhiều
|
|
249
|
+
> ngôn ngữ).
|
|
250
|
+
|
|
251
|
+
Nút mở y như ô nhập (`h-11 md:h-10`, icon `calendar` bên phải, placeholder
|
|
252
|
+
"Chọn ngày"), bấm ra popover lịch tháng.
|
|
253
|
+
|
|
254
|
+
```
|
|
255
|
+
┌────────────────────────────┐
|
|
256
|
+
│ [Tháng 9, 2026 ▾] ‹ › │ <- tiêu đề là NÚT: bấm để đổi tháng/năm
|
|
257
|
+
│ T2 T3 T4 T5 T6 T7 CN │
|
|
258
|
+
│ 31 1 2 3 4 5 6 │ <- ngày tháng khác: text-muted
|
|
259
|
+
│ … │
|
|
260
|
+
│ 21 [22] 23 … │ <- hôm nay: font-semibold + chấm dưới
|
|
261
|
+
└────────────────────────────┘
|
|
262
|
+
|
|
263
|
+
Bấm tiêu đề ──► ┌────────────────────────────┐
|
|
264
|
+
│ [2026 ▾] ‹ › │ <- ‹ › đổi năm
|
|
265
|
+
│ Th1 Th2 Th3 Th4 │
|
|
266
|
+
│ Th5 Th6 Th7 Th8 │ <- lưới 12 tháng
|
|
267
|
+
│ Th9 Th10 Th11 Th12 │
|
|
268
|
+
└────────────────────────────┘
|
|
269
|
+
Bấm năm ──► lưới 12 năm (2020–2031), ‹ › nhảy 12 năm
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
- **Đổi tháng, năm không bắt người dùng bấm mũi tên từng tháng.** Tiêu đề "Tháng 9, 2026" là một nút (`chevron-down` nhỏ bên cạnh): bấm vào ra **lưới 12 tháng**, bấm năm ra **lưới 12 năm**. Chọn xong thì lùi về lưới ngày của tháng đó. Muốn tới tháng 3 năm sau là hai cú bấm, không phải sáu cú mũi tên.
|
|
273
|
+
- Lưới tháng và năm **cùng khung, cùng cỡ** với lưới ngày: popover không co giãn khi chuyển tầng. Ô `h-10 rounded-xl`, tháng/năm đang chọn và hiện tại theo cùng quy ước với ô ngày bên dưới. **Khung lấy cỡ từ lưới ngày, không gõ tay một số**: lưới ngày (hàng thứ + 6 hàng) **luôn nằm trong luồng** và quyết định cao rộng. Sang tầng tháng / năm thì lưới ngày vẫn render, chỉ `invisible` + `aria-hidden`, lưới tháng / năm `absolute inset-0` phủ lên. Xoá lưới ngày khỏi DOM khi đổi tầng thì khung còn 0px, lưới phủ lòi ra ngoài. Gõ `height: 272px` trong khi lưới ngày cần 288px thì popover ra thanh cuộn (`layouts/overlay.md`, "Lớp nổi không có thanh cuộn").
|
|
274
|
+
- **Ngày đang chọn**: nền `primary`, chữ `primary-foreground`. Ngoại lệ có tên của "đang chọn không tô màu nhấn" ở select: ô ngày chỉ là một con số, chữ đậm thôi không đủ tách khỏi 41 số bên cạnh, và cả lịch chỉ có đúng một ô đặc. **Hôm nay**: `font-semibold` + chấm `size-1` dưới số, không nền. Hover: `bg-background`. Hôm nay cũng là ngày đang chọn thì giữ cả hai: ô nền `primary`, chấm đổi sang màu `primary-foreground` để vẫn thấy.
|
|
275
|
+
- **Lưới ngày `gap-y-1`, không khe ngang**, cho mọi lịch (một ngày, khoảng ngày, ngày giờ) dùng chung một lưới. Khe dọc tách từng tuần; khe ngang thì để dải khoảng ngày chạy liền.
|
|
276
|
+
- **Luôn 6 hàng**, kể cả tháng chỉ cần 5: chuyển tháng thì popover không nhảy cao thấp. Ngày của tháng trước/sau `text-muted`.
|
|
277
|
+
- Tuần bắt đầu **thứ Hai** (`T2 … CN`), không tô màu riêng cho cuối tuần. Copy tiếng Anh thì ngày đầu tuần theo locale (`T28`).
|
|
278
|
+
- Gợi ý (người dùng quyết): cho **gõ thẳng vào ô** theo `dd/mm/yyyy`. Ngày xa (ngày sinh, hạn hợp đồng năm sau) gõ `15/03/1990` nhanh hơn mọi lưới. Khi cho gõ thì câu lỗi mới được kèm ví dụ định dạng; không cho gõ thì đừng ghi "ví dụ 31/12/2026", người dùng không có chỗ nào để gõ.
|
|
279
|
+
- Câu lỗi nói **chuyện gì sai**, không lặp lời placeholder: "Chưa chọn ngày hết hạn", "Ngày hết hạn phải sau hôm nay". Không phải "Chọn ngày hết hạn".
|
|
280
|
+
- **Mở từ một ô trong bảng (sửa tại chỗ, như hạn chót của công việc)** thì khác ô trong form ở ba chỗ:
|
|
281
|
+
- **Bấm ngày là lưu và đóng**, không nút "Áp dụng". `Esc` hoặc bấm ra ngoài thì đóng, giữ giá trị cũ; tiêu điểm về lại ô.
|
|
282
|
+
- **Có đường gỡ giá trị.** Ô đang có hạn thì dưới lưới có một hàng gỡ: đường chia `border-t border-border` tràn hết bề ngang popover (`F25`), dưới đó `p-1` rồi một **hàng kiểu mục menu rộng hết bề ngang**: `flex h-10 w-full items-center gap-2.5 rounded-xl px-3 text-sm text-foreground hover:bg-item-hover`, icon `calendar-x` `size-4 text-muted` + "Xoá hạn", căn trái. Không có hàng này thì đặt hạn một lần là không bao giờ về lại `—` được.
|
|
283
|
+
- **Không dùng nút `ghost` co theo chữ.** Một nút nhỏ nằm một mình trong cả một hàng có đường chia trông lẻ loi, như thiếu nút còn lại. Hàng rộng hết bề ngang là ngôn ngữ của mục menu, đọc ra "một lựa chọn nữa của popover này", và mốc nhanh thêm sau này nằm cùng khuôn.
|
|
284
|
+
- **Căn trái, không căn phải.** Mép phải của hàng cuối popover là chỗ của nút xác nhận ("Áp dụng"); ở đây bấm ngày là lưu nên không có nút đó, đặt "Xoá hạn" bên phải là trông như nút chính.
|
|
285
|
+
- **Không đỏ, kể cả lúc rê.** Đỏ khi rê (`I4`) dành cho xoá **bản ghi**: mất một thứ. Gỡ hạn là đổi một giá trị về trống, đặt lại là có: ba câu của `I4` đều "không". Tô đỏ thì người ta ngần ngại bấm một việc vô hại, và làm nhạt nghĩa của đỏ ở mục "Xoá công việc" ngay trong menu ba chấm cùng dòng. Không hỏi xác nhận.
|
|
286
|
+
- Ô chưa có hạn thì không có hàng này (không có gì để gỡ).
|
|
287
|
+
- Ô mở popover giữ nền hover suốt lúc popover mở (`aria-expanded:bg-foreground/8`, không `bg-surface-hover` vì trùng nền dòng đang rê, không `/5` vì gần trùng, `I10`), để biết lịch này đang sửa ô nào.
|
|
288
|
+
- **Dấu bấm được lúc đứng yên**: ô sửa tại chỗ là việc phụ (hạn chót giữa ba bốn ô sửa được của một dòng việc) thì chỉ hiện lúc rê; ô là **việc chính của trang** (vai trò ở trang thành viên) thì `ChevronDown` luôn hiện. Xem "Trang thành viên và phân quyền" trong `../layouts/app.md`.
|
|
289
|
+
- Gợi ý (người dùng quyết): hàng mốc nhanh "Hôm nay · Ngày mai · Thứ Hai tới" trên lưới. Hạn chót đa phần rơi vào vài ngày tới.
|
|
290
|
+
- Kiểu khác, chỉ dựng khi đề yêu cầu: **hai select tháng và năm** thay cho tiêu đề (hợp ô ngày sinh, năm lùi vài chục năm). Mặc định vẫn là tiêu đề bấm được ở trên.
|
|
291
|
+
|
|
292
|
+
**Ngày bị khoá** (quá khứ, ngày nghỉ, ngoài hạn mức): **ngày nào bị khoá là
|
|
293
|
+
logic, người dùng quyết** (truyền vào qua prop kiểu `disabledDays`, `minDate`).
|
|
294
|
+
Skill chỉ lo nó trông ra sao: `text-muted opacity-50`, `cursor-not-allowed`,
|
|
295
|
+
không hover, không vào dải khoảng. Khác ngày của tháng bên cạnh (chỉ
|
|
296
|
+
`text-muted`, vẫn bấm được) đúng một nấc mờ. Khoảng có sẵn nào đè lên ngày bị
|
|
297
|
+
khoá thì mục đó cũng khoá theo.
|
|
298
|
+
|
|
299
|
+
### Khoảng ngày
|
|
300
|
+
|
|
301
|
+
**Kế thừa hết luật của ô chọn ngày ở trên** (tiêu đề bấm ra lưới tháng/năm, luôn
|
|
302
|
+
6 hàng, hôm nay chữ đậm + chấm, tuần từ thứ Hai, câu lỗi). Phần dưới chỉ là
|
|
303
|
+
cái thêm vào.
|
|
304
|
+
|
|
305
|
+
```
|
|
306
|
+
┌──────────────┬──────────────────────────────┬──────────────────────────────┐
|
|
307
|
+
│ [7 ngày qua] │ [Tháng 9, 2026 ▾] ‹ │ › │
|
|
308
|
+
│ 30 ngày qua │ … │ Tháng 10, 2026 │
|
|
309
|
+
│ Tháng này │ 14 15 (16)▓17▓18▓19▓20▓ │ … │
|
|
310
|
+
│ │ ▓21▓(22) 23 … │ │
|
|
311
|
+
│ │ 7 ngày · 16/09 – 22/09/2026 │ │
|
|
312
|
+
└──────────────┴──────────────────────────────┴──────────────────────────────┘
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
- **Hai tháng cạnh nhau từ `md`**, một tháng ở màn hẹp. **Hai tháng thì bỏ ngày của tháng khác** (ô trống), không thì 30, 31 hiện hai lần ở hai lưới. Popover cao theo tháng nhiều hàng hơn. Mở ra thì **tháng chứa ngày cuối nằm bên phải**, để khoảng vừa chọn luôn thấy trọn. Khoảng vắt qua cuối tháng là ca thường gặp nhất, một tháng thì phải bấm ‹ › giữa chừng lúc đang chọn. ‹ ở mép trái tháng đầu, › ở mép phải tháng sau; tiêu đề tháng đầu bấm được như ô một ngày.
|
|
316
|
+
- **Ngày đầu và ngày cuối**: nền `primary` y như ngày đang chọn, hai ô **cùng đúng một class** `bg-primary text-primary-foreground`, không `bg-primary/90` hay màu hover cho một đầu. Hai đầu lệch sắc là mắt đọc ra hai loại ngày khác nhau. Rê chuột lên một đầu thì nó sang `primary-hover`, nhạt hơn một chút: đúng, nhưng lúc chụp ảnh duyệt thì bỏ chuột ra ngoài popover kẻo tưởng hai đầu khác màu. Hôm nay rơi vào đó thì chấm đổi màu `primary-foreground`.
|
|
317
|
+
- **Dải giữa**: nền `bg-item-hover` (nền sáng bằng màu nền trang; nền tối là trắng phủ mờ, `bg-background` ở đó thành dải lỗ đen trong popover, `M21`), chạy liền giữa hai đầu. Dải **lót sau nửa ô** của ngày đầu (nửa phải) và ngày cuối (nửa trái), để góc bo của ô đặc nằm trên dải, không lộ khe hở ở bốn góc. Dải chạm hết hàng tuần thì **bo đầu hàng** (`rounded-l-xl` ở thứ Hai, `rounded-r-xl` ở Chủ nhật), không cắt vuông.
|
|
318
|
+
- **Dải liền theo chiều ngang, có khe theo chiều dọc.** Trong một tuần các ô không cách nhau, dải đọc ra là một khoảng liền; chèn khe ngang thì dải vỡ thành từng viên rời. Nhưng giữa các hàng tuần thêm `gap-y-1`: không có khe thì dải của tuần này dính sát dải tuần sau, thành một khối bậc thang, không còn thấy từng tuần.
|
|
319
|
+
- **Giữa hai lần bấm**: dải nhạt chạy theo con trỏ từ ngày đầu tới ngày đang rê, để thấy trước khoảng sẽ chọn.
|
|
320
|
+
- **Dòng dưới lịch nói bước đang làm**, đổi theo trạng thái: chưa bấm gì "Chọn ngày bắt đầu", đã bấm một ngày "Chọn ngày kết thúc", đủ hai đầu thì tóm tắt khoảng "7 ngày · 16/09 – 22/09/2026". Chọn xong mà vẫn ghi "Chọn ngày bắt đầu" là nói sai trạng thái.
|
|
321
|
+
- **Khoảng có sẵn bên trái**: cột `w-40`, mỗi mục `h-10 rounded-xl`, đang trùng khoảng nào thì mục đó nền `bg-item-hover` + `font-medium`; mục chưa chọn chữ `text-foreground/70`, không mờ tới `text-muted` (trông như bị khoá, `I8`). Danh sách khoảng nào là do người dùng quyết. Màn hẹp (và khuôn gọn trong popover Lọc) thì cột này lên thành **hàng chip trên lịch, `flex-wrap`, không cuộn ngang**: popover lịch hẹp cỡ ô (~310px ở 375px), ba mốc "7 ngày tới · 30 ngày tới · Tháng này" hụt 28px, cuộn ngang thì mốc cuối bị cắt thành "Tháng nà" sát mép, mà vài mốc cố định thì không đáng một vùng cuộn. Cùng lý do chip trong popover Lọc được xuống dòng. Hàng chip nằm trong khối `px-*` của popover, không `-mx-*` kéo ra (`N11`).
|
|
322
|
+
- Ô hiển thị `16/09/2026 – 22/09/2026`, gạch nối là `–` có dấu cách hai bên.
|
|
323
|
+
- **Chưa có khoảng thì mở ra ở đâu tuỳ trường nhìn về phía nào.** Trường nhìn lùi (báo cáo, lịch sử đơn, mốc "7 ngày qua") thì tháng hiện tại nằm bên phải, tháng trước bên trái. Trường nhìn tới (hạn chót, ngày đặt lịch, mốc "7 ngày tới") thì **tháng hiện tại bên trái**, tháng sau bên phải: mở lọc hạn chót ra tháng 8 + tháng 9 khi hôm nay là 26/09 là nửa lịch toàn ngày đã qua.
|
|
324
|
+
- Nằm trong một lớp nổi khác (popover Lọc) thì dùng khuôn gọn ở mọi bề rộng: một tháng, mốc nhanh thành hàng chip, rộng bằng ô (`../layouts/overlay.md`, "Popover lọc").
|
|
325
|
+
- Popover hai tháng rộng hơn ô: tràn mép phải màn thì **dịch ngang** vào trong (dịch ngang không che ô, được phép), không bóp lưới.
|
|
326
|
+
|
|
327
|
+
**Popover không bao giờ che chính ô mở ra nó** (áp cho mọi popover: select,
|
|
328
|
+
ô chọn giờ, ô chọn ngày). Dưới không đủ chỗ thì lật lên trên ô, vẫn chừa ô
|
|
329
|
+
nhìn thấy được; lên trên cũng không đủ thì để trang cuộn, đừng dịch popover đè
|
|
330
|
+
lên ô. Radix: `side="bottom"` + `avoidCollisions`, không bật `sticky="always"`.
|
|
331
|
+
|
|
332
|
+
### Ngày và giờ chung một ô
|
|
333
|
+
|
|
334
|
+
**Kế thừa lịch của ô chọn ngày và bánh xe của ô chọn giờ.** Phần thêm:
|
|
335
|
+
|
|
336
|
+
```
|
|
337
|
+
┌───────────────────────────────┬──────────────┐
|
|
338
|
+
│ [Tháng 9, 2026 ▾] ‹ › │ Giờ Phút │
|
|
339
|
+
│ lưới ngày │ 22 04 │
|
|
340
|
+
│ │ ▓ 23 ▓▓ 05 ▓ │
|
|
341
|
+
│ │ 00 06 │
|
|
342
|
+
├───────────────────────────────┴──────────────┤
|
|
343
|
+
│ 23/09/2026 23:05 [Xong] │
|
|
344
|
+
└──────────────────────────────────────────────┘
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
- **Lịch bên trái, bánh xe bên phải** từ `sm`, ngăn bằng `border-l border-border`. Màn hẹp thì bánh xe xuống dưới lịch.
|
|
348
|
+
- **Bánh xe cao bằng vùng lịch**, dải chọn ở giữa chiều cao đó. Bánh xe ngắn hơn lịch thì dưới nó trống một khoảng, cột bên phải trông như chưa dựng xong.
|
|
349
|
+
- **Có nút Xong, nên giá trị chỉ ghi vào ô khi bấm Xong** (hoặc Enter). Trong lúc chọn, ô giữ nguyên giá trị cũ hoặc placeholder; mốc đang chọn hiện ở **dòng tóm tắt** bên trái footer (`text-sm text-muted tabular-nums`). Esc hay bấm ra ngoài là bỏ, ô không đổi. Đừng vừa có Xong vừa ghi vào ô ngay từng lần đổi: hai mô hình lẫn nhau, người dùng không biết Esc có hoàn lại không.
|
|
350
|
+
- **Xong** là nút `primary` duy nhất của popover (`I3`), căn phải footer. Footer `border-t border-border`, `px-4 py-3`.
|
|
351
|
+
- Icon trong ô là `calendar-clock` (lucide), không phải `calendar`: nhìn ô là biết có cả giờ.
|
|
352
|
+
- Ô hiển thị `23/09/2026 23:05`, ngày và giờ cách một dấu cách, `tabular-nums`. Có giây thì ô và khung rộng thêm cho cột giây.
|
|
353
|
+
|
|
354
|
+
## Đủ trạng thái chưa
|
|
355
|
+
|
|
356
|
+
| Trạng thái | Checkbox / radio | Công tắc | Select |
|
|
357
|
+
| --- | --- | --- | --- |
|
|
358
|
+
| Thường | viền `--border-strong` | track `muted/40` | viền `--border-strong` |
|
|
359
|
+
| Rê vào | chưa chọn: viền `--foreground`; đã chọn: giữ nguyên màu nhấn | track `muted/60` / `--primary-hover` | giữ nguyên, `cursor-pointer` |
|
|
360
|
+
| Tab tới | không đổi (`I13`) | không đổi (`I13`) | viền `--border-focus` |
|
|
361
|
+
| Đã chọn / bật / đang mở | tô nhấn theo kiểu | track nhấn, núm sang phải | viền + ring, chevron xoay |
|
|
362
|
+
| Một phần | icon `minus` (chỉ checkbox) | — | — |
|
|
363
|
+
| Lỗi | viền `red-500` + câu lỗi | — | viền `red-500` + ring đỏ |
|
|
364
|
+
| Khoá | `opacity-50`, `cursor-not-allowed`, **cả nhãn** | như trái | như trái |
|
|
365
|
+
|
|
366
|
+
Khoá thì mờ cả nhãn đi cùng ô (`peer-disabled:` hoặc `has-disabled:` trên
|
|
367
|
+
`<label>`). Ô mờ mà nhãn vẫn đen thì người dùng bấm vào nhãn, không có gì xảy ra.
|
|
368
|
+
|
|
369
|
+
**Khoá thì nói vì sao, ngay dưới ô**, bằng dòng gợi ý `text-xs text-muted`: "Liên hệ quản trị viên để đổi giờ chốt sổ". Trong **hàng cài đặt** có mô tả `text-sm` thì câu lý do (và câu lỗi) thay chỗ mô tả và giữ `text-sm`, không xuống `text-xs` (`../layouts/app.md`, "Trang cài đặt"). Ô mờ mà không một lời thì người dùng tưởng app lỗi, và không biết đi đâu để đổi.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Khu bình luận có trả lời lồng nhau
|
|
2
|
+
|
|
3
|
+
Bình luận dưới một bản ghi: task, tài liệu, đơn hàng. Chữ mượn dòng danh sách
|
|
4
|
+
(`list-row.md`), nút theo `button.md`, menu ba chấm theo `layouts/overlay.md`.
|
|
5
|
+
|
|
6
|
+
- **Mỗi bình luận**: avatar `size-8` (`avatar.md`), tên `text-sm font-medium`, thời gian `text-xs text-muted` kèm giờ tuyệt đối ở `title` (`T16b`), nội dung `text-sm` cho xuống dòng, dưới cùng là nút `ghost` có icon `Reply` ghi "Trả lời". Hai cỡ chữ, không hơn.
|
|
7
|
+
- **Thụt lề mỗi tầng ~29px** (`ml-4` + viền + `pl-3`), đường dọc `border-l border-border-strong` (như cây thư mục, `tree.md`) rơi đúng tâm avatar của bình luận cha.
|
|
8
|
+
- **Màn hẹp chỉ thụt tối đa 2 tầng.** Sâu hơn thì hàng con **không thụt nữa**, thay bằng dòng `text-xs text-muted` "Trả lời **<tên>**" ở đầu bình luận. Thụt tiếp là cột chữ còn ~140px, ô trả lời vỡ hai dòng placeholder, và nội dung xuống dòng từng hai ba chữ (`R5`).
|
|
9
|
+
- **Ô trả lời ở màn hẹp bỏ avatar bên trái** (lấy lại 44px), ô viết chiếm hết bề ngang. Ô gốc ở cuối khu thì vẫn có avatar.
|
|
10
|
+
- **Nút trong ô viết**: gửi là `primary` (`I3`: một nút chính trong cụm) và **khoá khi ô trống** (`opacity-50`), huỷ là `secondary`. Một nút thì **không kéo rộng hết hàng ở màn hẹp** — `R3` chỉ áp khi cụm nút không vừa.
|
|
11
|
+
- **Bốn trạng thái của một bình luận** (`N2`), mỗi cái một hình riêng:
|
|
12
|
+
- *đang gửi*: spinner `size-4` cạnh tên, cả khối `text-muted`, link "Trả lời" mờ và không bấm được;
|
|
13
|
+
- *gửi lỗi*: dòng `text-xs text-red-600` kèm icon cạnh tên ("Gửi không thành công") — **`red` chứ không phải `rose`**, vì gửi hỏng là việc đã xảy ra rồi, không phải cảnh báo trước khi bấm (`M30`) — cộng hai nút "Thử lại" (`outline`) và "Xoá" (nền `rose-500/10`, chữ `rose-700`, theo `I4`);
|
|
14
|
+
- *đã xoá*: một dòng xám "Bình luận đã bị xoá" kèm thời gian, **vẫn giữ nguyên các trả lời bên dưới**, không xoá cả nhánh;
|
|
15
|
+
- *thường*: menu ba chấm **luôn hiện** (`I11`: từ 3 hành động trở lên hoặc có xoá thì không ẩn theo hover, nếu không trên điện thoại sẽ không ai thấy), mục "Xoá bình luận" chỉ đỏ lúc rê (`I4`).
|
|
16
|
+
- **Rỗng**: một dòng chữ mờ "Chưa có bình luận nào" (`empty-state.md`), ô viết vẫn đứng nguyên chỗ cũ (`N1`).
|
|
17
|
+
- Đếm số ở tiêu đề khu ("Bình luận 7") là số trơn `text-muted`, không badge màu.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Nhãn và giá trị (description list)
|
|
2
|
+
|
|
3
|
+
Khối thông tin của trang chi tiết: email, số điện thoại, trạng thái, ngày tạo.
|
|
4
|
+
Nằm trong card (`card.md`), mỗi hàng một cặp nhãn và giá trị, theo `T23`.
|
|
5
|
+
|
|
6
|
+
```html
|
|
7
|
+
<dl class="@container space-y-3 text-sm">
|
|
8
|
+
<!-- Mỗi cặp một khối. Đổi khuôn theo bề rộng chính <dl>, không theo màn: dưới 384px nhãn trên giá trị
|
|
9
|
+
dưới, từ @sm (384px) hai cột nhãn 7rem, từ @xl (576px) nhãn 10rem -->
|
|
10
|
+
<div class="grid gap-1 @sm:grid-cols-[7rem_minmax(0,1fr)] @sm:items-baseline @sm:gap-6 @xl:grid-cols-[10rem_minmax(0,1fr)]">
|
|
11
|
+
<dt class="text-muted">Email</dt>
|
|
12
|
+
<dd class="min-w-0 font-medium text-foreground [overflow-wrap:anywhere]">minhanh.nguyen@lumen.vn</dd>
|
|
13
|
+
</div>
|
|
14
|
+
<div class="grid gap-1 @sm:grid-cols-[7rem_minmax(0,1fr)] @sm:items-baseline @sm:gap-6 @xl:grid-cols-[10rem_minmax(0,1fr)]">
|
|
15
|
+
<dt class="text-muted">Trạng thái</dt>
|
|
16
|
+
<dd><!-- badge M7, KHÔNG phải chữ trơn "Đang giao dịch" -->
|
|
17
|
+
<span class="inline-flex items-center gap-1.5 rounded-full bg-emerald-50 px-2.5 py-1 text-xs font-medium text-emerald-700 ring-1 ring-inset ring-black/5">
|
|
18
|
+
<span class="size-1.5 rounded-full bg-current"></span>Đang giao dịch
|
|
19
|
+
</span>
|
|
20
|
+
</dd>
|
|
21
|
+
</div>
|
|
22
|
+
<div class="grid gap-1 @sm:grid-cols-[7rem_minmax(0,1fr)] @sm:items-baseline @sm:gap-6 @xl:grid-cols-[10rem_minmax(0,1fr)]">
|
|
23
|
+
<dt class="text-muted">Nhãn</dt>
|
|
24
|
+
<dd class="flex flex-wrap gap-1.5"><!-- mỗi nhãn một pill, KHÔNG nối bằng dấu phẩy -->
|
|
25
|
+
<span class="rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600">VIP</span>
|
|
26
|
+
<span class="rounded-full bg-zinc-100 px-2 py-0.5 text-xs font-medium text-zinc-600">Khách quen</span>
|
|
27
|
+
</dd>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="grid gap-1 @sm:grid-cols-[7rem_minmax(0,1fr)] @sm:items-baseline @sm:gap-6 @xl:grid-cols-[10rem_minmax(0,1fr)]">
|
|
30
|
+
<dt class="text-muted">Tổng doanh thu</dt>
|
|
31
|
+
<dd class="font-medium tabular-nums text-foreground">1.284.500.000<span class="ml-1 text-muted">đ</span></dd>
|
|
32
|
+
</div>
|
|
33
|
+
<div class="grid gap-1 @sm:grid-cols-[7rem_minmax(0,1fr)] @sm:items-baseline @sm:gap-6 @xl:grid-cols-[10rem_minmax(0,1fr)]">
|
|
34
|
+
<dt class="text-muted">Số điện thoại</dt>
|
|
35
|
+
<dd class="text-muted">—</dd>
|
|
36
|
+
</div>
|
|
37
|
+
</dl>
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
- **`<dl>` / `<dt>` / `<dd>`**, không dựng bằng `<div>`: trình đọc màn hình đọc ra đúng cặp nhãn và giá trị.
|
|
41
|
+
- **`@sm:items-baseline`** để chữ nhãn thẳng dòng chữ trong badge hay pill: badge có `py-1` nên cao hơn dòng chữ thường, căn đỉnh thì nhãn lệch lên vài px so với chữ trong badge.
|
|
42
|
+
- **Cột nhãn rộng cố định** để mọi giá trị thẳng một mép. Nhãn dài hơn cột thì cho xuống dòng trong cột, không nới cột theo nhãn dài nhất.
|
|
43
|
+
- **Khuôn đổi theo bề rộng `<dl>` (`@container`), không theo viewport.** Cùng một card Liên hệ nằm ở cột
|
|
44
|
+
phải 22rem, trong panel 448px, hay giữa khung rộng; `sm:` chỉ biết màn rộng 1440px, không biết khối
|
|
45
|
+
chỉ còn 310px. Ba bậc theo khối:
|
|
46
|
+
- **Dưới 384px: xếp chồng**, nhãn trên, giá trị ngay dưới (`gap-1`), giữa các cặp `space-y-3`. Khe trong
|
|
47
|
+
cặp nhỏ hơn khe giữa các cặp thì mắt mới gom đúng nhãn với giá trị của nó. Cột phải trang chi tiết,
|
|
48
|
+
màn điện thoại rơi vào đây.
|
|
49
|
+
- **384–575px: nhãn `7rem`** (panel trượt, card nửa khung). Panel 448px trừ lề còn ~400px: nhãn `10rem`
|
|
50
|
+
chiếm gần nửa, giá trị bị ép xuống 3–4 dòng.
|
|
51
|
+
- **Từ 576px: nhãn `10rem`.**
|
|
52
|
+
|
|
53
|
+
Ví dụ: card Liên hệ ở cột phải, `<dl>` 310px, `sm:grid-cols-[7rem_…]` bật vì
|
|
54
|
+
màn 1440px: giá trị còn 174px, email vỡ ba dòng ("…thi@" / "quangtrung-" / "logistics.com.vn"), địa chỉ
|
|
55
|
+
bốn dòng, nhãn "Thuốc đang dùng" hai dòng. Xếp chồng cùng khối: email và địa chỉ hai dòng. `probe.mjs` báo
|
|
56
|
+
"nhãn–giá trị hai cột trong khối hẹp". Tailwind v3 cần plugin `@tailwindcss/container-queries`; không có
|
|
57
|
+
thì truyền prop bố cục từ chỗ đặt card (cột phải thì xếp chồng), đừng quay về `sm:`.
|
|
58
|
+
- **Giá trị `font-medium text-foreground`, nhãn `text-muted`** (`T23`). Giá trị dài xuống dòng, bám mép trên cùng nhãn (`items-start`), không `truncate`: đây là chỗ để đọc đủ.
|
|
59
|
+
- **Giá trị có avatar (người phụ trách) là `flex h-5 items-center gap-2`, không `inline-flex`.** `inline-flex` nằm trong dòng chữ nên cả cụm bị đẩy theo baseline: hàng cao 24px thay vì 20px, chữ tên thấp hơn chữ nhãn 1,5px, nhìn rõ nhãn "Phụ trách" nổi lên cao hơn tên (đổi avatar xuống `size-5` vẫn lệch). `h-5` giữ hàng đúng một dòng chữ, avatar `size-6` tràn 2px trên dưới vào khoảng cách giữa hai hàng.
|
|
60
|
+
- **`[overflow-wrap:anywhere]` cho giá trị**: email, URL, mã dài không có dấu cách nên không tự xuống dòng, sẽ đẩy tràn card ở màn hẹp.
|
|
61
|
+
- **Email chèn `<wbr>` ngay sau `@`**, để email dài xuống dòng ở ranh giới tên / tên miền. `overflow-wrap:anywhere` chỉ là lưới đỡ: một mình nó thì bẻ ở bất kỳ ký tự nào vừa hết chỗ. Ví dụ: thêm nút sao chép cạnh email, cột giá trị hẹp đi 28px, "…@hoanggiap" / "hat-import-export.com.vn" vỡ giữa chữ.
|
|
62
|
+
- **Email nằm giữa câu chữ** (hộp xác nhận, toast, dòng "Đang chờ xác nhận…") thì `<wbr>` chưa đủ: tên miền có gạch nối thì trình duyệt còn bẻ ở gạch nối, ra "…khang@evondev-" / "studio.com" (ở 375px còn vỡ "…@evo" / "ndev-studio.com"). Tách email làm hai khúc `inline-block max-w-full`, mỗi khúc chỉ bẻ bên trong khi tự nó dài hơn cả dòng:
|
|
63
|
+
|
|
64
|
+
```tsx
|
|
65
|
+
interface EmailTextProps {
|
|
66
|
+
email: string;
|
|
67
|
+
// Dấu câu ngay sau email ("." cuối câu, ","): phải nằm trong khúc cuối, xem dưới.
|
|
68
|
+
suffix?: string;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function EmailText({ email, suffix }: EmailTextProps) {
|
|
72
|
+
const atIndex = email.lastIndexOf("@");
|
|
73
|
+
|
|
74
|
+
if (atIndex < 0) return <span className="wrap-anywhere">{email}{suffix}</span>;
|
|
75
|
+
|
|
76
|
+
const localSegments = email.slice(0, atIndex + 1).split(".");
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
// Cả email là một khối: vừa một dòng thì xuống dòng nguyên cụm, không bẻ sau "@".
|
|
80
|
+
<span className="inline-block max-w-full wrap-anywhere">
|
|
81
|
+
<span className="inline-block max-w-full">
|
|
82
|
+
{localSegments.map((segment, index) => (
|
|
83
|
+
<Fragment key={index}>
|
|
84
|
+
{index > 0 && <wbr />}
|
|
85
|
+
{index > 0 && "."}
|
|
86
|
+
{segment}
|
|
87
|
+
</Fragment>
|
|
88
|
+
))}
|
|
89
|
+
</span>
|
|
90
|
+
<span className="inline-block max-w-full">
|
|
91
|
+
{email.slice(atIndex + 1)}
|
|
92
|
+
{suffix}
|
|
93
|
+
</span>
|
|
94
|
+
</span>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Dùng chung một component này cho mọi chỗ in email, kể cả hàng giá trị ở trên.
|
|
100
|
+
- **Dấu câu ngay sau email đi vào `suffix`, không viết sau thẻ.** Sau một khối `inline-block`
|
|
101
|
+
trình duyệt được phép xuống dòng, nên `<EmailText />.` ở câu dài ra dòng mở đầu bằng dấu
|
|
102
|
+
chấm: "…evondev.com.vn" / ". Đổi tài khoản".
|
|
103
|
+
Viết `<EmailText email={email} suffix="." />`. Bọc cả email và dấu bằng `whitespace-nowrap`
|
|
104
|
+
thì không được: mất luôn chỗ xuống dòng sau `@`. Probe báo lỗi này ở mục "Dấu câu rơi xuống
|
|
105
|
+
đầu dòng".
|
|
106
|
+
- **Khối bọc ngoài cũng `inline-block max-w-full`.** Để khối ngoài là inline thường thì trình
|
|
107
|
+
duyệt chọn ngắt ngay sau `@` dù cả email vừa một dòng: "Bạn đang đăng nhập bằng
|
|
108
|
+
tran.nguyen.anh.tuan.khang@" / "evondev-studio.com. Đổi tài khoản", đọc như hai mẩu, câu căn
|
|
109
|
+
giữa thì hai dòng lệch hẳn nhau. Khối ngoài `inline-block` thì
|
|
110
|
+
email vừa dòng sẽ xuống nguyên cụm; chỉ khi dài hơn cả dòng mới ngắt sau `@` như trên (thử ở
|
|
111
|
+
320, 375, 1280px).
|
|
112
|
+
- **Phần trước `@` dài hơn cả dòng thì xuống dòng trước dấu chấm** (`<wbr>` trước mỗi `.`, như trên). Không có nó thì `wrap-anywhere` bẻ ở ký tự vừa hết chỗ, và hay rơi đúng trước `@`: một dòng chỉ có mỗi "@" ("…toan.tong.hop" / "@" / "congty-…"). Có `<wbr>` thì ra "…toan.tong" / ".hop@" / "congty-…". Không dính `@` vào ký tự cuối bằng `nowrap`: ra "…tong.ho" / "p@", vẫn vỡ giữa chữ.
|
|
113
|
+
- **Giá trị trống là `—` `text-muted`**, một ký hiệu cho mọi ô trống, giống ô trống trong bảng (`layouts/app.md`, `T18`). Không viết "Chưa có", "Chưa gắn nhãn", mỗi dòng một câu.
|
|
114
|
+
- **Giá trị có khuôn riêng thì dùng đúng component của nó**, không viết chữ trơn: trạng thái là badge màu (`M7`), nhãn phân loại là pill (`M8`, `list-row.md`), tiền dùng `đ` không `₫` (`charts.md`), số `tabular-nums`, mã và ID `font-mono` (`T17`).
|
|
115
|
+
- **Email là link `mailto:`, số điện thoại là link `tel:`**, chữ vẫn `text-foreground`, rê vào gạch chân. Kèm icon button `copy` `size-7` hiện khi rê vào hàng, luôn hiện trên màn chạm (`I11`); bấm thì icon đổi `check` 1,5 giây, không toast. **Vùng bấm nới ra 40px mà hình giữ 28px**: `relative before:absolute before:-inset-1.5` (số âm buộc phải giữ theo `N11`, cùng cách tay cầm ở `range-slider.md`: nút to lên `size-10` thì cột giá trị hẹp thêm 12px và mỗi hàng trên điện thoại cao thêm 12px). Nút 28px trơn trên màn chạm là dưới mức 32px. Hai hàng có nút sao chép cách nhau 56px nên vùng 40px không chồng nhau. Mặc định ở trang chi tiết và panel xem bản ghi; ở form xác nhận, màn chỉ đọc lại thông tin vừa nhập thì để chữ trơn. **Không lặp các việc này vào menu ⋯** ("Gọi điện", "Sao chép email"): việc gắn với một giá trị thì nằm cạnh giá trị đó (`layouts/app.md`, "Trang chi tiết bản ghi").
|
|
116
|
+
- Không kẻ đường chia giữa các hàng khi dưới 8 hàng: khoảng trắng đủ tách. Nhiều hơn thì chia nhóm có tiêu đề nhỏ, không kẻ từng hàng.
|