# Cowork UI Kit — Interaction Rules

> "Rules" — không phải "examples". Đây là LUẬT mọi component phải tuân theo.
>
> AI là engineer xếp component, không phải designer — file này tồn tại để compensate cho gap đó. Mỗi rule có lý do, mỗi token có scale rõ ràng, mỗi anti-pattern là vết thương đã từng phạm. Xem cross-ref `feedback_ai_engineer_not_designer` trong memory.
>
> **Scope:** mọi component trong `atoms/`, `molecules/`, `organisms/`, `templates/`, `pages/`, `react/`, và mọi page Dang yêu cầu build mới.

---

## 1. Transition timing

Mọi animation/transition phải pick duration từ scale này. KHÔNG dùng default browser timing (300ms generic), KHÔNG dùng arbitrary value như 250ms / 350ms.

| Speed   | Token                | Value | Use case                                    |
|---------|----------------------|-------|---------------------------------------------|
| Instant | `--duration-instant` | 0ms   | drag follow, scroll, real-time tracking     |
| Fast    | `--duration-fast`    | 120ms | hover/tap micro feedback, color swap        |
| Normal  | `--duration-normal`  | 200ms | dropdown, tooltip fade, toggle, accordion   |
| Slow    | `--duration-slow`    | 320ms | modal open, drawer slide, sheet rise        |
| Slower  | `--duration-slower`  | 480ms | page transition, complex morphs, hero anim  |

**Rule cứng:**
- `<100ms` → quá nhanh, user perceive là "đột ngột"
- `>500ms` → quá lag, cảm giác chậm chạp
- Async action UI feedback nằm trong khoảng `120-320ms` để vừa kịp visual ack vừa không cản flow

```css
/* ✅ correct */
.btn { transition: background var(--duration-fast) var(--ease-out); }

/* ❌ wrong — arbitrary value */
.btn { transition: background 250ms ease; }
```

---

## 2. Easing curves

Linear easing cảm giác robot. Mọi UI transition phải dùng curve từ scale này.

| Token              | Curve                                  | When to use                                          |
|--------------------|----------------------------------------|------------------------------------------------------|
| `--ease-linear`    | `linear`                               | CHỈ cho loading spinner rotate, progress bar liên tục |
| `--ease-out`       | `cubic-bezier(0.32, 0.72, 0, 1)`       | **Default** — iOS-like, decelerate mạnh, dùng cho 90% UI feedback (hover, dropdown, modal open) |
| `--ease-out-soft`  | `cubic-bezier(0.4, 0, 0.2, 1)`         | Material standard — drawer, sheet, transitions có chiều dài |
| `--ease-in-out`    | `cubic-bezier(0.65, 0, 0.35, 1)`       | Symmetric anim — accordion expand/collapse, height auto-grow |
| `--ease-spring`    | `cubic-bezier(0.5, 1.5, 0.5, 1)`       | Delight micro-interaction — like button, success check, badge pop |
| `--ease-bounce`    | `cubic-bezier(0.68, -0.55, 0.265, 1.55)` | Celebration only — confetti, achievement unlock. KHÔNG dùng cho serious UI |

**Rule cứng:**
- Default cho mọi component không spec gì → `--ease-out`
- Spring/bounce chỉ cho delight moment, KHÔNG cho modal/drawer/dropdown thường ngày
- Linear chỉ cho infinite loop animation

---

## 3. Hover states

**Rule cứng:**
- Background-only change: ±5-10% lightness, KHÔNG full color swap (vd `primary` → `accent` là sai)
- Interactive element (button, link, card clickable) phải có `cursor: pointer` + transform optional
- KHÔNG dùng default Tailwind `hover:bg-blue-700` không qua token
- Mobile (touch device): hover không có ý nghĩa → fallback `:active` state thay thế
- Transition target chỉ `background-color`, `color`, `border-color`, `transform`, `opacity` — KHÔNG `transition: all`

```css
.btn {
  background: var(--primary);
  transition: var(--trans-bg);
}
.btn:hover  { background: var(--primary-hover); }   /* +/- 5-10% lightness, defined trong brand preset */
.btn:active { background: var(--primary-active); transform: scale(0.98); }

@media (hover: none) {
  .btn:hover { background: var(--primary); } /* reset, touch device không hover */
}
```

---

## 4. Focus states

**Rule cứng (a11y — non-negotiable):**
- Mọi interactive element BẮT BUỘC có `:focus-visible` style
- Outline `2px solid var(--primary)` + `outline-offset: 2px`
- KHÔNG dùng `outline: none` không kèm thay thế — vi phạm a11y
- `border-radius` của outline match element radius (browser tự handle với `outline-offset`)
- Focus ring KHÔNG được clip bởi `overflow: hidden` của parent

```css
.btn:focus-visible {
  outline: 2px solid var(--primary);
  outline-offset: 2px;
}

/* ❌ wrong */
.btn:focus { outline: none; } /* a11y violation */
```

---

## 5. Active/pressed states

**Rule cứng:**
- Visual feedback ngay lập tức: `transform: scale(0.98)` HOẶC darker background (`--primary-active`)
- Duration `--duration-fast` (120ms) — phải feel instant
- KHÔNG dùng `transform: scale(0.9)` — quá mạnh, cảm giác "nút lún"

```css
.btn:active {
  background: var(--primary-active);
  transform: scale(0.98);
  transition: transform var(--duration-fast) var(--ease-out);
}
```

---

## 6. Disabled states

**Rule cứng:**
- `opacity: 0.5` + `cursor: not-allowed` + `pointer-events: none`
- KHÔNG chỉ grey-out background — user vẫn click được, sai semantic
- Disabled element KHÔNG có hover/focus state (đã có `pointer-events: none`)
- Form input disabled phải kèm `aria-disabled="true"` cho screen reader

```css
.btn[disabled],
.btn:disabled {
  opacity: 0.5;
  cursor: not-allowed;
  pointer-events: none;
}
```

---

## 7. Loading states

**Rule cứng:**
- Button async action: replace label bằng spinner, hold tối thiểu `200ms` (anti-flash — nếu response <100ms thì user thấy chớp)
- List/grid loading: skeleton screen match layout, KHÔNG spinner trắng giữa màn hình
- Page loading: shimmer skeleton — màu base `--surface`, highlight `--surface-2`, animation 1.5s linear infinite
- Spinner color = `currentColor` để adapt theo context (dark button → light spinner)

```css
.btn--loading {
  pointer-events: none;
  color: transparent; /* hide label */
  position: relative;
}
.btn--loading::after {
  content: "";
  position: absolute;
  inset: 0;
  margin: auto;
  width: 16px; height: 16px;
  border: 2px solid currentColor;
  border-top-color: transparent;
  border-radius: 50%;
  animation: spin 800ms var(--ease-linear) infinite;
}

@keyframes spin { to { transform: rotate(360deg); } }
```

---

## 8. Modal animation

**Open sequence:**
- Backdrop: opacity 0 → 1, `--duration-normal` (200ms), `--ease-out`
- Container: scale 0.95 → 1 + opacity 0 → 1, `--duration-slow` (320ms), `--ease-out`
- Stagger: backdrop start trước container 50ms (cảm giác "lớp" mở ra)

**Close sequence:**
- Reverse + `--duration-normal` (close phải nhanh hơn open — UX rule)
- Backdrop click → close (BẮT BUỘC, trừ destructive confirm modal)
- ESC key → close (BẮT BUỘC)
- Focus trap khi mở, restore focus về trigger khi đóng

```css
.modal-backdrop {
  opacity: 0;
  transition: opacity var(--duration-normal) var(--ease-out);
}
.modal-backdrop[data-open] { opacity: 1; }

.modal-container {
  opacity: 0;
  transform: scale(0.95);
  transition:
    opacity var(--duration-slow) var(--ease-out),
    transform var(--duration-slow) var(--ease-out);
}
.modal-container[data-open] {
  opacity: 1;
  transform: scale(1);
}
```

---

## 9. Drawer animation

**Rule:** Slide từ cạnh (right/left/top), `translateX(100%)` → `translateX(0)`, `--duration-slow`, `--ease-out-soft`.

- Width fixed (320-480px desktop, 85% viewport mobile)
- Backdrop optional — nếu có thì `opacity 0 → 0.5`
- Swipe-to-close mobile BẮT BUỘC (drag từ cạnh trong)
- ESC close BẮT BUỘC

```css
.drawer {
  position: fixed;
  top: 0; right: 0;
  width: min(420px, 85vw);
  height: 100vh;
  transform: translateX(100%);
  transition: transform var(--duration-slow) var(--ease-out-soft);
}
.drawer[data-open] { transform: translateX(0); }
```

---

## 10. Sheet (bottom sheet mobile)

**Rule:** Slide-up từ `translateY(100%)`, max-height `85vh`, `--ease-spring` cho cảm giác "rise + settle".

- Drag handle visible (4px x 32px, rounded full, `--text-muted`) ở top
- Drag-to-dismiss: drag down >50% height → close
- Snap points optional (peek 40% / full 85%)
- Backdrop opacity 0 → 0.4

```css
.sheet {
  position: fixed;
  bottom: 0; left: 0; right: 0;
  max-height: 85vh;
  border-radius: 16px 16px 0 0;
  transform: translateY(100%);
  transition: transform var(--duration-slow) var(--ease-spring);
}
.sheet[data-open] { transform: translateY(0); }
```

---

## 11. Toast animation

**Rule:**
- Position: top-right desktop, bottom-center mobile (safe area inset)
- Slide-in: `translateY(-100%)` → `0` (top) hoặc `translateY(100%)` → `0` (bottom), `--duration-normal`
- Auto-dismiss: 4s default, 6s cho error, infinite cho action-required
- Hover pause: dừng timer khi hover (`onMouseEnter` clear, `onMouseLeave` resume)
- Stack: tối đa 3 toast cùng lúc, FIFO

```css
.toast {
  transform: translateY(-100%);
  opacity: 0;
  transition:
    transform var(--duration-normal) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}
.toast[data-open] {
  transform: translateY(0);
  opacity: 1;
}
```

---

## 12. Dropdown / Popover

**Rule:**
- Origin matches trigger position — top-trigger → `transform-origin: top`, bottom → `bottom`
- Scale 0.95 → 1 + opacity 0 → 1, `--duration-normal`, `--ease-out`
- Click outside → close
- ESC → close + return focus to trigger
- Position auto-flip nếu sát viewport edge (Floating UI hoặc tự handle)

```css
.dropdown {
  transform: scale(0.95);
  transform-origin: top;
  opacity: 0;
  transition:
    transform var(--duration-normal) var(--ease-out),
    opacity var(--duration-normal) var(--ease-out);
}
.dropdown[data-open] {
  transform: scale(1);
  opacity: 1;
}
```

---

## 13. Tooltip

**Rule:**
- Opacity-only fade (KHÔNG scale, KHÔNG slide — tooltip phải "xuất hiện" không phân tâm)
- Delay-open: `600ms` (chống flicker khi cursor lướt qua)
- Delay-close: `100ms` (đủ để user di chuyển sang tooltip nếu cần copy text)
- Duration: `--duration-fast` (120ms)
- Touch device: long-press 500ms để show, tap outside để close

```css
.tooltip {
  opacity: 0;
  transition: opacity var(--duration-fast) var(--ease-out);
  transition-delay: 0ms;
  pointer-events: none;
}
.tooltip[data-open] {
  opacity: 1;
  transition-delay: 600ms; /* delay-open */
}
```

---

## 14. Scroll behavior

**Rule:**
- Anchor nav (in-page link): `html { scroll-behavior: smooth; }` — với `@media (prefers-reduced-motion: reduce)` override `auto`
- iOS momentum: `-webkit-overflow-scrolling: touch` trên container scroll
- Modal/drawer/sheet: `overscroll-behavior: contain` để scroll bên trong không leak ra body
- Scroll lock khi modal open: `body { overflow: hidden; }` + preserve scrollbar width (`scrollbar-gutter: stable`)

```css
html { scroll-behavior: smooth; }

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

.modal-body {
  overflow-y: auto;
  overscroll-behavior: contain;
  -webkit-overflow-scrolling: touch;
}
```

---

## 15. Drag & drop

**Rule:**
- Cursor: `grab` (idle) → `grabbing` (active drag)
- Dragged element: `opacity: 0.5` + slight scale `1.02` để "lift" khỏi context
- Ghost preview / placeholder ở drop zone: dashed border `2px var(--primary)`, bg `var(--primary-50)`
- Drop zone hover: highlight bg `var(--primary-100)`
- Duration micro-feedback: `--duration-fast`

```css
.draggable { cursor: grab; transition: var(--trans-all-fast); }
.draggable:active { cursor: grabbing; }
.draggable[data-dragging] {
  opacity: 0.5;
  transform: scale(1.02);
}

.dropzone[data-over] {
  background: var(--primary-100);
  border: 2px dashed var(--primary);
}
```

---

## 16. Page transitions

**Rule:**
- Default: fade-through 200ms (`opacity 0 → 1`), KHÔNG slide
- Mobile back gesture (PWA / native): slide-right cho back nav, slide-left cho forward
- KHÔNG dùng router transition mặc định Next.js / React Router không config — cảm giác cheap
- Loading state trước khi route ready: thin progress bar top (`nprogress` style, 2px, `--primary`)

```css
.page-enter {
  opacity: 0;
  transition: opacity var(--duration-normal) var(--ease-out);
}
.page-enter-active { opacity: 1; }
```

---

## ✗ Anti-patterns

Các pattern dưới đây ĐÃ phạm trong các project trước. Đừng lặp.

- ❌ Hover effect default Tailwind không custom (`hover:bg-blue-700` không qua brand token)
- ❌ `transition: all` blanket → animate cả layout properties (width, height, padding) → janky FPS
- ❌ Linear easing cho UI feedback (cảm giác robot, không "natural")
- ❌ Duration `<100ms` hoặc `>500ms` cho hover/click (quá nhanh hoặc lag)
- ❌ Spinner trắng giữa màn hình thay vì skeleton screen cho async list
- ❌ Modal không có backdrop click-to-close (trap user)
- ❌ Modal không có ESC close (a11y violation)
- ❌ Drawer không có swipe-to-close mobile (UX phá vỡ pattern native)
- ❌ Bounce ease cho serious UI (modal/drawer/dropdown thường ngày) — chỉ cho delight/celebration
- ❌ `outline: none` mà không có focus-visible thay thế (a11y critical)
- ❌ Disabled state chỉ đổi màu, không có `pointer-events: none` → user click được
- ❌ Hover state full color swap (primary → accent) thay vì ±5-10% lightness
- ❌ Tooltip không có delay-open → flicker khi cursor lướt qua nhiều element
- ❌ `transform: scale(0.9)` cho active state → quá mạnh, cảm giác "nút lún"
- ❌ Toast auto-dismiss <3s (đọc không kịp) hoặc >8s (annoying)
- ❌ Page transition slide ngang default (cảm giác cheap unless mobile back gesture)
- ❌ Ignore `@media (prefers-reduced-motion: reduce)` — a11y violation cho user có vestibular disorder

---

## Reduced motion override

**Rule cứng:** Mọi animation BẮT BUỘC respect `prefers-reduced-motion`. Đây là a11y non-negotiable.

```css
@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}
```

Đặt block này ở cuối `foundation.css` hoặc global reset.

---

## Implementation checklist (per component)

Mỗi component (atom/molecule/organism) phải pass checklist này trước khi merge vào kit:

```
[ ] Default state có transition defined (--trans-* hoặc explicit duration+ease từ token)
[ ] Hover state có +/- lightness (không full color swap)
[ ] Focus-visible outline 2px tinted primary, offset 2px
[ ] Active state có scale 0.98 hoặc darker bg (--primary-active)
[ ] Disabled state có opacity 0.5 + cursor not-allowed + pointer-events none
[ ] Loading state có spinner (button) hoặc skeleton (list/page) — KHÔNG spinner trắng cho list
[ ] Animation duration pick từ scale (--duration-instant/fast/normal/slow/slower), không arbitrary
[ ] Easing pick từ token (--ease-out default), không browser default
[ ] Mobile: hover→active fallback (@media (hover: none))
[ ] Reduced motion: respect @media (prefers-reduced-motion: reduce)
[ ] A11y: focus-visible, ARIA states (aria-expanded, aria-disabled, etc.), ESC close cho overlay
[ ] Touch target ≥44x44px (Apple HIG) cho mọi interactive element mobile
```

---

## Token reference quick map

| Concern             | Token                          | Example                                          |
|---------------------|--------------------------------|--------------------------------------------------|
| Color swap (hover)  | `--trans-color`, `--trans-bg`  | `transition: var(--trans-bg);`                   |
| Generic fast        | `--trans-all-fast`             | hover micro-feedback                             |
| Generic normal      | `--trans-all-norm`             | dropdown, accordion                              |
| Custom modal/drawer | compose: `<prop> <dur> <ease>` | `transition: transform var(--duration-slow) var(--ease-out);` |

Tokens defined: `00-templates/ui-kit/tokens/foundation.css` (lines 195-220).

---

> **Khi build component mới:** Đọc file này trước khi viết CSS. Mỗi rule vi phạm = bug đã từng phạm và Dang đã từng phàn nàn. Đây là institutional memory của design system.
