Files
cowork-local/theme/qss_controls.py
T

359 lines
18 KiB
Python

"""Nửa sau của khuôn QSS: bề mặt, tab, ô nhập, nút, badge, log.
Cắt đôi khuôn QSS đúng mạch của chính nó: ``theme/qss.py`` giữ phần vỏ
(reset + shell: thanh rail, khung chính), file này giữ phần điều khiển.
Hai nửa được nối lại trong ``theme/qss.py``.
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
QSS_CONTROLS = """/* ---- surfaces --------------------------------------------------------- */
QGroupBox {
background: $surface; border: 1px solid $border; border-radius: ${radius_lg}px;
margin-top: 14px; padding: 16px 12px 12px 12px; font-weight: 600;
}
QGroupBox::title {
subcontrol-origin: margin; subcontrol-position: top left; left: 12px; top: 1px;
padding: 0 6px; color: $text_muted; font-size: 12px; font-weight: 600;
}
QFrame[frameShape="4"], QFrame[frameShape="5"] { background: $border; border: none; }
QScrollArea { background: transparent; border: none; }
QAbstractScrollArea::corner { background: transparent; }
/* ---- tabs: an underline, not a pill. -------------------------------------
The old pill tabs read as buttons and fought the real buttons for
attention. A 2px rule under the active label is quieter and unambiguous. */
QTabWidget::pane { background: $bg; border: none; border-top: 1px solid $border; top: -1px; }
QTabBar { background: transparent; qproperty-drawBase: 0; }
QTabBar::tab {
background: transparent; color: $text_muted; padding: 9px 14px; margin: 0 2px 0 0;
border: none; border-bottom: 2px solid transparent; font-weight: 500;
}
QTabBar::tab:selected { color: $text; border-bottom: 2px solid $accent; font-weight: 600; }
QTabBar::tab:hover:!selected { color: $text; background: $hover; }
/* Co4E flow strip — browser-style tabs, so these stay enclosed. */
QTabBar#flowTabs::tab {
background: $surface; color: $text_muted; border: 1px solid $border;
border-radius: ${radius}px; padding: 6px 10px; margin: 0 3px 0 0; min-height: 22px;
}
QTabBar#flowTabs::tab:selected { background: $surface_raised; color: $text; border-color: $border_strong; }
QTabBar#flowTabs::tab:hover:!selected { background: $hover; color: $text; }
QPushButton#flowAddBtn {
background: transparent; color: $text_muted; border: 1px solid $border;
border-radius: ${radius}px; padding: 6px 0; font-size: 15px; min-height: 22px;
}
QPushButton#flowAddBtn:hover { background: $hover; color: $text; }
/* Co4E icon sidebar — no chrome until it is the active one. */
QTabBar#co4eSideTabs { qproperty-iconSize: 18px 18px; }
QTabBar#co4eSideTabs::tab {
background: transparent; color: $text_muted; border: none;
border-radius: ${radius}px; padding: 6px; margin: 0 4px 0 0;
}
QTabBar#co4eSideTabs::tab:selected { background: $accent_soft; color: $text; }
QTabBar#co4eSideTabs::tab:hover:!selected { background: $hover; color: $text; }
QGraphicsView#co4eCanvas {
background: $surface; border: 1px solid $border; border-radius: ${radius_lg}px;
}
/* ---- text entry & item views ------------------------------------------ */
QPlainTextEdit, QTextEdit, QTextBrowser, QLineEdit, QSpinBox, QDoubleSpinBox,
QTreeView, QListView, QTableView, QTreeWidget, QListWidget, QTableWidget {
background: $surface_raised; color: $text; border: 1px solid $border;
border-radius: ${radius}px; selection-background-color: $selection_bg;
selection-color: $selection_fg; outline: 0;
}
QPlainTextEdit, QTextEdit, QLineEdit, QSpinBox, QDoubleSpinBox { padding: 6px 8px; }
QPlainTextEdit:hover, QTextEdit:hover, QLineEdit:hover { border-color: $border_strong; }
QPlainTextEdit:focus, QTextEdit:focus, QLineEdit:focus,
QSpinBox:focus, QDoubleSpinBox:focus { border: 1px solid $focus_ring; }
QLineEdit:disabled, QPlainTextEdit:disabled, QTextEdit:disabled {
background: $surface; color: $text_disabled;
}
/* Explicit up/down button geometry: once ANY spin-box subcontrol is styled,
Qt uses exactly this rect for both painting AND hit-testing, so the
clickable area can no longer drift from what's drawn (the previous
unstyled default arrows misaligned their own click region at 125%/150%
Windows display scaling — this pins both to the same rect instead). */
QSpinBox::up-button, QDoubleSpinBox::up-button {
subcontrol-origin: border; subcontrol-position: top right;
width: 18px; height: 15px; border: none; background: transparent;
}
QSpinBox::down-button, QDoubleSpinBox::down-button {
subcontrol-origin: border; subcontrol-position: bottom right;
width: 18px; height: 15px; border: none; background: transparent;
}
QSpinBox::up-button:hover, QDoubleSpinBox::up-button:hover,
QSpinBox::down-button:hover, QDoubleSpinBox::down-button:hover { background: $hover; }
QSpinBox::up-button:pressed, QDoubleSpinBox::up-button:pressed,
QSpinBox::down-button:pressed, QDoubleSpinBox::down-button:pressed { background: $active; }
QSpinBox::up-arrow, QDoubleSpinBox::up-arrow { image: url($chevron_up); width: 9px; height: 9px; }
QSpinBox::down-arrow, QDoubleSpinBox::down-arrow { image: url($chevron_down); width: 9px; height: 9px; }
QSpinBox::up-arrow:disabled, QSpinBox::down-arrow:disabled,
QDoubleSpinBox::up-arrow:disabled, QDoubleSpinBox::down-arrow:disabled { image: none; }
QTreeView::item, QListView::item, QTableView::item { padding: 4px 3px; border-radius: ${radius_sm}px; }
QTreeView::item:hover, QListView::item:hover { background: $hover; }
QTreeView::item:selected, QListView::item:selected, QTableView::item:selected {
background: $accent_soft; color: $text;
}
/* The platform style draws its own dotted/solid focus rect on the current
cell on top of the selection tint above — visible as a stray light border
on a click. The selection tint already marks "current row"; drop the rect. */
QTreeView::item:focus, QListView::item:focus, QTableView::item:focus { outline: none; border: none; }
/* Kanban lanes (Schedule Task): seven lanes share the board width, so a card's
own inset competes with the other six for space the same way the inter-lane
gap did — trimmed to match. */
QListWidget#kanbanLane::item { padding: 3px 2px; }
QHeaderView::section {
background: $bg; color: $text_muted; border: none;
border-bottom: 1px solid $border; padding: 7px 6px; font-weight: 600;
}
/* ---- buttons -----------------------------------------------------------
Default is a quiet outline. Weight is reserved for #primary / #danger, so
at most one button per view should carry a fill. */
QPushButton {
background: $surface_raised; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 7px 14px; font-weight: 500;
}
QPushButton:hover { background: $hover; border-color: $border_strong; }
QPushButton:pressed { background: $active; }
QPushButton:disabled { color: $text_disabled; background: $surface; border-color: $border; }
QPushButton:focus { border: 1px solid $focus_ring; }
QPushButton#primary {
background: $accent_solid; color: $on_accent; border: 1px solid transparent; font-weight: 600;
}
QPushButton#primary:hover { background: $accent_solid_hover; }
QPushButton#primary:pressed { background: $accent_solid_active; }
QPushButton#primary:disabled { background: $surface; color: $text_disabled; border-color: $border; }
QPushButton#danger {
background: $danger_solid; color: $on_accent; border: 1px solid transparent; font-weight: 600;
}
QPushButton#danger:hover { background: $danger_solid_hover; }
QPushButton#danger:disabled { background: $surface; color: $text_disabled; border-color: $border; }
/* Tinted, not filled. `warning` and `success` are tuned as TEXT colours — the
same reason `accent` and `accent_solid` are separate tokens: a hue readable
as ink on a surface is too light to carry white as a fill (dark `warning` is
#CCA700, which white fails AA against). A soft ground with the token as ink
keeps the contrast that was already verified, in both themes. */
QPushButton#warning {
background: $warning_soft; color: $warning; border: 1px solid $warning; font-weight: 600;
}
QPushButton#warning:hover { background: $hover; }
QPushButton#warning:pressed { background: $active; }
QPushButton#warning:disabled { background: $surface; color: $text_disabled; border-color: $border; }
QPushButton#success {
background: $success_soft; color: $success; border: 1px solid $success; font-weight: 600;
}
QPushButton#success:hover { background: $hover; }
QPushButton#success:pressed { background: $active; }
QPushButton#success:disabled { background: $surface; color: $text_disabled; border-color: $border; }
/* Ghost buttons: nav section headers and icon-only chrome. */
QPushButton#navMenuBtn {
background: transparent; border: none; border-radius: ${radius}px; padding: 5px 6px;
font-weight: 600; font-size: 11px; letter-spacing: 0.6px; color: $text_faint; text-align: left;
}
QPushButton#navMenuBtn:hover { background: $hover; color: $text; }
QPushButton#navMenuBtn:pressed { background: $active; }
QToolButton {
background: transparent; color: $text_muted; border: none;
border-radius: ${radius}px; padding: 5px;
}
QToolButton:hover { background: $hover; color: $text; }
QToolButton:pressed { background: $active; }
QToolButton::menu-indicator { image: none; }
/* ---- pickers ----------------------------------------------------------- */
QComboBox {
background: $surface_raised; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 6px 10px;
}
QComboBox:hover { background: $hover; }
QComboBox:focus { border-color: $focus_ring; }
QComboBox:disabled { color: $text_disabled; background: $surface; border-color: $border; }
QComboBox::drop-down { border: none; width: 20px; }
QComboBox::down-arrow { image: url($chevron_down); width: 10px; height: 10px; }
QComboBox::down-arrow:disabled { image: none; }
QComboBox QAbstractItemView {
background: $overlay; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 4px; outline: none;
selection-background-color: $accent_soft; selection-color: $text;
}
QMenu { background: $overlay; color: $text; border: 1px solid $border_strong;
border-radius: ${radius}px; padding: 4px; }
QMenu::item { padding: 6px 14px; border-radius: ${radius_sm}px; }
QMenu::item:selected { background: $accent_soft; color: $text; }
QMenu::item:disabled { color: $text_disabled; }
QMenu::separator { height: 1px; background: $border; margin: 4px 6px; }
QMenuBar { background: $bg; color: $text; border-bottom: 1px solid $border; }
QMenuBar::item { padding: 5px 10px; border-radius: ${radius_sm}px; background: transparent; }
QMenuBar::item:selected { background: $hover; }
/* ---- toggles ----------------------------------------------------------- */
QCheckBox, QRadioButton { spacing: 8px; background: transparent; }
QCheckBox::indicator, QRadioButton::indicator {
width: 16px; height: 16px; background: $surface_raised;
border: 1px solid $border_strong; border-radius: ${radius_sm}px;
}
QRadioButton::indicator { border-radius: 9px; }
QCheckBox::indicator:hover, QRadioButton::indicator:hover { border-color: $accent; }
QCheckBox::indicator:checked, QRadioButton::indicator:checked {
background: $accent_solid; border-color: $accent_solid;
}
QCheckBox::indicator:disabled, QRadioButton::indicator:disabled {
background: $surface; border-color: $border;
}
QCheckBox::indicator:checked:disabled, QRadioButton::indicator:checked:disabled {
background: $border_strong; border-color: $border_strong;
}
QSlider::groove:horizontal { height: 4px; background: $border; border-radius: 2px; }
QSlider::sub-page:horizontal { background: $accent_solid; border-radius: 2px; }
QSlider::handle:horizontal {
width: 14px; height: 14px; margin: -6px 0; border-radius: 7px;
background: $surface_raised; border: 1px solid $border_strong;
}
QSlider::handle:horizontal:hover { border-color: $accent; }
QProgressBar {
background: $surface; border: none; border-radius: 3px;
height: 6px; text-align: center; color: $text_muted;
}
QProgressBar::chunk { background: $accent_solid; border-radius: 3px; }
/* ---- scrollbars: overlay-thin, no arrows. ------------------------------ */
QScrollBar:vertical { background: transparent; width: 10px; margin: 2px; }
QScrollBar::handle:vertical { background: $scroll_handle; border-radius: 4px; min-height: 28px; }
QScrollBar::handle:vertical:hover { background: $scroll_handle_hover; }
QScrollBar:horizontal { background: transparent; height: 10px; margin: 2px; }
QScrollBar::handle:horizontal { background: $scroll_handle; border-radius: 4px; min-width: 28px; }
QScrollBar::handle:horizontal:hover { background: $scroll_handle_hover; }
QScrollBar::add-line, QScrollBar::sub-line { height: 0; width: 0; border: none; background: none; }
QScrollBar::add-page, QScrollBar::sub-page { background: none; }
/* ---- badges & inline text tones ---------------------------------------
One shape, seven tones. Pick by meaning: badgeSuccess for a finished run,
badgeDanger for a failed one — not by which colour looks nice. badgeNeutral
is the odd one out: it deliberately uses OPAQUE tokens ($active/$text_muted,
not an rgba() *_soft one) since it renders inside table cells that can sit
over a selection tint — an rgba() background there would composite
differently selected vs not (see Monitoring ▸ Sự kiện bảo mật's Hành động
pill, which hit exactly this). */
QLabel#badge, QLabel#badgeSuccess, QLabel#badgeWarn, QLabel#badgeDanger,
QLabel#badgePurple, QLabel#badgePink, QLabel#badgeNeutral {
border-radius: ${radius_sm}px; padding: 2px 8px; font-size: 12px; font-weight: 600;
}
QLabel#badge { background: $info_soft; color: $info; }
QLabel#badgeSuccess { background: $success_soft; color: $success; }
QLabel#badgeWarn { background: $warning_soft; color: $warning; }
QLabel#badgeDanger { background: $danger_soft; color: $danger; }
QLabel#badgePurple { background: $purple_soft; color: $purple; }
QLabel#badgePink { background: $pink_soft; color: $pink; }
QLabel#badgeNeutral { background: $active; color: $text_muted; }
/* ---- event-detail panel (Monitoring ▸ Sự kiện bảo mật) -----------------
A neutral, low-emphasis tag — the "Loại" chip: a category label with no
colour coding of its own (colour is reserved for the Trạng thái badge
beside it). */
QLabel#neutralTag {
background: $surface_raised; border: 1px solid $border; border-radius: ${radius_sm}px;
padding: 2px 8px; font-size: 12px;
}
/* A short identifier shown as a bordered monospace chip (machine name,
event id). */
QLabel#monoChip {
font-family: "Cascadia Code", Consolas, monospace; background: $surface_raised;
border: 1px solid $border; border-radius: ${radius_sm}px; padding: 1px 6px; font-size: 12px;
}
/* Section caption inside the panel — the same quiet caps heading as
Monitoring ▸ Overview's group titles (monSection::title above), with a
hairline under it since the panel has no group-box border of its own. */
QLabel#detailSectionHdr {
color: $text_faint; font-size: 11px; font-weight: 700; letter-spacing: 0.5px;
padding-bottom: 4px; margin-top: 6px; border-bottom: 1px solid $border;
}
/* The blocked-detail text renders as a fixed dark "terminal" block — the
same look in both themes, like a code snippet, so it reads consistently
against whichever tint the row around it happens to carry. */
QWidget#detailCodeBlock { background: #1B1A19; border-radius: ${radius}px; }
QLabel#detailCodeText {
color: #CCFF00; font-family: "Cascadia Code", Consolas, monospace; font-size: 12px;
}
QPushButton#detailCopyBtn {
background: rgba(255,255,255,0.15); color: #FFFFFF; border: none;
border-radius: ${radius_sm}px; padding: 3px 10px; font-size: 11px;
}
QPushButton#detailCopyBtn:hover { background: rgba(255,255,255,0.25); }
QLabel { background: transparent; }
QLabel#hint { color: $text_muted; }
/* Man gioi thieu cua khung chat (trang thai rong). */
QWidget#chatWelcome { background: $bg; }
QLabel#welcomeGreeting { color: $text; font-size: 19px; font-weight: 600; }
QLabel#welcomeMark {
background: $accent_soft; border: 1px solid $border_strong;
border-radius: ${radius}px;
}
QPushButton#welcomeCard {
background: $surface; border: 1px solid $border; border-radius: ${radius}px;
text-align: left; padding: 0px;
}
QPushButton#welcomeCard:hover { background: $hover; border-color: $border_strong; }
QPushButton#welcomeCard:focus { border: 1px solid $focus_ring; }
QLabel#welcomeCardTitle { color: $text; font-weight: 600; }
/* Panel "dang tai" phu len khung do thi GraphRAG. Dung khung QWebEngineView la
viec DONG BO tren GUI thread (~1-2s), nen thanh tien trinh o day khong chay
duoc trong giai doan do — panel van phai nhin thay duoc, va do la ly do no co
nen dac cua rieng minh thay vi chi la mot dong chu. */
QWidget#graphBusy { background: $overlay; border: 1px solid $border_strong; border-radius: ${radius}px; }
QWidget#graphBusy QLabel { color: $text; font-weight: 600; }
/* Lop phu ca cua so trong luc doi ngon ngu — cung ly do voi #graphBusy o tren:
viec chan chay DONG BO tren GUI thread. Nen mo chu khong dac: nguoi dung con
thay man hinh cu mo di, nen hieu la app dang lam viec chu khong phai da nhay
sang mot man hinh khac. */
QWidget#busyOverlay { background: rgba(0, 0, 0, 0.45); }
QWidget#busyOverlayPanel {
background: $overlay; border: 1px solid $border_strong;
border-radius: ${radius}px;
}
QWidget#busyOverlayPanel QLabel { color: $text; font-weight: 600; }
QLabel#faint { color: $text_faint; }
QLabel#warning { color: $warning; font-weight: 600; }
QLabel#error { color: $danger; font-weight: 600; }
QLabel#success { color: $success; font-weight: 600; }
QLabel#sectionTitle { color: $text; font-size: 15px; font-weight: 600; }
/* ---- code, terminals & logs -------------------------------------------
These read as "sunken" surfaces: the eye goes in, not across. */
QPlainTextEdit#codeEditor, QPlainTextEdit#termOutput, QPlainTextEdit#logView {
background: $code_bg; color: $code_fg; border: none;
font-family: $font_mono; selection-background-color: $code_selection;
}
QLineEdit#termInput {
background: $code_bg; color: $code_fg; border: none; border-top: 1px solid $border;
font-family: $font_mono; border-radius: 0; padding: 7px 10px;
}
QLineEdit#termInput:focus { border-top-color: $accent; }
/* The help-agent dock styles itself from these same tokens — it is a floating
overlay that re-applies on every theme switch. See ui/help_agent_widget.py. */
"""