# 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)