Nền tảng
Khoảng cách
Comitor dùng thang khoảng cách cơ số 4 (1 đơn vị = 4 px). Mọi khoảng cách trong UI phải lấy từ thang này — không dùng giá trị pixel tuỳ ý. Bản thân cơ số là --spacing: 0.25rem của Tailwind, và gói cố ý để nguyên nó.
Thang khoảng cách
Thanh vẽ ở tỉ lệ 2× cho dễ đọc. Vì thang là calc(var(--spacing) * n) bằng rem, mọi giá trị ở đây co giãn theo trục cỡ chữ — kể cả cỡ icon, vốn lấy từ thang này chứ không từ thang chữ.
Bo góc
Một biến, --radius: 6px, và ba bậc suy ra từ nó. Sáu pixel là dấu ấn thị giác của hệ thống. Nó viết bằng px, không phải rem, và đó chính là chủ ý: mọi giá trị rem đi theo trục cỡ chữ, nên bo góc bằng rem sẽ thành 6,75px ở bậc lớn và dấu ấn sẽ trôi theo tuỳ chọn của người dùng. rounded-xl là bậc duy nhất vẫn khai bằng rem.
| Utility | Khai báo | Ở mặc định | Dùng cho |
|---|---|---|---|
| rounded-sm | calc(var(--radius) - 2px) | 4px | Chip lồng nhau, bề mặt nhỏ bên trong |
| rounded-md | --radius | 6px | Mặc định. Nút, ô nhập, badge, mục menu |
| rounded-lg | calc(var(--radius) + 4px) | 10px | Thẻ, popover, dialog |
| rounded-xl | 0.75rem | 12px | Panel lớn và bề mặt hero |
Độ nổi
Bốn bậc, từ thấp tới cao. Giá trị thô nằm ở các biến CSS --elevation-* chứ không ở trong @theme, vì đổ bóng phải đổi giữa sáng và tối lúc chạy. Ở theme tối, hình dạng giữ nguyên nhưng bóng màu mực được thay bằng màu đen thuần với alpha cao hơn nhiều — trên #0E1116 bóng màu mực đơn giản là không thấy, và một dialog không có bóng trông như dán phẳng lên trang.
shadow-surface
Một mặt phẳng đứng yên, nơi viền vẫn là tín hiệu chính — Card mặc định
shadow-control
Một control tương tác nhô nhẹ khỏi trang — Button fill đặc
shadow-card
Một thẻ hoặc panel nổi — StatCard, Toast, Card variant elevated, tooltip biểu đồ
| Utility | Biến | Sáng | Tối |
|---|---|---|---|
| shadow-surface | --elevation-surface | 0 1px 2px rgba(14,17,22,.04) | 0 1px 2px rgba(0,0,0,.32) |
| shadow-control | --elevation-control | 0 1px 2px rgba(14,17,22,.08) | 0 1px 2px rgba(0,0,0,.4) |
| shadow-card | --elevation-card | 0 1px 2px rgba(14,17,22,.04), 0 8px 24px rgba(14,17,22,.06) | 0 1px 2px rgba(0,0,0,.32), 0 8px 24px rgba(0,0,0,.4) |
| shadow-overlay | --elevation-overlay | 0 8px 32px rgba(14,17,22,.16) | 0 8px 32px rgba(0,0,0,.64) |
Muốn đổi độ nổi trên toàn app, ghi đè bốn biến --elevation-* — đừng bao giờ ghi đè --shadow-*. Một hệ quả của việc giữ giá trị bằng var(): Tailwind không tách được thành phần màu, nên shadow-card/50 và các modifier màu như shadow-red không có tác dụng.
Số đo của khung app
Ba số đo được công bố vào không gian tên spacing để tầng shell không bao giờ phải viết một con số ma thuật.
| Biến | Utility | Giá trị | Đo cái gì |
|---|---|---|---|
| --spacing-header | h-header | 3.5rem · 56px | Chiều cao header của app. Có bản sao là layout.headerHeight trong @comitor/ui/tokens. |
| --spacing-sidebar | w-sidebar | 16rem · 256px | Bề rộng thanh bên khi mở. layout.sidebarWidth. |
| --spacing-sidebar-icon | w-sidebar-icon | calc(3.25rem + 1px) · 53px | Bề rộng thanh bên khi thu về icon. layout.sidebarCollapsedWidth. |
| --spacing-module-rail | w-module-rail | 5.25rem · 84px | Rail module của moduleLayout="rail" — ô 72px, cách hai mép 6px. layout.moduleRailWidth. |
| --spacing-module-panel | w-module-panel | 15rem · 240px | Thanh bên cạnh rail module. layout.modulePanelWidth. |
Cả ba khai bằng rem, nên đi theo trục cỡ chữ. Các số px chép sang @comitor/ui/tokens là giá trị ở bậc mặc định — dùng chúng cho canvas và PDF, không dùng để dàn DOM.
Mật độ là cài đặt của người dùng
Bố cục gọn là một trong năm trục hiển thị, bật bằng <html data-density="compact">. Nó ghi đè đúng năm token và không chạm vào gì khác. Để ý điều nó không làm: nó không ghi đè --spacing toàn cục. Làm thế chỉ mất một dòng, nhưng cũng sẽ thu hẹp đệm ngang và kéo mọi icon size-4 xuống khoảng 12,8px. Gọn nghĩa là nhiều hàng hơn trên màn hình — không phải chữ dí sát mép — nên trục này chỉ theo chiều dọc.
| Biến | Thoáng | Gọn | Đệm cho cái gì |
|---|---|---|---|
| --row-py | 0.75rem · 12px | 0.375rem · 6px | Đệm dọc của một ô bảng |
| --menu-item-py | 0.375rem · 6px | 0.25rem · 4px | Đệm dọc của một mục menu |
| --list-item-py | 0.75rem · 12px | 0.5rem · 8px | Đệm dọc của một mục danh sách |
| --nav-item-py | 0.5rem · 8px | 0.25rem · 4px | Đệm dọc của một mục thanh bên |
| --table-head-h | 2.5rem · 40px | 2rem · 32px | Chiều cao hàng tiêu đề của bảng |
/* Consume a density token the way the package does — as a Tailwind
arbitrary-property value, not as a hardcoded py-3. */
<td className="py-(--row-py)">…</td>
<div className="py-(--list-item-py)">…</div>'use client'
// The density axis is wired once with <DensityProvider> in the root layout.
// Its label and description follow ComitorLocaleProvider — "Compact layout" in
// English, "Bố cục gọn" under locale="vi"; pass label / description only to reword.
import { DensityToggle } from '@comitor/ui/shell'
export function DisplaySettings() {
return <DensityToggle />
}Bố cục trang
Lề trang và bề rộng nội dung lấy từ PageContainer chứ không từ một max-w-… px-… viết riêng ở từng trang. Nếu trang nào cũng tự viết, lề trái của hai app trong hệ sinh thái sẽ lệch nhau vài pixel — khó thấy, nhưng đủ làm cả hệ thống trông như ráp vội.
import { PageContainer } from '@comitor/ui'
// Every variant renders mx-auto w-full px-4 md:px-6 py-4 md:py-6,
// plus one of four max widths:
// sm max-w-2xl 42rem — settings pages, one-column forms
// md max-w-5xl 64rem — the default, detail pages
// lg max-w-[90rem] — wide multi-column tables
// full max-w-none — full-bleed tables, kanban boards
<PageContainer width="lg" as="main">…</PageContainer>
// flush drops the vertical padding when the page paces itself.
<PageContainer width="full" flush>…</PageContainer>Breakpoint
Có bản sao là layout.breakpoints trong @comitor/ui/tokens cho mã phải rẽ nhánh trong JS.
Vùng chạm
WCAG 2.5.5. Mật độ gọn làm hàng thấp đi nhưng không bao giờ được đưa một vùng tương tác xuống dưới mức này trên bề mặt cảm ứng.