Một design system cho mọi sản phẩm chúng tôi làm.

@comitor/ui là gói duy nhất đứng sau mọi sản phẩm Comitor: primitive, composite, khung app và các màn hình hoàn chỉnh để chép theo, bằng tiếng Anh và tiếng Việt.

pnpm add @comitor/ui

React 19 · Tailwind v4 · TypeScript strict · chỉ ESM · giấy phép MIT

Ba màn hình dựng từ component của @comitor/ui: pipeline CRM trong khung app, với các thẻ số liệu kèm đường xu hướng và bảng deal có status pill cùng avatar người phụ trách; một cột board Tasks với pill độ ưu tiên và người được giao; và một điện thoại chạy app dịch vụ hiện trường, với các lượt ghé thăm hôm nay trên một timeline.

Ba tầng, một gói, chín cửa vào

Mỗi tầng chỉ dựng từ các tầng bên dưới nó, và site này chia mục đúng như vậy. Gói được cắt theo thứ mỗi phần cần cài, không theo chủ đề: entry chính không có peer tuỳ chọn nào, nên app chỉ cài những gì nó import.

Mỗi con số là số trang trong mục đó của site, đếm lúc build site.

  • @comitor/ui

    Tầng 1 và Tầng 2, ComitorLocaleProvider cùng bảng chữ tiếng Anh và tiếng Việt, cn và các token. Chạy ở mọi nơi React chạy — Vite, Storybook, trình chạy test.

    Cần cài: Không có peer tuỳ chọn

  • @comitor/ui/shell

    Tầng 3: AppShell và các phần của nó, provider của theme và các trục hiển thị, cùng các dấu hiệu thương hiệu.

    Cần cài: nextnext-themes

  • @comitor/ui/chart

    Primitive Chart của shadcn, thêm biểu đồ đường, cột, vùng dựng sẵn và CHART_COLORS.

    Cần cài: recharts

  • @comitor/ui/data-grid

    DataGrid: hàng ảo hoá, chọn vùng bằng bàn phím, chép và dán qua lại với Excel, ghim cột, lọc, sắp xếp, hoàn tác.

    Cần cài: @tanstack/react-table

  • @comitor/ui/uploader

    FileUpload, ImageUploadField và useFileUpload — Uppy chạy headless, nên markup vẫn là của gói.

    Cần cài: @uppy/core@uppy/xhr-upload

  • @comitor/ui/editor

    MarkdownEditor lưu Markdown, có thanh nổi kiểu Linear, và RichTextEditor lưu HTML, có thanh công cụ ghim — bên dưới chung một lõi Tiptap, cả mười ba gói @tiptap cùng một phiên bản.

    Cần cài: @tiptap/*lowlight

  • @comitor/ui/chat

    Khung của một cuộc trò chuyện với trợ lý AI: luồng tin bám đáy, tin nhắn vẽ bằng MarkdownEditor chỉ đọc kèm nhãn “Do AI tạo”, các bước của agent, nguồn tham khảo, và ô soạn nhấn Enter là gửi.

    Cần cài: @tiptap/*lowlight

  • @comitor/ui/form

    Lớp gắn react-hook-form: Form, FormField, FormMessage và phần còn lại của API shadcn.

    Cần cài: react-hook-form

  • @comitor/ui/tokens

    Giá trị token dạng object thường, cho canvas, PDF và email HTML. Không bao giờ dùng trong mã Tailwind — dùng class.

    Cần cài: Không có peer tuỳ chọn

Chín entry, thêm styles.css. Hướng dẫn cài đặt có peer của từng entry, và hai cái tên thật sự mang nghĩa khác nhau ở những cửa khác nhau. Cài đặt

Component thật, đang chạy trên trang này

Không có gì bên dưới là ảnh chụp màn hình. Mỗi ô đều import từ @comitor/ui 3.17.3 đã phát hành — sửa lưới, kéo thẻ, rê chuột lên biểu đồ, nhấn Tab tới các avatar.

Bảng tính để nhập hàng loạt. Nhấp đúp vào ô để sửa, kéo qua các ô để chọn vùng, chép nó, và dán thẳng từ Excel.

Biểu đồ đường, cột và vùng trên token biểu đồ, tooltip theo định dạng số của người đọc.

Mọi tông màu ở cả ba biến thể. Chữ và viền dùng bậc -ink của từng màu, đo được từ 4,5:1 trở lên ở cả hai theme.

soft
DraftFeaturedPaidDue soonSevereOverdueSent
outline
DraftFeaturedPaidDue soonSevereOverdueSent
dot
DraftFeaturedPaidDue soonSevereOverdueSent

Kéo thẻ bằng chuột hay ngón tay — hoặc focus vào thẻ, nhấn Space, rồi di chuyển bằng phím mũi tên. Mỗi lần di chuyển đều được đọc cho trình đọc màn hình.

To do2 thẻ

Quote the Lotus Office Park chargers

HighQuân Đặng

Book the pole inspection crane

LowLinh Võ
In progress2 thẻ

Onboard Linh Võ to the Field app

MediumHà Lê

Renew the portal’s TLS certificate

HighĐức Nguyễn
Done1 thẻ

Publish the Q4 tariff sheet

MediumMai Trần

Chữ viết tắt lấy họ và tên — Nguyễn Văn An là NA, không phải NV — và tông màu suy ra từ tên, nên một người là một màu trên mọi màn hình.

Nguyễn Văn AnNguyễn Văn AnNguyễn Văn AnNguyễn Văn AnNguyễn Văn Antừ xs tới xl
TasksCRMFieldChatIconAvatar, mỗi khoá app một tông màu

Không chỉ là bộ component

Phần tốn công của một design system không phải cái nút. Đó là ngôn ngữ, màu sắc, trợ năng và ranh giới server — quyết một lần, ghi thành văn, và kiểm trước mỗi bản phát hành.

Một provider cho ngôn ngữ và định dạng

Từ 3.0.0, tiếng Anh là mặc định và tiếng Việt có sẵn. Một ComitorLocaleProvider ở gốc đổi mọi chuỗi gói render — chữ hiển thị, tên truy cập, thông báo kéo-thả — và mọi định dạng đằng sau chúng: ngày, số, thời gian tương đối, đồng hồ 12 hay 24 giờ.

Ngôn ngữ khác thì bắt đầu từ bảng tiếng Anh và truyền messages. Vùng là chuyện riêng: ngôn ngữ của người đọc không bao giờ quyết định tiền tệ hay quốc gia của một workspace.

Hoá đơn INV-0431

Tạo

Màu là một hợp đồng

Mỗi sắc màu có ba token, và hậu tố cho biết mỗi token được dùng ở đâu. Component không bao giờ gọi thẳng một màu thô, nên theme sáng, theme tối và bảng tương phản cao đều đi ra từ cùng một bộ class.

--x
Tô nền, bằng fill đặc hoặc tint.
--x-foreground
Chữ và icon nằm trên fill đặc đó.
--x-ink
Cùng màu đó trên nền trang: chữ, icon đứng riêng, viền.

Đo chứ không nhìn bằng mắt: trên nền trắng, text-success đạt 3,13:1, dưới mức AA; text-success-ink đạt 6,52:1. Vài chỗ hụt đã biết của bảng tương phản mặc định được liệt kê ở trang Màu sắc, còn với bảng tương phản cao, hụt một chỗ là build hỏng.

Năm trục hiển thị, mỗi người dùng tự chỉnh

Sáng hay tối, độ tương phản, mật độ hàng, cỡ chữ và màu thương hiệu — độc lập với nhau, mỗi trục một thuộc tính trên <html>, nhớ theo từng thiết bị. Thử xem: chúng áp dụng cho toàn bộ site này.

Theme

Đậm màu viền, công tắc và chữ phụ. Hữu ích trên màn hình độ sáng thấp hoặc khi dùng ngoài trời.

Giảm khoảng cách dọc giữa các dòng trong bảng, danh sách và menu.

Cỡ chữ
Bộ màu theme

Không bản nào phát hành mà chưa qua cổng

Mọi điều khiển đều dùng được bằng bàn phím, mọi tên mà trình đọc màn hình đọc lên đều dịch được, và chỗ nào một component biết mình còn thiếu thì trang của nó nói rõ. Trước khi một phiên bản lên npm, phải qua 15 bước:

  1. typecheckTypeScript strict trên toàn bộ mã nguồn.
  2. test:i18nKhông có chữ tiếng Việt nào nằm ngoài bảng tiếng Việt.
  3. buildMỗi file nguồn thành một ES module, nên file nào cũng giữ directive của riêng nó.
  4. verify:directivesMọi “use client” còn nguyên sau build; không hằng thường nào nằm trong file client.
  5. smoke:entriesCả chín entry đều nạp được trong Node thuần, qua exports map — và cũng resolve được theo điều kiện require của nó, đường mà webpack đi cho next/dynamic.
  6. check:exportspublint và Are the Types Wrong soát hình dạng gói khi phát hành.
  7. test:nav-activeThanh bên sáng đúng hàng, kể cả khi URL có query.
  8. test:grid-contractKhông class bố cục nào lặng lẽ làm vỡ lưới chứa nó.
  9. test:grid-renderDataGrid được render thật bằng react-dom/server.
  10. test:editor-markdownTài liệu → Markdown → tài liệu quay về nguyên vẹn, và HTML các editor ghi ra giữ giá trị được lưu, không phải giá trị vẽ ra.
  11. test:gantt-treePhép tính ngày, luật phụ thuộc và đường mũi tên của Gantt; hợp đồng di chuyển, cộng dồn và đánh số của TreeView; và cả hai được render trên server bằng hai ngôn ngữ.
  12. test:dom-renderMount và render lại bằng react-dom/client trên happy-dom — thứ đúng ở lần render đầu mà sai sau khi một prop đổi, như avatar mất ảnh.
  13. test:a11y-gateCổng tương phản vẫn bắt được mọi trường hợp từng lọt qua nó.
  14. lint:a11yĐo tương phản trên CSS đã biên dịch, ở cả hai bảng tương phản và cả hai theme.
  15. lint:palettes

Làm cho Server Component

Gói phát hành mỗi file nguồn thành một module, nên "use client" chỉ nằm ở file cần nó, và mọi hằng đều ở module mà Server Component đọc được. Thứ gì không qua được ranh giới thì báo lỗi rõ ràng, thay vì đến nơi thành undefined.

Chính trang này là một ví dụ: các con số gần đầu trang đến từ StatCard, render trên server. Prop mang một hàm — một formatter, một locale của date-fns — thì đi qua một file client nhỏ do app tự viết.

app/products/[id]/page.tsx
// app/products/[id]/page.tsx — a Server Component
import { Rating } from '@comitor/ui'

// ✓ strings, numbers and booleans cross to a client component
<Rating value={4.5} step={0.5} readOnly aria-label="Rated 4.5 out of 5" />

// ✗ a function does not — the page fails to render with
//   "Functions cannot be passed directly to Client Components…"
<Rating value={4.5} readOnly itemLabel={(value) => `${value} étoiles`} />

Màn hình hoàn chỉnh để chép theo

Mười màn hình sản phẩm ráp từ gói, chạy thật, có mã bên cạnh từng màn hình. Bắt đầu từ màn hình gần nhất với app cần làm.

  • Ứng dụng web

    Cả khung app: shell, thông báo workspace và tour lần đầu, thẻ KPI, bảng thành viên có thao tác hàng loạt, luồng tạo và sửa, thiết lập và cài đặt.

    AppShellDataTableTourStepper

  • CRM

    Lưới liên hệ lọc, sắp xếp và sửa tại chỗ, bản ghi của hàng đang focus, board deal và các con số nó cộng dồn lên.

    DataGridKanbanKeyValuePhoneInput

  • Tasks

    Theo dõi công việc: lưới task sửa được với thanh thao tác hàng loạt, chi tiết sửa tại chỗ, @mention, board kéo-thả và lộ trình.

    DataGridActionBarKanbanMention

  • Nhân sự

    Chín màn hình quản lý nhân sự: dashboard, danh bạ và hồ sơ, board tuyển dụng và phiếu đánh giá, onboarding, nghỉ phép, chấm công và bảng lương.

    KanbanStepperDateRangePickerTimePicker

  • Bán hàng

    Cửa hàng và phần quản trị phía sau: trang sản phẩm, đánh giá, giỏ hàng và bước thanh toán kiểm từng bước, rồi đơn hàng, tồn kho và trình sửa sản phẩm.

    StepperRatingDataGridCropper

  • Email

    Ứng dụng mail ba khung: thư mục, danh sách lọc được với nhãn gộp thành +N, luồng thư thu gọn được, thao tác hàng loạt và ô soạn có hẹn giờ gửi.

    BadgeOverflowActionBarTagsInputTimePicker

  • Chat

    Hộp thư chung của nhóm: hội thoại đã lọc, bản ghi gom nhóm, ô soạn có @mention và hẹn giờ gửi, trạng thái online, đang gõ và đã gửi.

    MentionTimePickerSearchFilterBar

  • Mạng xã hội

    Mạng nội bộ chỉ vào bằng lời mời: ô soạn nói rõ ai sẽ thấy, bảng tin, danh bạ thành viên, không gian riêng, hộp hoạt động và cài đặt quyền riêng tư.

    MentionCropperTagsInput

  • Di động

    App điều phối ở 375px: thanh bên nhường chỗ cho MobileMenu, danh sách ở hai mật độ, chi tiết trong drawer, đăng nhập, và một form dài chia thành các bước.

    MobileMenuResponsiveDialogStepperInputOTP

  • Trang giới thiệu

    Mười bốn dải ghép thành một website công khai, từ thanh điều hướng tới chân trang — cố ý phần lớn là bố cục, chỉ dùng tới gói ở chỗ gói có sẵn phần đó.

    BannerNavigationMenuAccordionRating

Từ app Next trống tới component đầu tiên

Ba bước đưa một app Next trống tới component đầu tiên được render. Hướng dẫn cài đặt có phần còn lại — các peer tuỳ chọn, cấu hình PostCSS, ngôn ngữ trong một sản phẩm thật — và lỗi mà mỗi bước giúp tránh.

  1. 01

    Cài gói và các peer

    Bắt buộc có React 19 và Tailwind v4; next, recharts và các gói khác chỉ cần cho entry dùng đến chúng.

    terminal
    pnpm add @comitor/ui
    pnpm add react react-dom lucide-react
    pnpm add -D tailwindcss @tailwindcss/postcss
    
    # optional peers — only for the entries you import (see Installation)

    02

    Nạp stylesheet

    Tailwind trước, rồi đến gói. Token và cả hai bảng tương phản đi kèm theo gói.

    app/globals.css
    /* app/globals.css — exactly these two lines, in this order */
    @import "tailwindcss";
    @import "@comitor/ui/styles.css";
  2. 03

    Nối root layout

    Font, ngôn ngữ và các trục hiển thị — các provider độc lập, nên thứ tự lồng nhau không quan trọng.

    app/layout.tsx
    // app/layout.tsx — a Server Component
    import { ComitorLocaleProvider } from '@comitor/ui'
    import { ContrastProvider, DensityProvider, FontSizeProvider, ThemeProvider } from '@comitor/ui/shell'
    import { Inter } from 'next/font/google'
    import './globals.css'
    
    // The package consumes --font-inter; it never loads a font itself.
    const inter = Inter({
      subsets: ['latin', 'vietnamese'],
      weight: ['400', '500', '600', '700', '800'],
      display: 'swap',
      variable: '--font-inter',
    })
    
    export default function RootLayout({ children }: { children: React.ReactNode }) {
      return (
        // suppressHydrationWarning is required: the display providers touch
        // <html> before React hydrates, so server and client HTML never match.
        <html lang="vi" suppressHydrationWarning className={inter.variable}>
          <body className="min-h-dvh bg-background font-sans text-foreground antialiased">
            {/* No provider renders English. A product passes the user's own
                language here rather than pinning one. */}
            <ComitorLocaleProvider locale="vi">
              <ThemeProvider>
                <ContrastProvider>
                  <DensityProvider>
                    <FontSizeProvider>{children}</FontSizeProvider>
                  </DensityProvider>
                </ContrastProvider>
              </ThemeProvider>
            </ComitorLocaleProvider>
          </body>
        </html>
      )
    }

@comitor/ui 3.17.3

An Avatar whose image is taken away shows its initials again.

1 bản sửa · không có gì phá vỡ

Đọc nhật ký thay đổi