Skip to content

Chuẩn giao diện đo được: grid, spacing, màu, token (SRC-1293) ​

Chủ dự án 08.10.2026: component "bị tạo loạn lên, xô lệch, không có tiêu chuẩn về Grid hay Spacing, không có tiêu chuẩn về màu sắc, hay về token", nên cần audit cực chặt, và làm thì phải đúng chuẩn.

DS-001 đã có luật màu (Ocean Palette §2·1·3), thang chữ (§2·1·5), radius và bóng (§4), luật dùng token (§7). Thứ còn THIẾU là thang spacing và lưới: hai thứ đó trước nay là gu của từng phiên, nên cùng một thẻ có chỗ p-5, chỗ p-[18px], chỗ p-7. Trang này lấp chỗ thiếu, và gom mọi luật đo được về một bảng để máy kiểm.

Máy kiểm: npm run audit:ui (scripts/audit-ui.mjs). Quy trình làm và quy trình rà: skill /ui-build và /audit-ui.

1. Bảng luật máy kiểm ​

LuậtĐúngSaiLý do
COLOR-RAWbg-primary, text-muted-foreground, var(--n12-ocean)#2C82B8, rgb(...), oklch(...) trong componentMàu thô là nguồn màu thứ hai; đổi palette thì nó không đổi theo
COLOR-TW-PALETTEmàu khai trong packages/design-system/tailwind.cssbg-amber-100, text-gray-600, border-blue-300Bảng màu mặc định của Tailwind không thuộc Nemo, không qua cổng contrast
ARBITRARYbước trong thang, tokenp-[18px], text-[0.95rem], rounded-[14px], z-[60]Giá trị tuỳ ý là "gần đúng", và gần đúng là xô lệch
SPACING-OFF-SCALEthang ở §2p-7, gap-2.5, mt-14, px-5.5Hai khoảng cách khác nhau 4px trông như lỗi chứ không như ý đồ
TYPE-BELOW-FLOORtext-sm (14px) trở lêntext-xs, text-[11px]Sàn chữ 14px (--text-floor) cho người đọc tiếng Việt có dấu
LAYOUT-WIDTHthang ở §3max-w-4xl, max-w-xl, max-w-7xlBề rộng khung tuỳ hứng trên một site làm mép trái nhảy khi chuyển trang
VARIANT-OVERRIDE<Button size="sm" variant="secondary"><Button className="h-9 px-3 bg-sky">Vẽ lại component bằng className là cách một nút thành mười kiểu nút; thiếu biến thể thì thêm vào component
Z-INDEXz-(--z-modal), z-10..z-50z-60, z-[999]Lớp chồng ngoài thang là cuộc chạy đua số, thắng lần này thua lần sau (SRC-819)
INLINE-STYLEclass tokenthuộc tính style gán padding hay color thẳng trên phần tửDS-001 §7: inline chỉ cho width/height/transform/biến CSS

Miễn trừ một dòng: chú thích ui-audit-ignore: <lý do> trên chính dòng đó. Không có lý do thì không được miễn. Hình vẽ bằng thuật toán (*Art.tsx, draw.ts, cover.ts, palette.ts) được dùng màu thô: đó là bảng màu của hình, không phải của giao diện.

2. Thang spacing ​

Bội của 4px, thêm hai bước 2px và 6px cho chi tiết nhỏ. Hết.

Bước TailwindpxDùng cho
0.5 · 1 · 1.52 · 4 · 6icon cạnh chữ, khe trong badge
2 · 38 · 12khe giữa các phần tử trong một hàng, padding nút nhỏ
416padding thẻ trên điện thoại, khe lưới thẻ, lề trang ở 375px
5 · 620 · 24padding thẻ từ sm trở lên, khe giữa các khối trong một section
8 · 10 · 1232 · 40 · 48khe giữa nhóm lớn, padding hero
16 · 20 · 2464 · 80 · 96nhịp dọc giữa các section

Ba nhịp cố định, đừng chọn lại mỗi lần:

  • Section: py-12 sm:py-16 lg:py-20.
  • Thẻ: p-4 sm:p-6 (DS-001 §2·1·7 nói 24-32px cho thẻ thật sự cần đựng: sm:p-6 hoặc lg:p-8).
  • Chồng dọc trong thẻ: gap-2 (nhãn với giá trị), gap-4 (khối với khối).

Khoảng cách giữa anh em dùng gap-* trên cha (flex/grid), không dùng mt-* rải trên từng con: margin rải từng con là nguồn số một của "lệch một chút" khi thêm hay bỏ một phần tử.

3. Lưới và bề rộng ​

Lề trang: px-4 sm:px-6 lg:px-8. Một mép trái cho mọi section của một trang.

Bề rộng khung, sáu giá trị:

ClasspxDùng cho
max-w-sm384popover, thẻ đứng một mình
max-w-md448form, dialog, màn đăng nhập
max-w-2xl · max-w-prose672 · 65chkhối đọc hẹp: câu hỏi, bài đọc
max-w-3xl768trang đọc, trang chi tiết
max-w-5xl1024khung gọn: trang có hai cột nhưng chủ dự án muốn chặt (trang Speaking, SRC-1293 giữ nguyên 33 chỗ)
max-w-6xl1152khung trang có lưới nhiều cột

Luật tối giản SRC-048 nói "full-width": nền section chạy hết bề ngang, NỘI DUNG nằm trong một trong sáu khung trên, căn giữa bằng mx-auto.

Lưới thẻ: mobile-first, một cột ở 375px; sm:grid-cols-2, rồi lg:grid-cols-3 (hoặc 4 với thẻ số liệu nhỏ). Khe gap-4, lg:gap-6 khi thẻ có padding p-6. Breakpoint chỉ sm và lg; md chỉ khi có lý do ghi lại trong code.

Lưới nhận đúng số con nó khai (DS-001 §4, SRC-301): phần tử chen vào lưới phải col-span-full hoặc nằm ngoài lưới. Mọi thẻ cùng hàng cao bằng nhau: dùng grid (không phải flex-wrap) và h-full trên thẻ; nút cuối thẻ đẩy xuống đáy bằng mt-auto trong thẻ flex flex-col.

4. Màu, chữ, hình khối: trỏ về DS-001 ​

  • Màu: chỉ token semantic (background, foreground, primary, muted, accent, accent-deep, border, destructive, success, warning) và Ocean Palette (DS-001 §2·1·3). Tỉ lệ 70/20/7/3, coral hiếm. Chữ trên nền coral dùng accent-deep. Đỏ không dùng cho trạng thái học tập (REQ-UX-03).
  • Chữ: thang DS-001 §2·1·5 (text-display-l ... text-body-l, text-sm 14px là sàn).
  • Radius: rounded-sm · rounded (thẻ) · rounded-lg (hero, dialog) · rounded-full (nút pill). Bóng: --shadow-1/2/3.
  • Chuyển động: chỉ preset trong packages/design-system/ui/Motion.tsx, thời lượng --dur-*.
  • Component: shadcn trong apps/<app>/src/components/ui/ hoặc packages/design-system/ui/. Thiếu biến thể thì thêm biến thể vào component đó, không dựng component song song.

5. Nợ đã trả hết, cổng chặn CI (08.10.2026) ​

Lượt đo đầu tiên trên cả repo (571 file) ra 2.458 vi phạm, phần lớn là text-xs (960), text-[11px] (123), cỡ chữ tuỳ ý và bước spacing ngoài thang ở apps/web và apps/learn. Chủ dự án yêu cầu xử lý toàn bộ ngay trong ngày:

  • Codemod đổi những gì có ánh xạ một nghĩa: text-xs thành text-sm, cỡ chữ tuỳ ý về bậc gần nhất, bước spacing về bước gần nhất của thang, bề rộng khung về khung gần nhất, màu mặc định của Tailwind về token cùng vai (vàng thành sun, xanh lá thành success/seafoam, đỏ thành destructive, xanh dương thành ocean-brand/sky/sea, xám thành muted/border).
  • Sửa tay phần cần phán đoán: màu thô, style inline, nút và badge vẽ lại bằng className.

Từ đó npm run check:code chạy audit-ui.mjs --strict: một vi phạm mới ở bất kỳ đâu là CI đỏ.

Vị trí và kích thước là hình học, không phải nhịp. top-[69px] neo dưới một thanh cao 69px, w-[...] khớp một ảnh: đổi chúng về "bước gần nhất" làm lệch thứ chúng bám theo. Vì vậy cổng không bắt top/left/right/bottom/inset, w/h/min/max, size, grid-cols-[...].

6. Token cấu trúc (SRC-1293) ​

Phép thử "đủ token": mọi giá trị nhìn thấy được trên màn hình đều có tên. Màu có tên từ lâu (DS-001 §2·1·3); các nhóm dưới đây là phần trước 08.10.2026 mỗi phiên tự chọn một con số. Giá trị ở packages/design-system/tokens/base.css, tên Tailwind ở packages/design-system/tailwind.css.

Ba tầng: nguyên liệu (thang bội 4px, Ocean Palette) · ngữ nghĩa (tên theo vai, bảng dưới) · component (biến thể cva của shadcn). Component chỉ đọc tầng ngữ nghĩa.

NhómToken CSSClass TailwindGiá trị
Control--control-h-sm/md/lgh-control-sm/md/lg36 · 44 · 52px
Icon--icon-sm/md/lgsize-icon-sm/md/lg16 · 20 · 24px
Chồng dọc--space-stack-sm, --space-stackgap-stack-sm, gap-stack8 · 16px
Thẻ--space-card-sm, --space-cardp-card-sm sm:p-card16 · 24px
Lưới--gutter, --gutter-lggap-gutter lg:gap-gutter-lg16 · 24px
Lề trang--page-x-sm/-/-lgpx-page-x-sm sm:px-page-x lg:px-page-x-lg16 · 24 · 32px
Nhịp section--section-y-sm/-/-lgpy-section-sm sm:py-section lg:py-section-lg48 · 64 · 80px
Khung--n12-container-*max-w-popover/form/read-narrow/read/wide/page384 · 448 · 672 · 768 · 1024 · 1152px
Lớp chồng--z-raised/sticky/dropdown/overlay/modal/toast/tourz-(--z-modal)10 · 20 · 30 · 40 · 50 · 60 · 70
Kiểu chữ--lh-*, --fw-*text-h1 ... tự mang dòng và độ đậmdòng 1,2 · 1,35 · 1,6
Viền, focus--border-hairline/strong, --ring-width, --ring-offset1 · 2 · 2 · 2px
Trạng thái--opacity-disabled, --overlay0,5 · deep-ocean 55%

text-h2 nay mang sẵn line-height và font-weight: viết text-h2 là đủ, không thêm leading-* hay font-* cạnh nó.

Shadcn không thay được bảng này. Shadcn chuẩn hoá HÀNH VI (bàn phím, focus, aria) và khoảng 20 token màu cộng một --radius. Spacing, cỡ chữ, khung, lớp chồng nó để nguyên thang mặc định của Tailwind, một thang rộng tới mức p-7, text-xs, max-w-4xl đều hợp lệ. Dùng đúng shadcn mà vẫn xô lệch là chuyện bình thường nếu không có tầng ngữ nghĩa.

Còn để dành: màu trạng thái đủ bộ (info, success, warning kèm nền nhạt và chữ trên nền) cần một lượt đo contrast riêng; gộp hai bộ radius v0.4 và v1; bỏ --dur-1/2 cũ. Khi làm thì thêm vào bảng này, không mở bảng thứ hai.

7. Bốn khung bố cục ​

packages/design-system/ui/Layout.tsx: Section (vai nền + nhịp dọc), Container (một trong sáu khung + lề trang), CardGrid (lưới thẻ mobile-first, con cao bằng nhau), Stack (chồng dọc bằng gap). App dùng thì chép bản sao y hệt như Motion.tsx (DS-001 §1).

tsx
<Section band="foam">
  <Container>
    <Stack>
      <h2 className="text-h2">...</h2>
      <CardGrid cols={3}>...</CardGrid>
    </Stack>
  </Container>
</Section>

Màn mới dựng bằng bốn khung này. Màn cũ chuyển dần khi có người sửa, cùng luật "file mình chạm thì phải sạch" ở §5.