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

1
4pxKhe giữa icon trong dòng, badge sát
1.5
6pxĐệm của mục menu và mục thanh bên — mặc định của --menu-item-py
2
8pxĐệm bên trong component (sm)
3
12pxĐệm của hàng bảng và mục danh sách — mặc định của --row-py
4
16pxLề PageContainer trên mobile, đệm thẻ (gọn)
5
20pxĐệm của composite — StatCard và FormSection
6
24pxĐệm thẻ (mặc định), lề PageContainer từ md trở lên
8
32pxKhoảng giữa các mục, lề trang
10
40pxĐệm thẻ lớn; --table-head-h ở mật độ mặc định
12
48pxĐệm mục (mobile)
16
64pxĐệm mục (desktop)
24
96pxĐệm trên của hero
32
128pxĐệm dưới của hero

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.

rounded-sm4px
rounded-md6px
rounded-lg10px
rounded-xl12px
UtilityKhai báoỞ mặc địnhDùng cho
rounded-smcalc(var(--radius) - 2px)4pxChip lồng nhau, bề mặt nhỏ bên trong
rounded-md--radius6pxMặc định. Nút, ô nhập, badge, mục menu
rounded-lgcalc(var(--radius) + 4px)10pxThẻ, popover, dialog
rounded-xl0.75rem12pxPanel 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 đồ

shadow-overlay

Một lớp phủ trên nội dung — Dialog, Sheet, Popover, menu, Select, Tooltip

UtilityBiếnSángTối
shadow-surface--elevation-surface0 1px 2px rgba(14,17,22,.04)0 1px 2px rgba(0,0,0,.32)
shadow-control--elevation-control0 1px 2px rgba(14,17,22,.08)0 1px 2px rgba(0,0,0,.4)
shadow-card--elevation-card0 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-overlay0 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ếnUtilityGiá trịĐo cái gì
--spacing-headerh-header3.5rem · 56pxChiều cao header của app. Có bản sao là layout.headerHeight trong @comitor/ui/tokens.
--spacing-sidebarw-sidebar16rem · 256pxBề rộng thanh bên khi mở. layout.sidebarWidth.
--spacing-sidebar-iconw-sidebar-iconcalc(3.25rem + 1px) · 53pxBề rộng thanh bên khi thu về icon. layout.sidebarCollapsedWidth.
--spacing-module-railw-module-rail5.25rem · 84pxRail module của moduleLayout="rail" — ô 72px, cách hai mép 6px. layout.moduleRailWidth.
--spacing-module-panelw-module-panel15rem · 240pxThanh 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ếnThoángGọnĐệm cho cái gì
--row-py0.75rem · 12px0.375rem · 6pxĐệm dọc của một ô bảng
--menu-item-py0.375rem · 6px0.25rem · 4pxĐệm dọc của một mục menu
--list-item-py0.75rem · 12px0.5rem · 8pxĐệm dọc của một mục danh sách
--nav-item-py0.5rem · 8px0.25rem · 4pxĐệm dọc của một mục thanh bên
--table-head-h2.5rem · 40px2rem · 32pxChiều cao hàng tiêu đề của bảng
tsx
/* 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>
trang cài đặt
'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.

tsx
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

Mobile:768px
Tablet:1024px
Desktop:1280px

Có bản sao là layout.breakpoints trong @comitor/ui/tokens cho mã phải rẽ nhánh trong JS.

Vùng chạm

Tối thiểu:44px
Token:layout.minTapTarget

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.