UI Enhancements.

This commit is contained in:
2026-03-28 18:01:49 +05:30
parent 79b120ff91
commit a90e3a0d84
15 changed files with 292 additions and 58 deletions

View File

@@ -3,6 +3,8 @@
> **AI-first API testing desktop client** - built with Python + PyQt6. > **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. > 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.
![alt text](assets/app-ss.png)
--- ---
## Features ## Features

24
app/core/fonts.py Normal file
View 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)

View File

@@ -1,20 +1,23 @@
"""APIClient - Agent - Main Window.""" """APIClient - Agent - Main Window."""
import os
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QMainWindow, QSplitter, QWidget, QVBoxLayout, QHBoxLayout, QMainWindow, QSplitter, QWidget, QVBoxLayout, QHBoxLayout,
QLabel, QComboBox, QPushButton, QStatusBar, QTabWidget, QLabel, QComboBox, QPushButton, QStatusBar, QTabWidget,
QInputDialog, QMessageBox, QFileDialog, QApplication QInputDialog, QMessageBox, QFileDialog, QApplication
) )
from PyQt6.QtCore import Qt, QThread, pyqtSignal 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.tabs_manager import TabsManager
from app.ui.response_panel import ResponsePanel from app.ui.response_panel import ResponsePanel
from app.ui.sidebar import CollectionsSidebar 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 import http_client, storage
from app.core.test_runner import run_tests from app.core.test_runner import run_tests
from app.models import HttpRequest from app.models import HttpRequest
_ASSETS_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "assets")
APP_VERSION = "2.0.0" APP_VERSION = "2.0.0"
APP_NAME = "APIClient - Agent" APP_NAME = "APIClient - Agent"
@@ -50,22 +53,62 @@ class EnvBar(QWidget):
def __init__(self, parent=None): def __init__(self, parent=None):
super().__init__(parent) super().__init__(parent)
self.setObjectName("envBar") self.setObjectName("envBar")
self.setFixedHeight(46) self.setFixedHeight(48)
layout = QHBoxLayout(self) layout = QHBoxLayout(self)
layout.setContentsMargins(16, 0, 16, 0) layout.setContentsMargins(12, 0, 12, 0)
layout.setSpacing(6) 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 = QLabel("APIClient")
brand.setObjectName("brandName") brand.setObjectName("brandName")
sub = QLabel("Agent") sub = QLabel("- Agent")
sub.setObjectName("brandSub") sub.setObjectName("brandSub")
layout.addWidget(brand) layout.addWidget(brand)
layout.addWidget(sub) layout.addWidget(sub)
layout.addStretch() 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 = QLabel("ENV")
env_label.setObjectName("envChip") env_label.setObjectName("envChip")
layout.addWidget(env_label) 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.manage_btn.clicked.connect(self._open_env_dialog)
self.env_bar.theme_btn.clicked.connect(self._toggle_theme) self.env_bar.theme_btn.clicked.connect(self._toggle_theme)
self.env_bar.ai_btn.clicked.connect(self._toggle_ai_chat) 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) root_layout.addWidget(self.env_bar)
splitter = QSplitter(Qt.Orientation.Horizontal) splitter = QSplitter(Qt.Orientation.Horizontal)
@@ -203,6 +248,10 @@ class MainWindow(QMainWindow):
view_m = mb.addMenu("View") view_m = mb.addMenu("View")
view_m.addAction("Search Requests", self._open_search).setShortcut("Ctrl+F") view_m.addAction("Search Requests", self._open_search).setShortcut("Ctrl+F")
view_m.addAction("Toggle Theme", self._toggle_theme) 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 = mb.addMenu("Tools")
tools_m.addAction("Environments…", self._open_env_dialog).setShortcut("Ctrl+E") tools_m.addAction("Environments…", self._open_env_dialog).setShortcut("Ctrl+E")
@@ -231,6 +280,10 @@ class MainWindow(QMainWindow):
QShortcut(QKeySequence("Ctrl+Shift+A"), self, self._toggle_ai_chat) QShortcut(QKeySequence("Ctrl+Shift+A"), self, self._toggle_ai_chat)
QShortcut(QKeySequence("Escape"), self, self._cancel_request) QShortcut(QKeySequence("Escape"), self, self._cancel_request)
QShortcut(QKeySequence("Ctrl+Q"), self, self.close) 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 ─────────────────────────────────────────────────────────── # ── Environment ───────────────────────────────────────────────────────────
@@ -443,6 +496,23 @@ class MainWindow(QMainWindow):
elif atype == "test": elif atype == "test":
rp.apply_test_script(content) 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 ───────────────────────────────────────────────────────────────── # ── Theme ─────────────────────────────────────────────────────────────────
def _toggle_theme(self): def _toggle_theme(self):

View File

@@ -3,6 +3,8 @@ APIClient - Agent - Central Theme Engine
All styling lives here in the global QSS. All styling lives here in the global QSS.
UI widgets use setObjectName() selectors - never inline setStyleSheet() for static colors. UI widgets use setObjectName() selectors - never inline setStyleSheet() for static colors.
Only truly dynamic values (per-request method color, status badge) stay inline. 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.QtGui import QColor, QPalette
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
@@ -100,6 +102,17 @@ class LightColors:
Colors = DarkColors Colors = DarkColors
_is_dark = True _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: def method_color(method: str) -> str:
return { return {
@@ -121,8 +134,12 @@ def status_color(code: int) -> str:
return Colors.STATUS_5XX 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 ───────────────────────────────────────────────────────── # ── Global Stylesheet ─────────────────────────────────────────────────────────
# Everything static lives here. Object names are the API between theme and UI.
def _build_stylesheet(C) -> str: def _build_stylesheet(C) -> str:
return f""" return f"""
@@ -133,8 +150,8 @@ def _build_stylesheet(C) -> str:
QWidget {{ QWidget {{
background-color: {C.BG_MAIN}; background-color: {C.BG_MAIN};
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
font-family: "Segoe UI", "SF Pro Text", "Inter", "Helvetica Neue", sans-serif; font-family: {_BODY_FONT};
font-size: 13px; font-size: {_z(13)};
border: none; border: none;
outline: none; outline: none;
}} }}
@@ -178,6 +195,9 @@ QMenuBar {{
border-bottom: 1px solid {C.BORDER}; border-bottom: 1px solid {C.BORDER};
padding: 2px 4px; padding: 2px 4px;
spacing: 4px; spacing: 4px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}} }}
QMenuBar::item {{ QMenuBar::item {{
background: transparent; padding: 4px 10px; border-radius: 4px; background: transparent; padding: 4px 10px; border-radius: 4px;
@@ -191,6 +211,8 @@ QMenu {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 6px; border-radius: 6px;
padding: 4px; padding: 4px;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QMenu::item {{ padding: 7px 28px 7px 12px; border-radius: 4px; }} QMenu::item {{ padding: 7px 28px 7px 12px; border-radius: 4px; }}
QMenu::item:selected {{ background-color: {C.BG_HOVER}; }} QMenu::item:selected {{ background-color: {C.BG_HOVER}; }}
@@ -206,6 +228,8 @@ QLineEdit {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 5px; border-radius: 5px;
padding: 6px 10px; padding: 6px 10px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
selection-background-color: {C.ACCENT}; selection-background-color: {C.ACCENT};
}} }}
QLineEdit:focus {{ QLineEdit:focus {{
@@ -224,6 +248,8 @@ QTextEdit, QPlainTextEdit {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 5px; border-radius: 5px;
padding: 6px; padding: 6px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
selection-background-color: {C.ACCENT}; selection-background-color: {C.ACCENT};
}} }}
QTextEdit:focus, QPlainTextEdit:focus {{ QTextEdit:focus, QPlainTextEdit:focus {{
@@ -236,6 +262,8 @@ QSpinBox {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 5px; border-radius: 5px;
padding: 5px 8px; padding: 5px 8px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
}} }}
QSpinBox:focus {{ border-color: {C.BORDER_FOCUS}; }} QSpinBox:focus {{ border-color: {C.BORDER_FOCUS}; }}
QSpinBox::up-button, QSpinBox::down-button {{ QSpinBox::up-button, QSpinBox::down-button {{
@@ -257,6 +285,8 @@ QComboBox {{
border-radius: 5px; border-radius: 5px;
padding: 5px 10px; padding: 5px 10px;
min-width: 80px; min-width: 80px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
}} }}
QComboBox:hover {{ border-color: {C.BORDER_FOCUS}; }} QComboBox:hover {{ border-color: {C.BORDER_FOCUS}; }}
QComboBox:focus {{ border-color: {C.BORDER_FOCUS}; }} QComboBox:focus {{ border-color: {C.BORDER_FOCUS}; }}
@@ -270,6 +300,8 @@ QComboBox QAbstractItemView {{
selection-background-color: {C.BG_SELECTED}; selection-background-color: {C.BG_SELECTED};
outline: none; outline: none;
padding: 2px; padding: 2px;
font-family: {_BODY_FONT};
font-size: {_z(13)};
}} }}
/* ════════════════════════════════════════════════════════ /* ════════════════════════════════════════════════════════
@@ -279,6 +311,8 @@ QCheckBox {{
color: {C.TEXT_SECONDARY}; color: {C.TEXT_SECONDARY};
spacing: 6px; spacing: 6px;
background: transparent; background: transparent;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QCheckBox::indicator {{ QCheckBox::indicator {{
width: 14px; height: 14px; width: 14px; height: 14px;
@@ -299,7 +333,9 @@ QPushButton {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 5px; border-radius: 5px;
padding: 6px 14px; padding: 6px 14px;
font-weight: 500; font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}} }}
QPushButton:hover {{ QPushButton:hover {{
background-color: {C.BG_HOVER}; background-color: {C.BG_HOVER};
@@ -312,7 +348,9 @@ QPushButton#accent {{
background-color: {C.ACCENT}; background-color: {C.ACCENT};
color: #FFFFFF; color: #FFFFFF;
border: none; 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:hover {{ background-color: {C.ACCENT_HOVER}; }}
QPushButton#accent:pressed {{ background-color: {C.ACCENT_PRESSED}; }} QPushButton#accent:pressed {{ background-color: {C.ACCENT_PRESSED}; }}
@@ -327,6 +365,9 @@ QPushButton#ghost {{
color: {C.TEXT_SECONDARY}; color: {C.TEXT_SECONDARY};
padding: 4px 8px; padding: 4px 8px;
border-radius: 4px; border-radius: 4px;
font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600;
}} }}
QPushButton#ghost:hover {{ QPushButton#ghost:hover {{
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
@@ -338,6 +379,9 @@ QPushButton#danger {{
background-color: transparent; background-color: transparent;
color: {C.ERROR}; color: {C.ERROR};
border: 1px solid {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#danger:hover {{ background-color: {C.ACCENT_SUBTLE}; }}
@@ -347,8 +391,9 @@ QPushButton#sendBtn {{
border: none; border: none;
border-radius: 6px; border-radius: 6px;
padding: 8px 22px; padding: 8px 22px;
font-family: {_UI_FONT};
font-weight: 700; font-weight: 700;
font-size: 13px; font-size: {_z(13)};
letter-spacing: 0.3px; letter-spacing: 0.3px;
}} }}
QPushButton#sendBtn:hover {{ background-color: {C.ACCENT_HOVER}; }} QPushButton#sendBtn:hover {{ background-color: {C.ACCENT_HOVER}; }}
@@ -358,6 +403,22 @@ QPushButton#sendBtn:disabled {{
color: {C.TEXT_MUTED}; 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 TABS
════════════════════════════════════════════════════════ */ ════════════════════════════════════════════════════════ */
@@ -372,8 +433,9 @@ QTabBar::tab {{
border: none; border: none;
border-bottom: 2px solid transparent; border-bottom: 2px solid transparent;
padding: 8px 16px; padding: 8px 16px;
font-size: 12px; font-family: {_UI_FONT};
font-weight: 500; font-size: {_z(12)};
font-weight: 600;
}} }}
QTabBar::tab:selected {{ QTabBar::tab:selected {{
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
@@ -406,8 +468,9 @@ QTabWidget#workspaceTabs QTabBar::tab {{
border: none; border: none;
border-right: 1px solid {C.BORDER}; border-right: 1px solid {C.BORDER};
padding: 10px 20px; padding: 10px 20px;
font-size: 12px; font-family: {_UI_FONT};
font-weight: 600; font-size: {_z(12)};
font-weight: 700;
border-bottom: none; border-bottom: none;
border-top: 2px solid transparent; border-top: 2px solid transparent;
}} }}
@@ -432,6 +495,8 @@ QTableWidget {{
gridline-color: {C.BORDER}; gridline-color: {C.BORDER};
selection-background-color: {C.BG_SELECTED}; selection-background-color: {C.BG_SELECTED};
selection-color: {C.TEXT_PRIMARY}; selection-color: {C.TEXT_PRIMARY};
font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QTableWidget::item {{ QTableWidget::item {{
padding: 5px 8px; padding: 5px 8px;
@@ -448,8 +513,9 @@ QHeaderView::section {{
border-bottom: 1px solid {C.BORDER}; border-bottom: 1px solid {C.BORDER};
border-right: 1px solid {C.BORDER}; border-right: 1px solid {C.BORDER};
padding: 6px 8px; padding: 6px 8px;
font-size: 11px; font-family: {_UI_FONT};
font-weight: 600; font-size: {_z(11)};
font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
}} }}
@@ -464,6 +530,8 @@ QTreeWidget {{
border: none; border: none;
outline: none; outline: none;
show-decoration-selected: 1; show-decoration-selected: 1;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QTreeWidget::item {{ QTreeWidget::item {{
padding: 4px 4px; padding: 4px 4px;
@@ -485,6 +553,8 @@ QListWidget {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 5px; border-radius: 5px;
outline: none; outline: none;
font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QListWidget::item {{ padding: 8px 10px; border-radius: 3px; }} QListWidget::item {{ padding: 8px 10px; border-radius: 3px; }}
QListWidget::item:selected {{ QListWidget::item:selected {{
@@ -504,7 +574,8 @@ QListWidget#sidebarList::item {{
padding: 10px 14px; padding: 10px 14px;
border-bottom: 1px solid {C.BORDER}; border-bottom: 1px solid {C.BORDER};
border-radius: 0; border-radius: 0;
font-size: 13px; font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QListWidget#sidebarList::item:selected {{ QListWidget#sidebarList::item:selected {{
background: {C.BG_SELECTED}; color: {C.TEXT_PRIMARY}; background: {C.BG_SELECTED}; color: {C.TEXT_PRIMARY};
@@ -518,7 +589,8 @@ QStatusBar {{
background-color: {C.BG_DARKEST}; background-color: {C.BG_DARKEST};
color: {C.TEXT_SECONDARY}; color: {C.TEXT_SECONDARY};
border-top: 1px solid {C.BORDER}; border-top: 1px solid {C.BORDER};
font-size: 11px; font-family: {_BODY_FONT};
font-size: {_z(11)};
padding: 0 8px; padding: 0 8px;
}} }}
QStatusBar::item {{ border: none; }} QStatusBar::item {{ border: none; }}
@@ -547,8 +619,9 @@ QGroupBox {{
border-radius: 6px; border-radius: 6px;
margin-top: 8px; margin-top: 8px;
padding: 8px; padding: 8px;
font-size: 11px; font-family: {_UI_FONT};
font-weight: 600; font-size: {_z(11)};
font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.5px; letter-spacing: 0.5px;
}} }}
@@ -565,7 +638,8 @@ QToolTip {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 4px; border-radius: 4px;
padding: 5px 8px; 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 */ /* Top brand / env bar */
@@ -594,24 +668,36 @@ QWidget#envBar {{
}} }}
QLabel#brandName {{ QLabel#brandName {{
color: {C.ACCENT}; color: {C.ACCENT};
font-size: 15px; font-family: {_UI_FONT};
font-size: {_z(15)};
font-weight: 800; font-weight: 800;
letter-spacing: 2px; letter-spacing: 2px;
background: transparent; background: transparent;
}} }}
QLabel#brandSub {{ QLabel#brandSub {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 11px; font-family: {_UI_FONT};
font-weight: 500; font-size: {_z(11)};
font-weight: 600;
background: transparent; background: transparent;
}} }}
QLabel#envChip {{ QLabel#envChip {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 10px; font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700; font-weight: 700;
letter-spacing: 1px; letter-spacing: 1px;
background: transparent; 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 */ /* Sidebar */
QWidget#sidebar {{ QWidget#sidebar {{
@@ -636,8 +722,8 @@ QLineEdit#urlBar {{
border: 1.5px solid {C.BORDER}; border: 1.5px solid {C.BORDER};
border-radius: 6px; border-radius: 6px;
padding: 8px 12px; padding: 8px 12px;
font-size: 13px; font-family: {_MONO_FONT};
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace; font-size: {_z(13)};
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
}} }}
QLineEdit#urlBar:focus {{ QLineEdit#urlBar:focus {{
@@ -647,8 +733,9 @@ QLineEdit#urlBar:focus {{
/* Method combo (color set inline per method, only layout here) */ /* Method combo (color set inline per method, only layout here) */
QComboBox#methodCombo {{ QComboBox#methodCombo {{
font-family: {_UI_FONT};
font-weight: 800; font-weight: 800;
font-size: 12px; font-size: {_z(12)};
border-radius: 6px; border-radius: 6px;
padding: 8px 10px; padding: 8px 10px;
min-width: 100px; min-width: 100px;
@@ -677,14 +764,16 @@ QWidget#responseBar {{
}} }}
QLabel#responseTitle {{ QLabel#responseTitle {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 10px; font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700; font-weight: 700;
letter-spacing: 1.2px; letter-spacing: 1.2px;
background: transparent; background: transparent;
}} }}
QLabel#metaLabel {{ QLabel#metaLabel {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 11px; font-family: {_BODY_FONT};
font-size: {_z(11)};
background: transparent; background: transparent;
padding: 0 6px; padding: 0 6px;
}} }}
@@ -708,26 +797,30 @@ QWidget#panelBody {{
/* Labels inside panels */ /* Labels inside panels */
QLabel#panelTitle {{ QLabel#panelTitle {{
font-size: 14px; font-family: {_UI_FONT};
font-size: {_z(14)};
font-weight: 700; font-weight: 700;
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
background: transparent; background: transparent;
}} }}
QLabel#sectionLabel {{ QLabel#sectionLabel {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 10px; font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700; font-weight: 700;
letter-spacing: 1px; letter-spacing: 1px;
background: transparent; background: transparent;
}} }}
QLabel#hintText {{ QLabel#hintText {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 11px; font-family: {_BODY_FONT};
font-size: {_z(11)};
background: transparent; background: transparent;
}} }}
QLabel#fieldLabel {{ QLabel#fieldLabel {{
color: {C.TEXT_SECONDARY}; color: {C.TEXT_SECONDARY};
font-size: 12px; font-family: {_BODY_FONT};
font-size: {_z(12)};
background: transparent; background: transparent;
}} }}
@@ -737,8 +830,8 @@ QTextEdit#codeEditor {{
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
border: none; border: none;
padding: 8px; padding: 8px;
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace; font-family: {_MONO_FONT};
font-size: 11px; font-size: {_z(11)};
}} }}
/* Loading overlay */ /* Loading overlay */
@@ -747,7 +840,8 @@ QWidget#loadingOverlay {{
}} }}
QLabel#loadingLabel {{ QLabel#loadingLabel {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 13px; font-family: {_BODY_FONT};
font-size: {_z(13)};
background: transparent; background: transparent;
}} }}
@@ -757,7 +851,8 @@ QLineEdit#searchBar {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 4px; border-radius: 4px;
padding: 4px 8px; padding: 4px 8px;
font-size: 12px; font-family: {_BODY_FONT};
font-size: {_z(12)};
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
}} }}
QLineEdit#searchBar:focus {{ border-color: {C.BORDER_FOCUS}; }} QLineEdit#searchBar:focus {{ border-color: {C.BORDER_FOCUS}; }}
@@ -768,7 +863,8 @@ QLineEdit#filterInput {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 4px; border-radius: 4px;
padding: 5px 8px; padding: 5px 8px;
font-size: 12px; font-family: {_BODY_FONT};
font-size: {_z(12)};
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
}} }}
QLineEdit#filterInput:focus {{ border-color: {C.BORDER_FOCUS}; }} 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 */ /* WebSocket / Mock status indicator labels */
QLabel#statusOk {{ QLabel#statusOk {{
color: {C.SUCCESS}; color: {C.SUCCESS};
font-size: 12px; font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600; font-weight: 600;
background: transparent; background: transparent;
}} }}
QLabel#statusWarn {{ QLabel#statusWarn {{
color: {C.WARNING}; color: {C.WARNING};
font-size: 12px; font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600; font-weight: 600;
background: transparent; background: transparent;
}} }}
QLabel#statusErr {{ QLabel#statusErr {{
color: {C.ERROR}; color: {C.ERROR};
font-size: 12px; font-family: {_UI_FONT};
font-size: {_z(12)};
font-weight: 600; font-weight: 600;
background: transparent; background: transparent;
}} }}
@@ -796,6 +895,8 @@ QLabel#statusErr {{
/* Auth "none" hint */ /* Auth "none" hint */
QLabel#authNone {{ QLabel#authNone {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-family: {_BODY_FONT};
font-size: {_z(12)};
padding: 12px; padding: 12px;
background: transparent; background: transparent;
}} }}
@@ -811,7 +912,7 @@ QPushButton#tabCloseBtn {{
border: none; border: none;
border-radius: 3px; border-radius: 3px;
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 14px; font-size: {_z(14)};
font-weight: 700; font-weight: 700;
padding: 0; padding: 0;
}} }}
@@ -830,15 +931,17 @@ QTextEdit#aiOutput {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 5px; border-radius: 5px;
padding: 8px; padding: 8px;
font-size: 12px; font-family: {_BODY_FONT};
font-size: {_z(12)};
}} }}
QLabel#aiStatusLabel {{ QLabel#aiStatusLabel {{
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
font-size: 11px; font-family: {_BODY_FONT};
font-size: {_z(11)};
background: transparent; background: transparent;
}} }}
/* ── AI Chat Panel ─────────────────────────────────────── */ /* -- AI Chat Panel ─────────────────────────────────────── */
QWidget#aiChatPanel {{ QWidget#aiChatPanel {{
background-color: {C.BG_SIDEBAR}; background-color: {C.BG_SIDEBAR};
border-left: 1px solid {C.BORDER}; border-left: 1px solid {C.BORDER};
@@ -849,7 +952,8 @@ QWidget#aiChatHeader {{
}} }}
QLabel#aiChatTitle {{ QLabel#aiChatTitle {{
color: {C.ACCENT}; color: {C.ACCENT};
font-size: 13px; font-family: {_UI_FONT};
font-size: {_z(13)};
font-weight: 700; font-weight: 700;
letter-spacing: 0.5px; letter-spacing: 0.5px;
background: transparent; background: transparent;
@@ -871,7 +975,8 @@ QFrame#aiBubble {{
margin: 0px; margin: 0px;
}} }}
QLabel#chatRoleLabel {{ QLabel#chatRoleLabel {{
font-size: 10px; font-family: {_UI_FONT};
font-size: {_z(10)};
font-weight: 700; font-weight: 700;
color: {C.TEXT_MUTED}; color: {C.TEXT_MUTED};
background: transparent; background: transparent;
@@ -881,7 +986,8 @@ QLabel#chatRoleLabel {{
QLabel#chatMessageText {{ QLabel#chatMessageText {{
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
background: transparent; background: transparent;
font-size: 12px; font-family: {_BODY_FONT};
font-size: {_z(12)};
line-height: 1.6; line-height: 1.6;
}} }}
QWidget#chatInputArea {{ QWidget#chatInputArea {{
@@ -893,7 +999,8 @@ QTextEdit#chatInput {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 6px; border-radius: 6px;
padding: 6px 10px; padding: 6px 10px;
font-size: 12px; font-family: {_BODY_FONT};
font-size: {_z(12)};
color: {C.TEXT_PRIMARY}; color: {C.TEXT_PRIMARY};
}} }}
QTextEdit#chatInput:focus {{ QTextEdit#chatInput:focus {{
@@ -909,9 +1016,10 @@ QPushButton#qaBtn {{
border: 1px solid {C.BORDER}; border: 1px solid {C.BORDER};
border-radius: 10px; border-radius: 10px;
padding: 2px 8px; padding: 2px 8px;
font-size: 11px; font-family: {_UI_FONT};
font-size: {_z(11)};
color: {C.TEXT_SECONDARY}; color: {C.TEXT_SECONDARY};
font-weight: 500; font-weight: 600;
}} }}
QPushButton#qaBtn:hover {{ QPushButton#qaBtn:hover {{
background-color: {C.BG_HOVER}; background-color: {C.BG_HOVER};
@@ -928,8 +1036,8 @@ QTextEdit#applyCode {{
background-color: transparent; background-color: transparent;
border: none; border: none;
padding: 4px; padding: 4px;
font-size: 10px; font-family: {_MONO_FONT};
font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", "Consolas", monospace; font-size: {_z(10)};
color: {C.TEXT_SECONDARY}; color: {C.TEXT_SECONDARY};
}} }}
@@ -974,6 +1082,34 @@ def is_dark() -> bool:
return _is_dark 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: def restyle(widget, obj_name: str) -> None:
"""Change a widget's objectName and force Qt to re-evaluate CSS rules.""" """Change a widget's objectName and force Qt to re-evaluate CSS rules."""
widget.setObjectName(obj_name) widget.setObjectName(obj_name)

BIN
assets/app-ss.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

BIN
assets/ekika_logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
assets/ekika_logo.webp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,6 @@
import sys import sys
from PyQt6.QtWidgets import QApplication from PyQt6.QtWidgets import QApplication
from app.core.fonts import load_fonts
from app.ui.theme import apply from app.ui.theme import apply
from app.ui.main_window import MainWindow from app.ui.main_window import MainWindow
@@ -11,6 +12,7 @@ if __name__ == "__main__":
app.setApplicationName(APP_NAME) app.setApplicationName(APP_NAME)
app.setApplicationVersion(APP_VERSION) app.setApplicationVersion(APP_VERSION)
app.setOrganizationName("EKIKA") app.setOrganizationName("EKIKA")
load_fonts()
apply(app, dark=True) apply(app, dark=True)
window = MainWindow() window = MainWindow()
window.show() window.show()