Thư mục gốc: 22 file .py -> 7
--------------------------------
13 file "thành phần" nằm rải rác ngay ngoài thư mục gốc, mỗi file chỉ có ĐÚNG
MỘT nơi import — chính cái hub của nó:
i18n.py + 10 file i18n_*.py -> i18n/__init__.py + i18n/*.py
theme.py + 3 file theme_*.py -> theme/__init__.py + theme/*.py
Đổi hub thành `__init__.py` nên 78 chỗ `from ..i18n import tr` và 24 chỗ
`from ..theme import current_palette` KHÔNG phải sửa một dòng nào. Git nhận ra
11/15 file là đổi tên thuần, 0 dòng thay đổi; 4 file còn lại chỉ sửa đúng dòng
import và mấy tham chiếu tên file trong docstring.
Đối chiếu với bản trước khi gom, cùng một phép băm:
số khoá i18n 1431 -> 1431 hash STRINGS a06cc34b... (trùng)
QSS dark hash 7bb230a4... (trùng)
QSS light hash 884f73ce... (trùng)
`check_loc.py` phải khai thêm "i18n", "theme" vào DEFAULT_TARGET_DIRS: chúng
từng được quét theo diện "module nằm ở thư mục gốc", gom vào gói rồi thì không
khai là lặng lẽ tuột khỏi tầm quét.
Bánh cóc `ui/widgets.py` siết 505 -> 466 sau khi tách SegmentedControl — nợ cũ
co lại thì con số phải co theo, không thì bánh cóc đứng yên mãi ở mức cũ.
Một file requirements
---------------------
Xoá `requirements-test.txt`. Nó chỉ có `pytest` + `pydantic`, nhưng 64/108 file
test dựng widget thật và 20 file trong đó import PySide6 thẳng ở đầu file không
có bảo vệ — nên CI cài mỗi file kia thì pytest chết ngay lúc thu thập test chứ
không phải "vài test bị bỏ qua". Hai file cho một danh sách gần trùng nhau chỉ
tạo thêm một chỗ để lệch phiên bản, và `pydantic` đã bị chép ở cả hai.
CI đổi sang cài `requirements.txt`. Người dùng cuối cài thừa pytest vài MB.
Kèm theo: `install.bat` bỏ cờ `--dev` (không còn gì để cài thêm). Khối `if`
rỗng còn sót lại làm cmd.exe báo "( was unexpected at this time" và script chết
ngay sau bước cài thư viện — đã gỡ hẳn.
859 test xanh · 4/4 cổng CASAN · check_design_parity 32/32 ·
check_layout_geometry trùng từng byte với bản trước refactor.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
199 lines
10 KiB
Python
199 lines
10 KiB
Python
"""Khuôn QSS của toàn ứng dụng — 470 dòng bảng kiểu.
|
|
|
|
Tách khỏi ``theme.py`` vì nó là **dữ liệu**, không phải logic: một chuỗi
|
|
``string.Template`` mà ``stylesheet()`` thay biến vào. Để chung thì mỗi lần
|
|
muốn sửa một hàm nhỏ trong theme.py lại phải cuộn qua 470 dòng CSS.
|
|
|
|
Sửa màu thì sang ``theme/palettes.py``; ở đây chỉ sửa hình dạng và khoảng cách.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass, asdict
|
|
from string import Template
|
|
|
|
|
|
from .qss_controls import QSS_CONTROLS
|
|
|
|
_QSS_SHELL = """
|
|
/* ---- reset ------------------------------------------------------------ */
|
|
* { font-family: $font_family; font-size: ${font_size}px; }
|
|
QWidget { background: $bg; color: $text; }
|
|
QMainWindow::separator { background: $border; width: 1px; height: 1px; }
|
|
QSplitter::handle { background: $border; }
|
|
QSplitter::handle:horizontal { width: 1px; }
|
|
QSplitter::handle:vertical { height: 1px; }
|
|
QSplitter::handle:hover { background: $border_strong; }
|
|
QStatusBar { background: $bg; color: $text_faint; border-top: 1px solid $border; }
|
|
QToolTip {
|
|
background: $overlay; color: $text; border: 1px solid $border_strong;
|
|
border-radius: ${radius}px; padding: 5px 9px;
|
|
}
|
|
|
|
/* Icons are drawn at text scale, not as decoration. */
|
|
QPushButton, QToolButton, QComboBox, QTabBar { qproperty-iconSize: 15px 15px; }
|
|
QTreeWidget#navrail { qproperty-iconSize: 22px 16px; }
|
|
|
|
/* ---- shell ------------------------------------------------------------ */
|
|
QWidget#topbar { background: $bg; border: none; border-bottom: 1px solid $border; }
|
|
QLabel#brand { color: $text; font-size: 15px; font-weight: 700; background: transparent; }
|
|
QWidget#navWrap { background: $nav_bg; border-right: 1px solid $nav_border; }
|
|
QWidget#navWrap QTreeWidget, QWidget#navWrap QListWidget { background: transparent; border: none; }
|
|
QWidget#contentArea { background: $bg; }
|
|
|
|
/* Rail rows sit on nav_bg, so they need their own hover/selected steps — the
|
|
generic ones are tuned against `bg` and wash out here. The active item also
|
|
carries a 2px accent marker, so which section you are in survives even at a
|
|
glance or for anyone who cannot separate the two greys. */
|
|
QWidget#navWrap QTreeWidget::item, QWidget#navWrap QListWidget::item {
|
|
padding: 6px 4px; border-radius: ${radius}px;
|
|
}
|
|
QWidget#navWrap QTreeWidget::item:hover, QWidget#navWrap QListWidget::item:hover {
|
|
background: $nav_hover;
|
|
}
|
|
QWidget#navWrap QTreeWidget::item:selected, QWidget#navWrap QListWidget::item:selected {
|
|
background: $nav_selected; color: $text;
|
|
border-left: 2px solid $accent; font-weight: 600;
|
|
}
|
|
/* Rows the project gate is holding shut: still listed, visibly not open. */
|
|
QWidget#navWrap QTreeWidget::item:disabled { color: $text_faint; }
|
|
/* The pinned bottom group (Dashboard, Monitoring) — one hairline separates it
|
|
from the list above so "occasional" reads apart from "everyday". */
|
|
QTreeWidget#navrailBottom { border-top: 1px solid $nav_border; }
|
|
QScrollArea#navScroll { background: transparent; border: none; }
|
|
QScrollArea#navScroll > QWidget > QWidget { background: transparent; }
|
|
/* Monitoring ▸ Overview reads as titled sections down one column, the way the
|
|
audit page draws it — a quiet caps heading with the content flat underneath,
|
|
not six bordered boxes competing with the cards inside them. */
|
|
QGroupBox#monSection {
|
|
background: transparent; border: none; margin-top: 16px;
|
|
padding: 6px 0 0 0; font-weight: 700;
|
|
}
|
|
QGroupBox#monSection::title {
|
|
subcontrol-origin: margin; subcontrol-position: top left; left: 2px; top: 0;
|
|
padding: 0; color: $text_faint; font-size: 11px; letter-spacing: 0.5px;
|
|
}
|
|
/* Segmented control: two-to-four choices shown side by side (language, theme)
|
|
instead of a drop-list you must open to see what the options even are. */
|
|
QPushButton#segItem {
|
|
background: $surface_raised; color: $text_muted; border: 1px solid $border;
|
|
padding: 4px 12px; margin: 0; border-radius: 0;
|
|
}
|
|
QPushButton#segItem:hover { background: $hover; color: $text; }
|
|
QPushButton#segItem:checked {
|
|
background: $accent_solid; color: #FFFFFF; border-color: $accent_solid; font-weight: 600;
|
|
}
|
|
/* Table of contents down the left of the long dialogs (Settings, Task editor). */
|
|
QListWidget#sectionIndex { background: $surface; border-right: 1px solid $border; }
|
|
QListWidget#sectionIndex::item { padding: 7px 10px; border-radius: ${radius}px; }
|
|
QListWidget#sectionIndex::item:hover { background: $hover; }
|
|
QListWidget#sectionIndex::item:selected {
|
|
background: $nav_selected; color: $text; font-weight: 600;
|
|
}
|
|
/* The strip under the typing box: agent · routing · usage · folder. Reads as
|
|
status, not as a second toolbar, so the eye lands on the input first. */
|
|
QWidget#composerStatus { border-top: 1px solid $border; background: transparent; }
|
|
QWidget#composerStatus QLabel { color: $text_faint; font-size: 11px; }
|
|
QWidget#composerStatus QPushButton, QWidget#composerStatus QComboBox {
|
|
background: transparent; border: none; color: $text_muted; font-size: 11px;
|
|
padding: 2px 6px; border-radius: ${radius_sm}px;
|
|
}
|
|
QWidget#composerStatus QPushButton:hover, QWidget#composerStatus QComboBox:hover {
|
|
background: $hover; color: $text;
|
|
}
|
|
/* Folder: the current path, written as the screen's title. */
|
|
QLabel#folderTitle { color: $text; font-size: 14px; font-weight: 600; background: transparent; }
|
|
/* A pair of view tabs inside a page header (Schedule, GraphRAG) — flatter and
|
|
quieter than the app's main tab bars, since they switch a view, not a page. */
|
|
QTabBar#viewTabs::tab {
|
|
background: transparent; color: $text_muted; border: none;
|
|
padding: 4px 12px; margin: 0 2px; border-radius: ${radius}px;
|
|
}
|
|
QTabBar#viewTabs::tab:hover { background: $hover; color: $text; }
|
|
QTabBar#viewTabs::tab:selected { background: $active; color: $text; font-weight: 600; }
|
|
/* Co4E sidebar section headings — same quiet caps as the rail's RECENTS, so
|
|
"which list am I looking at" is answered on screen, not in a tooltip. */
|
|
/* The action beside a Co4E section heading ("+ Mới", "Quản lý skill…"). The
|
|
wireframe writes these as small accent text; as full buttons they were the
|
|
loudest thing in the sidebar and each cost a row of height. */
|
|
QPushButton#co4eSectionAction {
|
|
background: transparent; border: none; color: $accent;
|
|
font-size: 11px; font-weight: 600; padding: 1px 4px;
|
|
border-radius: ${radius_sm}px; qproperty-iconSize: 11px 11px;
|
|
}
|
|
QPushButton#co4eSectionAction:hover { background: $hover; color: $accent; }
|
|
QPushButton#co4eSectionAction:pressed { background: $active; }
|
|
QPushButton#co4eSectionHdr {
|
|
color: $text_faint; font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
|
|
background: transparent; border: none; text-align: left; padding: 2px 0;
|
|
}
|
|
QPushButton#co4eSectionHdr:hover { color: $text; }
|
|
/* Account row at the foot of the rail: who you are + the settings that follow
|
|
you (provider, language, theme). Separated by a hairline like the group above. */
|
|
QWidget#navAccount { border-top: 1px solid $nav_border; }
|
|
QWidget#navAccount QComboBox {
|
|
background: $surface_raised; border: 1px solid $nav_border; color: $text;
|
|
padding: 3px 6px; border-radius: ${radius}px;
|
|
}
|
|
/* RECENTS section label — quiet, so the thread titles under it read first. */
|
|
QLabel#navSectionHdr {
|
|
color: $text_faint; font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
|
|
padding: 8px 8px 2px 8px; background: transparent;
|
|
}
|
|
QTreeWidget#navRecents { border-top: 1px solid $nav_border; }
|
|
|
|
/* Icon library cells. The audit page's note on this screen is that the cells
|
|
had no visible edge on hover or selection, so you could not tell what you
|
|
were about to pick — "cùng ngôn ngữ thẻ với Agent/Công cụ". */
|
|
QListWidget#iconGrid { background: transparent; border: none; }
|
|
QListWidget#iconGrid::item {
|
|
border: 1px solid transparent; border-radius: ${radius}px;
|
|
color: $text_muted; padding: 4px;
|
|
}
|
|
QListWidget#iconGrid::item:hover {
|
|
border: 1px solid $accent; background: $hover; color: $text;
|
|
}
|
|
QListWidget#iconGrid::item:selected {
|
|
border: 1px solid $accent; background: $accent_wash; color: $text;
|
|
}
|
|
/* Screen title beside its actions, same weight the other admin screens use. */
|
|
QLabel#monTitle { font-size: 15px; font-weight: 700; color: $text; }
|
|
/* Rail header — the primary action, so it is the one filled button up there. */
|
|
QPushButton#navNewChatBtn {
|
|
background: $accent_solid; color: #FFFFFF; border: none; font-weight: 600;
|
|
padding: 7px 10px; border-radius: ${radius}px; text-align: left;
|
|
}
|
|
QPushButton#navNewChatBtn:hover { background: $accent_solid_hover; }
|
|
QPushButton#navNewChatBtn:disabled { background: $border_strong; color: $text_faint; }
|
|
QComboBox#navProjectPick {
|
|
background: $surface_raised; border: 1px solid $nav_border; color: $text;
|
|
padding: 4px 8px; border-radius: ${radius}px;
|
|
}
|
|
/* Stand-in for the picker while the rail is 54px wide: icon only, no arrow —
|
|
the arrow would eat a third of the width for no information. */
|
|
QToolButton#navProjectPickMini {
|
|
background: $surface_raised; border: 1px solid $nav_border; border-radius: ${radius}px;
|
|
padding: 4px; qproperty-iconSize: 16px 16px;
|
|
}
|
|
QToolButton#navProjectPickMini:hover { background: $nav_hover; }
|
|
QToolButton#navProjectPickMini:disabled { background: transparent; border-color: $border; }
|
|
QToolButton#navProjectPickMini::menu-indicator { image: none; width: 0; }
|
|
|
|
QPushButton#navSettingsBtn {
|
|
background: transparent; border: none; color: $text_muted;
|
|
/* Padding stays at 0: the row lays its own icon and label out, so that
|
|
the spacing does not change with the platform's button style. */
|
|
padding: 0; text-align: left; border-radius: ${radius}px;
|
|
/* No side margin: Settings reads as one more row under Dashboard/Giám sát,
|
|
so its icon has to start on their x. A 6px margin put it at 14 — near
|
|
enough the middle of the collapsed 54px rail to look centred. */
|
|
margin: 2px 0px 6px 0px;
|
|
}
|
|
QPushButton#navSettingsBtn:hover { background: $nav_hover; color: $text; }
|
|
QPushButton#navSettingsBtn:pressed { background: $active; }
|
|
|
|
|
|
"""
|
|
|
|
#: Hai nửa nối lại. Cắt đôi vì một chuỗi 470 dòng vượt ngưỡng 400 dòng/file.
|
|
_TEMPLATE = Template(_QSS_SHELL + QSS_CONTROLS)
|