@comitor/ui 0.1.0 → 0.2.1
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 +303 -43
- package/dist/chart/chart.d.ts +35 -3
- package/dist/chart/chart.js.map +1 -1
- package/dist/chart/comitor-chart.d.ts +16 -3
- package/dist/chart/comitor-chart.js.map +1 -1
- package/dist/chart/index.d.ts +2 -2
- package/dist/components/composite/avatar-group.js +1 -1
- package/dist/components/composite/avatar-group.js.map +1 -1
- package/dist/components/composite/combobox.d.ts +9 -2
- package/dist/components/composite/combobox.js +9 -2
- package/dist/components/composite/combobox.js.map +1 -1
- package/dist/components/composite/confirm-dialog.js +1 -1
- package/dist/components/composite/confirm-dialog.js.map +1 -1
- package/dist/components/composite/copy-button.js +1 -1
- package/dist/components/composite/copy-button.js.map +1 -1
- package/dist/components/composite/data-table.d.ts +17 -8
- package/dist/components/composite/data-table.js +8 -8
- package/dist/components/composite/data-table.js.map +1 -1
- package/dist/components/composite/filter-chips.js +6 -5
- package/dist/components/composite/filter-chips.js.map +1 -1
- package/dist/components/composite/form-field.js +3 -1
- package/dist/components/composite/form-field.js.map +1 -1
- package/dist/components/composite/form-section.js +5 -5
- package/dist/components/composite/form-section.js.map +1 -1
- package/dist/components/composite/inline-edit.js +1 -1
- package/dist/components/composite/inline-edit.js.map +1 -1
- package/dist/components/composite/keyboard-hint.d.ts +16 -0
- package/dist/components/composite/keyboard-hint.js +8 -6
- package/dist/components/composite/keyboard-hint.js.map +1 -1
- package/dist/components/composite/letter-avatar.d.ts +10 -18
- package/dist/components/composite/letter-avatar.js +6 -17
- package/dist/components/composite/letter-avatar.js.map +1 -1
- package/dist/components/composite/search-filter-bar.d.ts +41 -3
- package/dist/components/composite/search-filter-bar.js +27 -15
- package/dist/components/composite/search-filter-bar.js.map +1 -1
- package/dist/components/composite/stat-card.d.ts +16 -1
- package/dist/components/composite/stat-card.js +16 -9
- package/dist/components/composite/stat-card.js.map +1 -1
- package/dist/components/composite/status-config.d.ts +69 -16
- package/dist/components/composite/status-config.js +10 -10
- package/dist/components/composite/status-config.js.map +1 -1
- package/dist/components/composite/status-pill.js +5 -1
- package/dist/components/composite/status-pill.js.map +1 -1
- package/dist/components/composite/table-pagination.d.ts +46 -4
- package/dist/components/composite/table-pagination.js +27 -24
- package/dist/components/composite/table-pagination.js.map +1 -1
- package/dist/components/composite/text-utils.d.ts +12 -11
- package/dist/components/composite/text-utils.js +7 -16
- package/dist/components/composite/text-utils.js.map +1 -1
- package/dist/components/composite/toast.js +16 -6
- package/dist/components/composite/toast.js.map +1 -1
- package/dist/components/ui/alert.js +1 -1
- package/dist/components/ui/alert.js.map +1 -1
- package/dist/components/ui/badge.js +4 -4
- package/dist/components/ui/badge.js.map +1 -1
- package/dist/components/ui/button.js +3 -3
- package/dist/components/ui/button.js.map +1 -1
- package/dist/components/ui/calendar.d.ts +41 -0
- package/dist/components/ui/calendar.js +1 -1
- package/dist/components/ui/calendar.js.map +1 -1
- package/dist/components/ui/card.d.ts +35 -2
- package/dist/components/ui/card.js +17 -3
- package/dist/components/ui/card.js.map +1 -1
- package/dist/components/ui/checkbox.js +3 -3
- package/dist/components/ui/checkbox.js.map +1 -1
- package/dist/components/ui/command.js +1 -1
- package/dist/components/ui/command.js.map +1 -1
- package/dist/components/ui/context-menu.js +1 -1
- package/dist/components/ui/context-menu.js.map +1 -1
- package/dist/components/ui/drawer.js +1 -1
- package/dist/components/ui/drawer.js.map +1 -1
- package/dist/components/ui/dropdown-menu.js +1 -1
- package/dist/components/ui/dropdown-menu.js.map +1 -1
- package/dist/components/ui/empty.js +1 -1
- package/dist/components/ui/empty.js.map +1 -1
- package/dist/components/ui/field.js +4 -4
- package/dist/components/ui/field.js.map +1 -1
- package/dist/components/ui/form.js +8 -2
- package/dist/components/ui/form.js.map +1 -1
- package/dist/components/ui/input-group.js +3 -3
- package/dist/components/ui/input-group.js.map +1 -1
- package/dist/components/ui/input-otp.js +1 -1
- package/dist/components/ui/input-otp.js.map +1 -1
- package/dist/components/ui/input.js +3 -3
- package/dist/components/ui/input.js.map +1 -1
- package/dist/components/ui/item.js +1 -1
- package/dist/components/ui/item.js.map +1 -1
- package/dist/components/ui/menubar.js +1 -1
- package/dist/components/ui/menubar.js.map +1 -1
- package/dist/components/ui/navigation-menu.js +1 -1
- package/dist/components/ui/navigation-menu.js.map +1 -1
- package/dist/components/ui/pagination.js +1 -1
- package/dist/components/ui/pagination.js.map +1 -1
- package/dist/components/ui/progress.d.ts +26 -0
- package/dist/components/ui/progress.js +5 -2
- package/dist/components/ui/progress.js.map +1 -1
- package/dist/components/ui/radio-group.js +2 -2
- package/dist/components/ui/radio-group.js.map +1 -1
- package/dist/components/ui/resizable.d.ts +14 -0
- package/dist/components/ui/resizable.js +2 -2
- package/dist/components/ui/resizable.js.map +1 -1
- package/dist/components/ui/scroll-area.js +1 -1
- package/dist/components/ui/scroll-area.js.map +1 -1
- package/dist/components/ui/select.d.ts +33 -0
- package/dist/components/ui/select.js +3 -3
- package/dist/components/ui/select.js.map +1 -1
- package/dist/components/ui/slider.d.ts +19 -0
- package/dist/components/ui/slider.js +3 -3
- package/dist/components/ui/slider.js.map +1 -1
- package/dist/components/ui/sonner.d.ts +21 -3
- package/dist/components/ui/sonner.js +1 -2
- package/dist/components/ui/sonner.js.map +1 -1
- package/dist/components/ui/switch.js +2 -2
- package/dist/components/ui/switch.js.map +1 -1
- package/dist/components/ui/table.d.ts +16 -0
- package/dist/components/ui/table.js +1 -1
- package/dist/components/ui/table.js.map +1 -1
- package/dist/components/ui/tabs.js +1 -1
- package/dist/components/ui/tabs.js.map +1 -1
- package/dist/components/ui/textarea.js +1 -1
- package/dist/components/ui/textarea.js.map +1 -1
- package/dist/components/ui/toggle.js +1 -1
- package/dist/components/ui/toggle.js.map +1 -1
- package/dist/index.d.ts +6 -5
- package/dist/index.js +17 -2
- package/dist/index.js.map +1 -1
- package/dist/lib/avatar-tone.d.ts +126 -0
- package/dist/lib/avatar-tone.js +68 -0
- package/dist/lib/avatar-tone.js.map +1 -0
- package/dist/shell/app-header.js +12 -2
- package/dist/shell/app-header.js.map +1 -1
- package/dist/shell/app-launcher.js +2 -2
- package/dist/shell/app-launcher.js.map +1 -1
- package/dist/shell/app-shell.d.ts +2 -0
- package/dist/shell/index.d.ts +3 -3
- package/dist/shell/shell-context.d.ts +29 -8
- package/dist/shell/shell-context.js +6 -20
- package/dist/shell/shell-context.js.map +1 -1
- package/dist/shell/sidebar-group.js +5 -3
- package/dist/shell/sidebar-group.js.map +1 -1
- package/dist/shell/sidebar-menu-item.js +15 -7
- package/dist/shell/sidebar-menu-item.js.map +1 -1
- package/dist/shell/types.d.ts +17 -1
- package/dist/shell/types.js.map +1 -1
- package/dist/shell/user-menu.js +3 -2
- package/dist/shell/user-menu.js.map +1 -1
- package/dist/shell/utils.d.ts +36 -24
- package/dist/shell/utils.js +55 -20
- package/dist/shell/utils.js.map +1 -1
- package/dist/shell/workspace-switcher.js +23 -8
- package/dist/shell/workspace-switcher.js.map +1 -1
- package/dist/tokens.d.ts +165 -17
- package/dist/tokens.js +122 -25
- package/dist/tokens.js.map +1 -1
- package/package.json +8 -5
- package/styles.css +312 -22
package/README.md
CHANGED
|
@@ -8,11 +8,16 @@ component ghép sẵn và khung ứng dụng (shell) kiểu workspace × app.
|
|
|
8
8
|
- Token brand nằm ở **`styles.css`** — đổi token, bump version, mọi app nhận cùng một thay đổi.
|
|
9
9
|
- Server-component-friendly: `"use client"` chỉ nằm ở file thật sự cần.
|
|
10
10
|
|
|
11
|
-
> Trạng thái: `0.
|
|
12
|
-
>
|
|
13
|
-
>
|
|
14
|
-
>
|
|
15
|
-
>
|
|
11
|
+
> Trạng thái: `0.2.0` — 51 primitive + 24 composite + 18 file shell + chart, đã qua **một đợt sửa theo
|
|
12
|
+
> review** (tương phản màu theo WCAG, thang đổ bóng 4 bậc, a11y tầng shell, chuỗi hiển thị của Tier 2
|
|
13
|
+
> override được) và **một đợt dọn bề mặt API** (0.1.0 chưa app nào import — lần cuối đổi/xoá tên mà
|
|
14
|
+
> không ai phải viết codemod).
|
|
15
|
+
>
|
|
16
|
+
> ⚠ **`0.2.0` phá vỡ tương thích với `0.1.0`**: đổi tên hàm màu avatar, bỏ `avatarPalette` /
|
|
17
|
+
> `LETTER_AVATAR_TONES` / `hashString` / `pickByString`, `useIsMac` chỉ còn một cửa, `DataTable` đổi
|
|
18
|
+
> tên prop, và **giá trị một số token màu đã đổi**. Bảng chuyển đổi đầy đủ:
|
|
19
|
+
> [CHANGELOG.md](CHANGELOG.md). Trong dải `0.x`, thay đổi phá vỡ tương thích đi kèm bump **minor**;
|
|
20
|
+
> API chốt ở `1.0.0`.
|
|
16
21
|
|
|
17
22
|
---
|
|
18
23
|
|
|
@@ -36,9 +41,19 @@ pnpm add react-hook-form # cho @comitor/ui/form
|
|
|
36
41
|
| `lucide-react` | `>=0.540.0` | ✅ | luôn (tránh 2 bản icon trong bundle) |
|
|
37
42
|
| `next` | `>=15` | — | `/shell` |
|
|
38
43
|
| `next-themes` | `>=0.4` | — | `/shell` (context singleton — app và package phải dùng **cùng một bản**) |
|
|
39
|
-
| `recharts` |
|
|
44
|
+
| `recharts` | `>=2.15.0` | — | `/chart` (kiểu đã dựng cho **cả 2.x lẫn 3.x**, xem ghi chú dưới) |
|
|
40
45
|
| `react-hook-form` | `^7.54.0` | — | `/form` (context singleton) |
|
|
41
46
|
|
|
47
|
+
> **Vì sao `recharts` là `>=` chứ không phải `^`.** `^2.15.0` loại thẳng recharts 3 — mà 3.x
|
|
48
|
+
> chạy tốt với gói này, nên app nào đã lên 3.x cũng ăn một dòng `unmet peer` không có cách sửa.
|
|
49
|
+
> Kiểu của tầng `/chart` không còn dẫn xuất `payload`/`label` từ `TooltipProps`/`LegendProps`
|
|
50
|
+
> của recharts (3.x đã bỏ hai trường đó khỏi props, chuyển sang context nội bộ) mà tự khai
|
|
51
|
+
> `ChartPayloadItem` — phần giao của hai bản. `pnpm typecheck && pnpm build` được chạy **trên cả
|
|
52
|
+
> recharts 2.15.4 lẫn 3.10.1** trước khi nới dải này.
|
|
53
|
+
>
|
|
54
|
+
> `lucide-react` cũng là `>=` vì cùng lý do: thang phiên bản của nó nhảy `0.x` → `1.x` mà API
|
|
55
|
+
> icon không đổi. Đã kiểm typecheck + build với `1.33.0`.
|
|
56
|
+
|
|
42
57
|
## 2. Nạp `styles.css` — ĐỌC KỸ, đây là chỗ dễ sai nhất
|
|
43
58
|
|
|
44
59
|
Đặt import trong **`app/globals.css`**, ngay sau `@import "tailwindcss"`:
|
|
@@ -117,7 +132,7 @@ barrel chính re-export **một** file có `import` tới peer optional là mọ
|
|
|
117
132
|
|
|
118
133
|
| Entry | Nội dung | Peer optional kéo theo |
|
|
119
134
|
|---|---|---|
|
|
120
|
-
| `@comitor/ui` | Tier 1 (primitive) + Tier 2 (composite) + `cn` + token TS | **không có** — chạy được ở Vite, Storybook, test runner |
|
|
135
|
+
| `@comitor/ui` | Tier 1 (primitive) + Tier 2 (composite) + `cn` + token TS + `useIsMac` | **không có** — chạy được ở Vite, Storybook, test runner |
|
|
121
136
|
| `@comitor/ui/shell` | Tier 3: `AppShell`, `Sidebar`, `AppHeader`, `WorkspaceSwitcher`, `AppLauncher`, `UserMenu`, `ThemeProvider`, `SonnerToaster` | `next`, `next-themes` |
|
|
122
137
|
| `@comitor/ui/chart` | `Chart*` (shadcn) + `ComitorAreaChart/BarChart/LineChart` + `CHART_COLORS` | `recharts` (~100KB) |
|
|
123
138
|
| `@comitor/ui/form` | Binding shadcn ⇄ react-hook-form: `Form`, `FormField`, `FormItem`, `FormControl`, `FormMessage`, `useFormField` | `react-hook-form` |
|
|
@@ -126,20 +141,25 @@ barrel chính re-export **một** file có `import` tới peer optional là mọ
|
|
|
126
141
|
|
|
127
142
|
### Tên xuất hiện ở nhiều entry
|
|
128
143
|
|
|
129
|
-
Số tên mỗi entry xuất ra (đo bằng trình biên dịch TypeScript
|
|
130
|
-
|
|
131
|
-
|
|
144
|
+
Số tên mỗi entry xuất ra (đo bằng trình biên dịch TypeScript trên đúng 5 file entry, giải alias về khai
|
|
145
|
+
báo gốc — không đếm tay): `.` **637** · `/shell` **70** · `/chart` **29** · `/form` **15** · `/tokens`
|
|
146
|
+
**15**. Đúng **19 tên** xuất hiện ở nhiều hơn một entry, gom thành 4 nhóm dưới đây — `export *` chỉ báo
|
|
147
|
+
đỏ khi trùng tên trong **cùng một** entry, nên trùng xuyên entry phải tra bằng bảng này:
|
|
132
148
|
|
|
133
149
|
| Tên | Ở `@comitor/ui` | Ở entry khác | Cùng một cài đặt? |
|
|
134
150
|
|---|---|---|---|
|
|
135
151
|
| `FormField`, `FormFieldProps` | Tier 2 — bố cục 12 cột (label + control + mô tả + lỗi) | `/form` — wrapper của `Controller` (API shadcn) | **KHÔNG** — hai component khác hẳn nhau |
|
|
136
|
-
| `
|
|
152
|
+
| `normalizeVi` | `composite/text-utils.ts` | `/shell` — **re-export đúng bản đó** | **CÓ** — một thân hàm duy nhất (`0.2.0` gộp; trước đó là hai khai báo riêng giống nhau chữ-cho-chữ) |
|
|
137
153
|
| `getVietnameseInitials` | `composite/text-utils.ts` (`toLocaleUpperCase("vi-VN")`) | `/shell` — **re-export đúng bản đó** | **CÓ** — một thân hàm duy nhất |
|
|
138
|
-
|
|
|
154
|
+
| 15 token: `gold`, `ash`, `accent`, `accentInk`, `accentForeground`, `brandNeutral`, `semantic`, `status`, `chart`, `layout`, `typography`, `ThemeMode`, `getAvatarToneColors`, `getAvatarToneIndex`, `AvatarToneColors` | `export * from "./tokens.js"` | `/tokens` — **cùng một module** | **CÓ** — `/tokens` là tập con của `.`, tách ra cho nơi chỉ cần hằng số (email HTML, PDF, canvas) |
|
|
139
155
|
|
|
140
156
|
Quy tắc dùng: hằng ngày lấy bản ở `@comitor/ui`; chỉ với tới `/form` khi app đã dùng react-hook-form.
|
|
141
|
-
|
|
142
|
-
|
|
157
|
+
Chỉ `FormField` là thật sự hai component khác nhau, phải nhìn đúng entry.
|
|
158
|
+
|
|
159
|
+
`0.2.0` đã dọn ba chỗ của `0.1.0`: **`useIsMac`** (từng có hai thân hàm khác nhau ở `.` và `/shell`)
|
|
160
|
+
nay chỉ còn cửa `@comitor/ui`; **`avatarPalette`** (bảng màu avatar thứ hai) bị xoá; **`normalizeVi`**
|
|
161
|
+
gộp về một binding. Còn đúng **`FormField`/`FormFieldProps`** là hai cài đặt khác nhau trong bảng trên
|
|
162
|
+
— 16/18 tên còn lại đều trỏ vào cùng một binding. Xem [CHANGELOG.md](CHANGELOG.md).
|
|
143
163
|
|
|
144
164
|
> **`Toaster` KHÔNG phải tên trùng.** Có hai toaster nhưng chúng mang hai tên: `Toaster` ở
|
|
145
165
|
> `@comitor/ui` (toast có icon theo trạng thái, nhận prop `theme`) và **`SonnerToaster`** ở
|
|
@@ -281,6 +301,39 @@ import { ThemeProvider } from "@comitor/ui/shell";
|
|
|
281
301
|
App **không chạy trên Next**: truyền `pathname` và `LinkComponent` cho `ShellProvider`/`AppShell`
|
|
282
302
|
để khỏi chạm `next/navigation`.
|
|
283
303
|
|
|
304
|
+
#### Mục menu phân biệt nhau bằng query — prop `searchParams`
|
|
305
|
+
|
|
306
|
+
`usePathname()` của Next **không bao giờ** chứa `?query`. Vì vậy mục khai
|
|
307
|
+
`href: "/cai-dat?tab=thanh-vien"` được so bằng phần ĐƯỜNG DẪN (`/cai-dat`) — nó sáng khi bạn đang
|
|
308
|
+
ở `/cai-dat`, và nếu menu có nhiều tab cùng đường dẫn thì **tất cả cùng sáng**, vì shell không
|
|
309
|
+
thấy query.
|
|
310
|
+
|
|
311
|
+
Muốn đúng từng tab thì đưa query cho shell qua prop **optional** `searchParams`:
|
|
312
|
+
|
|
313
|
+
```tsx
|
|
314
|
+
"use client";
|
|
315
|
+
import { useSearchParams } from "next/navigation";
|
|
316
|
+
import { AppShell } from "@comitor/ui/shell";
|
|
317
|
+
|
|
318
|
+
export function Shell({ children }: { children: React.ReactNode }) {
|
|
319
|
+
const searchParams = useSearchParams(); // nhớ có <Suspense> ở trên trang
|
|
320
|
+
return <AppShell nav={nav} searchParams={searchParams}>{children}</AppShell>;
|
|
321
|
+
}
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
Nhận cả `URLSearchParams`, chuỗi `"?tab=x"`, lẫn object `searchParams` của Server Component.
|
|
325
|
+
Luật khi đã có nó:
|
|
326
|
+
|
|
327
|
+
- mục có query sáng khi query của nó là **tập con** của URL hiện tại (`?tab=a` vẫn sáng ở
|
|
328
|
+
`?tab=a&q=abc&trang=2`);
|
|
329
|
+
- mục **không** query nhường cho mục cùng đường dẫn có query khớp — tab mặc định không sáng cùng
|
|
330
|
+
lúc với tab đang mở;
|
|
331
|
+
- luật cũ "chỉ `startsWith` khi không có mục nào khớp chính xác" giữ nguyên.
|
|
332
|
+
|
|
333
|
+
> Shell **không** tự gọi `useSearchParams()`: hook đó ép trang phải bọc `<Suspense>` và đẩy trang
|
|
334
|
+
> sang render phía client khi build tĩnh. Cái giá đó phải do app chọn, không phải hệ quả của việc
|
|
335
|
+
> import shell.
|
|
336
|
+
|
|
284
337
|
> ⚠ Entry `/shell` chỉ nạp được **qua bundler** (webpack/turbopack/vite). Nạp bằng Node ESM thuần sẽ
|
|
285
338
|
> lỗi `Cannot find module 'next/link'` — vì `next@16` không khai `exports` map nên Node không resolve
|
|
286
339
|
> được subpath không đuôi. Test runner chạy resolution kiểu Node thì alias `next/link` → `next/link.js`,
|
|
@@ -290,17 +343,27 @@ App **không chạy trên Next**: truyền `pathname` và `LinkComponent` cho `S
|
|
|
290
343
|
### 4.3 Token ngoài Tailwind
|
|
291
344
|
|
|
292
345
|
```ts
|
|
293
|
-
import { chart,
|
|
346
|
+
import { chart, getAvatarToneColors, layout, semantic } from "@comitor/ui/tokens";
|
|
347
|
+
|
|
348
|
+
const seriesColor = chart.light[0]; // #E8B824
|
|
349
|
+
const headerPx = layout.headerHeight; // 56
|
|
350
|
+
const errorText = semantic.dark.destructiveInk; // mực đỏ trên nền tối
|
|
294
351
|
|
|
295
|
-
|
|
296
|
-
const
|
|
352
|
+
// Avatar trong email HTML / canvas / PDF — cùng tone với avatar trên web
|
|
353
|
+
const { background, foreground } = getAvatarToneColors("Nguyễn Đức Thành", "light");
|
|
354
|
+
// { background: "#E0F2E9", foreground: "#216A48" }
|
|
297
355
|
```
|
|
298
356
|
|
|
357
|
+
> Hai cửa cho **một** bảng màu avatar: `getAvatarToneClasses(name)` ở `@comitor/ui` (có Tailwind → trả
|
|
358
|
+
> chuỗi class, đổi theo theme) và `getAvatarToneColors(name, mode)` ở `@comitor/ui/tokens` (không có
|
|
359
|
+
> Tailwind → trả hex đục đã trộn sẵn tint). Cùng bảng tone, cùng hàm băm, nên một người ra một màu ở cả
|
|
360
|
+
> hai nơi. **Đừng** tự băm tên rồi bốc màu từ `accent` — đó là đường ngắn nhất để web và email lệch màu.
|
|
361
|
+
|
|
299
362
|
## 5. Bảng component
|
|
300
363
|
|
|
301
|
-
Số liệu thực tế của `0.1
|
|
302
|
-
(chưa kể barrel) — số tên xuất ra theo entry: `.` **
|
|
303
|
-
**15** · `/tokens` **
|
|
364
|
+
Số liệu thực tế của `0.2.1`: **51 file primitive · 24 file composite · 18 file shell · 2 file chart**
|
|
365
|
+
(chưa kể barrel) — số tên xuất ra theo entry: `.` **637** · `/shell` **70** · `/chart` **29** · `/form`
|
|
366
|
+
**15** · `/tokens` **15** (xem §3 cho các tên xuất hiện ở nhiều entry). Bảng dưới liệt kê component gốc;
|
|
304
367
|
các thành phần con (`CardHeader`, `DialogFooter`, `SelectItem`…) export kèm theo file tương ứng.
|
|
305
368
|
|
|
306
369
|
### Tier 1 — primitive (`@comitor/ui`)
|
|
@@ -324,6 +387,35 @@ shadcn/ui style `new-york`, đã tùy biến token theo brand Comitor.
|
|
|
324
387
|
> cookie riêng. App nào thật sự cần bản shadcn thì `npx shadcn@latest add sidebar` vào repo của mình.
|
|
325
388
|
> Lý do đầy đủ: [docs/quyet-dinh-tich-hop.md](docs/quyet-dinh-tich-hop.md) §17.
|
|
326
389
|
|
|
390
|
+
#### `CardHeader` có nút ở góc phải — prop `actions`
|
|
391
|
+
|
|
392
|
+
`CardHeader` mặc định xếp DỌC (`flex-col space-y-1.5`). Thẻ có nút ở góc phải vì thế phải ghi đè
|
|
393
|
+
đúng một chuỗi bốn lớp, ở mọi app, mọi thẻ:
|
|
394
|
+
|
|
395
|
+
```tsx
|
|
396
|
+
/* ✗ Cách cũ — chép tay, dễ sót `space-y-0` (thiếu nó thì nút bị đẩy lệch xuống) */
|
|
397
|
+
<CardHeader className="flex-row items-start justify-between gap-3 space-y-0">
|
|
398
|
+
<div>
|
|
399
|
+
<CardTitle>Thông tin đơn vị</CardTitle>
|
|
400
|
+
<CardDescription>Cập nhật lần cuối hôm qua</CardDescription>
|
|
401
|
+
</div>
|
|
402
|
+
<Button size="sm" variant="ghost">Sửa</Button>
|
|
403
|
+
</CardHeader>
|
|
404
|
+
|
|
405
|
+
/* ✓ Từ 0.2.1 — `actions` gói đúng chuỗi đó lại */
|
|
406
|
+
<CardHeader actions={<Button size="sm" variant="ghost">Sửa</Button>}>
|
|
407
|
+
<CardTitle>Thông tin đơn vị</CardTitle>
|
|
408
|
+
<CardDescription>Cập nhật lần cuối hôm qua</CardDescription>
|
|
409
|
+
</CardHeader>
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Không truyền `actions` thì lớp và cây DOM **y hệt `0.2.0`** — prop này chỉ thêm một nhánh, không
|
|
413
|
+
đổi mặc định. Cụm nút nhận `actionsClassName` riêng khi cần đổi `gap`/cho xuống dòng.
|
|
414
|
+
|
|
415
|
+
Quan hệ với `CardAction`: `CardAction` là ô `ml-auto` để tự lắp khi bạn **đã** tự đặt header thành
|
|
416
|
+
hàng ngang; `actions` làm cả hai việc (đổi bố cục *và* đặt chỗ) nên là đường đi mặc định.
|
|
417
|
+
`CardAction` giữ nguyên cho ai đã dùng.
|
|
418
|
+
|
|
327
419
|
### Tier 2 — composite (`@comitor/ui`)
|
|
328
420
|
|
|
329
421
|
Giá trị thật của package: pattern lặp đi lặp lại ở mọi app, ghép từ nhiều primitive.
|
|
@@ -331,31 +423,37 @@ Giá trị thật của package: pattern lặp đi lặp lại ở mọi app, gh
|
|
|
331
423
|
| Component | Công dụng |
|
|
332
424
|
|---|---|
|
|
333
425
|
| `PageHeader`, `PageContainer` | Khung đầu trang (breadcrumb + hành động) và khung nội dung chuẩn |
|
|
334
|
-
| `DataTable`, `DataTableColumnToggle
|
|
335
|
-
| `
|
|
426
|
+
| `DataTable`, `DataTableColumnToggle` | Bảng dữ liệu: sort, chọn dòng, ẩn/hiện cột. Mọi chuỗi (kể cả `aria-label`) override được qua prop `labels` — xem `DataTableLabels` / `DEFAULT_DATA_TABLE_LABELS` |
|
|
427
|
+
| `TablePagination` | Phân trang đi kèm bảng. Chuỗi override qua prop `labels` — `TablePaginationLabels` / `DEFAULT_TABLE_PAGINATION_LABELS` (khoá có số/đơn vị chèn vào là **hàm**, vd `range(start, end, total, unitLabel)`) |
|
|
428
|
+
| `SearchFilterBar`, `FilterChips` | Thanh tìm + lọc + đếm kết quả. `SearchFilterBar` override chuỗi qua `labels` — `SearchFilterBarLabels` / `DEFAULT_SEARCH_FILTER_BAR_LABELS`; `FilterChips` nhận nhãn thẳng trong dữ liệu chip nên không có `labels` |
|
|
336
429
|
| `StatCard`, `StatusPill`, `EmptyState` | Số liệu có delta, nhãn trạng thái, trạng thái rỗng |
|
|
337
430
|
| `FormSection`, `FormField` | Bố cục form 12 cột; uỷ quyền label/mô tả/lỗi xuống `Field` |
|
|
338
|
-
| `Combobox`, `MultiCombobox` | Chọn có tìm kiếm, **khớp không dấu tiếng Việt** |
|
|
431
|
+
| `Combobox`, `MultiCombobox` | Chọn có tìm kiếm, **khớp không dấu tiếng Việt**. `aria-invalid` đổi trigger sang **viền đỏ** (`--destructive-ink`), cùng tín hiệu với `Input`; nhận cả `boolean` lẫn `"true"`/`"false"` để dùng thẳng `aria-invalid={!!errors.x}` |
|
|
339
432
|
| `DatePicker`, `DateRangePicker` | Chọn ngày/khoảng ngày, locale `vi`, preset dựng sẵn |
|
|
340
|
-
| `LetterAvatar`, `AvatarGroup` | Avatar chữ cái theo tên tiếng Việt (tông màu deterministic), nhóm người tham gia |
|
|
433
|
+
| `LetterAvatar`, `AvatarGroup` | Avatar chữ cái theo tên tiếng Việt (tông màu deterministic), nhóm người tham gia. Lấy tone bằng `getAvatarToneClasses(name)`; bản hex cho canvas/email/PDF là `getAvatarToneColors(name, mode)` ở `/tokens` — **cùng một bảng tone**, cùng một hàm băm |
|
|
341
434
|
| `ConfirmDialog`, `CommandPalette` | Hộp xác nhận, bảng lệnh `⌘K` |
|
|
342
435
|
| `Toaster`, `toast()`, `toastPromise()`, `dismissToast()` | Thông báo có icon theo trạng thái |
|
|
343
436
|
| `CopyButton`, `RelativeTime`, `InlineEdit`, `KeyboardHint` | Tiện ích nhỏ dùng khắp nơi |
|
|
344
|
-
| `useIsMac` | Hook đi kèm `KeyboardHint` — `true` khi chạy trên macOS (render đầu luôn `false`, xem §3) |
|
|
437
|
+
| `useIsMac` | Hook đi kèm `KeyboardHint` — `true` khi chạy trên macOS (render đầu luôn `false`, xem §3). **Cửa xuất duy nhất của cả gói**; `/shell` không re-export |
|
|
345
438
|
|
|
346
439
|
Kèm 2 module thuần (không phải component, dùng được cả hai phía):
|
|
347
440
|
|
|
348
441
|
| Module | Export |
|
|
349
442
|
|---|---|
|
|
350
443
|
| `status-config` | `STATUS_TONES`, `getStatusConfig`, `getStatusClasses`, `getStatusLabel`, `sortByStatusPriority` |
|
|
351
|
-
| `text-utils` | `removeVietnameseTones`, `
|
|
444
|
+
| `text-utils` | `removeVietnameseTones`, `normalizeVi`, `matchesSearch`, `getVietnameseInitials`, `truncate` |
|
|
445
|
+
|
|
446
|
+
> `text-utils` **không còn** `hashString` / `pickByString` (0.1.0 có). Hàm băm là chuyện nội bộ của phép
|
|
447
|
+
> chọn tone avatar; xuất nó ra ngoài chỉ mời app dựng lại phép chọn của riêng mình — đúng thứ làm cùng
|
|
448
|
+
> một người hiện hai màu ở web và email. Cần màu avatar thì dùng `getAvatarToneClasses` /
|
|
449
|
+
> `getAvatarToneColors`; cần một màu tuỳ ý thì truyền `toneClassName` cho `LetterAvatar`.
|
|
352
450
|
|
|
353
451
|
### Tier 3 — shell (`@comitor/ui/shell`)
|
|
354
452
|
|
|
355
453
|
| Component | Công dụng |
|
|
356
454
|
|---|---|
|
|
357
455
|
| `AppShell` | Lắp sẵn `ShellProvider` + `Sidebar` + `AppHeader` + `<main>` + `MobileMenu`; hai layout `sidebar-first` / `header-first` |
|
|
358
|
-
| `ShellProvider`, `useShell
|
|
456
|
+
| `ShellProvider`, `useShell` | Context + phím tắt toàn cục; dùng khi tự lắp khung. (`useIsMac` **không** ở entry này — nhập từ `@comitor/ui`) |
|
|
359
457
|
| `WorkspaceSwitcher` | **Trục 1** — đổi ngữ cảnh dữ liệu; dùng `Popover` (không `DropdownMenu`, vì typeahead của Radix cướp phím ô tìm kiếm) |
|
|
360
458
|
| `AppLauncher`, `AppLauncherPanel` | **Trục 2** — lưới 3×3 đổi sản phẩm; app chưa mua hiện khoá và gọi `onUpsell` |
|
|
361
459
|
| `AppHeader`, `Sidebar`, `SidebarGroup`, `SidebarMenuItem`, `MobileMenu` | Thành phần khung điều hướng |
|
|
@@ -363,7 +461,7 @@ Kèm 2 module thuần (không phải component, dùng được cả hai phía):
|
|
|
363
461
|
| `ThemeProvider`, `ThemeToggle`, `SonnerToaster` | Theme (next-themes) + toast bám theme |
|
|
364
462
|
| `ShellNavLink` | Link nội bộ của shell; thay `next/link` bằng `LinkComponent` khi không chạy Next |
|
|
365
463
|
| `ComitorLogo`, `ComitorLockup` | Vector brand — **không** có `"use client"`, RSC render thẳng (hex trong SVG là brand book, không tokenize) |
|
|
366
|
-
| `DEFAULT_SHELL_LABELS` + `utils` | Chuỗi hiển thị mặc định; helper `flattenNavItems`, `isNavItemActive`, `resolveCurrentApp`, `getEntitledApps`, `normalizeVi
|
|
464
|
+
| `DEFAULT_SHELL_LABELS` + `utils` | Chuỗi hiển thị mặc định (`ShellLabels`, 32 khoá); helper `flattenNavItems`, `isNavItemActive`, `isNavBranchActive`, `toNavGroups`, `resolveCurrentApp`, `getEntitledApps`, `getLockedApps`, `appAccentStyle`, `normalizeVi`, `getVietnameseInitials` |
|
|
367
465
|
|
|
368
466
|
Hợp đồng dữ liệu (type): `Workspace`, `AppDescriptor`, `NavItem`, `NavGroup`, `ShellNav`, `ShellUser`,
|
|
369
467
|
`UserMenuItem`, `ShellLabels`, `ShellIcon`, `ShellLinkComponent`.
|
|
@@ -382,29 +480,163 @@ Hợp đồng dữ liệu (type): `Workspace`, `AppDescriptor`, `NavItem`, `NavG
|
|
|
382
480
|
`Form`, `FormField`, `FormItem`, `FormLabel`, `FormControl`, `FormDescription`, `FormMessage`, `useFormField`
|
|
383
481
|
— binding shadcn ⇄ react-hook-form. ⚠ `FormField` ở đây **khác** `FormField` của Tier 2 (§3).
|
|
384
482
|
|
|
385
|
-
|
|
483
|
+
### Cơ chế `labels` — đổi ngôn ngữ mà không fork component
|
|
484
|
+
|
|
485
|
+
Chuỗi hiển thị mặc định của gói là **tiếng Việt**, kể cả chuỗi chỉ trình đọc màn hình nghe thấy
|
|
486
|
+
(`aria-label`, `sr-only`). App dùng ngôn ngữ khác override qua prop `labels` — không phải fork
|
|
487
|
+
component, không phải bọc lại.
|
|
488
|
+
|
|
489
|
+
| Component | Type | Hằng mặc định |
|
|
490
|
+
|---|---|---|
|
|
491
|
+
| `DataTable`, `DataTableColumnToggle` | `DataTableLabels` | `DEFAULT_DATA_TABLE_LABELS` |
|
|
492
|
+
| `TablePagination` | `TablePaginationLabels` | `DEFAULT_TABLE_PAGINATION_LABELS` |
|
|
493
|
+
| `SearchFilterBar` | `SearchFilterBarLabels` | `DEFAULT_SEARCH_FILTER_BAR_LABELS` |
|
|
494
|
+
| Toàn bộ Tier 3 (`AppShell`/`ShellProvider` truyền xuống) | `ShellLabels` (32 khoá) | `DEFAULT_SHELL_LABELS` |
|
|
495
|
+
| `ThemeToggle` (dùng độc lập, ngoài shell) | `ThemeToggleLabels` = 4 khoá theme của `ShellLabels` | `DEFAULT_SHELL_LABELS` |
|
|
496
|
+
|
|
497
|
+
Ba quy tắc chung cho mọi bộ nhãn:
|
|
498
|
+
|
|
499
|
+
1. **`Partial<>` — override từng khoá.** Component luôn trải `{ ...DEFAULT_*, ...labels }`, nên truyền
|
|
500
|
+
một khoá không làm mất phần còn lại.
|
|
501
|
+
2. **Khoá có số/đơn vị chèn vào là HÀM, không phải chuỗi có placeholder.** Nối chuỗi bằng dấu cộng ở
|
|
502
|
+
phía app là cách chắc chắn sai trật tự từ với ngôn ngữ khác:
|
|
503
|
+
`range(start, end, total, unitLabel)`, `pageStatus(page, pageCount)`, `filterCount(count)`,
|
|
504
|
+
`pageSizeSelect(unitLabel)`. `resultSummary` còn trả `ReactNode` vì con số cần thẻ riêng
|
|
505
|
+
(`tabular-nums` để số không nhảy cột).
|
|
506
|
+
3. **Prop rời thắng `labels`.** `unitLabel`, `label`, `searchPlaceholder` vẫn là prop riêng và vẫn ghi
|
|
507
|
+
đè giá trị trong `labels`: chúng là chuỗi theo **nội dung** của từng bảng ("việc", "liên hệ"), còn
|
|
508
|
+
`labels` là chuỗi theo **ngôn ngữ** ("mục" / "items").
|
|
509
|
+
|
|
510
|
+
```tsx
|
|
511
|
+
const EN_PAGINATION: Partial<TablePaginationLabels> = {
|
|
512
|
+
region: "Pagination",
|
|
513
|
+
unitLabel: "items",
|
|
514
|
+
pageSizePrefix: "Show",
|
|
515
|
+
range: (start, end, total, unit) => `${start}–${end} of ${total} ${unit}`,
|
|
516
|
+
pageStatus: (page, count) => `Page ${page} of ${count}`,
|
|
517
|
+
firstPage: "First page",
|
|
518
|
+
previousPage: "Previous page",
|
|
519
|
+
nextPage: "Next page",
|
|
520
|
+
lastPage: "Last page"
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
<TablePagination {...paging} labels={EN_PAGINATION} unitLabel="contacts" />
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
`FilterChips` **không** có `labels` — nhãn của nó nằm thẳng trong dữ liệu chip, không có chuỗi nào chôn
|
|
527
|
+
trong component.
|
|
528
|
+
|
|
529
|
+
## 6. Màu và token
|
|
386
530
|
|
|
387
531
|
Toàn bộ token nằm ở [`styles.css`](./styles.css); bản mirror TS ở [`src/tokens.ts`](./src/tokens.ts).
|
|
388
532
|
|
|
533
|
+
### 6.1 Hợp đồng BA VAI TRÒ của màu — đọc trước khi dùng bất kỳ màu nào
|
|
534
|
+
|
|
535
|
+
Đây là quy tắc quan trọng nhất của gói. Mỗi màu có **ba** biến thể, đọc tên là biết dùng ở đâu:
|
|
536
|
+
|
|
537
|
+
| Tên | Vai trò | Đo tương phản với | Ví dụ dùng |
|
|
538
|
+
|---|---|---|---|
|
|
539
|
+
| `--x` | tô **NỀN** — fill đặc hoặc tint `/10`–`/25` | — | `bg-teal`, `bg-teal/15` |
|
|
540
|
+
| `--x-foreground` | chữ/icon **NẰM TRÊN** nền `--x` đặc | `--x` | `bg-destructive text-destructive-foreground` |
|
|
541
|
+
| `--x-ink` | **chính màu đó** khi đi lên nền TRANG: chữ, icon rời, viền, vạch active | `--background` / `--card` / `--muted`, hoặc tint của chính nó | `text-teal-ink`, `border-red-ink` |
|
|
542
|
+
|
|
543
|
+
**Vì sao phải có bậc `-ink` riêng thay vì dùng thẳng `--teal` / `--green` / `--red`**: bản light của màu
|
|
544
|
+
phụ được chọn để tô **nền** nên quá nhạt cho vai trò chữ. Đo trên tint 15% của chính nó — nền thật mà
|
|
545
|
+
chữ initials avatar và badge tonal đứng lên:
|
|
546
|
+
|
|
547
|
+
```
|
|
548
|
+
teal #1F8A8B trên #DDEDEE = 3,44:1 ✗ → --teal-ink #1A6668 = 5,54:1 ✓
|
|
549
|
+
green #2EA56A trên #E0F2E9 = 2,69:1 ✗ → --green-ink #216A48 = 5,60:1 ✓
|
|
550
|
+
red #D64545 trên #F9E3E3 = 3,57:1 ✗ → --red-ink #9A3537 = 5,83:1 ✓
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
Ở **dark** không có bậc riêng: khối `.dark` vốn đã nâng `--teal/--green/--red/--yellow` lên mức làm chữ
|
|
554
|
+
được, nên bên đó `--x-ink` chỉ trỏ về `var(--x)`. Nhờ vậy `text-teal-ink` viết **một lần** là đúng cả
|
|
555
|
+
hai theme, không cần biến thể `dark:`.
|
|
556
|
+
|
|
557
|
+
Ba vai trò áp cho mọi họ token:
|
|
558
|
+
|
|
559
|
+
| Họ | Nền | Chữ trên nền đó | Trên nền trang |
|
|
560
|
+
|---|---|---|---|
|
|
561
|
+
| Màu phụ | `--teal` `--green` `--red` `--yellow` `--navy` | — (dùng `--ink`/`--paper` tuỳ nền) | `--teal-ink` `--green-ink` `--red-ink` `--yellow-ink` `--navy-ink` |
|
|
562
|
+
| Trạng thái | `--success` `--warning` `--info` `--destructive` | `--success-foreground` … `--destructive-foreground` | `--success-ink` `--warning-ink` `--info-ink` `--destructive-ink` |
|
|
563
|
+
| Brand | `--primary` | `--primary-foreground` | `--primary-ink` |
|
|
564
|
+
| Accent theo app | `--app-accent` | `--app-accent-foreground` | `--app-accent-ink` (mặc định `= --primary-ink`) |
|
|
565
|
+
|
|
566
|
+
Brand cũng cần bậc `-ink` vì đúng lý do trên: gold `#E8B824` là màu **sáng** nên tô nền thì tuyệt, còn
|
|
567
|
+
làm ranh giới trạng thái trên nền trang chỉ được **1,86:1** ✗. `--primary-ink` mượn `--gold-500`
|
|
568
|
+
`#7A5800` ở light (**6,51:1** ✓) và trỏ về chính `--primary` ở dark (**12,67:1** ✓). Đây là màu của
|
|
569
|
+
rãnh `Switch` khi BẬT và ô `Checkbox` đã chọn. `--ring` **cố ý vẫn là** `--primary`: vòng focus thuộc
|
|
570
|
+
tiêu chí AAA của WCAG 2.2, ngoài mục tiêu AA của gói.
|
|
571
|
+
|
|
572
|
+
Ba lỗi hay gặp nhất, và cách sửa:
|
|
573
|
+
|
|
574
|
+
| Viết sai | Hỏng ở đâu | Viết đúng |
|
|
575
|
+
|---|---|---|
|
|
576
|
+
| `text-red` cho dòng báo lỗi | 4,38:1 ở light ✗; ở dark thì đây là thang brand thô | `text-destructive-ink` |
|
|
577
|
+
| `text-success` / `text-info` / `text-warning` cho chữ hoặc icon trạng thái | Token TÔ NỀN: trên tint `/15` của chính nó chỉ 2,69:1 / 3,44:1 ✗ ở light; trên nền trang 3,13:1 / 4,14:1 ✗ | `text-success-ink` / `text-info-ink` / `text-warning-ink` (5,5–6,7:1 light · 7,1–12,4:1 dark) |
|
|
578
|
+
| `bg-primary` làm rãnh `Switch` khi BẬT / ô `Checkbox` đã chọn | 1,86:1 với nền trắng ✗ — trạng thái BẬT chỉ còn phân biệt bằng vị trí núm | `bg-primary-ink` (6,51:1 light / 12,67:1 dark) |
|
|
579
|
+
| `border-border` làm viền của control | `--border` là ranh giới **TRANG TRÍ**, 1,16:1 ✗ | `border-input` (= `--control-edge`, 3,83:1 / 4,94:1) |
|
|
580
|
+
| `text-app-accent` cho icon/vạch active | accent mặc định là gold `#E8B824` → 1,7–1,9:1 trên nền sidebar ✗ | `text-app-accent-ink`, hoặc nền ĐẶC + `text-app-accent-foreground` |
|
|
581
|
+
| `text-destructive` cho chữ trên nền trang | `--destructive` là màu **NỀN** (ghim một giá trị đậm cho chữ trắng đứng lên); dùng làm chữ ở dark chỉ 3,76:1 ✗ | `text-destructive-ink` (7,15:1 light / 6,42:1 dark) |
|
|
582
|
+
|
|
583
|
+
> ⚠ **App ghi đè `--app-accent` thì PHẢI set kèm `--app-accent-ink`** (và `--app-accent-foreground` nếu
|
|
584
|
+
> accent sáng). Gói cố ý **không** dẫn xuất `-ink` bằng `color-mix` từ `--app-accent`: dẫn xuất tự động
|
|
585
|
+
> cho ra con số tương phản không ai kiểm, và mỗi màu nền cần một hệ số khác nhau.
|
|
586
|
+
>
|
|
587
|
+
> ⚠ **Ghi đè `--primary` cũng PHẢI set kèm `--primary-ink`** — cùng một luật, cùng một lý do. Quên thì
|
|
588
|
+
> công tắc/checkbox vẫn đọc được nhưng còn màu gold mặc định: sai màu, không sai chuẩn.
|
|
589
|
+
>
|
|
590
|
+
> ⚠ **Đổi `--teal` / `--green` / `--red` thì phải tính lại bậc `-ink` tương ứng** — chúng là hex đã đóng
|
|
591
|
+
> băng, không tự đi theo. Đóng băng là chủ ý: một chỗ duy nhất để đo lại, component không phải nhắc lại
|
|
592
|
+
> chuỗi `color-mix(…)` (dễ chép lệch % giữa `Badge`/`LetterAvatar`/`Input`), và `tokens.ts` mirror được
|
|
593
|
+
> ra hex thật cho canvas/email/PDF.
|
|
594
|
+
|
|
595
|
+
Bản mirror TS đi theo đúng hợp đồng này: `accent` (nền) ⇄ `accentInk` (mực), `status.light.success` ⇄
|
|
596
|
+
`status.light.successInk`, `semantic.light.destructive` ⇄ `semantic.light.destructiveInk`.
|
|
597
|
+
|
|
598
|
+
### 6.2 Bảng token
|
|
599
|
+
|
|
389
600
|
| Nhóm | Token | Ghi chú |
|
|
390
601
|
|---|---|---|
|
|
391
|
-
| Brand | `gold-50 … gold-600` | `gold-300 #E8B824` = `--primary` (light); dark nâng lên `gold-200 #F5D060` |
|
|
602
|
+
| Brand | `gold-50 … gold-600` | `gold-300 #E8B824` = `--primary` (light); dark nâng lên `gold-200 #F5D060`. `--primary-ink` = `gold-500 #7A5800` (light) / `= --primary` (dark) — bậc gold cho vai trò **trên nền trang**, xem §6.1 |
|
|
392
603
|
| Trung tính brand | `ink`, `ink-soft`, `paper`, `warm` | `warm #EDE8D8` = `--secondary` (light) |
|
|
393
604
|
| Thang xám | `ash-50/100/300/500/700/900` | **Không phải `gray-*`** — xem cạm bẫy bên dưới |
|
|
394
|
-
| Màu phụ | `navy`, `teal`, `red`, `green`, `yellow` | `red #D64545`
|
|
395
|
-
|
|
|
396
|
-
|
|
|
397
|
-
|
|
|
605
|
+
| Màu phụ (NỀN) | `navy`, `teal`, `red`, `green`, `yellow` | Vai trò **tô nền** (fill đặc hoặc tint). Bản dark đã được nâng sáng. ⚠ `red #D64545` **khác** `--destructive` `#C93B3B` — cái sau là nền nút xoá, ghim một giá trị cho cả hai theme để chữ trắng đạt 5,03:1 |
|
|
606
|
+
| Mực của màu phụ | `teal-ink`, `green-ink`, `red-ink`, `yellow-ink`, `navy-ink` | Cùng màu đó ở vai trò **chữ/icon/viền trên nền trang** — xem §6.1. Ở light là hex đã đóng băng (`#1A6668` / `#216A48` / `#9A3537` / `= gold-500` / `= navy`); ở dark trỏ về chính màu phụ |
|
|
607
|
+
| Trạng thái | `success`, `warning`, `info`, `destructive` (+ `-foreground`, + `-ink`) | Ba cái đầu alias sang `green` / `yellow` / `teal` và tự sáng lên ở dark; `destructive` ghim một giá trị cho cả hai theme. `-ink` là bậc để làm **chữ** trạng thái (`text-destructive-ink` cho dòng lỗi) |
|
|
608
|
+
| Ranh giới control | `control-edge` `#7E8290` | Token dành riêng cho **ranh giới của control** (rãnh `Switch` khi tắt, viền ô nhập/checkbox chưa chọn) — WCAG 1.4.11 đòi ≥3:1 cho thành phần phi văn bản. Ghim một giá trị cho cả hai theme: 3,83:1 với nền trắng, 4,94:1 với nền dark. **`--input` trỏ vào đúng token này**; `--border` thì KHÔNG — nó là ranh giới trang trí (1,16:1, cố ý) |
|
|
609
|
+
| Biểu đồ | `chart-1 … chart-5` | Đổi bảng màu biểu đồ độc lập với brand. `chart-3` trỏ vào `--navy-ink` (nét biểu đồ phải NỔI trên nền trang) |
|
|
610
|
+
| Avatar | `getAvatarToneColors(name, mode)`, `getAvatarToneIndex(name)`, type `AvatarToneColors` | Trả `{ background, foreground }` dạng **hex đục** cho canvas/email/PDF — đã trộn sẵn tint với nền `--card` vì ngoài CSS không có gì để hợp thành. Cùng bảng tone, cùng hàm băm với `getAvatarToneClasses(name)` ở `@comitor/ui`, nên một người luôn ra một màu ở mọi nơi. Khoá băm là **tên hiển thị**, không phải email/slug |
|
|
611
|
+
| Theo app | `app-accent`, `app-accent-foreground`, `app-accent-ink` | Mỗi app Comitor tô một accent riêng — khai ở CSS của app (có nhánh `.dark`), không cần build lại package. Ghi đè `--app-accent` thì **phải** set kèm `--app-accent-ink` (§6.1) |
|
|
398
612
|
| Khung | `--sidebar-width`, `--sidebar-width-icon`, `--header-height` | Có utility `w-sidebar`, `w-sidebar-icon`, `h-header` |
|
|
399
613
|
| Đổ bóng | `--elevation-surface`, `--elevation-control`, `--elevation-card`, `--elevation-overlay` | 4 bậc độ cao, sinh utility `shadow-surface` / `shadow-control` / `shadow-card` / `shadow-overlay`. Đổi theo theme (bản dark dùng đen thuần, alpha cao hơn) |
|
|
400
614
|
| Khác | `--radius 0.375rem` | Bo góc 6px là chữ ký thị giác |
|
|
401
615
|
|
|
402
|
-
Đổi accent theo app
|
|
616
|
+
Đổi accent theo app — khai bằng **CSS, có nhánh dark**, không phải inline style trên `<body>`:
|
|
403
617
|
|
|
404
|
-
```
|
|
405
|
-
|
|
618
|
+
```css
|
|
619
|
+
/* app/globals.css — sau @import "@comitor/ui/styles.css" */
|
|
620
|
+
:root {
|
|
621
|
+
--app-accent: #1f8a8b; /* NỀN đặc */
|
|
622
|
+
--app-accent-foreground: #0e1116; /* chữ trên nền đó → 4,56:1 ✓ */
|
|
623
|
+
--app-accent-ink: #1a6668; /* màu app trên nền trang → 6,68:1 trên trắng ✓ */
|
|
624
|
+
}
|
|
625
|
+
.dark {
|
|
626
|
+
--app-accent: #2dd4bf;
|
|
627
|
+
--app-accent-foreground: #0e1116; /* 10,16:1 ✓ */
|
|
628
|
+
--app-accent-ink: #2dd4bf; /* 10,16:1 trên --background ✓ */
|
|
629
|
+
}
|
|
406
630
|
```
|
|
407
631
|
|
|
632
|
+
Ba biến, không phải hai: bỏ `--app-accent-ink` thì icon/vạch active của app rơi về gold mặc định.
|
|
633
|
+
Và phải khai ở CSS chứ không phải inline style vì **một giá trị inline không có nhánh `dark`** — accent
|
|
634
|
+
sáng vừa đủ cho light mode thường quá tối trên nền `#0E1116`.
|
|
635
|
+
|
|
636
|
+
> Trong `AppLauncher`, mỗi app còn tự tô ô icon của mình qua `AppDescriptor.accent` /
|
|
637
|
+
> `accentForeground` (inline style, đúng vai trò nền + chữ-trên-nền). Ô icon dùng **nền đặc**, nên hai
|
|
638
|
+
> biến đó là đủ; `--app-accent-ink` là chuyện của cả trang.
|
|
639
|
+
|
|
408
640
|
Đổi độ nổi (bóng) toàn hệ — ghi đè đúng 4 biến `--elevation-*`, **không** ghi đè `--shadow-*`:
|
|
409
641
|
|
|
410
642
|
```css
|
|
@@ -415,16 +647,19 @@ Toàn bộ token nằm ở [`styles.css`](./styles.css); bản mirror TS ở [`s
|
|
|
415
647
|
> Vì `--shadow-*` giữ giá trị dạng `var()`, Tailwind không tách được phần màu: `shadow-card/50` và
|
|
416
648
|
> `shadow-<màu>` **không** có tác dụng. Đổi tông/độ đậm thì ghi đè `--elevation-*` như trên.
|
|
417
649
|
|
|
418
|
-
### Cạm bẫy phải nhớ
|
|
650
|
+
### 6.3 Cạm bẫy phải nhớ
|
|
419
651
|
|
|
420
|
-
1.
|
|
652
|
+
1. **Nền hay mực? Chọn sai vai trò là hỏng tương phản, không phải hỏng thẩm mỹ.** `--x` tô nền,
|
|
653
|
+
`--x-ink` làm chữ/icon/viền trên nền trang (§6.1). Utility `text-teal` / `text-app-accent` **vẫn được
|
|
654
|
+
Tailwind sinh ra** — nó sinh cả bộ cho mọi `--color-*` — nên trình biên dịch sẽ không nhắc gì cả.
|
|
655
|
+
2. **`ash-*` chứ không phải `gray-*`.** Thang xám Comitor chỉ có 6 bậc; nếu đặt tên `gray-*` thì
|
|
421
656
|
`gray-200/400/600/800` rơi về gray mặc định của Tailwind (ám xanh) và trộn với gray Comitor
|
|
422
657
|
(ám tím) → lệch tông. Đổi namespace để hai thang không đụng nhau.
|
|
423
|
-
|
|
658
|
+
3. **Không sinh class động** (`` `bg-${color}-300` ``) ở bất kỳ đâu — Tailwind quét tĩnh, class động
|
|
424
659
|
bị loại khỏi bundle của app.
|
|
425
|
-
|
|
660
|
+
4. **Không hardcode hex trong component** — trừ vector logo brand. Đọc hex từ `/tokens` rồi nhét vào
|
|
426
661
|
style inline là mất dark mode.
|
|
427
|
-
|
|
662
|
+
5. `@theme **inline**` là chủ ý: `inline` nhúng thẳng `var(--x)` vào utility nên `.dark { --primary }`
|
|
428
663
|
đổi được màu tại runtime. Dùng `@theme` thường sẽ đóng băng giá trị lúc build → dark mode chết.
|
|
429
664
|
|
|
430
665
|
## 7. Phát triển
|
|
@@ -435,10 +670,18 @@ pnpm build # tsup: ESM, bundle:false (transpile từng file để giữ "
|
|
|
435
670
|
pnpm typecheck # tsc --noEmit
|
|
436
671
|
pnpm lint # biome check .
|
|
437
672
|
pnpm lint:fix
|
|
673
|
+
pnpm lint:a11y # hợp đồng màu: vai trò token + tương phản WCAG ở CẢ hai theme
|
|
674
|
+
pnpm test:cong-a11y # tự kiểm chính cổng trên: 56 khẳng định, cổng còn bắt đủ không
|
|
675
|
+
pnpm test:nav-active # active matching của sidebar (hàm thuần) — 34 ca
|
|
438
676
|
pnpm verify:directives # mọi file nguồn có "use client" ⇄ file dist tương ứng
|
|
439
677
|
pnpm check:exports # publint --strict + attw
|
|
440
678
|
```
|
|
441
679
|
|
|
680
|
+
`pnpm test:nav-active` chạy `scripts/kiem-nav-active.mjs`: Node trần bóc kiểu ngay trên
|
|
681
|
+
`src/shell/utils.ts`, không test runner, không dependency mới. Nó khoá lại đúng lỗi mà
|
|
682
|
+
comitor-starter báo về ở `0.2.0` — mục menu có `?query` trong `href` không bao giờ sáng — cùng
|
|
683
|
+
toàn bộ hành vi active matching cũ, để lần sửa sau không đổi ngầm luật nào.
|
|
684
|
+
|
|
442
685
|
> Gặp `ERR_PNPM_IGNORED_BUILDS: esbuild` khi cài? Đã xử lý sẵn trong `pnpm-workspace.yaml`
|
|
443
686
|
> (`allowBuilds: { esbuild: true }`) — pnpm 11 không còn đọc setting từ trường `pnpm` của
|
|
444
687
|
> `package.json` nữa. esbuild cần chạy postinstall để nạp binary theo nền tảng, cấm chạy thì
|
|
@@ -450,9 +693,26 @@ File thuần dữ liệu (`lib/cn.ts`, `tokens.ts`, `shell/types.ts`) **không**
|
|
|
450
693
|
|
|
451
694
|
`scripts/verify-directives.mjs` đi bộ **toàn bộ `src/`** (không riêng `shell/`): file nguồn nào mở đầu
|
|
452
695
|
bằng `"use client"` thì file `dist/` tương ứng phải mở đầu y hệt — thiếu file build cũng tính là hỏng.
|
|
453
|
-
Hiện **70/
|
|
696
|
+
Hiện **70/102** file nguồn cần directive, trải khắp cả composite lẫn primitive (`pnpm verify:directives`
|
|
697
|
+
in ra đúng con số này — đừng chép số từ đây mà không chạy lại). Đừng thay script bằng
|
|
454
698
|
`grep -L "use client" dist/shell/*.js`: lệnh đó soi chưa tới một phần ba số file phải soi.
|
|
455
699
|
|
|
700
|
+
### Cổng hợp đồng màu — `pnpm lint:a11y`
|
|
701
|
+
|
|
702
|
+
`scripts/check-contrast.mjs` quét **toàn bộ** `src/**/*.{ts,tsx}` theo QUY TẮC (không theo danh sách
|
|
703
|
+
phát hiện) và đối chiếu với `styles.css`: token TÔ NỀN đem làm màu chữ/icon/viền là sai *vai trò*, và
|
|
704
|
+
mọi cặp chữ/nền phải đạt 4,5:1 (chữ) hoặc 3:1 (thành phần phi văn bản) ở **cả** light lẫn dark. Mọi loại
|
|
705
|
+
phát hiện đều `exit 1` — kể cả lớp dùng bảng màu mặc định của Tailwind và miễn trừ đã thừa. Miễn trừ ghi
|
|
706
|
+
ở `scripts/contrast-exemptions.json`, mỗi mục bắt buộc kèm lý do bằng chữ. Hợp đồng đầy đủ + giới hạn đã
|
|
707
|
+
biết: [docs/hop-dong-mau.md](docs/hop-dong-mau.md).
|
|
708
|
+
|
|
709
|
+
App tiêu thụ chạy được cùng cổng này trên bảng token đã ghi đè của mình: lấy script từ repo rồi trỏ
|
|
710
|
+
`COMITOR_UI_STYLES` / `COMITOR_UI_SRC` sang file của app (script **không** nằm trong tarball npm).
|
|
711
|
+
|
|
712
|
+
Trước khi phát hành: bump `version`, ghi mục mới trong [CHANGELOG.md](CHANGELOG.md), rồi chạy lại **cả
|
|
713
|
+
khối lệnh ở đầu mục này** (`lint` và `lint:fix` là hai dạng của cùng một bước) — quy trình đầy đủ ở
|
|
714
|
+
[CLAUDE.md](CLAUDE.md) §Publish.
|
|
715
|
+
|
|
456
716
|
## 8. Giấy phép
|
|
457
717
|
|
|
458
718
|
[MIT](./LICENSE) © Comitor
|
package/dist/chart/chart.d.ts
CHANGED
|
@@ -60,7 +60,35 @@ type ChartStyleProps = {
|
|
|
60
60
|
declare const ChartStyle: ({ id, config }: ChartStyleProps) => React.JSX.Element | null;
|
|
61
61
|
/** `Tooltip` của recharts — đặt `content={<ChartTooltipContent />}` để có giao diện theo token. */
|
|
62
62
|
declare const ChartTooltip: typeof RechartsPrimitive.Tooltip;
|
|
63
|
+
/**
|
|
64
|
+
* Một phần tử `payload` do recharts bơm vào `content` của Tooltip/Legend.
|
|
65
|
+
*
|
|
66
|
+
* ─ VÌ SAO KHAI TẠI ĐÂY, KHÔNG DẪN XUẤT TỪ `TooltipProps`/`LegendProps` ─
|
|
67
|
+
* recharts **3** đã bỏ `payload` (và `label`) khỏi props của `<Tooltip>`/`<Legend>` — hai
|
|
68
|
+
* trường đó nay đi qua context nội bộ, nên `ComponentProps<typeof Tooltip>["payload"]` và
|
|
69
|
+
* `Pick<LegendProps, "payload">` **không còn tồn tại** ở 3.x. Dẫn xuất từ chúng khiến chính
|
|
70
|
+
* file `.d.ts` phát hành ra npm hỏng kiểu ngay khi app cài recharts 3 (app nào tắt
|
|
71
|
+
* `skipLibCheck` là thấy liền), tức peer `>=2.15.0` sẽ là một lời hứa suông.
|
|
72
|
+
*
|
|
73
|
+
* Hình dạng dưới đây là phần GIAO của 2.x và 3.x, và chỉ gồm đúng những trường component
|
|
74
|
+
* này thật sự đọc. Ở 2.x nó **giao** với kiểu gốc của recharts (props Tooltip vẫn còn
|
|
75
|
+
* `payload`) nên không nới lỏng gì; ở 3.x nó là nguồn duy nhất.
|
|
76
|
+
*/
|
|
77
|
+
interface ChartPayloadItem {
|
|
78
|
+
value?: number;
|
|
79
|
+
name?: string;
|
|
80
|
+
dataKey?: string | number;
|
|
81
|
+
color?: string;
|
|
82
|
+
/** Bản ghi gốc của điểm dữ liệu — nơi `fill` và các cột khác của hàng nằm. */
|
|
83
|
+
payload?: Record<string, unknown> & {
|
|
84
|
+
fill?: string;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
63
87
|
type ChartTooltipContentProps = React.ComponentProps<typeof RechartsPrimitive.Tooltip> & React.ComponentProps<"div"> & {
|
|
88
|
+
/** Do recharts bơm vào — đừng truyền tay. */
|
|
89
|
+
payload?: ChartPayloadItem[];
|
|
90
|
+
/** Do recharts bơm vào — đừng truyền tay. */
|
|
91
|
+
label?: React.ReactNode;
|
|
64
92
|
/** Ẩn dòng nhãn (trục X) ở đầu tooltip. */
|
|
65
93
|
hideLabel?: boolean;
|
|
66
94
|
/** Ẩn ô màu đứng trước tên series. */
|
|
@@ -75,8 +103,12 @@ type ChartTooltipContentProps = React.ComponentProps<typeof RechartsPrimitive.To
|
|
|
75
103
|
/** Nội dung tooltip theo design token Comitor: nền `bg-background`, viền `border-border/50`. */
|
|
76
104
|
declare function ChartTooltipContent({ active, payload, className, indicator, hideLabel, hideIndicator, label, labelFormatter, labelClassName, formatter, color, nameKey, labelKey }: ChartTooltipContentProps): React.JSX.Element | null;
|
|
77
105
|
/** `Legend` của recharts — đặt `content={<ChartLegendContent />}` để lấy nhãn từ `config`. */
|
|
78
|
-
declare const ChartLegend:
|
|
79
|
-
type ChartLegendContentProps = React.ComponentProps<"div"> &
|
|
106
|
+
declare const ChartLegend: React.MemoExoticComponent<(outsideProps: RechartsPrimitive.LegendProps) => React.ReactPortal | null>;
|
|
107
|
+
type ChartLegendContentProps = React.ComponentProps<"div"> & {
|
|
108
|
+
/** Do recharts bơm vào — đừng truyền tay. Xem `ChartPayloadItem` về lý do không dẫn xuất từ `LegendProps`. */
|
|
109
|
+
payload?: ChartPayloadItem[];
|
|
110
|
+
/** Do recharts bơm vào — quyết định chú giải đệm trên hay đệm dưới. */
|
|
111
|
+
verticalAlign?: "top" | "middle" | "bottom";
|
|
80
112
|
/** Ẩn icon riêng của series (nếu `config` có khai `icon`). */
|
|
81
113
|
hideIcon?: boolean;
|
|
82
114
|
/** Khoá dùng để tra `config` (mặc định `dataKey`). */
|
|
@@ -85,4 +117,4 @@ type ChartLegendContentProps = React.ComponentProps<"div"> & Pick<RechartsPrimit
|
|
|
85
117
|
/** Chú giải nằm ngang, nhãn lấy từ `config` chứ không phải `dataKey` thô. */
|
|
86
118
|
declare function ChartLegendContent({ className, hideIcon, payload, verticalAlign, nameKey }: ChartLegendContentProps): React.JSX.Element | null;
|
|
87
119
|
|
|
88
|
-
export { type ChartConfig, ChartContainer, type ChartContainerProps, ChartLegend, ChartLegendContent, type ChartLegendContentProps, ChartStyle, type ChartStyleProps, ChartTooltip, ChartTooltipContent, type ChartTooltipContentProps, useChart };
|
|
120
|
+
export { type ChartConfig, ChartContainer, type ChartContainerProps, ChartLegend, ChartLegendContent, type ChartLegendContentProps, type ChartPayloadItem, ChartStyle, type ChartStyleProps, ChartTooltip, ChartTooltipContent, type ChartTooltipContentProps, useChart };
|