982 lines
31 KiB
Python
982 lines
31 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.
|
|
"""
|
|
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
|
|
|
|
|
|
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
|
|
|
|
|
|
# ── Global Stylesheet ─────────────────────────────────────────────────────────
|
|
# Everything static lives here. Object names are the API between theme and UI.
|
|
|
|
def _build_stylesheet(C) -> str:
|
|
return f"""
|
|
|
|
/* ════════════════════════════════════════════════════════
|
|
BASE
|
|
════════════════════════════════════════════════════════ */
|
|
QWidget {{
|
|
background-color: {C.BG_MAIN};
|
|
color: {C.TEXT_PRIMARY};
|
|
font-family: "Segoe UI", "SF Pro Text", "Inter", "Helvetica Neue", sans-serif;
|
|
font-size: 13px;
|
|
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;
|
|
}}
|
|
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;
|
|
}}
|
|
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;
|
|
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;
|
|
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;
|
|
}}
|
|
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;
|
|
}}
|
|
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;
|
|
}}
|
|
|
|
/* ════════════════════════════════════════════════════════
|
|
CHECKBOX
|
|
════════════════════════════════════════════════════════ */
|
|
QCheckBox {{
|
|
color: {C.TEXT_SECONDARY};
|
|
spacing: 6px;
|
|
background: transparent;
|
|
}}
|
|
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-weight: 500;
|
|
}}
|
|
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-weight: 600;
|
|
}}
|
|
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;
|
|
}}
|
|
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};
|
|
}}
|
|
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-weight: 700;
|
|
font-size: 13px;
|
|
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};
|
|
}}
|
|
|
|
/* ════════════════════════════════════════════════════════
|
|
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-size: 12px;
|
|
font-weight: 500;
|
|
}}
|
|
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-size: 12px;
|
|
font-weight: 600;
|
|
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};
|
|
}}
|
|
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-size: 11px;
|
|
font-weight: 600;
|
|
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;
|
|
}}
|
|
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;
|
|
}}
|
|
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-size: 13px;
|
|
}}
|
|
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-size: 11px;
|
|
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-size: 11px;
|
|
font-weight: 600;
|
|
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-size: 12px;
|
|
}}
|
|
|
|
/* ════════════════════════════════════════════════════════
|
|
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-size: 15px;
|
|
font-weight: 800;
|
|
letter-spacing: 2px;
|
|
background: transparent;
|
|
}}
|
|
QLabel#brandSub {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 11px;
|
|
font-weight: 500;
|
|
background: transparent;
|
|
}}
|
|
QLabel#envChip {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 1px;
|
|
background: transparent;
|
|
}}
|
|
|
|
/* 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-size: 13px;
|
|
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
|
|
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-weight: 800;
|
|
font-size: 12px;
|
|
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-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 1.2px;
|
|
background: transparent;
|
|
}}
|
|
QLabel#metaLabel {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 11px;
|
|
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-size: 14px;
|
|
font-weight: 700;
|
|
color: {C.TEXT_PRIMARY};
|
|
background: transparent;
|
|
}}
|
|
QLabel#sectionLabel {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 10px;
|
|
font-weight: 700;
|
|
letter-spacing: 1px;
|
|
background: transparent;
|
|
}}
|
|
QLabel#hintText {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 11px;
|
|
background: transparent;
|
|
}}
|
|
QLabel#fieldLabel {{
|
|
color: {C.TEXT_SECONDARY};
|
|
font-size: 12px;
|
|
background: transparent;
|
|
}}
|
|
|
|
/* Body/code editors */
|
|
QTextEdit#codeEditor {{
|
|
background-color: {C.BG_PANEL};
|
|
color: {C.TEXT_PRIMARY};
|
|
border: none;
|
|
padding: 8px;
|
|
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
|
|
font-size: 11px;
|
|
}}
|
|
|
|
/* Loading overlay */
|
|
QWidget#loadingOverlay {{
|
|
background-color: {C.BG_PANEL};
|
|
}}
|
|
QLabel#loadingLabel {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 13px;
|
|
background: transparent;
|
|
}}
|
|
|
|
/* Search in response bar */
|
|
QLineEdit#searchBar {{
|
|
background: {C.BG_INPUT};
|
|
border: 1px solid {C.BORDER};
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 12px;
|
|
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-size: 12px;
|
|
color: {C.TEXT_PRIMARY};
|
|
}}
|
|
QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }}
|
|
|
|
/* WebSocket / Mock status indicator labels */
|
|
QLabel#statusOk {{
|
|
color: {C.SUCCESS};
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
background: transparent;
|
|
}}
|
|
QLabel#statusWarn {{
|
|
color: {C.WARNING};
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
background: transparent;
|
|
}}
|
|
QLabel#statusErr {{
|
|
color: {C.ERROR};
|
|
font-size: 12px;
|
|
font-weight: 600;
|
|
background: transparent;
|
|
}}
|
|
|
|
/* Auth "none" hint */
|
|
QLabel#authNone {{
|
|
color: {C.TEXT_MUTED};
|
|
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: 14px;
|
|
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-size: 12px;
|
|
}}
|
|
QLabel#aiStatusLabel {{
|
|
color: {C.TEXT_MUTED};
|
|
font-size: 11px;
|
|
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-size: 13px;
|
|
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-size: 10px;
|
|
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-size: 12px;
|
|
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-size: 12px;
|
|
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-size: 11px;
|
|
color: {C.TEXT_SECONDARY};
|
|
font-weight: 500;
|
|
}}
|
|
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-size: 10px;
|
|
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
|
|
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 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)
|