docs(agent): thư viện instruction cho việc sửa bug UI/UX

Bộ 7 role chuyên biệt (triage → specialist → implementer → reviewer) cùng
lớp dùng chung: guardrail, tri thức về repo, checklist, và contract đầu ra.

Vì sao có: bug UI/UX được báo bằng lời kể triệu chứng, và người sửa hay bỏ
qua ba thứ mà repo này rất dễ vi phạm — luật "không file nào ngoài theme/
được đặt tên một màu", trần LOC theo bánh cóc, và việc ui/ với presentation/
cùng tồn tại nên sửa nhầm file là "đã fix mà vẫn thấy lỗi".

knowledge/qt_pitfalls.md chép lại 20 nguyên nhân gốc hay gặp của bug PySide6;
examples/bad_fix.md có hai ca CÓ THẬT, gồm ca chính bản vá trong nhánh này
từng mắc (compare_digest trên str ngoài ASCII) và lọt qua vòng review đầu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-07 19:55:02 +09:00
co-authored by Claude Opus 5
parent 5d23a415e1
commit 7bd2b95a57
29 changed files with 3513 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
# Theme & Design Tokens — luật màu sắc của Cowork Local
Nguồn: docstring đầu `theme/__init__.py`, `theme/palettes.py`, `theme/qss.py`,
`theme/qss_controls.py`.
---
## 1. Luật gốc
> **Không file nào ngoài `theme/` được đặt tên một màu.**
Cơ chế duy nhất:
```text
Palette (token ngữ nghĩa) → _TEMPLATE (một QSS duy nhất) → stylesheet(theme)
```
Hai cách hợp lệ để một widget có màu:
1. **Khai báo** — gán `objectName` cho widget, style nó trong `_TEMPLATE`
(`theme/qss.py`). Đây là cách mặc định.
2. **Vẽ tay** — widget vẽ bằng `QPainter` (chart, canvas, syntax highlighter) thì gọi
`current_palette()` rồi đọc token.
Cách **không** hợp lệ, bị reject review:
```python
self.label.setStyleSheet("color: #dc2626;") # ❌ hex ngoài theme/
pen.setColor(QColor("red")) # ❌ tên màu literal
self.card.setStyleSheet("background: rgba(0,0,0,.1)") # ❌
```
## 2. API cần nhớ
| Hàm | Dùng khi |
|---|---|
| `theme.stylesheet(theme)` | Sinh QSS toàn app, truyền vào `QApplication.setStyleSheet` |
| `theme.set_active_theme(theme)` | **Phải** gọi ngay cạnh mỗi `setStyleSheet(stylesheet(...))` |
| `theme.current_theme()` | `'dark'` / `'light'` đang hiển thị |
| `theme.current_palette()` | Token của theme đang hiển thị — dùng trong `paintEvent` |
| `theme.palette(theme)` | Token của một theme cụ thể |
| `theme.resolve_theme('system')` | Suy ra dark/light từ color scheme của OS |
| `theme.role_colors(theme)` | Màu theo vai trò hội thoại: user/assistant/tool/result/error |
`current_palette()` tồn tại để code vẽ **không** phải đọc lại `config.json` mỗi lần
repaint — đó từng là bug hiệu năng thật. Không thay bằng đọc config.
## 3. Nhóm token
Palette là `@dataclass(frozen=True)`. Các nhóm chính:
| Nhóm | Token | Ý nghĩa |
|---|---|---|
| Bề mặt (thang 4 bậc) | `bg` | nền cửa sổ / canvas |
| | `surface` | panel, card, group box (**không** phải nav rail) |
| | `surface_raised` | input, list, tree — thứ người dùng gõ/chọn |
| | `overlay` | menu, tooltip, popup |
| | `sunken` | log, code, terminal — thứ để đọc vào |
| | `hover` / `active` | trạng thái hover / đang bấm |
| Chữ | `text`, `text_muted`, ... | |
| Nhấn | `accent`, `accent_solid` | **Hai token khác nhau có chủ đích**: màu đọc được *dạng chữ* trên nền tối thì quá nhạt để làm *nền* cho chữ trắng |
| Trạng thái | `danger`, ... | |
| Vai trò hội thoại | `role_user`, `role_assistant`, `role_tool`, `role_result`, `role_error` | |
| Code | `code_string`, ... | syntax highlighting |
Token là **ngữ nghĩa**, không phải literal: `danger` / `text_muted` — không bao giờ
`blue` / `grey2`. Thêm một theme = thêm một `Palette`, không phải sửa stylesheet.
## 4. Ràng buộc thiết kế (đừng "sửa" nhầm thành bug)
- **Không gradient, không glow.** Bảng màu lấy từ VS Code "Dark Modern" / "Light Modern".
Bề mặt phẳng, góc gần vuông, một màu accent chỉ dành cho thứ người dùng thao tác.
- **Chiều sâu đến từ thang bề mặt và viền mảnh**, không từ màu.
- **Silhouette VS Code:** nav rail **tối hơn** vùng nội dung, không sáng hơn.
Người dùng báo "menu trái tối quá" — đó là thiết kế, không phải bug. Xem `examples/bad_fix.md`.
- **Contrast giữ ở WCAG AA (4.5:1)** cho body text và cho chữ trên nút đặc.
- Bốn giá trị của VS Code không đạt AA đã được nhích lên vừa đủ (số dòng dark 3.59:1,
chữ mờ trên sidebar sáng 4.28:1, xanh lá sáng 4.33:1, hổ phách sáng 3.12:1). Mỗi chỗ có
comment ghi giá trị gốc — **không** trả chúng về giá trị VS Code.
## 5. Mũi tên combo box (`_chevron_asset`)
QSS `image:` chỉ nhận đường dẫn file/resource, không nhận `QPixmap`. Và một khi
`::drop-down` / `::up-button` / `::down-button` bị style, Qt **ngừng vẽ mũi tên mặc định**.
Vì vậy `theme/palettes.py::_chevron_asset` render sẵn PNG chevron ra thư mục tạm và cache
theo hash `(direction, color)`.
Hệ quả khi debug:
- "Combo box mất mũi tên" → gần như luôn do một stylesheet cục bộ đè lên `::drop-down`.
- File cache nằm ở `%TEMP%/cowork_local_theme/chevron_*.png`. Xoá nó để buộc render lại
khi test màu mới.
## 6. Checklist sửa bug liên quan màu sắc
- [ ] Đã kiểm tra bug xuất hiện ở **cả** dark và light chưa? (`docs/screens/*-dark.png` / `*-light.png`)
- [ ] Bản sửa dùng token, không dùng hex?
- [ ] Nếu thêm token mới: đã thêm cho **cả** `DARK` và `LIGHT`?
- [ ] Nếu là chữ trên nền đặc: đã dùng `accent_solid` thay vì `accent`?
- [ ] Contrast còn ≥ 4.5:1?
- [ ] Widget dựng sau khi đổi theme có nhận đúng stylesheet? (xem `qt_pitfalls.md` P07)