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.
> 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

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."""
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):

View File

@@ -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

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
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()