Initial release — APIClient - Agent v2.0.0
AI-first API testing desktop client built with Python + PyQt6. Features: - Multi-tab HTTP request editor with params/headers/body/auth/tests - KeyValueTable with per-row enable/disable checkboxes and 36px rows - Format JSON button, syntax highlighting, pre-request & test scripts - Collections, environments, history, import/export (Postman v2.1, cURL) - OpenAPI 3.x / Swagger 2.0 local parser (no AI tokens) - EKIKA Odoo API Framework generator — JSON-API, REST JSON, GraphQL, Custom REST JSON with all auth types (instant, no AI tokens) - Persistent AI chat sidebar (Claude-powered co-pilot) with streaming, context-aware suggestions, and one-click Apply to request editor - AI collection generator from any docs URL or pasted spec - WebSocket client, Mock server, Collection runner, Code generator - Dark/light theme engine (global QSS, object-name selectors) - SSL error detection with actionable hints - MIT License Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
981
app/ui/theme.py
Normal file
981
app/ui/theme.py
Normal file
@@ -0,0 +1,981 @@
|
||||
"""
|
||||
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)
|
||||
Reference in New Issue
Block a user