UI Enhancements.
This commit is contained in:
@@ -3,6 +3,8 @@
|
||||
> **AI-first API testing desktop client** - built with Python + PyQt6.
|
||||
> Specialised for the [EKIKA Odoo API Framework](https://apps.odoo.com/apps/modules/19.0/api_framework), but works with any REST, GraphQL, or WebSocket API.
|
||||
|
||||

|
||||
|
||||
---
|
||||
|
||||
## Features
|
||||
|
||||
24
app/core/fonts.py
Normal file
24
app/core/fonts.py
Normal file
@@ -0,0 +1,24 @@
|
||||
"""Font loader - registers bundled Quicksand and Open Sans with Qt."""
|
||||
import os
|
||||
from PyQt6.QtGui import QFontDatabase
|
||||
|
||||
_ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "assets", "fonts")
|
||||
|
||||
_FONT_FILES = [
|
||||
"Quicksand-Regular.ttf",
|
||||
"Quicksand-Medium.ttf",
|
||||
"Quicksand-SemiBold.ttf",
|
||||
"Quicksand-Bold.ttf",
|
||||
"OpenSans-Regular.ttf",
|
||||
"OpenSans-SemiBold.ttf",
|
||||
"OpenSans-Bold.ttf",
|
||||
]
|
||||
|
||||
|
||||
def load_fonts() -> None:
|
||||
"""Register all bundled fonts with QFontDatabase. Call once after QApplication is created."""
|
||||
fonts_dir = os.path.normpath(_ASSETS_DIR)
|
||||
for filename in _FONT_FILES:
|
||||
path = os.path.join(fonts_dir, filename)
|
||||
if os.path.exists(path):
|
||||
QFontDatabase.addApplicationFont(path)
|
||||
@@ -1,20 +1,23 @@
|
||||
"""APIClient - Agent - Main Window."""
|
||||
import os
|
||||
from PyQt6.QtWidgets import (
|
||||
QMainWindow, QSplitter, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QLabel, QComboBox, QPushButton, QStatusBar, QTabWidget,
|
||||
QInputDialog, QMessageBox, QFileDialog, QApplication
|
||||
)
|
||||
from PyQt6.QtCore import Qt, QThread, pyqtSignal
|
||||
from PyQt6.QtGui import QKeySequence, QShortcut
|
||||
from PyQt6.QtGui import QKeySequence, QShortcut, QPixmap
|
||||
|
||||
from app.ui.tabs_manager import TabsManager
|
||||
from app.ui.response_panel import ResponsePanel
|
||||
from app.ui.sidebar import CollectionsSidebar
|
||||
from app.ui.theme import Colors, toggle as toggle_theme, is_dark
|
||||
from app.ui.theme import Colors, toggle as toggle_theme, is_dark, zoom_in, zoom_out, zoom_reset, get_zoom
|
||||
from app.core import http_client, storage
|
||||
from app.core.test_runner import run_tests
|
||||
from app.models import HttpRequest
|
||||
|
||||
_ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "assets")
|
||||
|
||||
APP_VERSION = "2.0.0"
|
||||
APP_NAME = "APIClient - Agent"
|
||||
|
||||
@@ -50,22 +53,62 @@ class EnvBar(QWidget):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("envBar")
|
||||
self.setFixedHeight(46)
|
||||
self.setFixedHeight(48)
|
||||
|
||||
layout = QHBoxLayout(self)
|
||||
layout.setContentsMargins(16, 0, 16, 0)
|
||||
layout.setContentsMargins(12, 0, 12, 0)
|
||||
layout.setSpacing(6)
|
||||
|
||||
# EKIKA logo
|
||||
logo_path = os.path.join(_ASSETS_DIR, "ekika_logo.png")
|
||||
if os.path.exists(logo_path):
|
||||
logo_lbl = QLabel()
|
||||
logo_lbl.setObjectName("ekikaLogo")
|
||||
pix = QPixmap(logo_path)
|
||||
logo_lbl.setPixmap(pix.scaledToHeight(26, Qt.TransformationMode.SmoothTransformation))
|
||||
logo_lbl.setToolTip("EKIKA")
|
||||
layout.addWidget(logo_lbl)
|
||||
# Thin separator
|
||||
sep = QLabel("|")
|
||||
sep.setObjectName("brandSub")
|
||||
sep.setFixedWidth(12)
|
||||
sep.setAlignment(Qt.AlignmentFlag.AlignCenter)
|
||||
layout.addWidget(sep)
|
||||
|
||||
brand = QLabel("APIClient")
|
||||
brand.setObjectName("brandName")
|
||||
|
||||
sub = QLabel("Agent")
|
||||
sub = QLabel("- Agent")
|
||||
sub.setObjectName("brandSub")
|
||||
|
||||
layout.addWidget(brand)
|
||||
layout.addWidget(sub)
|
||||
layout.addStretch()
|
||||
|
||||
# Zoom controls
|
||||
zoom_out_btn = QPushButton("-")
|
||||
zoom_out_btn.setObjectName("zoomBtn")
|
||||
zoom_out_btn.setFixedSize(24, 24)
|
||||
zoom_out_btn.setToolTip("Zoom Out (Ctrl+-)")
|
||||
layout.addWidget(zoom_out_btn)
|
||||
self.zoom_out_btn = zoom_out_btn
|
||||
|
||||
self.zoom_label = QLabel("100%")
|
||||
self.zoom_label.setObjectName("zoomLabel")
|
||||
self.zoom_label.setFixedWidth(42)
|
||||
self.zoom_label.setToolTip("Current zoom level (Ctrl+0 to reset)")
|
||||
layout.addWidget(self.zoom_label)
|
||||
|
||||
zoom_in_btn = QPushButton("+")
|
||||
zoom_in_btn.setObjectName("zoomBtn")
|
||||
zoom_in_btn.setFixedSize(24, 24)
|
||||
zoom_in_btn.setToolTip("Zoom In (Ctrl+=)")
|
||||
layout.addWidget(zoom_in_btn)
|
||||
self.zoom_in_btn = zoom_in_btn
|
||||
|
||||
# Thin gap
|
||||
layout.addSpacing(8)
|
||||
|
||||
env_label = QLabel("ENV")
|
||||
env_label.setObjectName("envChip")
|
||||
layout.addWidget(env_label)
|
||||
@@ -119,6 +162,8 @@ class MainWindow(QMainWindow):
|
||||
self.env_bar.manage_btn.clicked.connect(self._open_env_dialog)
|
||||
self.env_bar.theme_btn.clicked.connect(self._toggle_theme)
|
||||
self.env_bar.ai_btn.clicked.connect(self._toggle_ai_chat)
|
||||
self.env_bar.zoom_in_btn.clicked.connect(self._zoom_in)
|
||||
self.env_bar.zoom_out_btn.clicked.connect(self._zoom_out)
|
||||
root_layout.addWidget(self.env_bar)
|
||||
|
||||
splitter = QSplitter(Qt.Orientation.Horizontal)
|
||||
@@ -203,6 +248,10 @@ class MainWindow(QMainWindow):
|
||||
view_m = mb.addMenu("View")
|
||||
view_m.addAction("Search Requests", self._open_search).setShortcut("Ctrl+F")
|
||||
view_m.addAction("Toggle Theme", self._toggle_theme)
|
||||
view_m.addSeparator()
|
||||
view_m.addAction("Zoom In", self._zoom_in).setShortcut("Ctrl+=")
|
||||
view_m.addAction("Zoom Out", self._zoom_out).setShortcut("Ctrl+-")
|
||||
view_m.addAction("Reset Zoom", self._zoom_reset).setShortcut("Ctrl+0")
|
||||
|
||||
tools_m = mb.addMenu("Tools")
|
||||
tools_m.addAction("Environments…", self._open_env_dialog).setShortcut("Ctrl+E")
|
||||
@@ -227,10 +276,14 @@ class MainWindow(QMainWindow):
|
||||
QShortcut(QKeySequence("Ctrl+W"), self, self.tabs_manager.close_current_tab)
|
||||
QShortcut(QKeySequence("Ctrl+S"), self, self._save_to_collection)
|
||||
QShortcut(QKeySequence("Ctrl+F"), self, self._open_search)
|
||||
QShortcut(QKeySequence("Ctrl+E"), self, self._open_env_dialog)
|
||||
QShortcut(QKeySequence("Ctrl+Shift+A"), self, self._toggle_ai_chat)
|
||||
QShortcut(QKeySequence("Ctrl+E"), self, self._open_env_dialog)
|
||||
QShortcut(QKeySequence("Ctrl+Shift+A"), self, self._toggle_ai_chat)
|
||||
QShortcut(QKeySequence("Escape"), self, self._cancel_request)
|
||||
QShortcut(QKeySequence("Ctrl+Q"), self, self.close)
|
||||
QShortcut(QKeySequence("Ctrl+="), self, self._zoom_in)
|
||||
QShortcut(QKeySequence("Ctrl++"), self, self._zoom_in)
|
||||
QShortcut(QKeySequence("Ctrl+-"), self, self._zoom_out)
|
||||
QShortcut(QKeySequence("Ctrl+0"), self, self._zoom_reset)
|
||||
|
||||
# ── Environment ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -443,6 +496,23 @@ class MainWindow(QMainWindow):
|
||||
elif atype == "test":
|
||||
rp.apply_test_script(content)
|
||||
|
||||
# ── Zoom ──────────────────────────────────────────────────────────────────
|
||||
|
||||
def _zoom_in(self):
|
||||
level = zoom_in(QApplication.instance())
|
||||
self._update_zoom_label(level)
|
||||
|
||||
def _zoom_out(self):
|
||||
level = zoom_out(QApplication.instance())
|
||||
self._update_zoom_label(level)
|
||||
|
||||
def _zoom_reset(self):
|
||||
level = zoom_reset(QApplication.instance())
|
||||
self._update_zoom_label(level)
|
||||
|
||||
def _update_zoom_label(self, level: float):
|
||||
self.env_bar.zoom_label.setText(f"{round(level * 100)}%")
|
||||
|
||||
# ── Theme ─────────────────────────────────────────────────────────────────
|
||||
|
||||
def _toggle_theme(self):
|
||||
|
||||
238
app/ui/theme.py
238
app/ui/theme.py
@@ -3,6 +3,8 @@ 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
|
||||
@@ -100,6 +102,17 @@ class LightColors:
|
||||
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 {
|
||||
@@ -121,8 +134,12 @@ def status_color(code: int) -> str:
|
||||
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 ─────────────────────────────────────────────────────────
|
||||
# Everything static lives here. Object names are the API between theme and UI.
|
||||
|
||||
def _build_stylesheet(C) -> str:
|
||||
return f"""
|
||||
@@ -133,8 +150,8 @@ def _build_stylesheet(C) -> str:
|
||||
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;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(13)};
|
||||
border: none;
|
||||
outline: none;
|
||||
}}
|
||||
@@ -178,6 +195,9 @@ QMenuBar {{
|
||||
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;
|
||||
@@ -191,6 +211,8 @@ QMenu {{
|
||||
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}; }}
|
||||
@@ -206,6 +228,8 @@ QLineEdit {{
|
||||
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 {{
|
||||
@@ -224,6 +248,8 @@ QTextEdit, QPlainTextEdit {{
|
||||
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 {{
|
||||
@@ -236,6 +262,8 @@ QSpinBox {{
|
||||
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 {{
|
||||
@@ -257,6 +285,8 @@ QComboBox {{
|
||||
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}; }}
|
||||
@@ -270,6 +300,8 @@ QComboBox QAbstractItemView {{
|
||||
selection-background-color: {C.BG_SELECTED};
|
||||
outline: none;
|
||||
padding: 2px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(13)};
|
||||
}}
|
||||
|
||||
/* ════════════════════════════════════════════════════════
|
||||
@@ -279,6 +311,8 @@ QCheckBox {{
|
||||
color: {C.TEXT_SECONDARY};
|
||||
spacing: 6px;
|
||||
background: transparent;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
}}
|
||||
QCheckBox::indicator {{
|
||||
width: 14px; height: 14px;
|
||||
@@ -299,7 +333,9 @@ QPushButton {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 5px;
|
||||
padding: 6px 14px;
|
||||
font-weight: 500;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(12)};
|
||||
font-weight: 600;
|
||||
}}
|
||||
QPushButton:hover {{
|
||||
background-color: {C.BG_HOVER};
|
||||
@@ -312,7 +348,9 @@ QPushButton#accent {{
|
||||
background-color: {C.ACCENT};
|
||||
color: #FFFFFF;
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
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}; }}
|
||||
@@ -327,6 +365,9 @@ QPushButton#ghost {{
|
||||
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};
|
||||
@@ -338,6 +379,9 @@ 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}; }}
|
||||
|
||||
@@ -347,8 +391,9 @@ QPushButton#sendBtn {{
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
padding: 8px 22px;
|
||||
font-family: {_UI_FONT};
|
||||
font-weight: 700;
|
||||
font-size: 13px;
|
||||
font-size: {_z(13)};
|
||||
letter-spacing: 0.3px;
|
||||
}}
|
||||
QPushButton#sendBtn:hover {{ background-color: {C.ACCENT_HOVER}; }}
|
||||
@@ -358,6 +403,22 @@ QPushButton#sendBtn:disabled {{
|
||||
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
|
||||
════════════════════════════════════════════════════════ */
|
||||
@@ -372,8 +433,9 @@ QTabBar::tab {{
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
padding: 8px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(12)};
|
||||
font-weight: 600;
|
||||
}}
|
||||
QTabBar::tab:selected {{
|
||||
color: {C.TEXT_PRIMARY};
|
||||
@@ -406,8 +468,9 @@ QTabWidget#workspaceTabs QTabBar::tab {{
|
||||
border: none;
|
||||
border-right: 1px solid {C.BORDER};
|
||||
padding: 10px 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(12)};
|
||||
font-weight: 700;
|
||||
border-bottom: none;
|
||||
border-top: 2px solid transparent;
|
||||
}}
|
||||
@@ -432,6 +495,8 @@ QTableWidget {{
|
||||
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;
|
||||
@@ -448,8 +513,9 @@ QHeaderView::section {{
|
||||
border-bottom: 1px solid {C.BORDER};
|
||||
border-right: 1px solid {C.BORDER};
|
||||
padding: 6px 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(11)};
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}}
|
||||
@@ -464,6 +530,8 @@ QTreeWidget {{
|
||||
border: none;
|
||||
outline: none;
|
||||
show-decoration-selected: 1;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
}}
|
||||
QTreeWidget::item {{
|
||||
padding: 4px 4px;
|
||||
@@ -485,6 +553,8 @@ QListWidget {{
|
||||
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 {{
|
||||
@@ -504,7 +574,8 @@ QListWidget#sidebarList::item {{
|
||||
padding: 10px 14px;
|
||||
border-bottom: 1px solid {C.BORDER};
|
||||
border-radius: 0;
|
||||
font-size: 13px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
}}
|
||||
QListWidget#sidebarList::item:selected {{
|
||||
background: {C.BG_SELECTED}; color: {C.TEXT_PRIMARY};
|
||||
@@ -518,7 +589,8 @@ QStatusBar {{
|
||||
background-color: {C.BG_DARKEST};
|
||||
color: {C.TEXT_SECONDARY};
|
||||
border-top: 1px solid {C.BORDER};
|
||||
font-size: 11px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(11)};
|
||||
padding: 0 8px;
|
||||
}}
|
||||
QStatusBar::item {{ border: none; }}
|
||||
@@ -547,8 +619,9 @@ QGroupBox {{
|
||||
border-radius: 6px;
|
||||
margin-top: 8px;
|
||||
padding: 8px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(11)};
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}}
|
||||
@@ -565,7 +638,8 @@ QToolTip {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
}}
|
||||
|
||||
/* ════════════════════════════════════════════════════════
|
||||
@@ -584,7 +658,7 @@ QFrame[frameShape="4"], QFrame[frameShape="5"] {{
|
||||
}}
|
||||
|
||||
/* ════════════════════════════════════════════════════════
|
||||
── NAMED WIDGET RULES (setObjectName API) ──
|
||||
-- NAMED WIDGET RULES (setObjectName API) --
|
||||
════════════════════════════════════════════════════════ */
|
||||
|
||||
/* Top brand / env bar */
|
||||
@@ -594,24 +668,36 @@ QWidget#envBar {{
|
||||
}}
|
||||
QLabel#brandName {{
|
||||
color: {C.ACCENT};
|
||||
font-size: 15px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(15)};
|
||||
font-weight: 800;
|
||||
letter-spacing: 2px;
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#brandSub {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(11)};
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#envChip {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 10px;
|
||||
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 {{
|
||||
@@ -636,8 +722,8 @@ QLineEdit#urlBar {{
|
||||
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;
|
||||
font-family: {_MONO_FONT};
|
||||
font-size: {_z(13)};
|
||||
color: {C.TEXT_PRIMARY};
|
||||
}}
|
||||
QLineEdit#urlBar:focus {{
|
||||
@@ -647,8 +733,9 @@ QLineEdit#urlBar:focus {{
|
||||
|
||||
/* Method combo (color set inline per method, only layout here) */
|
||||
QComboBox#methodCombo {{
|
||||
font-family: {_UI_FONT};
|
||||
font-weight: 800;
|
||||
font-size: 12px;
|
||||
font-size: {_z(12)};
|
||||
border-radius: 6px;
|
||||
padding: 8px 10px;
|
||||
min-width: 100px;
|
||||
@@ -677,14 +764,16 @@ QWidget#responseBar {{
|
||||
}}
|
||||
QLabel#responseTitle {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 10px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(10)};
|
||||
font-weight: 700;
|
||||
letter-spacing: 1.2px;
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#metaLabel {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 11px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(11)};
|
||||
background: transparent;
|
||||
padding: 0 6px;
|
||||
}}
|
||||
@@ -708,26 +797,30 @@ QWidget#panelBody {{
|
||||
|
||||
/* Labels inside panels */
|
||||
QLabel#panelTitle {{
|
||||
font-size: 14px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(14)};
|
||||
font-weight: 700;
|
||||
color: {C.TEXT_PRIMARY};
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#sectionLabel {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 10px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(10)};
|
||||
font-weight: 700;
|
||||
letter-spacing: 1px;
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#hintText {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 11px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(11)};
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#fieldLabel {{
|
||||
color: {C.TEXT_SECONDARY};
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
background: transparent;
|
||||
}}
|
||||
|
||||
@@ -737,8 +830,8 @@ QTextEdit#codeEditor {{
|
||||
color: {C.TEXT_PRIMARY};
|
||||
border: none;
|
||||
padding: 8px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
|
||||
font-size: 11px;
|
||||
font-family: {_MONO_FONT};
|
||||
font-size: {_z(11)};
|
||||
}}
|
||||
|
||||
/* Loading overlay */
|
||||
@@ -747,7 +840,8 @@ QWidget#loadingOverlay {{
|
||||
}}
|
||||
QLabel#loadingLabel {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 13px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(13)};
|
||||
background: transparent;
|
||||
}}
|
||||
|
||||
@@ -757,7 +851,8 @@ QLineEdit#searchBar {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 4px 8px;
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
color: {C.TEXT_PRIMARY};
|
||||
}}
|
||||
QLineEdit#searchBar:focus {{ border-color: {C.BORDER_FOCUS}; }}
|
||||
@@ -768,7 +863,8 @@ QLineEdit#filterInput {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 4px;
|
||||
padding: 5px 8px;
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
color: {C.TEXT_PRIMARY};
|
||||
}}
|
||||
QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }}
|
||||
@@ -776,19 +872,22 @@ QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }}
|
||||
/* WebSocket / Mock status indicator labels */
|
||||
QLabel#statusOk {{
|
||||
color: {C.SUCCESS};
|
||||
font-size: 12px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(12)};
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#statusWarn {{
|
||||
color: {C.WARNING};
|
||||
font-size: 12px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(12)};
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
}}
|
||||
QLabel#statusErr {{
|
||||
color: {C.ERROR};
|
||||
font-size: 12px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(12)};
|
||||
font-weight: 600;
|
||||
background: transparent;
|
||||
}}
|
||||
@@ -796,6 +895,8 @@ QLabel#statusErr {{
|
||||
/* Auth "none" hint */
|
||||
QLabel#authNone {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
padding: 12px;
|
||||
background: transparent;
|
||||
}}
|
||||
@@ -811,7 +912,7 @@ QPushButton#tabCloseBtn {{
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 14px;
|
||||
font-size: {_z(14)};
|
||||
font-weight: 700;
|
||||
padding: 0;
|
||||
}}
|
||||
@@ -830,15 +931,17 @@ QTextEdit#aiOutput {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 5px;
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
}}
|
||||
QLabel#aiStatusLabel {{
|
||||
color: {C.TEXT_MUTED};
|
||||
font-size: 11px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(11)};
|
||||
background: transparent;
|
||||
}}
|
||||
|
||||
/* ── AI Chat Panel ─────────────────────────────────────── */
|
||||
/* -- AI Chat Panel ─────────────────────────────────────── */
|
||||
QWidget#aiChatPanel {{
|
||||
background-color: {C.BG_SIDEBAR};
|
||||
border-left: 1px solid {C.BORDER};
|
||||
@@ -849,7 +952,8 @@ QWidget#aiChatHeader {{
|
||||
}}
|
||||
QLabel#aiChatTitle {{
|
||||
color: {C.ACCENT};
|
||||
font-size: 13px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(13)};
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
background: transparent;
|
||||
@@ -871,7 +975,8 @@ QFrame#aiBubble {{
|
||||
margin: 0px;
|
||||
}}
|
||||
QLabel#chatRoleLabel {{
|
||||
font-size: 10px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(10)};
|
||||
font-weight: 700;
|
||||
color: {C.TEXT_MUTED};
|
||||
background: transparent;
|
||||
@@ -881,7 +986,8 @@ QLabel#chatRoleLabel {{
|
||||
QLabel#chatMessageText {{
|
||||
color: {C.TEXT_PRIMARY};
|
||||
background: transparent;
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
line-height: 1.6;
|
||||
}}
|
||||
QWidget#chatInputArea {{
|
||||
@@ -893,7 +999,8 @@ QTextEdit#chatInput {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-size: 12px;
|
||||
font-family: {_BODY_FONT};
|
||||
font-size: {_z(12)};
|
||||
color: {C.TEXT_PRIMARY};
|
||||
}}
|
||||
QTextEdit#chatInput:focus {{
|
||||
@@ -909,9 +1016,10 @@ QPushButton#qaBtn {{
|
||||
border: 1px solid {C.BORDER};
|
||||
border-radius: 10px;
|
||||
padding: 2px 8px;
|
||||
font-size: 11px;
|
||||
font-family: {_UI_FONT};
|
||||
font-size: {_z(11)};
|
||||
color: {C.TEXT_SECONDARY};
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
}}
|
||||
QPushButton#qaBtn:hover {{
|
||||
background-color: {C.BG_HOVER};
|
||||
@@ -928,8 +1036,8 @@ QTextEdit#applyCode {{
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
padding: 4px;
|
||||
font-size: 10px;
|
||||
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace;
|
||||
font-family: {_MONO_FONT};
|
||||
font-size: {_z(10)};
|
||||
color: {C.TEXT_SECONDARY};
|
||||
}}
|
||||
|
||||
@@ -974,6 +1082,34 @@ 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)
|
||||
|
||||
BIN
assets/app-ss.png
Normal file
BIN
assets/app-ss.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 137 KiB |
BIN
assets/ekika_logo.png
Normal file
BIN
assets/ekika_logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
BIN
assets/ekika_logo.webp
Normal file
BIN
assets/ekika_logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.1 KiB |
BIN
assets/fonts/OpenSans-Bold.ttf
Normal file
BIN
assets/fonts/OpenSans-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-Regular.ttf
Normal file
BIN
assets/fonts/OpenSans-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/OpenSans-SemiBold.ttf
Normal file
BIN
assets/fonts/OpenSans-SemiBold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Quicksand-Bold.ttf
Normal file
BIN
assets/fonts/Quicksand-Bold.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Quicksand-Medium.ttf
Normal file
BIN
assets/fonts/Quicksand-Medium.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Quicksand-Regular.ttf
Normal file
BIN
assets/fonts/Quicksand-Regular.ttf
Normal file
Binary file not shown.
BIN
assets/fonts/Quicksand-SemiBold.ttf
Normal file
BIN
assets/fonts/Quicksand-SemiBold.ttf
Normal file
Binary file not shown.
2
main.py
2
main.py
@@ -1,5 +1,6 @@
|
||||
import sys
|
||||
from PyQt6.QtWidgets import QApplication
|
||||
from app.core.fonts import load_fonts
|
||||
from app.ui.theme import apply
|
||||
from app.ui.main_window import MainWindow
|
||||
|
||||
@@ -11,6 +12,7 @@ if __name__ == "__main__":
|
||||
app.setApplicationName(APP_NAME)
|
||||
app.setApplicationVersion(APP_VERSION)
|
||||
app.setOrganizationName("EKIKA")
|
||||
load_fonts()
|
||||
apply(app, dark=True)
|
||||
window = MainWindow()
|
||||
window.show()
|
||||
|
||||
Reference in New Issue
Block a user