Files
APIClient-Agent/app/ui/theme.py
2026-03-28 18:01:49 +05:30

1118 lines
34 KiB
Python

"""
APIClient - Agent - Central Theme Engine
All styling lives here in the global QSS.
UI widgets use setObjectName() selectors - never inline setStyleSheet() for static colors.
Only truly dynamic values (per-request method color, status badge) stay inline.
Fonts: Quicksand (headings/brand/UI labels) + Open Sans (body text/inputs)
"""
from PyQt6.QtGui import QColor, QPalette
from PyQt6.QtWidgets import QApplication
# ── Color Palettes ────────────────────────────────────────────────────────────
class DarkColors:
BG_DARKEST = "#0D0D0D"
BG_SIDEBAR = "#111111"
BG_MAIN = "#181818"
BG_PANEL = "#1E1E1E"
BG_ELEVATED = "#242424"
BG_INPUT = "#2A2A2A"
BG_HOVER = "#303030"
BG_SELECTED = "#383838"
BORDER = "#2C2C2C"
BORDER_FOCUS = "#505050"
TEXT_PRIMARY = "#E4E4E4"
TEXT_SECONDARY = "#8A8A8A"
TEXT_MUTED = "#505050"
TEXT_DISABLED = "#3A3A3A"
ACCENT = "#E05C2C"
ACCENT_HOVER = "#F06030"
ACCENT_PRESSED = "#C04C20"
ACCENT_SUBTLE = "#2A1208"
SUCCESS = "#3FB950"
WARNING = "#D29922"
ERROR = "#F85149"
INFO = "#58A6FF"
METHOD_GET = "#61AFFE"
METHOD_POST = "#49CC90"
METHOD_PUT = "#FCA130"
METHOD_PATCH = "#50E3C2"
METHOD_DELETE = "#F93E3E"
METHOD_HEAD = "#9012FE"
METHOD_OPTIONS = "#0D5AA7"
STATUS_1XX = "#8C8C8C"
STATUS_2XX = "#3FB950"
STATUS_3XX = "#D29922"
STATUS_4XX = "#F85149"
STATUS_5XX = "#FF4444"
class LightColors:
BG_DARKEST = "#E2E2E2"
BG_SIDEBAR = "#ECECEC"
BG_MAIN = "#F2F2F2"
BG_PANEL = "#FFFFFF"
BG_ELEVATED = "#E8E8E8"
BG_INPUT = "#FFFFFF"
BG_HOVER = "#DCDCDC"
BG_SELECTED = "#D0D0D0"
BORDER = "#D0D0D0"
BORDER_FOCUS = "#A0A0A0"
TEXT_PRIMARY = "#1A1A1A"
TEXT_SECONDARY = "#555555"
TEXT_MUTED = "#999999"
TEXT_DISABLED = "#BBBBBB"
ACCENT = "#C94A14"
ACCENT_HOVER = "#E05520"
ACCENT_PRESSED = "#A83C0E"
ACCENT_SUBTLE = "#FDEEE6"
SUCCESS = "#1A7F37"
WARNING = "#7A5800"
ERROR = "#C01020"
INFO = "#0550AE"
METHOD_GET = "#0550AE"
METHOD_POST = "#1A7F37"
METHOD_PUT = "#7A3800"
METHOD_PATCH = "#116329"
METHOD_DELETE = "#C01020"
METHOD_HEAD = "#6639BA"
METHOD_OPTIONS = "#0550AE"
STATUS_1XX = "#777777"
STATUS_2XX = "#1A7F37"
STATUS_3XX = "#7A5800"
STATUS_4XX = "#C01020"
STATUS_5XX = "#C01020"
# ── Active palette (module-level singleton) ───────────────────────────────────
Colors = DarkColors
_is_dark = True
# ── Zoom level (1.0 = 100%) ───────────────────────────────────────────────────
_zoom = 1.0
_ZOOM_MIN = 0.7
_ZOOM_MAX = 1.8
_ZOOM_STEP = 0.1
# Font families
_UI_FONT = '"Quicksand", "Segoe UI", "SF Pro Text", sans-serif'
_BODY_FONT = '"Open Sans", "Segoe UI", "SF Pro Text", sans-serif'
_MONO_FONT = '"JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace'
def method_color(method: str) -> str:
return {
"GET": Colors.METHOD_GET,
"POST": Colors.METHOD_POST,
"PUT": Colors.METHOD_PUT,
"PATCH": Colors.METHOD_PATCH,
"DELETE": Colors.METHOD_DELETE,
"HEAD": Colors.METHOD_HEAD,
"OPTIONS": Colors.METHOD_OPTIONS,
}.get(method.upper(), Colors.TEXT_SECONDARY)
def status_color(code: int) -> str:
if code < 200: return Colors.STATUS_1XX
if code < 300: return Colors.STATUS_2XX
if code < 400: return Colors.STATUS_3XX
if code < 500: return Colors.STATUS_4XX
return Colors.STATUS_5XX
def _z(size: float) -> str:
"""Scale a font size by current zoom level and return px string."""
return f"{round(size * _zoom)}px"
# ── Global Stylesheet ─────────────────────────────────────────────────────────
def _build_stylesheet(C) -> str:
return f"""
/* ════════════════════════════════════════════════════════
BASE
════════════════════════════════════════════════════════ */
QWidget {{
background-color: {C.BG_MAIN};
color: {C.TEXT_PRIMARY};
font-family: {_BODY_FONT};
font-size: {_z(13)};
border: none;
outline: none;
}}
QMainWindow, QDialog {{
background-color: {C.BG_PANEL};
}}
/* ════════════════════════════════════════════════════════
SCROLLBARS
════════════════════════════════════════════════════════ */
QScrollBar:vertical {{
background: transparent; width: 8px; margin: 0;
}}
QScrollBar::handle:vertical {{
background: {C.BORDER_FOCUS}; border-radius: 4px; min-height: 28px;
}}
QScrollBar::handle:vertical:hover {{ background: {C.TEXT_MUTED}; }}
QScrollBar::add-line:vertical, QScrollBar::sub-line:vertical {{ height: 0; }}
QScrollBar:horizontal {{
background: transparent; height: 8px; margin: 0;
}}
QScrollBar::handle:horizontal {{
background: {C.BORDER_FOCUS}; border-radius: 4px; min-width: 28px;
}}
QScrollBar::handle:horizontal:hover {{ background: {C.TEXT_MUTED}; }}
QScrollBar::add-line:horizontal, QScrollBar::sub-line:horizontal {{ width: 0; }}
/* ════════════════════════════════════════════════════════
SPLITTER
════════════════════════════════════════════════════════ */
QSplitter::handle {{ background: {C.BORDER}; }}
QSplitter::handle:horizontal {{ width: 1px; }}
QSplitter::handle:vertical {{ height: 1px; }}
/* ════════════════════════════════════════════════════════
MENU
════════════════════════════════════════════════════════ */
QMenuBar {{
background-color: {C.BG_DARKEST};
color: {C.TEXT_SECONDARY};
border-bottom: 1px solid {C.BORDER};
padding: 2px 4px;
spacing: 4px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}}
QMenuBar::item {{
background: transparent; padding: 4px 10px; border-radius: 4px;
}}
QMenuBar::item:selected, QMenuBar::item:pressed {{
background-color: {C.BG_ELEVATED}; color: {C.TEXT_PRIMARY};
}}
QMenu {{
background-color: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 6px;
padding: 4px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QMenu::item {{ padding: 7px 28px 7px 12px; border-radius: 4px; }}
QMenu::item:selected {{ background-color: {C.BG_HOVER}; }}
QMenu::item:disabled {{ color: {C.TEXT_MUTED}; }}
QMenu::separator {{ height: 1px; background: {C.BORDER}; margin: 4px 8px; }}
/* ════════════════════════════════════════════════════════
INPUTS
════════════════════════════════════════════════════════ */
QLineEdit {{
background-color: {C.BG_INPUT};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
padding: 6px 10px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
selection-background-color: {C.ACCENT};
}}
QLineEdit:focus {{
border: 1px solid {C.BORDER_FOCUS};
background-color: {C.BG_ELEVATED};
}}
QLineEdit:disabled {{
color: {C.TEXT_DISABLED};
background-color: {C.BG_MAIN};
}}
QLineEdit::placeholder {{ color: {C.TEXT_MUTED}; }}
QTextEdit, QPlainTextEdit {{
background-color: {C.BG_INPUT};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
padding: 6px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
selection-background-color: {C.ACCENT};
}}
QTextEdit:focus, QPlainTextEdit:focus {{
border: 1px solid {C.BORDER_FOCUS};
}}
QSpinBox {{
background-color: {C.BG_INPUT};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
padding: 5px 8px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
}}
QSpinBox:focus {{ border-color: {C.BORDER_FOCUS}; }}
QSpinBox::up-button, QSpinBox::down-button {{
background: {C.BG_ELEVATED};
border: none;
width: 18px;
}}
QSpinBox::up-button:hover, QSpinBox::down-button:hover {{
background: {C.BG_HOVER};
}}
/* ════════════════════════════════════════════════════════
COMBOBOX
════════════════════════════════════════════════════════ */
QComboBox {{
background-color: {C.BG_INPUT};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
padding: 5px 10px;
min-width: 80px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
}}
QComboBox:hover {{ border-color: {C.BORDER_FOCUS}; }}
QComboBox:focus {{ border-color: {C.BORDER_FOCUS}; }}
QComboBox::drop-down {{ border: none; width: 20px; subcontrol-origin: padding; }}
QComboBox::down-arrow {{ width: 10px; height: 10px; }}
QComboBox QAbstractItemView {{
background-color: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 4px;
selection-background-color: {C.BG_SELECTED};
outline: none;
padding: 2px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
}}
/* ════════════════════════════════════════════════════════
CHECKBOX
════════════════════════════════════════════════════════ */
QCheckBox {{
color: {C.TEXT_SECONDARY};
spacing: 6px;
background: transparent;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QCheckBox::indicator {{
width: 14px; height: 14px;
border: 1px solid {C.BORDER_FOCUS};
border-radius: 3px;
background: {C.BG_INPUT};
}}
QCheckBox::indicator:checked {{
background: {C.ACCENT}; border-color: {C.ACCENT};
}}
/* ════════════════════════════════════════════════════════
BUTTONS
════════════════════════════════════════════════════════ */
QPushButton {{
background-color: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
padding: 6px 14px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}}
QPushButton:hover {{
background-color: {C.BG_HOVER};
border-color: {C.BORDER_FOCUS};
}}
QPushButton:pressed {{ background-color: {C.BG_SELECTED}; }}
QPushButton:disabled {{ color: {C.TEXT_MUTED}; border-color: {C.BORDER}; }}
QPushButton#accent {{
background-color: {C.ACCENT};
color: #FFFFFF;
border: none;
font-family: {_UI_FONT};
font-weight: 700;
font-size: {_z(12)};
}}
QPushButton#accent:hover {{ background-color: {C.ACCENT_HOVER}; }}
QPushButton#accent:pressed {{ background-color: {C.ACCENT_PRESSED}; }}
QPushButton#accent:disabled {{
background-color: {C.ACCENT_SUBTLE};
color: {C.TEXT_MUTED};
}}
QPushButton#ghost {{
background: transparent;
border: none;
color: {C.TEXT_SECONDARY};
padding: 4px 8px;
border-radius: 4px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}}
QPushButton#ghost:hover {{
color: {C.TEXT_PRIMARY};
background-color: {C.BG_HOVER};
}}
QPushButton#ghost:pressed {{ background-color: {C.BG_SELECTED}; }}
QPushButton#danger {{
background-color: transparent;
color: {C.ERROR};
border: 1px solid {C.ERROR};
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}}
QPushButton#danger:hover {{ background-color: {C.ACCENT_SUBTLE}; }}
QPushButton#sendBtn {{
background-color: {C.ACCENT};
color: white;
border: none;
border-radius: 6px;
padding: 8px 22px;
font-family: {_UI_FONT};
font-weight: 700;
font-size: {_z(13)};
letter-spacing: 0.3px;
}}
QPushButton#sendBtn:hover {{ background-color: {C.ACCENT_HOVER}; }}
QPushButton#sendBtn:pressed {{ background-color: {C.ACCENT_PRESSED}; }}
QPushButton#sendBtn:disabled {{
background-color: {C.ACCENT_SUBTLE};
color: {C.TEXT_MUTED};
}}
QPushButton#zoomBtn {{
background: transparent;
border: none;
color: {C.TEXT_MUTED};
padding: 2px 5px;
border-radius: 4px;
font-family: {_UI_FONT};
font-size: {_z(14)};
font-weight: 700;
}}
QPushButton#zoomBtn:hover {{
color: {C.TEXT_PRIMARY};
background-color: {C.BG_HOVER};
}}
QPushButton#zoomBtn:pressed {{ background-color: {C.BG_SELECTED}; }}
/* ════════════════════════════════════════════════════════
TABS
════════════════════════════════════════════════════════ */
QTabWidget::pane {{
border: none;
background-color: {C.BG_PANEL};
}}
QTabBar {{ background: transparent; }}
QTabBar::tab {{
background: transparent;
color: {C.TEXT_SECONDARY};
border: none;
border-bottom: 2px solid transparent;
padding: 8px 16px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}}
QTabBar::tab:selected {{
color: {C.TEXT_PRIMARY};
border-bottom: 2px solid {C.ACCENT};
}}
QTabBar::tab:hover:!selected {{
color: {C.TEXT_PRIMARY};
background-color: {C.BG_HOVER};
border-radius: 4px 4px 0 0;
}}
QTabBar::close-button {{
subcontrol-position: right;
border-radius: 3px;
margin: 3px 2px;
padding: 0;
width: 14px;
height: 14px;
}}
/* Request/Response inner tab bars sit on BG_MAIN strip */
QTabWidget#innerTabs QTabBar {{
background: {C.BG_MAIN};
border-bottom: 1px solid {C.BORDER};
}}
/* Top workspace tab bar (HTTP / WebSocket / Mock Server) */
QTabWidget#workspaceTabs QTabBar::tab {{
background: {C.BG_DARKEST};
color: {C.TEXT_SECONDARY};
border: none;
border-right: 1px solid {C.BORDER};
padding: 10px 20px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 700;
border-bottom: none;
border-top: 2px solid transparent;
}}
QTabWidget#workspaceTabs QTabBar::tab:selected {{
background: {C.BG_MAIN};
color: {C.TEXT_PRIMARY};
border-top: 2px solid {C.ACCENT};
}}
QTabWidget#workspaceTabs QTabBar::tab:hover:!selected {{
background: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
}}
/* ════════════════════════════════════════════════════════
TABLES
════════════════════════════════════════════════════════ */
QTableWidget {{
background-color: {C.BG_PANEL};
alternate-background-color: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: none;
gridline-color: {C.BORDER};
selection-background-color: {C.BG_SELECTED};
selection-color: {C.TEXT_PRIMARY};
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QTableWidget::item {{
padding: 5px 8px;
border-bottom: 1px solid {C.BORDER};
}}
QTableWidget::item:selected {{
background-color: {C.BG_SELECTED};
color: {C.TEXT_PRIMARY};
}}
QHeaderView::section {{
background-color: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: none;
border-bottom: 1px solid {C.BORDER};
border-right: 1px solid {C.BORDER};
padding: 6px 8px;
font-family: {_UI_FONT};
font-size: {_z(11)};
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}}
QHeaderView::section:last {{ border-right: none; }}
/* ════════════════════════════════════════════════════════
TREE
════════════════════════════════════════════════════════ */
QTreeWidget {{
background-color: {C.BG_SIDEBAR};
color: {C.TEXT_PRIMARY};
border: none;
outline: none;
show-decoration-selected: 1;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QTreeWidget::item {{
padding: 4px 4px;
border-radius: 3px;
}}
QTreeWidget::item:selected {{
background-color: {C.BG_SELECTED};
color: {C.TEXT_PRIMARY};
}}
QTreeWidget::item:hover:!selected {{ background-color: {C.BG_HOVER}; }}
QTreeWidget::branch {{ background: {C.BG_SIDEBAR}; }}
/* ════════════════════════════════════════════════════════
LIST
════════════════════════════════════════════════════════ */
QListWidget {{
background-color: {C.BG_PANEL};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
outline: none;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QListWidget::item {{ padding: 8px 10px; border-radius: 3px; }}
QListWidget::item:selected {{
background-color: {C.BG_SELECTED}; color: {C.TEXT_PRIMARY};
}}
QListWidget::item:hover:!selected {{ background-color: {C.BG_HOVER}; }}
/* ════════════════════════════════════════════════════════
SIDEBAR LIST (no border, flush)
════════════════════════════════════════════════════════ */
QListWidget#sidebarList {{
background: {C.BG_SIDEBAR};
border: none;
border-radius: 0;
}}
QListWidget#sidebarList::item {{
padding: 10px 14px;
border-bottom: 1px solid {C.BORDER};
border-radius: 0;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QListWidget#sidebarList::item:selected {{
background: {C.BG_SELECTED}; color: {C.TEXT_PRIMARY};
}}
QListWidget#sidebarList::item:hover:!selected {{ background: {C.BG_HOVER}; }}
/* ════════════════════════════════════════════════════════
STATUS BAR
════════════════════════════════════════════════════════ */
QStatusBar {{
background-color: {C.BG_DARKEST};
color: {C.TEXT_SECONDARY};
border-top: 1px solid {C.BORDER};
font-family: {_BODY_FONT};
font-size: {_z(11)};
padding: 0 8px;
}}
QStatusBar::item {{ border: none; }}
/* ════════════════════════════════════════════════════════
PROGRESS BAR
════════════════════════════════════════════════════════ */
QProgressBar {{
background-color: {C.BG_ELEVATED};
border: 1px solid {C.BORDER};
border-radius: 4px;
height: 6px;
text-align: center;
color: transparent;
}}
QProgressBar::chunk {{
background-color: {C.ACCENT}; border-radius: 4px;
}}
/* ════════════════════════════════════════════════════════
GROUP BOX
════════════════════════════════════════════════════════ */
QGroupBox {{
color: {C.TEXT_SECONDARY};
border: 1px solid {C.BORDER};
border-radius: 6px;
margin-top: 8px;
padding: 8px;
font-family: {_UI_FONT};
font-size: {_z(11)};
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}}
QGroupBox::title {{
subcontrol-origin: margin; left: 10px; padding: 0 4px;
}}
/* ════════════════════════════════════════════════════════
TOOLTIP
════════════════════════════════════════════════════════ */
QToolTip {{
background-color: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 4px;
padding: 5px 8px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
/* ════════════════════════════════════════════════════════
DIALOG BUTTON BOX
════════════════════════════════════════════════════════ */
QDialogButtonBox QPushButton {{ min-width: 80px; }}
/* ════════════════════════════════════════════════════════
FRAME SEPARATORS
════════════════════════════════════════════════════════ */
QFrame[frameShape="4"], QFrame[frameShape="5"] {{
background-color: {C.BORDER};
border: none;
max-height: 1px;
max-width: 1px;
}}
/* ════════════════════════════════════════════════════════
-- NAMED WIDGET RULES (setObjectName API) --
════════════════════════════════════════════════════════ */
/* Top brand / env bar */
QWidget#envBar {{
background-color: {C.BG_DARKEST};
border-bottom: 1px solid {C.BORDER};
}}
QLabel#brandName {{
color: {C.ACCENT};
font-family: {_UI_FONT};
font-size: {_z(15)};
font-weight: 800;
letter-spacing: 2px;
background: transparent;
}}
QLabel#brandSub {{
color: {C.TEXT_MUTED};
font-family: {_UI_FONT};
font-size: {_z(11)};
font-weight: 600;
background: transparent;
}}
QLabel#envChip {{
color: {C.TEXT_MUTED};
font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700;
letter-spacing: 1px;
background: transparent;
}}
QLabel#zoomLabel {{
color: {C.TEXT_MUTED};
font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 600;
background: transparent;
min-width: 36px;
qproperty-alignment: AlignCenter;
}}
/* Sidebar */
QWidget#sidebar {{
background-color: {C.BG_SIDEBAR};
border-right: 1px solid {C.BORDER};
}}
QWidget#sidebarHeader {{
background-color: {C.BG_SIDEBAR};
border-bottom: 1px solid {C.BORDER};
}}
QWidget#sidebarSearch {{
background-color: {C.BG_SIDEBAR};
}}
/* URL bar strip */
QWidget#urlBarStrip {{
background-color: {C.BG_MAIN};
border-bottom: 1px solid {C.BORDER};
}}
QLineEdit#urlBar {{
background-color: {C.BG_INPUT};
border: 1.5px solid {C.BORDER};
border-radius: 6px;
padding: 8px 12px;
font-family: {_MONO_FONT};
font-size: {_z(13)};
color: {C.TEXT_PRIMARY};
}}
QLineEdit#urlBar:focus {{
border-color: {C.ACCENT};
background-color: {C.BG_ELEVATED};
}}
/* Method combo (color set inline per method, only layout here) */
QComboBox#methodCombo {{
font-family: {_UI_FONT};
font-weight: 800;
font-size: {_z(12)};
border-radius: 6px;
padding: 8px 10px;
min-width: 100px;
border: 1px solid {C.BORDER};
background-color: {C.BG_INPUT};
}}
QComboBox#methodCombo:hover {{ border-color: {C.BORDER_FOCUS}; }}
QComboBox#methodCombo QAbstractItemView {{
background: {C.BG_ELEVATED};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
selection-background-color: {C.BG_SELECTED};
}}
/* Inner request/response tab strip */
QWidget#tabStrip {{
background-color: {C.BG_MAIN};
border-bottom: 1px solid {C.BORDER};
}}
/* Response top bar */
QWidget#responseBar {{
background-color: {C.BG_MAIN};
border-top: 1px solid {C.BORDER};
border-bottom: 1px solid {C.BORDER};
}}
QLabel#responseTitle {{
color: {C.TEXT_MUTED};
font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700;
letter-spacing: 1.2px;
background: transparent;
}}
QLabel#metaLabel {{
color: {C.TEXT_MUTED};
font-family: {_BODY_FONT};
font-size: {_z(11)};
background: transparent;
padding: 0 6px;
}}
/* Section/panel headers used in dialogs */
QWidget#panelHeader {{
background-color: {C.BG_ELEVATED};
border-bottom: 1px solid {C.BORDER};
}}
QWidget#panelFooter {{
background-color: {C.BG_ELEVATED};
border-top: 1px solid {C.BORDER};
}}
QWidget#sectionHeader {{
background-color: {C.BG_SIDEBAR};
border-bottom: 1px solid {C.BORDER};
}}
QWidget#panelBody {{
background-color: {C.BG_PANEL};
}}
/* Labels inside panels */
QLabel#panelTitle {{
font-family: {_UI_FONT};
font-size: {_z(14)};
font-weight: 700;
color: {C.TEXT_PRIMARY};
background: transparent;
}}
QLabel#sectionLabel {{
color: {C.TEXT_MUTED};
font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700;
letter-spacing: 1px;
background: transparent;
}}
QLabel#hintText {{
color: {C.TEXT_MUTED};
font-family: {_BODY_FONT};
font-size: {_z(11)};
background: transparent;
}}
QLabel#fieldLabel {{
color: {C.TEXT_SECONDARY};
font-family: {_BODY_FONT};
font-size: {_z(12)};
background: transparent;
}}
/* Body/code editors */
QTextEdit#codeEditor {{
background-color: {C.BG_PANEL};
color: {C.TEXT_PRIMARY};
border: none;
padding: 8px;
font-family: {_MONO_FONT};
font-size: {_z(11)};
}}
/* Loading overlay */
QWidget#loadingOverlay {{
background-color: {C.BG_PANEL};
}}
QLabel#loadingLabel {{
color: {C.TEXT_MUTED};
font-family: {_BODY_FONT};
font-size: {_z(13)};
background: transparent;
}}
/* Search in response bar */
QLineEdit#searchBar {{
background: {C.BG_INPUT};
border: 1px solid {C.BORDER};
border-radius: 4px;
padding: 4px 8px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
color: {C.TEXT_PRIMARY};
}}
QLineEdit#searchBar:focus {{ border-color: {C.BORDER_FOCUS}; }}
/* Sidebar filter input */
QLineEdit#filterInput {{
background: {C.BG_ELEVATED};
border: 1px solid {C.BORDER};
border-radius: 4px;
padding: 5px 8px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
color: {C.TEXT_PRIMARY};
}}
QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }}
/* WebSocket / Mock status indicator labels */
QLabel#statusOk {{
color: {C.SUCCESS};
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
background: transparent;
}}
QLabel#statusWarn {{
color: {C.WARNING};
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
background: transparent;
}}
QLabel#statusErr {{
color: {C.ERROR};
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
background: transparent;
}}
/* Auth "none" hint */
QLabel#authNone {{
color: {C.TEXT_MUTED};
font-family: {_BODY_FONT};
font-size: {_z(12)};
padding: 12px;
background: transparent;
}}
/* Sidebar panel (environment dialog left pane, etc.) */
QWidget#sidebarPanel {{
background-color: {C.BG_SIDEBAR};
}}
/* Custom tab close button */
QPushButton#tabCloseBtn {{
background: transparent;
border: none;
border-radius: 3px;
color: {C.TEXT_MUTED};
font-size: {_z(14)};
font-weight: 700;
padding: 0;
}}
QPushButton#tabCloseBtn:hover {{
background-color: {C.ERROR};
color: #FFFFFF;
}}
/* AI Assistant panel */
QWidget#aiPanel {{
background-color: {C.BG_PANEL};
}}
QTextEdit#aiOutput {{
background-color: {C.BG_INPUT};
color: {C.TEXT_PRIMARY};
border: 1px solid {C.BORDER};
border-radius: 5px;
padding: 8px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}}
QLabel#aiStatusLabel {{
color: {C.TEXT_MUTED};
font-family: {_BODY_FONT};
font-size: {_z(11)};
background: transparent;
}}
/* -- AI Chat Panel ─────────────────────────────────────── */
QWidget#aiChatPanel {{
background-color: {C.BG_SIDEBAR};
border-left: 1px solid {C.BORDER};
}}
QWidget#aiChatHeader {{
background-color: {C.BG_ELEVATED};
border-bottom: 1px solid {C.BORDER};
}}
QLabel#aiChatTitle {{
color: {C.ACCENT};
font-family: {_UI_FONT};
font-size: {_z(13)};
font-weight: 700;
letter-spacing: 0.5px;
background: transparent;
}}
QWidget#chatArea {{
background-color: {C.BG_SIDEBAR};
}}
QFrame#userBubble {{
background-color: {C.ACCENT_SUBTLE};
border: 1px solid {C.BORDER};
border-left: 3px solid {C.ACCENT};
border-radius: 6px;
margin: 0px;
}}
QFrame#aiBubble {{
background-color: {C.BG_ELEVATED};
border: 1px solid {C.BORDER};
border-radius: 6px;
margin: 0px;
}}
QLabel#chatRoleLabel {{
font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700;
color: {C.TEXT_MUTED};
background: transparent;
letter-spacing: 0.5px;
text-transform: uppercase;
}}
QLabel#chatMessageText {{
color: {C.TEXT_PRIMARY};
background: transparent;
font-family: {_BODY_FONT};
font-size: {_z(12)};
line-height: 1.6;
}}
QWidget#chatInputArea {{
background-color: {C.BG_ELEVATED};
border-top: 1px solid {C.BORDER};
}}
QTextEdit#chatInput {{
background-color: {C.BG_INPUT};
border: 1px solid {C.BORDER};
border-radius: 6px;
padding: 6px 10px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
color: {C.TEXT_PRIMARY};
}}
QTextEdit#chatInput:focus {{
border-color: {C.ACCENT};
}}
QWidget#quickActions {{
background-color: {C.BG_MAIN};
border-top: 1px solid {C.BORDER};
border-bottom: 1px solid {C.BORDER};
}}
QPushButton#qaBtn {{
background-color: {C.BG_INPUT};
border: 1px solid {C.BORDER};
border-radius: 10px;
padding: 2px 8px;
font-family: {_UI_FONT};
font-size: {_z(11)};
color: {C.TEXT_SECONDARY};
font-weight: 600;
}}
QPushButton#qaBtn:hover {{
background-color: {C.BG_HOVER};
border-color: {C.ACCENT};
color: {C.TEXT_PRIMARY};
}}
QFrame#applyBlock {{
background-color: {C.BG_PANEL};
border: 1px solid {C.BORDER};
border-left: 3px solid {C.ACCENT};
border-radius: 4px;
}}
QTextEdit#applyCode {{
background-color: transparent;
border: none;
padding: 4px;
font-family: {_MONO_FONT};
font-size: {_z(10)};
color: {C.TEXT_SECONDARY};
}}
"""
def _apply_palette(app: QApplication, C):
palette = QPalette()
palette.setColor(QPalette.ColorRole.Window, QColor(C.BG_MAIN))
palette.setColor(QPalette.ColorRole.WindowText, QColor(C.TEXT_PRIMARY))
palette.setColor(QPalette.ColorRole.Base, QColor(C.BG_INPUT))
palette.setColor(QPalette.ColorRole.AlternateBase, QColor(C.BG_ELEVATED))
palette.setColor(QPalette.ColorRole.Text, QColor(C.TEXT_PRIMARY))
palette.setColor(QPalette.ColorRole.PlaceholderText, QColor(C.TEXT_MUTED))
palette.setColor(QPalette.ColorRole.Button, QColor(C.BG_ELEVATED))
palette.setColor(QPalette.ColorRole.ButtonText, QColor(C.TEXT_PRIMARY))
palette.setColor(QPalette.ColorRole.Highlight, QColor(C.ACCENT))
palette.setColor(QPalette.ColorRole.HighlightedText, QColor("#FFFFFF"))
palette.setColor(QPalette.ColorRole.Link, QColor(C.INFO))
palette.setColor(QPalette.ColorRole.ToolTipBase, QColor(C.BG_ELEVATED))
palette.setColor(QPalette.ColorRole.ToolTipText, QColor(C.TEXT_PRIMARY))
app.setPalette(palette)
def apply(app: QApplication, dark: bool = True):
global Colors, _is_dark
_is_dark = dark
Colors = DarkColors if dark else LightColors
app.setStyle("Fusion")
_apply_palette(app, Colors)
app.setStyleSheet(_build_stylesheet(Colors))
def toggle(app: QApplication) -> bool:
"""Toggle dark/light theme. Returns True if now dark."""
global _is_dark
apply(app, dark=not _is_dark)
return _is_dark
def is_dark() -> bool:
return _is_dark
def zoom_in(app: QApplication) -> float:
"""Increase zoom one step. Returns new zoom level."""
global _zoom
_zoom = min(_ZOOM_MAX, round(_zoom + _ZOOM_STEP, 1))
app.setStyleSheet(_build_stylesheet(Colors))
return _zoom
def zoom_out(app: QApplication) -> float:
"""Decrease zoom one step. Returns new zoom level."""
global _zoom
_zoom = max(_ZOOM_MIN, round(_zoom - _ZOOM_STEP, 1))
app.setStyleSheet(_build_stylesheet(Colors))
return _zoom
def zoom_reset(app: QApplication) -> float:
"""Reset zoom to 100%. Returns new zoom level."""
global _zoom
_zoom = 1.0
app.setStyleSheet(_build_stylesheet(Colors))
return _zoom
def get_zoom() -> float:
return _zoom
def restyle(widget, obj_name: str) -> None:
"""Change a widget's objectName and force Qt to re-evaluate CSS rules."""
widget.setObjectName(obj_name)
widget.style().unpolish(widget)
widget.style().polish(widget)