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>
102 lines
5.0 KiB
Markdown
102 lines
5.0 KiB
Markdown
# 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)
|