@haposoft/cafekit 0.16.6 → 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 +112 -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 +18 -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 +52 -6
- package/src/claude/scripts/spec-resolver.cjs +1 -2
- package/src/claude/scripts/validate-spec-output.cjs +5 -32
- package/src/claude/scripts/workflow-policy.cjs +22 -10
- 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 +38 -44
- 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 +5 -0
- 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 +8 -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,142 @@
|
|
|
1
|
+
# Button
|
|
2
|
+
|
|
3
|
+
> ⚠️ Bản trước của file này dạy ba variant `primary` / `ghost` / `danger` và
|
|
4
|
+
> "nút mặc định không icon". **Cả hai đã đảo** — xem luật `I1`. Chủ dự án chốt:
|
|
5
|
+
> nút mặc định là **nút viền**. Icon lucide bên trái chữ chỉ khi glyph gọi đúng
|
|
6
|
+
> hành động; nút form và nút trong modal chỉ có chữ (xem `I1`).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Bốn dạng, hết
|
|
11
|
+
|
|
12
|
+
```tsx
|
|
13
|
+
type ButtonVariant = "outline" | "primary" | "secondary" | "ghost";
|
|
14
|
+
|
|
15
|
+
function getVariantClasses(variant: ButtonVariant): string {
|
|
16
|
+
return cn(
|
|
17
|
+
// MẶC ĐỊNH. Dựng nút mới thì dùng cái này.
|
|
18
|
+
// Hover: CHỈ đổi nền sang --button-hover (màu đặc), viền giữ nguyên.
|
|
19
|
+
variant === "outline" &&
|
|
20
|
+
"border border-border-strong bg-surface text-foreground hover:bg-button-hover",
|
|
21
|
+
// Hành động chính DUY NHẤT của một khu, khi thật cần nổi.
|
|
22
|
+
variant === "primary" &&
|
|
23
|
+
"bg-primary text-primary-foreground hover:bg-primary-hover",
|
|
24
|
+
// Nút phụ cần nhỉnh hơn ghost mà không tranh chỗ nút chính. Không viền.
|
|
25
|
+
variant === "secondary" &&
|
|
26
|
+
"bg-secondary text-foreground hover:bg-secondary-hover",
|
|
27
|
+
// Hành động phụ nằm trong hàng, mờ đi lúc thường.
|
|
28
|
+
// Hover foreground/5, KHÔNG bg-background: ghost hay đứng thẳng trên nền trang
|
|
29
|
+
// xám (header, toolbar), tô --background ở đó thì rê vào không thấy gì.
|
|
30
|
+
variant === "ghost" &&
|
|
31
|
+
"bg-transparent text-muted hover:bg-foreground/5 hover:text-foreground",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
<button
|
|
36
|
+
className={cn(
|
|
37
|
+
"inline-flex cursor-pointer items-center justify-center gap-2 rounded-xl",
|
|
38
|
+
// Cao 40px cố định cho nhãn một dòng, viền hay không viền cũng vậy. min-h chứ không h:
|
|
39
|
+
// nhãn dài xuống dòng thì nút cao thêm (T15).
|
|
40
|
+
"min-h-10 px-4 py-2 text-sm font-medium transition-colors",
|
|
41
|
+
// Nhãn tiếng Việt dài thì cho xuống dòng, đừng để tràn. Luật T15.
|
|
42
|
+
"max-w-full text-center leading-tight [overflow-wrap:anywhere]",
|
|
43
|
+
// Không vòng focus (I13). Dự án cần accessibility thì trả vòng lại đúng ở dòng này (I14)
|
|
44
|
+
"outline-hidden",
|
|
45
|
+
"disabled:cursor-not-allowed disabled:not-aria-busy:opacity-50",
|
|
46
|
+
getVariantClasses(variant),
|
|
47
|
+
)}
|
|
48
|
+
>
|
|
49
|
+
{Icon ? <Icon className="size-4 shrink-0" aria-hidden /> : null}
|
|
50
|
+
{children}
|
|
51
|
+
</button>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Đang xử lý (loading)**
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<Button variant="primary" aria-disabled={isSending || undefined} aria-busy={isSending || undefined}
|
|
58
|
+
onClick={(event) => { if (isSending) event.preventDefault(); }}>
|
|
59
|
+
{isSending ? <LoaderCircle className="size-4 shrink-0 animate-spin motion-reduce:animate-none" aria-hidden /> : <Send className="size-4 shrink-0" aria-hidden />}
|
|
60
|
+
Gửi lời mời
|
|
61
|
+
</Button>
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- Spinner **thế chỗ icon**, cùng `size-4`, chữ giữ nguyên. Nút không đổi bề rộng, hàng nút không xô. **Nút chỉ chữ** (không icon để thế chỗ): nút `relative`, chữ thêm `invisible` để vẫn giữ bề rộng, spinner `absolute inset-0 m-auto` nằm giữa.
|
|
65
|
+
- Không đổi chữ sang "Đang gửi…": chữ dài ngắn khác nhau là nút co giãn. Đổi thì phải giữ `min-w` bằng bản cũ.
|
|
66
|
+
- **Đang xử lý thì chặn bấm bằng `aria-disabled` + `preventDefault` trong `onClick`, KHÔNG đặt `disabled`.** Nút `disabled` bị trình duyệt tước tiêu điểm: người dùng bàn phím bấm Enter xong rơi về `<body>`, lúc xong việc phải Tab lại từ đầu trang. Chặn ở `onClick` là chặn luôn Enter trong ô nhập, vì submit ngầm của form cũng đi qua cú click của nút submit. Nên cài một lần trong component `Button` (prop `isLoading`), đừng viết lại ở từng trang.
|
|
67
|
+
- Nút đang xử lý **không mờ** như nút khoá thường, chỉ spinner nói đang chạy, con trỏ `cursor-progress`. Mờ đi thì đọc ra là "bấm không được vì sai gì đó". Class gốc vẫn giữ `disabled:not-aria-busy:opacity-50` cho dự án nào lỡ đặt `disabled` lúc đang xử lý.
|
|
68
|
+
|
|
69
|
+
**Vì sao ổn**
|
|
70
|
+
|
|
71
|
+
- **`outline` là mặc định**, không phải `primary`. Nút nền nhấn rải khắp nơi thì màu thương hiệu loang ra, tới lúc có một nút thật sự cần nổi thì nó không nổi được nữa (`I1`, `M2`).
|
|
72
|
+
- **Nút viền rê vào thì chỉ đổi nền, viền giữ nguyên** (`hover:bg-button-hover`, `#f1f1f3`), như phần lớn các bộ component. Nền hover phải là **màu đặc**, tách được cả khỏi nền ngay sau nút lẫn khỏi màu viền. Ba cách đã hỏng: `hover:bg-item-hover` ra đúng `#f4f4f6` của nền trang, nút tan vào trang; lớp phủ `foreground/5` trong suốt nên lấy màu nền phía sau, trên nền trang ra `#e9e9eb` **trùng màu viền** `#eaeaea`, nút thành mảng xám không viền; `--surface-hover` `#f8f8fa` thì trên card gần như không đổi. Cách chữa tạm "viền đậm lên `foreground/20`" thấy được ở đâu cũng được nhưng nặng, trông như nút đang bấm giữ hoặc đang chọn (chủ dự án chốt chỉ đổi nền). **Màu rê theo nền ngay sau nút**, tự đổi qua biến (`tokens.css`, khối "NỀN RÊ CỦA NÚT VIỀN"): trên card trắng `#f1f1f3`, thẳng trên nền trang hoặc trong dòng đang rê `#e4e4e7`. Một màu `#f1f1f3` cho mọi chỗ thì trên nền trang `#f4f4f6` chỉ lệch 3 mức, nút như tan vào trang (bác lý lẽ "chìm nhẹ mà viền vẫn còn"). Màu nền trang phải đậm hơn viền `#eaeaea` ít nhất 5 mức: `#e7e7ea` chỉ lệch 3, viền tan, nút thành mảng xám; `#e4e4e7` lệch 6. Màu đó trên card trắng thì quá nặng, nên card giữ `#f1f1f3`. Class vẫn là `hover:bg-button-hover`, không viết màu theo chỗ đặt. Nút có mũi tên mở danh sách lựa chọn thì không theo mục này mà theo ô Select (xem dưới).
|
|
73
|
+
- **`ghost` rê vào `hover:bg-foreground/5`**, không `bg-background` (ghost trong suốt, không có viền để đậm lên, nên phải là lớp phủ). Nút ghost nằm **trong dòng bảng / danh sách có nền rê** (⋯, icon button, ô sửa tại chỗ) thì `/8`, vì nó chồng lên nền dòng đang rê (`I10`).
|
|
74
|
+
- **Nút mở danh sách lựa chọn** (nút lọc "Vai trò ▾", "Trạng thái: Tất cả ▾", "Mỗi trang 10 ▾") **là ô Select, không phải nút viền**: dùng đúng class của ô Select (`choice-controls.md`), tức **không có hover**, chỉ `cursor-pointer`; lúc mở `aria-expanded:border-focus aria-expanded:ring-2 aria-expanded:ring-focus`, nền giữ `bg-surface`, mũi tên xoay. Mũi tên + con trỏ đã nói "bấm được", như ô Select trong form và ô nhập vốn không có hover; thêm hover nữa là tín hiệu thứ hai cho một ý (`N3`), và lệch khuôn với ô Select ngay trong modal (`N5`). Chủ dự án chốt. Nút mở **menu thao tác** (⋯, "Xuất ▾") thì không phải ô chọn: lúc mở giữ nền rê `aria-expanded:bg-foreground/5`.
|
|
75
|
+
- **`secondary` (nền xám, không viền)** dùng khi nút phụ cần có mặt rõ hơn `ghost` nhưng viền mảnh trông rỗng: nút rộng hết card (các gói thường trong bảng giá, `layouts/pricing.md`), hoặc nút phụ đứng cạnh nút `primary` trong footer. Không thay `outline` làm mặc định (chủ dự án chốt).
|
|
76
|
+
- **`ghost` đứng đầu hàng, thẳng cột với chữ phía trên.** Nền trong suốt nên mắt thấy mép của chữ chứ không thấy mép nút, để nguyên thì cả hàng trông lệch vào 16px so với tiêu đề và nhãn bên dưới. Nút có nền hoặc viền thì không bù. Bù theo thứ tự, không kéo nút ra bằng `-ml-*` / `-mr-*` (`N11`):
|
|
77
|
+
1. **Nút nằm ở đầu hay cuối một hàng riêng** (header, dòng danh sách): **hàng bớt padding phía đó** đúng bằng `px` của nút, ví dụ header `pl-4 pr-3` khi nút cuối `h-8 px-3` trong hàng `px-6`. Chữ "Đánh dấu đã đọc" ở header panel thông báo thẳng cột với chấm chưa đọc bên dưới; lúc rê vào nền xám sát mép panel hơn tiêu đề bên trái là đúng ý. Trùng từng pixel với bản số âm.
|
|
78
|
+
2. **Nút chỉ đổi màu chữ khi rê, không nền** (nút mở danh sách bước công cụ trong chat): bỏ `px`, chữ tự thẳng mép.
|
|
79
|
+
3. **Nút có nền rê nằm giữa một cột chữ** (hàng Sao chép / Tạo lại dưới câu trả lời): không có hàng riêng để bớt padding; chia padding từng khối thì mọi khối con phải tự nhớ `px-2` và `max-w` phải cộng bù (bong bóng `max-w-[80%]` và cột `55ch` lệch theo). Giữ `-ml-2` kèm comment lý do (`N11` bước 4).
|
|
80
|
+
- Icon lucide **bên trái chữ**, `size-4`, `shrink-0` để nó không bị bóp khi nhãn dài. `aria-hidden` vì chữ đã nói rồi.
|
|
81
|
+
- **Không `white-space: nowrap`.** Đo thật ở một dự án: hộp 140px, nút nowrap rộng 192px, tràn 60px ra ngoài. `leading-tight` để hai dòng không dính nhau. Luật `T15`.
|
|
82
|
+
- **Không `shadow`.** Nút nằm trong trang (`M15`).
|
|
83
|
+
- Chỉ `transition-colors`. Nút không phóng to, không nhấc lên, không đổ bóng thêm khi hover (`F22`).
|
|
84
|
+
- **Không vòng focus** (`I13`), kể cả khi Tab tới.
|
|
85
|
+
- **Chiều cao đặt bằng `min-h-10`, không để `py` tự lo.** `py-2.5` + `leading-tight` thì nút đặc cao 38px, nút viền cao 40px vì cộng 2px viền: hai nút cạnh nhau lệch đáy. `min-h-10` gồm cả viền, nên nút nào cũng 40px; nhãn dài xuống dòng thì nút cao thêm, không tràn (`T15`). Nút chỉ icon `size-10`, cao bằng.
|
|
86
|
+
- **Nút trong form hoặc footer modal** thêm `min-h-11 md:min-h-10` để cao đúng bằng ô nhập (`budgets.md`).
|
|
87
|
+
- Không có prop `size`. Cần nút khác cỡ thì truyền `className` — đỡ đẻ ra ma trận variant nhân size (`I6`). **Nút thấp hơn 40px thì truyền cả `min-h-*`** (`size-8 min-h-8`, link chữ `h-auto min-h-0`): `h-8` hay `size-8` không đè được `min-h-10` của class gốc, nút vẫn cao 40px.
|
|
88
|
+
- Logic class nằm trong `getVariantClasses()` ngoài JSX, không nhét ternary vào giữa markup.
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## Nút xoá
|
|
93
|
+
|
|
94
|
+
Nền đỏ mờ 10% và chữ đỏ, **lúc nào cũng vậy**, không đợi rê vào. Rê vào hoặc Tab
|
|
95
|
+
tới thì nền đậm lên một bậc:
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<Button
|
|
99
|
+
variant="ghost"
|
|
100
|
+
className="bg-rose-500/10 text-rose-700 hover:bg-rose-500/15 hover:text-rose-700 dark:text-rose-400"
|
|
101
|
+
>
|
|
102
|
+
<Trash2 className="size-4 shrink-0" aria-hidden />
|
|
103
|
+
Xoá
|
|
104
|
+
</Button>
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
- **Chữ `rose-700`, không `rose-500`** (nút nguy hiểm `I4`). Đo trên nền `rose-500/10` phủ trắng: `rose-500` chỉ 3.2:1, `rose-600` 3.9:1, đều trượt mức 4.5:1 của chữ 14px. `rose-700` được 5.2:1 mà vẫn đọc ra là đỏ. Nền tối thì ngược lại, chữ sáng lên `rose-400`.
|
|
108
|
+
- **Không dùng Tailwind** thì: nền `var(--danger-bg)`, rê vào `var(--danger-bg-hover)`, chữ + icon `var(--danger)`. Khối `.dark` trong `tokens.css` đã đổi sẵn sang `rose-400`.
|
|
109
|
+
- **Nền mờ, không đỏ đặc.** Nhận ra ngay là nút nguy hiểm nhưng không hét như nút `bg-rose-500 text-white` (`I4`).
|
|
110
|
+
- **Không viền đỏ** (`M30`). Nền mờ đã đủ tách nút khỏi nền trang.
|
|
111
|
+
- Icon cùng màu chữ — không để icon `text-muted` riêng.
|
|
112
|
+
- Bị khoá thì vẫn `opacity-50` như mọi nút.
|
|
113
|
+
- Chỉ áp cho **nút đứng riêng**: hàng nút, hộp xác nhận, khu nguy hiểm trong cài đặt. **Mục trong menu** (dropdown, sidebar) vẫn trung tính lúc thường, rê vào mới đỏ — xem `I4`.
|
|
114
|
+
|
|
115
|
+
> Chủ dự án chốt: nút xoá là nền danger 10% + chữ danger. Bản trước
|
|
116
|
+
> (xám lúc thường, rê vào mới đỏ) đã bỏ cho nút; vẫn giữ cho mục menu.
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Nút chỉ có icon
|
|
121
|
+
|
|
122
|
+
Vuông, cao **bằng đúng** nút chữ đứng cạnh nó, và luôn có `aria-label`:
|
|
123
|
+
|
|
124
|
+
```tsx
|
|
125
|
+
<button
|
|
126
|
+
aria-label="Lọc danh sách"
|
|
127
|
+
className="inline-flex size-10 cursor-pointer items-center justify-center rounded-xl border border-border-strong bg-surface text-muted outline-hidden hover:bg-button-hover hover:text-foreground"
|
|
128
|
+
>
|
|
129
|
+
<SlidersHorizontal className="size-4" aria-hidden />
|
|
130
|
+
</button>
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Chênh chiều cao với ô nhập hay nút chữ bên cạnh dù chỉ một bậc là nhìn ra ngay.
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
## Cảnh báo
|
|
138
|
+
|
|
139
|
+
Một bản `Button` ở project cũ đã phình lên **8 variant, 3 size** và một variant
|
|
140
|
+
`glow` dùng ba lớp radial gradient. Đó là ví dụ ngược. Muốn thêm variant thứ năm
|
|
141
|
+
thì phải trả lời được: nó khác bốn cái kia ở chỗ nào, và vì sao bốn cái kia không
|
|
142
|
+
làm được việc đó.
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Card
|
|
2
|
+
|
|
3
|
+
```tsx
|
|
4
|
+
<section className="flex flex-col rounded-2xl border border-border bg-surface p-5">
|
|
5
|
+
<header className="mb-4 flex items-start justify-between gap-3">
|
|
6
|
+
{/* min-h-10 = cao bằng nút action, CHỈ khi có action: một dòng thì tiêu đề nằm giữa nút, nhiều dòng thì nút bám góc trên */}
|
|
7
|
+
<div className={cn("flex min-w-0 items-center gap-2", action && "min-h-10")}>
|
|
8
|
+
{icon}
|
|
9
|
+
<h2 className="text-base font-semibold text-balance text-foreground">{title}</h2>
|
|
10
|
+
</div>
|
|
11
|
+
<div className="shrink-0">{action}</div>
|
|
12
|
+
</header>
|
|
13
|
+
|
|
14
|
+
<div className="min-h-0 flex-1">{children}</div>
|
|
15
|
+
</section>
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Vì sao ổn**
|
|
19
|
+
|
|
20
|
+
- Card tách khỏi nền bằng **đường tóc 1px + bo góc**, không bằng bóng. Đây là luật `M13`.
|
|
21
|
+
- Dùng đúng `--border`, cùng token với đường chia và khung dropdown, nên trong app không có chỗ đậm chỗ nhạt. Ô nhập thì dùng `--border-strong`, đậm hơn một bậc (`M14`).
|
|
22
|
+
- **Không `shadow-*`.** Bóng chỉ dành cho lớp nổi lên trên trang — modal, dropdown, popover (`M15`). Card nằm trong trang thì không.
|
|
23
|
+
- Tiêu đề card là `text-base font-semibold`, đúng một bậc trên chữ bên trong (`T8`). Không `text-2xl`, không uppercase, không tracking rộng.
|
|
24
|
+
- Header và body cách nhau `mb-4`, padding card `p-5`. Hai con số này lặp lại ở mọi card, không card nào tự chế.
|
|
25
|
+
- Chỗ đặt hành động là một slot `action` ở góc phải header, nên nút thêm hay nút lọc không bao giờ trôi xuống giữa nội dung.
|
|
26
|
+
- **Header `items-start`, không `items-center`.** Tiêu đề dài xuống hai dòng thì nút vẫn bám góc trên phải, không trôi xuống giữa. Khối tiêu đề `min-h-10` bằng chiều cao nút nên lúc chỉ một dòng, chữ vẫn nằm giữa nút. Tiêu đề `min-w-0 text-balance` để xuống dòng đều, nút `shrink-0` để không bị bóp.
|
|
27
|
+
- **`min-h-10` chỉ khi header có nút.** Card chỉ có tiêu đề mà vẫn giữ `min-h-10` thì dòng chữ 24px nằm giữa khung 40px, dư 8px trên 8px dưới: chữ tiêu đề cách mép trên ~32px trong khi nội dung cuối cách mép dưới ~20px, và tiêu đề cách nội dung của nó cũng ~32px. Tiêu đề lơ lửng giữa mép card và nội dung, không bám vào khối nó đặt tên, card hẫng đầu. Ngoại lệ: các card đứng cùng một hàng lưới mà chỉ vài cái có nút thì cả hàng giữ `min-h-10`, để tiêu đề thẳng hàng nhau.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Card chứa dòng có nền rê: padding đặt ở từng khối, không ở card
|
|
32
|
+
|
|
33
|
+
`p-5` trên card chỉ đúng khi thân card là chữ, số, form. Thân là danh sách có nền rê
|
|
34
|
+
(`list-row.md`, dòng `px-2`/`px-3` để nền rê không ôm sát chữ, `F13`) thì chữ dòng thụt
|
|
35
|
+
vào đúng bằng `px` của dòng, lệch mép tiêu đề card. Component `Card` dùng chung phải có sẵn
|
|
36
|
+
cách thứ hai, không để từng chỗ tự vá:
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// isFlushList: thân là danh sách dòng có nền rê. Card không px, tiêu đề px-5, khối danh
|
|
40
|
+
// sách px-2 (= 5 − 3), dòng px-3: chữ dòng thẳng mép chữ tiêu đề, nền rê vẫn tràn ra 12px.
|
|
41
|
+
<section className={cn("flex flex-col rounded-2xl border border-border bg-surface", isFlushList ? "py-5" : "p-5")}>
|
|
42
|
+
<header className={cn("flex items-start justify-between gap-3", isFlushList ? "px-5" : "mb-4", isFlushList && !action && "mb-1.5")}>…</header>
|
|
43
|
+
<div className={cn("min-h-0 flex-1", isFlushList && "px-2")}>{children}</div>
|
|
44
|
+
</section>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- Khối danh sách `px` = padding card − `px` của dòng: dòng `px-3` thì `px-2`, dòng `px-2` thì `px-3`.
|
|
48
|
+
Màn hẹp card `p-4` thì trừ từ 16.
|
|
49
|
+
- **Bớt `mb-4` của header khi thân là danh sách**: dòng đã có `py-2.5` ở trên chữ. Header có
|
|
50
|
+
action thì `mb-0` (khối `min-h-10` đã dư ~10px dưới chữ tiêu đề), không action thì `mb-1.5`.
|
|
51
|
+
Chữ tiêu đề tới chữ dòng đầu khi đó ~20px, bằng khoảng giữa hai dòng. Giữ `mb-4` thì thành
|
|
52
|
+
36px, tiêu đề lơ lửng.
|
|
53
|
+
- Không kéo danh sách ra bằng `-mx-3` (`N11`).
|
|
54
|
+
|
|
55
|
+
Card `p-5` mà dòng `px-2` nằm thẳng trong thân thì chữ dòng lệch chữ tiêu đề 8px, chữ tiêu đề tới
|
|
56
|
+
chữ dòng đầu 36px trong khi giữa hai dòng 20px.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Khi nào dùng `ring` thay `border`
|
|
61
|
+
|
|
62
|
+
`border` ăn vào hộp theo `box-sizing: border-box`, nên phần tử **cỡ cố định** sẽ
|
|
63
|
+
co lại 1px mỗi bên khi bật viền. Với card thì không sao — card co giãn được.
|
|
64
|
+
|
|
65
|
+
Dùng `ring-1` cho thứ có kích thước cố định: avatar, ô vuông icon 28px, thumbnail.
|
|
66
|
+
Xem luật `M17`.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Nhiều mục cùng loại thì MỘT khung, không phải nhiều card
|
|
71
|
+
|
|
72
|
+
Bốn card trắng giống hệt nhau xếp lưới thì mắt đọc ra bốn khối ngang hàng, không
|
|
73
|
+
đọc ra một danh sách. Gom lại:
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
<section className="rounded-2xl border border-border bg-surface">
|
|
77
|
+
<header className="flex items-center justify-between gap-3 px-5 py-4">
|
|
78
|
+
<h2 className="text-base font-semibold text-foreground">{title}</h2>
|
|
79
|
+
{action}
|
|
80
|
+
</header>
|
|
81
|
+
|
|
82
|
+
<ul className="divide-y divide-border border-t border-border">
|
|
83
|
+
{items.map((item) => <ListRow key={item.id} item={item} />)}
|
|
84
|
+
</ul>
|
|
85
|
+
|
|
86
|
+
<footer className="flex justify-end border-t border-border px-5 py-3">
|
|
87
|
+
<ViewAllButton />
|
|
88
|
+
</footer>
|
|
89
|
+
</section>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Dòng tiêu đề và dòng hành động cuối nằm **TRONG** khung. Luật `F3`.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Slot `action` ở header
|
|
97
|
+
|
|
98
|
+
Chỗ đặt hành động phụ của cả khối: "Xem tất cả", "Đọc thêm", nút lọc, nút thêm
|
|
99
|
+
mới. **Luôn căn phải, cùng hàng với tiêu đề.**
|
|
100
|
+
|
|
101
|
+
**Kiểu nút theo loại hành động**, không dùng chung một kiểu cho mọi action:
|
|
102
|
+
|
|
103
|
+
| Hành động | Nút |
|
|
104
|
+
| --- | --- |
|
|
105
|
+
| Dẫn sang màn khác: "Xem tất cả", "Đọc thêm" | **link chữ** `h-8`, không padding ngang, rê vào gạch chân, **không icon** (`I7`) |
|
|
106
|
+
| Làm một việc: tải, xuất, thêm, lọc | nút viền `outline` **có icon trái** (`I1`): `download`, `plus`, `filter` |
|
|
107
|
+
|
|
108
|
+
Đừng dựng "Tải báo cáo" y như "Xem tất cả" (không icon): đọc ra là link sang trang khác chứ không phải nút tải.
|
|
109
|
+
|
|
110
|
+
"Xem tất cả" là link chữ `text-foreground/70`, không màu nhấn, không icon mũi tên,
|
|
111
|
+
không padding ngang nên chữ thẳng mép phải nội dung card. Header có link thì khối
|
|
112
|
+
tiêu đề vẫn `min-h-10` như có nút (link cao `h-8`, thêm `my-1` để thẳng tâm dòng
|
|
113
|
+
tiêu đề). Xem luật `I7`.
|