# Design skill

Design skill là file luật cho agent code. Token quyết định giá trị, còn skill quyết định cách ghép chúng lại: cỡ chữ, khoảng cách, góc bo, chuyển động. Agent đọc file này trước khi viết một màn hình.

## 1. Trong file có gì

Mỗi luật có một cách nên viết và một cách nên tránh. Chọn một luật để đọc đầy đủ.

**Chữ**

- [1. Chữ nội dung là 14px](#rule-1)
- [2. Tiêu đề viết hoa kiểu câu](#rule-2)
- [3. Không đổi khoảng cách chữ](#rule-3)
- [4. Không dùng bold](#rule-4)
- [5. Code trong dòng là 0.9em](#rule-5)

**Khoảng cách và căn chỉnh**

- [6. Thứ liên quan nằm gần hơn thứ không liên quan](#rule-6)
- [7. Padding dọc quanh chữ nhỏ hơn một bậc](#rule-7)
- [8. Icon thẳng hàng với dòng chữ đầu tiên](#rule-8)

**Bề mặt**

- [9. Lớp có bóng đổ dùng ring, không dùng viền](#rule-9)
- [10. Góc lồng nhau đồng tâm](#rule-10)
- [11. Đừng đặt Card trong Card](#rule-11)
- [12. Thanh sticky được tách bằng viền](#rule-12)

**Chuyển động và trạng thái**

- [13. Hover và nhấn chuyển dần trong 160ms](#rule-13)
- [14. Mọi đổi trạng thái mà component tạo ra đều thấy trong chuyển động](#rule-14)
- [15. Nội dung thu gọn giữ nguyên kích thước](#rule-15)
- [16. Dialog luôn được mount và dùng `open`](#rule-16)

**Kích thước**

- [17. Kích thước phần tử lấy từ 12 class](#rule-17)

**Đang tải**

- [18. Không để trống nội dung khi nội dung tiếp theo đang tải](#rule-18)
- [19. Ảnh giữ chỗ và fade vào](#rule-19)

**Độ đục**

- [20. Độ đục lấy từ ba token vai trò](#rule-20)

**Kính**

- [21. Kính chỉ trên lớp nổi, tối đa hai](#rule-21)

**Bố cục**

- [22. Nội dung app nằm trong một surface inset](#rule-22)

## 2. Cài bằng CLI

Chạy `npx werewolf-ui init` một lần trong [app được hỗ trợ](https://www.werewolf-ui.dev/vi/docs/installation.md). Lệnh cài `.agents/skills/werewolf-design/SKILL.md`, một phần do CLI quản lý trong `AGENTS.md` và `.cursor/rules/werewolf.mdc`. Cho agent đọc skill trước khi viết UI.

## 3. Cài thủ công

Nếu cài thủ công hoặc qua registry, copy file `skills/werewolf-design/SKILL.md` vào thư mục mà agent của bạn đọc luật. File cài vào dự án giữ nguyên tiếng Anh, vì agent đọc bản gốc. Phần dưới là bản tiếng Việt của các luật đó.

## Các luật

### Chữ

#### 1. Chữ nội dung là 14px

Đoạn văn, giá trị trong input, ô bảng và control `md` (mặc định) cùng `lg` dùng 14px: `text-body` cho đoạn văn, `text-sm` bên trong control. 14px là cỡ mà cả thang chữ được chỉnh quanh, và trộn 14 với 15 hoặc 16 trong cùng một khối trông như một lỗi, không phải một phân cấp.

- 14px (`text-sm`) dùng cho chữ phụ cạnh một dòng chính: mô tả, chữ gợi ý và chữ lỗi, control `sm`.
- 12px (`text-xs`) dùng cho badge, token, chú thích và mốc thời gian.
- 16px trở lên (`text-md`, `text-heading-*`) chỉ dùng cho tiêu đề và control `xl`.

```tsx
// Do
<p className="text-body text-text-primary">Your trial ends in 12 days.</p>
// Avoid: body copy at 16px to "make it readable"
<p className="text-md text-text-primary">Your trial ends in 12 days.</p>
```

#### 2. Tiêu đề viết hoa kiểu câu

Chỉ viết hoa từ đầu tiên và danh từ riêng. Tên sản phẩm và tên tính năng giữ cách viết mà chủ của chúng dùng. Title Case và `uppercase` khiến tiêu đề đọc chậm hơn và to hơn nội dung bên dưới.

```tsx
// Do
<h2 className="text-heading-3">Billing history</h2>
// Avoid
<h2 className="text-heading-3">Billing History</h2>
<h2 className="text-heading-3 uppercase">Billing history</h2>
```

#### 3. Không đổi khoảng cách chữ

Không dùng class `tracking-*` và không dùng `letter-spacing` trong CSS. Geist và JetBrains Mono đã được giãn đúng cho các cỡ trong thang; siết hoặc nới chúng là cách UI được sinh ra bắt đầu trông như do máy sinh.

```tsx
// Do
<span className="text-xs font-medium text-text-secondary">Last 30 days</span>
// Avoid
<span className="text-xs font-medium tracking-wide uppercase text-text-secondary">Last 30 days</span>
```

#### 4. Không dùng bold

Thang có regular (400), medium (500) và semibold (600). Không có 700, và `font-bold` không có tác dụng trong một dự án Werewolf.

- Tiêu đề: độ đậm đi theo vai trò (`text-heading-1` đến `-3` đã đặt sẵn). Đừng thêm class độ đậm lên trên.
- Nhấn trong chữ, nhãn, chữ trên nút: `font-medium`.
- `font-semibold` chỉ khi medium không tách được một nhãn khỏi chữ medium ngay cạnh nó.

```tsx
// Do
<p className="text-body">Payment failed. <span className="font-medium">Update your card</span> to keep access.</p>
// Avoid
<p className="text-body">Payment failed. <b>Update your card</b> to keep access.</p>
```

#### 5. Code trong dòng là 0.9em

Chữ mono nằm trong một câu sans trông to hơn các từ xung quanh khi cùng một cỡ. Dùng `code-inline`: nó đặt font mono ở `0.9em`, nên nó theo đúng cỡ của câu.

```tsx
// Do
<p className="text-body">Run <code className="code-inline">npm run build</code> again.</p>
// Avoid
<p className="text-body">Run <code className="font-mono">npm run build</code> again.</p>
```

### Khoảng cách và căn chỉnh

#### 6. Thứ liên quan nằm gần hơn thứ không liên quan

Một tiêu đề và phần mô tả của nó là một đơn vị, nên khoảng cách giữa chúng nhỏ hơn khoảng cách tới đơn vị kế tiếp. Nếu mọi khoảng cách như nhau, người đọc không biết cái nào thuộc về cái nào.

- Tiêu đề tới mô tả, nhãn tới field, nhãn tới chữ gợi ý: `gap-xs` hoặc `gap-sm`.
- Field tới field, header tới phần thân trong một card hoặc dialog: `gap-lg`.
- Nhóm tới nhóm: `gap-xl`. Mục tới mục: `gap-2xl` hoặc hơn.

Khoảng cách bên trong luôn phải nhỏ hơn khoảng cách bên ngoài ít nhất một bậc.

```tsx
// Do
<div className="flex flex-col gap-lg">
  <div className="flex flex-col gap-xs">
    <h3 className="text-heading-3">Webhooks</h3>
    <p className="text-sm text-text-secondary">Send events to your server.</p>
  </div>
  <Button>Add endpoint</Button>
</div>
// Avoid: one gap for everything
<div className="flex flex-col gap-md">
  <h3 className="text-heading-3">Webhooks</h3>
  <p className="text-sm text-text-secondary">Send events to your server.</p>
  <Button>Add endpoint</Button>
</div>
```

#### 7. Padding dọc quanh chữ nhỏ hơn một bậc

Chiều cao dòng đã thêm khoảng trống phía trên và dưới các chữ, nên padding đều mọi phía trông cao hơn bề ngang. Quanh chữ, dùng một bậc khoảng cách nhỏ hơn ở trên và dưới so với hai bên. Control có chiều cao cố định (`h-element-*`) đã xử lý việc này; luật này dành cho khối bạn tự đặt padding: banner, callout, dòng danh sách, ô bảng.

```tsx
// Do
<div className="rounded-element bg-background-sunken px-lg py-md text-body">Changes are saved automatically.</div>
// Avoid
<div className="rounded-element bg-background-sunken p-lg text-body">Changes are saved automatically.</div>
```

#### 8. Icon thẳng hàng với dòng chữ đầu tiên

Icon cạnh chữ có thể xuống dòng nằm trong một hộp cao đúng một dòng, để nó căn giữa dòng đầu và đứng yên khi chữ xuống dòng. Căn nó vào giữa cả khối khiến nó trôi xuống giữa đoạn văn. Dùng cỡ icon khớp với chữ (12px với `text-xs`, 16px với `text-sm`, 20px với `text-md` trở lên) và `color-icon-*` cho màu của nó.

```tsx
// Do
<div className="flex gap-sm text-body">
  <span className="flex h-[1lh] shrink-0 items-center">
    <IconInfoCircle size={16} className="text-icon-secondary" aria-hidden />
  </span>
  <p>Keys can't be viewed again after you close this dialog.</p>
</div>
// Avoid
<div className="flex items-center gap-sm text-body">
  <IconInfoCircle size={16} className="text-icon-secondary" aria-hidden />
  <p>Keys can't be viewed again after you close this dialog.</p>
</div>
```

### Bề mặt

#### 9. Lớp có bóng đổ dùng ring, không dùng viền

Lớp nổi (menu, popover, dialog, toast) dùng `ring-1 ring-border` cho cạnh. Ring được vẽ cùng bóng đổ, nằm ngoài layout, nên cạnh vẫn sắc và kích thước lớp không đổi. Card nằm trên trang không có bóng và giữ `border border-border`.

```tsx
// Do
<div className="rounded-container bg-background-raised ring-1 ring-border shadow-med">...</div>
// Avoid
<div className="rounded-container bg-background-raised border border-border shadow-med">...</div>
```

Lớp kính theo cùng luật: `material-thick` với `ring-1 ring-border` và một `shadow-*` (luật 21).

Ở dark mode, các token `shadow-*` đã mang cạnh của lớp: một bóng tối hơn cộng một đường sáng 1px ở mặt trong cạnh trên. Đặt lớp trên `bg-background-raised` và dùng class bóng; đừng tự thêm viền sáng (không `border-t` sáng, không `inset-shadow-*`, không ring thêm), nếu không cạnh hiện hai lần. Lớp nào tự vẽ viền sáng cạnh thì đặt `--werewolf-shadow-edge: 0 0 #0000` để bỏ viền có sẵn; các class `material-*` đã làm vậy.

Các biến thể Button glossy (`glossy`, `destructive-glossy`) là control duy nhất có bóng đổ, và chúng tự vẽ cạnh. Dùng biến thể đó; đừng dựng một control nổi từ các class `shadow-*` và gradient.

#### 10. Góc lồng nhau đồng tâm

Khi một hình bo góc nằm trong hình khác và khe giữa các cạnh từ 8px trở xuống, bán kính ngoài bằng bán kính trong cộng khe đó. Nếu không, các góc bị bóp ở phía trong và trông không đều.

Với bán kính `default`, các bậc bán kính của Werewolf cách nhau 4px (`radius-inner`, `radius-element`, `radius-container`, `radius-page`), nên các bậc đồng tâm đúng khi khe là 4px (`p-xs`):

- `radius-inner` trong `radius-element` với khe 4px (một Token trong Input, một đoạn trong segmented control).
- `radius-element` trong `radius-container` với khe 4px (một mục menu trong menu).

`radius-inner` không bao giờ vượt 6px. Nên với bán kính `round` hoặc `pill`, hoặc bán kính px trên 10px, `radius-inner` trong `radius-element` không đồng tâm. Đó là ngoại lệ đã biết, và được chấp nhận. Chỗ các góc phải thẳng hàng đúng, hãy tính bán kính trong: `max(outer - gap, 0)`.

Danh sách menu, select hoặc combobox có `p-xs` cộng viền 1px, nên các hàng nằm cách cạnh ngoài 5px. Dùng `rounded-item` (`radius-item`, bán kính container trừ 5px) cho các hàng đó, không dùng `rounded-inner`.

Với mọi khe khác tới 8px, tính bán kính trong từ bán kính ngoài. Với khe trên 8px, các góc đã đủ xa nên chỉ cần dùng bậc thấp hơn kế tiếp. Nếu hình ngoài là `rounded-full`, hình trong cũng là `rounded-full`.

```tsx
// Do: 4px gap, steps line up
<div className="rounded-container p-xs ring-1 ring-border shadow-med bg-background-raised">
  <button className="rounded-element px-md h-element-sm w-full text-left">Rename</button>
</div>
// Do: 8px gap, compute it
<div className="rounded-container p-sm">
  <div className="rounded-[max(calc(var(--radius-container)-var(--spacing-sm)),0px)]">...</div>
</div>
// Avoid: 8px gap with the 4px-gap step
<div className="rounded-container p-sm">
  <div className="rounded-element">...</div>
</div>
```

#### 11. Đừng đặt Card trong Card

Hai viền và hai lớp padding chồng vào nhau tốn chỗ và làm mờ cái nào được nhóm với cái nào. Trong Card, tách các phần bằng khoảng cách (luật 6), một đường chia (`border-t border-border`), hoặc một khối lõm (`bg-background-sunken rounded-element`). Nếu phần trong thực sự là một đối tượng riêng, phần ngoài có lẽ không nên là Card.

```tsx
// Do
<Card>
  <CardHeader>...</CardHeader>
  <CardContent className="flex flex-col gap-lg">
    <div className="rounded-element bg-background-sunken px-lg py-md">...</div>
  </CardContent>
</Card>
// Avoid
<Card>
  <CardContent>
    <Card>...</Card>
  </CardContent>
</Card>
```

#### 12. Thanh sticky được tách bằng viền

Header, toolbar hoặc footer sticky nhận `border-b border-border` (hoặc `border-t` cho footer) để tách khỏi nội dung đang cuộn bên dưới. Bóng đổ dành cho lớp nổi (luật 9), và thanh sticky không nổi. Nếu nội dung nên lộ xuyên qua, dùng `material-thin`, lớp này tự vẽ viền sáng cạnh và chuyển đặc ở chỗ thiếu độ mờ (luật 21).

```tsx
// Do
<header className="sticky top-0 bg-background-page border-b border-border">...</header>
// Avoid
<header className="sticky top-0 bg-background-page shadow-low">...</header>
```

### Chuyển động và trạng thái

#### 13. Hover và nhấn chuyển dần trong 160ms

Các thay đổi hover, nhấn và focus dùng `duration-fast` (160ms) với `ease-standard`. Trên bất kỳ thứ gì có overlay hover hoặc pressed, dùng `transition-control`: nó fade màu, bóng, tỉ lệ nhấn và overlay, vốn là một lớp gradient mà một `background-color` được liệt kê không fade được. Trên phần tử không có overlay, hãy liệt kê các thuộc tính màu của nó. Không bao giờ dùng `transition-colors` hoặc `transition-all`: chúng gồm `outline-color`, nên vòng focus sẽ fade vào thay vì hiện ngay. Bản thân vòng không bao giờ được animate.

```tsx
// Do
<button className="transition-control duration-fast ease-standard hover:overlay-hover active:overlay-pressed motion-safe:active:scale-pressed focus-visible:focus-ring">...</button>
<a className="transition-[color] duration-fast ease-standard hover:text-text-primary focus-visible:focus-ring">...</a>
// Avoid
<a className="transition-colors duration-300 hover:overlay-hover">...</a>
<a className="transition-all hover:overlay-hover">...</a>
<a className="transition-[background-color] hover:overlay-hover">...</a> {/* the overlay still changes in one step */}
```

Tôn trọng `prefers-reduced-motion`: màu vẫn có thể đổi, chuyển động (tỉ lệ nhấn, trượt) đặt sau `motion-safe:`.

#### 14. Mọi đổi trạng thái mà component tạo ra đều thấy trong chuyển động

Một component chưa xong cho đến khi các đổi trạng thái nhìn thấy được của nó được animate. Phần tử tương tác (nút, toggle, checkbox, tab, nút đóng) dùng `transition-control duration-fast ease-standard` (luật 13), và control nhấn được thì thêm `motion-safe:active:scale-pressed`. Những thứ xuất hiện (dialog, sheet, popover, menu, tooltip, dòng mở rộng, toast) animate vào với `ease-enter`, `duration-medium` (`duration-slow` cho dialog và sheet, `duration-fast` cho tooltip), theo mẫu của Sheet:

```tsx
<div className="transition-[translate,opacity] duration-slow ease-enter starting:opacity-0 motion-safe:starting:-translate-y-full">
```

Chúng rời đi với `data-[state=closed]:animate-exit-fade`, `-scale`, `-slide-top`, `-slide-right`, `-slide-bottom`, `-slide-left` hoặc `-collapse`: keyframe dùng `ease-exit`, nhanh hơn lúc đến một bậc, và chỉ còn fade khi giảm chuyển động. Lớp unmount khi đóng chỉ còn trên trang khi một animation đang chạy, nên một transition không phát được đoạn thoát.

```tsx
<div className="transition-[translate,opacity] duration-slow ease-enter starting:opacity-0 motion-safe:starting:-translate-y-full data-[state=closed]:animate-exit-slide-top">
```

Popover hoặc menu lớn lên từ cạnh quay về phía trigger: `data-[side=bottom]:origin-top data-[side=top]:origin-bottom data-[side=left]:origin-right data-[side=right]:origin-left`. Dialog giữ ở giữa. Việc càng hay xảy ra thì càng ít chuyển động: command palette mở từ bàn phím thì mở ngay, và các tooltip đứng cạnh nhau nằm trong một `TooltipGroup` để cái kế tiếp mở ngay.

Khối chứa tĩnh (Card, Separator, Badge, Kbd, EmptyState, Metadata, Field) không cần chuyển động riêng, nhưng mọi phần tử tương tác mà chúng render phải mang chuyển động đó. Component mà các phần tương tác là component khác (Pagination dùng Button, ToggleGroup dùng Toggle, TableSelection dùng Checkbox) kế thừa chuyển động của chúng.

#### 15. Nội dung thu gọn giữ nguyên kích thước

Khi một mục mở hoặc đóng, hãy animate chiều cao của lớp bọc có cắt (`overflow-hidden`), và để nội dung bên trong giữ đủ chiều rộng và chiều cao suốt thời gian đó. Đừng animate padding, cỡ chữ hoặc scale của nội dung, và đừng để nó reflow khi lớp bọc co lại; chữ xuống dòng lại giữa animation trông như hỏng. Dùng `duration-medium`, `ease-enter` để mở và `ease-exit` để đóng.

```tsx
// Do: the wrapper animates, the content stays the same size
<div className="grid transition-[grid-template-rows] duration-medium ease-enter data-[state=closed]:grid-rows-[0fr] data-[state=open]:grid-rows-[1fr]">
  <div className="overflow-hidden"><div className="pt-md">...</div></div>
</div>
// Avoid: padding and content shrink together
<div className="transition-all data-[state=closed]:h-0 data-[state=closed]:py-0">...</div>
```

#### 16. Dialog luôn được mount và dùng `open`

Render Dialog mọi lúc và hiện hoặc ẩn nó bằng `open` và `onOpenChange`. Chỉ mount khi nó cần hiện thì bỏ animation thoát, và có thể mất dấu chỗ focus phải trở về khi nó đóng.

```tsx
// Do
<Dialog open={open} onOpenChange={setOpen}>
  <DialogContent>...</DialogContent>
</Dialog>
// Avoid
{open && (
  <Dialog open onOpenChange={setOpen}>
    <DialogContent>...</DialogContent>
  </Dialog>
)}
```

### Kích thước

#### 17. Kích thước phần tử lấy từ 12 class

Chiều cao, chiều rộng của control và slot vuông như nút icon lấy đúng từ 12 class này, nên chúng thẳng hàng với Button và Input cùng cỡ. Component dùng cùng các bậc với prop `size`: `sm`, `md` (mặc định), `lg`, `xl`. Cảm ứng (`pointer: coarse`) đẩy `md`, `lg` và `xl` lên bậc trên; `sm` giữ nguyên vẻ ngoài.

| Bậc | Chiều cao | Chiều rộng | Chiều rộng và chiều cao | Chuột | Cảm ứng (`pointer: coarse`) |
| --- | --- | --- | --- | --- | --- |
| sm | `h-element-sm` | `w-element-sm` | `size-element-sm` | 28px | 28px, vùng chạm qua `size-target-min` |
| md | `h-element-md` | `w-element-md` | `size-element-md` | 32px | 36px |
| lg | `h-element-lg` | `w-element-lg` | `size-element-lg` | 36px | 44px |
| xl | `h-element-xl` | `w-element-xl` | `size-element-xl` | 44px | 48px |

Không hợp lệ: không có cỡ phần tử `-xs` (`sm` là control nhỏ nhất), không có dạng viết tắt `h-sm`, `w-sm` hoặc `h-md`. Hãy dùng các class này.

Mỗi bậc mang cỡ chữ và icon riêng: `sm` chữ 14px (`text-sm`) với icon 16px, `md` và `lg` 14px (`text-sm`) với icon 16px, `xl` 16px (`text-md`) với icon 20px. Dùng `sm` cho toolbar, bảng và menu dày, `md` cho mọi chỗ khác theo mặc định (kể cả panel prop: `sm` giữ vẻ nhỏ trên điện thoại), `lg` cho form thoáng và trang cài đặt, `xl` cho hành động chính và màn hình ưu tiên cảm ứng. UI dày hay thoáng không phải một cài đặt toàn cục: chọn bậc theo từng vùng, và mỗi vùng chỉ một bậc. Hai bậc kề nhau trên một hàng (Input `md` cạnh Button `lg`) chỉ lệch 4px khi dùng chuột và trông như một lỗi.

Control thấp hơn 44px trên cảm ứng (`sm`, `md`, checkbox, nút icon) giữ vẻ ngoài và nới vùng chạm tới `size-target-min`. Input và textarea không làm được: trên cảm ứng, Input `sm` là 28px và `md` là 36px. Trên màn hình ưu tiên cảm ứng, dùng Input `lg` hoặc `xl`, hoặc bọc field trong `<label>` có padding.

```tsx
// Do: one step for the whole row
<div className="flex gap-sm">
  <Input size="lg" aria-label="Email" />
  <Button size="lg">Subscribe</Button>
</div>
// Avoid: neighbouring steps side by side
<div className="flex gap-sm">
  <Input aria-label="Email" />
  <Button size="lg">Subscribe</Button>
</div>
```

### Đang tải

#### 18. Không để trống nội dung khi nội dung tiếp theo đang tải

Khi người dùng đổi trang, tab, bộ lọc hoặc trang của một bảng, hãy giữ những gì đang ở trên màn hình cho đến khi nội dung tiếp theo sẵn sàng. Thực hiện thay đổi trong một transition (`useTransition`) và đánh dấu vùng là pending: `pending` trên `TabsContent` và `Table` làm mờ nó tới `opacity-pending` và đặt `aria-busy`. Chỉ hiện Skeleton khi không có gì để giữ (lần tải đầu), chỉ sau khoảng 150ms, và ít nhất 300ms sau khi đã hiện (`useSkeletonDelay`), để nó không nháy. Dựng skeleton theo hình nội dung: dòng chữ cùng cỡ font (mỗi dòng cao `h-lh`), control cùng `size` (`shape="control"`), cùng các góc. Bọc nó trong `SkeletonGroup`, thứ đặt `aria-busy` và nói "Loading" trong một status polite. Nhịp pulse chạy theo `duration-loop` và dừng khi giảm chuyển động.

```tsx
// Do
const [isPending, startTransition] = useTransition()
<Tabs value={tab} onValueChange={(next) => startTransition(() => setTab(next))}>
  <TabsContent value={tab} pending={isPending}>...</TabsContent>
</Tabs>
// Avoid
{isLoading ? <Spinner /> : <TabsContent value={tab}>...</TabsContent>}
```

#### 19. Ảnh giữ chỗ và fade vào

Giữ chỗ của ảnh trước khi nó tải (`ratio`, một class tỉ lệ hoặc một chiều cao), hiện ngay một bản mờ nhỏ của nó, và fade ảnh đầy đủ vào phía trên khi đã tải xong. Dùng `Image` với `placeholderSrc`: một data URL vài trăm byte được tạo lúc thêm ảnh, nên không cần request. Trong app Next.js, `next/image` với `placeholder="blur"` làm cùng việc cho ảnh tĩnh. Không fade khi giảm chuyển động.

```tsx
// Do
<Image src={post.cover} placeholderSrc={post.coverBlur} alt={post.coverAlt} ratio="16 / 9" />
// Avoid
<img src={post.cover} alt={post.coverAlt} />
```

### Độ đục

#### 20. Độ đục lấy từ ba token vai trò

Không có thang độ đục. Dùng một trong ba token, theo việc làm mờ để làm gì: `opacity-pending` trên nội dung được giữ trên màn hình khi nội dung tiếp theo đang tải (`pending` trên `TabsContent` và `Table` đã áp dụng nó), `opacity-disabled` trên nội dung không phải chữ trong control bị tắt mà không token nào đặt màu (một ô màu, một ảnh, một avatar ảnh), `opacity-subtle` trên hình trang trí nên lùi lại. Không viết bậc khác (`opacity-50`, `opacity-[.7]`) hoặc alpha trên một class màu (`text-text-primary/60`, `bg-accent/50`). Fade một thứ vào hoặc ra thì đi giữa `opacity-0` và `opacity-100`; việc đó ẩn hoặc hiện nó, không làm mờ nó.

Không dùng độ đục để làm chữ nhạt hơn. Chữ phụ dùng `text-text-secondary` (gợi ý và metadata dùng `text-text-tertiary`), đã được tính để giữ tương phản trên mọi nền; chữ ở một độ đục lấy bất kỳ thứ gì phía sau và không có tương phản đã kiểm. Nhãn và icon bị tắt dùng `text-text-disabled` và `text-icon-disabled`, không dùng `opacity-disabled`.

```tsx
// Do
<p className="text-sm text-text-secondary">Updated 5 minutes ago</p>
<span className="size-4 rounded-inner opacity-disabled" style={{ background: value }} />
// Avoid
<p className="text-sm text-text-primary opacity-60">Updated 5 minutes ago</p>
<button disabled className="disabled:opacity-50">Save</button>
```

### Kính

#### 21. Kính chỉ trên lớp nổi, tối đa hai

Kính là một class cho mỗi mức: `material-thin` cho các thanh (header, thanh nav, thanh tab), `material-thick` cho sheet, popover, dialog và menu, `material-refract` chỉ cho phần tử có kích thước cố định (thanh tab, nút hành động nổi, thanh nav). Class vẽ phần nền, độ mờ, viền sáng cạnh và đỉnh, cùng các nền đặc dự phòng; đừng tự dựng kính từ `bg-*`, `backdrop-blur-*` và một màu alpha. Nền của nó chỉ đủ đục để `text-text-primary` và `text-text-secondary` giữ 4.5:1 trên bất kỳ thứ gì phía sau, nên chỉ dùng hai token đó cho chữ trên kính.

- Tối đa hai lớp kính hiện cùng lúc trên một màn hình. Không kính chồng kính quá mức đó, nên một menu trên một sheet kính là giới hạn.
- Chỉ lớp nổi và lớp overlay. Không bao giờ nền trang, không bao giờ một Card, không bao giờ bên trong danh sách cuộn dài hoặc trên các dòng của danh sách.
- `material-refract` chỉ trên phần tử có kích thước và bán kính góc không bao giờ đổi. Thứ nào đổi kích thước, reflow theo nội dung hoặc animate kích thước thì dùng `material-thick`. Khúc xạ chỉ vẽ trên trình duyệt nền Chromium, sau khi `enableMaterialRefraction()` từ mục registry `material` đánh dấu root; mọi nơi khác nó trông đúng hệt `material-thick`. Trước khi một component dùng nó, hãy kiểm tra cuộn và animation bằng tay trên một iPhone đời cũ và một điện thoại Android cấu hình thấp.
- Khi giảm chuyển động, không có viền sáng được animate: thứ nào làm viền sáng chuyển động thì đặt sau `motion-safe:`.

```tsx
// Do
<SheetContent className="material-thick ring-1 ring-border shadow-high">...</SheetContent>
<header className="sticky top-0 material-thin">...</header>
// Avoid
<div className="bg-background-raised/60 backdrop-blur-lg">...</div>
<li className="material-thin">...</li>
```

### Bố cục

#### 22. Nội dung app nằm trong một surface inset

Một màn app có Sidebar đặt Sidebar trên nền canvas của trang và đặt nội dung trang trong một surface bên cạnh, để nội dung đọc như một tấm và điều hướng lùi lại phía sau. `<Sidebar inset>` bỏ nền và cạnh của Sidebar; `<SidebarInset>` là surface: một `main` có bán kính page, có viền, cách mép canvas 8px và đệm 32px (`p-2xl`), tràn viền với `p-lg` dưới md.

- SidebarInset giữ đệm của trang. Đừng đệm nội dung lần nữa và đừng bọc cả trang bằng một Card: nó đã là surface.
- Tách các section bên trong bằng `gap-2xl`, bằng đúng đệm của nó.
- Card trên đó giữ `border border-border` và không có bóng. Các metric liên quan dùng chung một khung có viền, chia bằng `border-s` hoặc `border-t`, không phải một hàng Card nhỏ.

```tsx
// Do
<SidebarProvider>
  <div className="flex h-dvh bg-background-page">
    <Sidebar inset>...</Sidebar>
    <SidebarInset className="gap-2xl">
      <h1 className="text-heading-2">Overview</h1>
      <section className="grid grid-cols-3 rounded-container border border-border">...</section>
    </SidebarInset>
  </div>
</SidebarProvider>
// Avoid
<Sidebar>...</Sidebar>
<main className="p-8"><Card className="p-6">...whole page...</Card></main>
```

### Trước khi trả UI

- Mọi giá trị là một class token; không hex, không px ngoài các giá trị arbitrary mà các luật trên chỉ.
- Không `font-bold`, không `tracking-*`, không tiêu đề Title Case.
- Không đặt `border` cùng một `shadow-*` trên cùng một phần tử.
- Tối đa một Button glossy mỗi view, và không bao giờ cạnh một Button solid: `glossy` thay `solid`, không thêm một mức.
- Khoảng cách trong nhỏ hơn khoảng cách ngoài.
- Bán kính lồng nhau theo luật 10.
- Mọi đổi trạng thái nhìn thấy được đều có chuyển động: transition màu, vào bằng `ease-enter`, ra bằng `ease-exit`, chuyển động đặt sau `motion-safe:` (luật 14).
- Kích thước phần tử lấy từ 12 class và prop `size`, một bậc mỗi vùng (luật 17).
- Không gì trên màn hình bị trống khi nội dung tiếp theo đang tải; skeleton khớp bố cục mà nó đứng thay (luật 18).
- Không class độ đục nào khác ngoài `opacity-pending`, `opacity-disabled`, `opacity-subtle` (và `opacity-0` / `opacity-100` cho một fade), không alpha màu như `/60`, và không làm chữ nhạt bằng độ đục (luật 20).
- Tối đa hai lớp kính trên một màn hình, chỉ trên lớp nổi, `material-refract` chỉ trên phần tử có kích thước cố định (luật 21).
- Một màn app có Sidebar đặt nội dung trong `SidebarInset` cạnh `<Sidebar inset>`, chỉ đệm một lần (luật 22).
