@comitor/ui
v1.10.0
Published
Comitor design system — Shadcn/UI tùy biến theo brand Comitor + component dùng chung cho hệ sinh thái SaaS (Tasks, Chat, CRM, HR…)
Maintainers
Readme
@comitor/ui
Hệ thống thiết kế dùng chung của hệ sinh thái Comitor (Tasks, Chat, CRM, HR, admin console,
marketing site). Đóng gói shadcn/ui style new-york đã tùy biến theo brand Comitor, cộng thêm các
component ghép sẵn và khung ứng dụng (shell) kiểu workspace × app.
- React 19 · Tailwind v4 (CSS-first) · TypeScript strict · ESM-only
- Token brand nằm ở
styles.css— đổi token, bump version, mọi app nhận cùng một thay đổi. - Server-component-friendly:
"use client"chỉ nằm ở file thật sự cần.
Trạng thái:
1.8.0— 52 file primitive (Tier 1) · 31 file composite (Tier 2) · 25 file shell (Tier 3) · 4 file chart · 5 file uploader. API chốt từ1.0.0.Tài liệu đầy đủ: https://design-system.comitor.ai — Get started · Components · Composites · Shell · Changelog · npm. README này là bản tra nhanh: cạm bẫy, hợp đồng màu, ranh giới server/client.
API đã chốt ở
1.0.0: từ đây thay đổi phá vỡ tương thích đi kèm bump major, không còn đi kèm minor như dải0.x— mỗi mục breaking vẫn có bảng chuyển đổi ở changelog.
1. Cài đặt
pnpm add @comitor/ui
# peer bắt buộc
pnpm add react react-dom lucide-react
pnpm add -D tailwindcss @tailwindcss/postcss
# peer optional — cài khi dùng entry tương ứng
pnpm add next next-themes # cho @comitor/ui/shell
pnpm add recharts # cho @comitor/ui/chart
pnpm add react-hook-form # cho @comitor/ui/form
pnpm add @uppy/core @uppy/xhr-upload # cho @comitor/ui/uploader| Peer | Range | Bắt buộc | Cần khi |
|---|---|---|---|
| react, react-dom | ^19.0.0 | ✅ | luôn |
| tailwindcss | ^4.1.0 | ✅ | luôn (styles.css phải được TW4 biên dịch) |
| lucide-react | >=0.540.0 | ✅ | luôn (tránh 2 bản icon trong bundle) |
| next | >=15 | — | /shell |
| next-themes | >=0.4 | — | /shell (context singleton — app và package phải dùng cùng một bản) |
| recharts | >=2.15.0 | — | /chart (kiểu đã dựng cho cả 2.x lẫn 3.x, xem ghi chú dưới) |
| react-hook-form | ^7.54.0 | — | /form (context singleton) |
| @uppy/core | ^6.0.0 | — | /uploader |
| @uppy/xhr-upload | ^6.0.0 | — | /uploader |
Vì sao
rechartsvàlucide-reactlà>=chứ không phải^.^2.15.0loại thẳng recharts 3 — mà 3.x chạy tốt với gói này: kiểu của tầng/chartkhông dẫn xuấtpayload/labeltừTooltipProps/LegendProps(3.x đã bỏ hai trường đó khỏi props) mà tự khaiChartPayloadItem, phần giao của hai bản. Đã chạytypecheck+buildtrên cả recharts 2.15.4 lẫn 3.10.1.lucide-reactcũng vậy: thang phiên bản của nó nhảy0.x→1.xmà API icon không đổi, đã kiểm với1.33.0.Hiệu ứng vẽ dần của recharts 2.15.x hỏng với React 19.2, nên
ComitorAreaChart/ComitorBarChart/ComitorLineChartmặc định tắt hiệu ứng (§4.2). Nếu tự lắp recharts trong<ChartContainer>thì gói không tắt hộ được — app phải tự ghiisAnimationActive={false}lên từng<Bar>/<Line>/<Area>(§4.2).
Cài đặt từng bước, kèm bản mẫu app: https://design-system.comitor.ai/get-started
2. Nạp styles.css — ĐỌC KỸ, đây là chỗ dễ sai nhất
styles.css phải nằm trong app/globals.css, ngay sau @import "tailwindcss" — không phải
import trong layout.tsx (layout.tsx chỉ import globals.css của chính app):
/* app/globals.css */
@import "tailwindcss";
@import "@comitor/ui/styles.css";KHÔNG viết
import "@comitor/ui/styles.css"tronglayout.tsx(thói quen Tailwind 3). File này chứa@theme/@custom-variant/@source— những at-rule chỉ có nghĩa khi được Tailwind biên dịch. Import ởlayout.tsxthì biến CSS vẫn chạy nhưng không utility nào (bg-gold-300,dark:*…) được sinh ra → giao diện vỡ mà không có lỗi build.
Dùng pnpm strict mà thấy mất sạch style thì khai thêm ở app:
@source "../node_modules/@comitor/ui/dist"; (gói đã tự khai @source "./dist"; Tailwind v4 mặc
định bỏ qua node_modules).
Font: app tự nạp Inter (subsets latin + vietnamese) và gán --font-inter; gói chỉ tiêu thụ.
--font-inter-mono là tuỳ chọn, gán y hệt cách gán --font-inter nếu muốn đổi font chữ đều (số
trong tooltip biểu đồ, Kbd…). Không gán thì mỗi biến rơi về fallback khai sẵn trong styles.css.
Cách gán bằng next/font: https://design-system.comitor.ai/get-started
Con trỏ: styles.css khai lại cursor: pointer cho button, [role="button"] và
label[for] ở @layer base. Preflight của Tailwind v3 có luật này, v4 bỏ nó để bám mặc
định trình duyệt — nên thiếu nó thì <button> và <a> trông giống hệt nhau lại có con trỏ khác
nhau, và một nút ghost đọc ra như một cái nhãn. App không cần rải cursor-pointer; control bị
vô hiệu (:disabled hoặc aria-disabled="true") vẫn giữ con trỏ mặc định.
Dark mode theo class (@custom-variant dark (&:is(.dark *))), khớp next-themes. Lưu ý:
biến thể dark chỉ khớp con cháu của .dark, không khớp chính phần tử mang class đó. Muốn dựng
preview 2 theme cạnh nhau phải bọc thêm một lớp: <div class="dark"><div>…</div></div>.
Cách dựng ThemeProvider, thứ tự lồng với ContrastProvider, và suppressHydrationWarning bắt
buộc ở thẻ <html>: https://design-system.comitor.ai/shell
3. Entry point
Package có 6 entry, tách theo peer optional mà mỗi tầng kéo theo:
| Entry | Nội dung | Peer optional kéo theo |
|---|---|---|
| @comitor/ui | Tier 1 (primitive) + Tier 2 (composite) + cn + token TS + useIsMac | không có — chạy được ở Vite, Storybook, test runner |
| @comitor/ui/shell | Tier 3: khung ứng dụng workspace × app, theme, ba trục hiển thị | next, next-themes |
| @comitor/ui/chart | Lớp shadcn bọc recharts + Comitor*Chart + CHART_COLORS | recharts (~100KB) |
| @comitor/ui/uploader | FileUpload (tệp tài liệu) · ImageUploadField (ảnh đại diện, logo) · useFileUpload | @uppy/core, @uppy/xhr-upload (~13KB gzip) |
| @comitor/ui/form | Binding shadcn ⇄ react-hook-form | react-hook-form |
| @comitor/ui/tokens | Token dạng object TS — cho nơi không có Tailwind: canvas, email HTML, PDF | không có |
⚠ Một số tên xuất hiện ở nhiều hơn một entry, và export * chỉ báo đỏ khi trùng trong cùng
một entry — trùng xuyên entry thì im lặng hoàn toàn. (Đếm chính xác bao nhiêu tên thì tuỳ cách
đếm — "cùng ký tự" hay "cùng binding sau khi giải alias" cho hai kết quả khác nhau — nên đừng ghim
một con số ở đây. Bất biến cần giữ: cùng tên ⇒ cùng một binding.) Đúng 2 trong số đó là hai cài đặt khác nhau
(FormField / FormFieldProps: ở Tier 2 là bố cục form 12 cột, ở /form là wrapper của
Controller); 18 tên còn lại trỏ về cùng một binding nên lấy ở đâu cũng như nhau. Bảng đầy đủ, kèm
hai ca không trùng tên hay bị tưởng nhầm (Toaster ⇄ SonnerToaster, và useIsMac chỉ xuất ở
@comitor/ui): https://design-system.comitor.ai/get-started
4. Cạm bẫy hay gặp
4.1 Shell — hai trục, và mục menu phân biệt nhau bằng query
Shell dựng trên hai trục điều hướng: WorkspaceSwitcher đổi ngữ cảnh dữ liệu
(Acme Corp ▸ Beta Ltd), AppLauncher đổi sản phẩm trong cùng workspace (Tasks ▸ Chat ▸ CRM ▸ HR).
Cả hai không nhận dữ liệu qua prop — chúng đọc context của ShellProvider mà AppShell đã bọc
sẵn, nên workspaces / apps / nav chỉ truyền một lần cho AppShell.
⚠ usePathname() của Next không bao giờ chứa ?query. Vì vậy nhiều mục cùng đường dẫn
(/cai-dat?tab=a, ?tab=b) sẽ cùng sáng một lúc — không lỗi, không warning. Đưa query cho shell
qua prop optional searchParams thì mỗi tab sáng đúng phần của nó.
Cách lắp, toàn bộ luật active matching, và bản dùng ngoài Next: https://design-system.comitor.ai/shell · https://design-system.comitor.ai/components/sidebar-nav
4.2 Biểu đồ — hiệu ứng vẽ dần, và ranh giới server/client
⚠ Với recharts 2.15.x + React 19.2, hiệu ứng vẽ dần không bao giờ chạy tới khung hình cuối, nên
mark đứng nguyên ở khung đầu — tức là vô hình: đủ trục, đủ lưới, đủ chú giải, KHÔNG có dữ liệu,
và chờ bao lâu cũng không tự hết. ComitorAreaChart / ComitorBarChart / ComitorLineChart vì thế
mặc định tắt hiệu ứng. Nhưng khi tự lắp recharts trong <ChartContainer> thì mark là children do
app viết, container không đặt hộ được — app phải tự ghi isAnimationActive={false} lên từng
<Bar> / <Line> / <Area>. Dải peer của gói là >=2.15.0 nên cứ ghi: trên 3.x lỗi không tồn tại
và prop chỉ còn nghĩa "không hoạt hình". https://design-system.comitor.ai/components/chart
Ranh giới server/client là cạm bẫy cùng họ, và hỏng theo hai kiểu ngược nhau: 7 helper dựng chuỗi
class (buttonVariants, toggleVariants…) gọi từ Server Component thì ném lỗi to, còn một hằng
dữ liệu lọt vào sau file "use client" thì ra undefined — không throw, không warning — trong
khi TypeScript vẫn khai kiểu literal. Danh sách đầy đủ 97 tên, ai gọi được ai không:
https://design-system.comitor.ai/foundations/server-client
5. Bảng component
Số file nguồn hiện tại: 52 primitive · 31 composite · 25 shell · 4 chart · 5 uploader. Mỗi component có trang riêng kèm demo, bảng prop và cạm bẫy của nó:
| Tầng | Là gì | Trang |
|---|---|---|
| Tier 1 — primitive · 52 file · @comitor/ui | shadcn/ui style new-york, đã tùy biến token theo brand Comitor | https://design-system.comitor.ai/components |
| Tier 2 — composite · 31 file · @comitor/ui | Giá trị thật của gói: pattern lặp ở mọi app, ghép từ nhiều primitive (DataTable, PageHeader, Combobox, DatePicker, StatCard…) | https://design-system.comitor.ai/composites |
| Tier 3 — shell · 25 file · @comitor/ui/shell | Khung ứng dụng workspace × app: sidebar, header, theme, ba trục hiển thị (tương phản · mật độ · cỡ chữ) | https://design-system.comitor.ai/shell |
| chart · 4 file · @comitor/ui/chart | Lớp shadcn bọc recharts, cùng ba bản dựng sẵn theo brand. ⚠ Ba wrapper Comitor*Chart mặc định TẮT hiệu ứng vẽ dần (isAnimationActive = false) — bật lại là biểu đồ kẹt ở khung đầu: có trục, có lưới, KHÔNG có dữ liệu. Mặc định đó KHÔNG áp cho <ChartContainer> + recharts do app tự lắp | https://design-system.comitor.ai/components/chart |
| uploader · 5 file · @comitor/ui/uploader | FileUpload (tệp) · ImageUploadField (ảnh) · useFileUpload — kéo thả, xem trước, tiến độ. Peer optional: @uppy/core + @uppy/xhr-upload | https://design-system.comitor.ai/components |
| form · @comitor/ui/form | Binding shadcn ⇄ react-hook-form | https://design-system.comitor.ai/components/form |
Cơ chế labels — đổi ngôn ngữ mà không fork component. 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 (aria-label, sr-only); app dùng ngôn ngữ khác
override qua prop labels. Kiểu là Partial<> và component luôn trải { ...DEFAULT_*, ...labels },
nên truyền một khoá không làm mất phần còn lại. Component chỉ có một hai chuỗi thì mở bằng prop
rời thay cho cả một object — clearLabel, pendingLabel, overflowLabel, placeholder,
emptyText: cùng cơ chế, chỉ khác hình dạng. ⚠ Khoá có số/đơn vị chèn vào là HÀM, không phải
chuỗi có placeholder (range(start, end, total, unitLabel), pageStatus(page, pageCount),
overflowLabel(count)) — nối chuỗi bằng dấu cộng ở phía app là cách chắc chắn sai trật tự từ với
ngôn ngữ khác.
6. Màu và token
Nguồn token là @comitor/ui/styles.css (ship kèm gói, mở được ở
node_modules/@comitor/ui/styles.css); bản mirror TS là entry @comitor/ui/tokens. Hai hợp đồng
dưới đây là phần quan trọng nhất của gói — sai một trong hai thì hỏng im lặng, không có lỗi build.
1. Có HAI bảng màu, dùng chung một bộ component. Mặc định là bảng của comitor-ds — design
system đã được duyệt, chính là bảng trên trang tài liệu, và là nguồn sự thật về hình thức. Bật
<html data-contrast="high"> thì đổi sang bảng đã đo WCAG, dành cho màn hình độ sáng thấp, màn
hình rẻ, hoặc dùng ngoài trời. Bảng mặc định cố ý có những cặp dưới ngưỡng: đó là quyết định của
người duyệt, không phải lỗi bỏ sót.
⚠ Điều kiện để hai bảng cùng sống: mọi component phải đi qua token vai trò (
--control-on,--control-edge,--choice-edge,--switch-track,--slider-track,--input-fill,--primary-ink,--link-ink…) chứ không viết thẳngbg-gold-300/bg-ash-300/text-gold-400. Viết thẳng thang màu là khoá cứng một bảng, và bảng kia lặng lẽ sai.
2. Mỗi màu có BA vai trò — đọc tên là biết dùng ở đâu:
| Tên | Vai trò | Đo tương phản với | Ví dụ dùng |
|---|---|---|---|
| --x | tô NỀN — fill đặc hoặc tint /10–/25 | — | bg-teal, bg-teal/15 |
| --x-foreground | chữ/icon NẰM TRÊN nền --x đặc | --x | bg-destructive text-destructive-foreground |
| --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 |
Phải có bậc -ink riêng vì bản light của màu phụ được chọn để tô nền nên quá nhạt cho vai trò
chữ — --teal trên tint 15% của chính nó chỉ 3,44:1 ✗, còn --teal-ink được 5,54:1 ✓. Ở
dark không có bậc riêng: --x-ink trỏ về var(--x), nhờ vậy text-teal-ink viết một lần là
đúng cả hai theme, không cần biến thể dark:. ⚠ Chọn sai vai trò thì không có gì báo: utility
text-teal / text-app-accent vẫn được Tailwind sinh ra như thường.
⚠ App ghi đè
--app-accentthì PHẢI set kèm--app-accent-ink(và--app-accent-foregroundnếu accent sáng), khai bằng CSS có nhánh.darkchứ không phải inline style — một giá trị inline không có nhánh dark. Gói cố ý không dẫn xuất-inkbằngcolor-mix: dẫn xuất tự động cho ra con số tương phản không ai kiểm. Cùng luật đó khi đổi--teal/--green/--red— chúng là hex đã đóng băng, không tự đi theo.
Bảng token đầy đủ, bản đối chiếu viết-sai ⇄ viết-đúng, cách bật bảng tương phản cao, và mọi số đo: https://design-system.comitor.ai/foundations/colors
7. Kiểm định trước mỗi lần phát hành
Mỗi bản publish đi qua cùng một chuỗi cổng (prepublishOnly nối chúng lại): typecheck → build →
verify:directives → smoke:entries → check:exports → test:nav-active → test:grid-contract
→ test:a11y-gate → lint:a11y. Những cổng đáng để người dùng gói biết là chúng tồn tại:
lint:a11y— hợp đồng màu. Quét toàn bộsrc/**/*.{ts,tsx}theo QUY TẮC (không theo danh sách phát hiện) và đối chiếu vớistyles.css: token TÔ NỀN đem làm màu chữ/icon/viền là sai vai trò, và 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. Bảng màu là một TRỤC (từ1.6.0): cổng đo cả bốn tổ hợp bảng × theme, nhưng chỉ CHẶN ở bảng tương phản cao. Bảng mặc định vào một sổ riêng, IN RA mỗi lần chạy nhưng không làm đỏ — nó là bản duyệt và đã được chấp nhận với đúng những con số dưới ngưỡng ở §6; bắt nó là bắt một quyết định, và một cổng lúc nào cũng đỏ thì lần sau không ai chạy nữa. ⚠ Cổng xanh vì thế KHÔNG có nghĩa bảng mặc định đạt — đọc khối "BẢNG MÀU MẶC ĐỊNH: N cặp dưới ngưỡng" mà nó in ra, cùng khối "Bốn lớp nhiều nhất" ngay dưới — đọc output, đừng đọc con số ở đây: nó đổi theo mỗi lần chạm token. Hình dạng của khoản nợ thì ổn định, và đó mới là thứ đáng nhớ: gần ba phần tư nằm ở đúngtext-muted-foreground, nên "chặn được chúng" nghĩa là đổi MỘT token và đổi vẻ ngoài mọi app. Trước1.6.0cổng còn không ĐO bảng mặc định lần nào, tức nó in ✓ cho một bảng nó chưa từng nhìn. Ở bảng tương phản cao thì mọi loại phát hiện đềuexit 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ừ mỗi mục bắt buộc kèm lý do bằng chữ.verify:directives— ranh giới server/client. File nguồn nào mở đầu bằng"use client"thì filedist/tương ứng phải mở đầu y hệt (thiếu file build cũng tính là hỏng), và không hằng dữ liệu thuần nào được kẹt trong file client (§4.2).smoke:entriesnạp cả 6 entry bằng Node ESM thuần, qua đúngexportsmap (§3).test:a11y-gatetự kiểm chính cổng màu (cổng còn bắt đủ không);test:nav-activekhoá toàn bộ luật active matching của sidebar dưới dạng hàm thuần (§4.1).
Quy ước "use client" của gói: đặt per-file, ở dòng đầu tiên, và directive được giữ nguyên
trong dist/ → Next.js đánh đúng ranh giới server/client. Module thuần dữ liệu (lib/cn.ts,
tokens.ts, shell/types.ts) không mang directive nên đọc được từ Server Component (§4.2).
App tiêu thụ chạy được cùng cổng màu trên bảng token đã ghi đè của mình: script nhận
COMITOR_UI_STYLES / COMITOR_UI_SRC trỏ sang file của app. Nhưng script không nằm trong
tarball npm — hợp đồng màu đầy đủ và các giới hạn đã biết ở
https://design-system.comitor.ai/foundations/colors
8. Giấy phép
MIT © Comitor
