From 578f0d2b15fd5993fd47248e57a66a2b23f14c4f Mon Sep 17 00:00:00 2001 From: Daniil Polyakov Date: Thu, 2 Apr 2026 16:14:26 +0300 Subject: [PATCH] feat: better styling --- CMakeLists.txt | 5 +- nix/app.nix | 2 +- nix/default.nix | 1 + src/ExplorerWidget.cpp | 7 +- src/Style.h | 132 ++++++++++++++++++++++++++---- src/explorer_resources.qrc | 12 +++ src/icons/activity.svg | 3 + src/icons/arrow-left.svg | 4 + src/icons/arrow-right.svg | 4 + src/icons/box.svg | 5 ++ src/icons/file-text.svg | 7 ++ src/icons/home.svg | 4 + src/icons/search.svg | 4 + src/icons/user.svg | 4 + src/pages/AccountPage.cpp | 62 ++++++++++---- src/pages/BlockPage.cpp | 122 +++++++++++++++++++-------- src/pages/MainPage.cpp | 150 +++++++++++++++++++++++++--------- src/pages/MainPage.h | 8 +- src/pages/TransactionPage.cpp | 61 ++++++++++---- src/widgets/ClickableFrame.h | 20 +++++ src/widgets/NavigationBar.cpp | 34 ++++++-- src/widgets/SearchBar.cpp | 10 ++- 22 files changed, 520 insertions(+), 141 deletions(-) create mode 100644 src/explorer_resources.qrc create mode 100644 src/icons/activity.svg create mode 100644 src/icons/arrow-left.svg create mode 100644 src/icons/arrow-right.svg create mode 100644 src/icons/box.svg create mode 100644 src/icons/file-text.svg create mode 100644 src/icons/home.svg create mode 100644 src/icons/search.svg create mode 100644 src/icons/user.svg diff --git a/CMakeLists.txt b/CMakeLists.txt index e6ae9cc..34362a3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -8,7 +8,7 @@ set(CMAKE_AUTORCC ON) set(CMAKE_AUTOUIC ON) # Find Qt packages -find_package(Qt6 REQUIRED COMPONENTS Core Widgets) +find_package(Qt6 REQUIRED COMPONENTS Core Widgets Svg SvgWidgets) # Try to find the component-interfaces package first find_package(component-interfaces QUIET) @@ -46,6 +46,7 @@ set(SOURCES src/pages/TransactionPage.h src/pages/AccountPage.cpp src/pages/AccountPage.h + src/explorer_resources.qrc ) # Create the plugin library @@ -68,6 +69,8 @@ target_include_directories(lez_explorer_ui PRIVATE target_link_libraries(lez_explorer_ui PRIVATE Qt6::Core Qt6::Widgets + Qt6::Svg + Qt6::SvgWidgets component-interfaces ) diff --git a/nix/app.nix b/nix/app.nix index 6f96a0d..0e9f287 100644 --- a/nix/app.nix +++ b/nix/app.nix @@ -19,7 +19,7 @@ pkgs.stdenv.mkDerivation rec { pkgs.libglvnd ]); - qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins"; + qtPluginPath = "${pkgs.qt6.qtbase}/lib/qt-6/plugins:${pkgs.qt6.qtsvg}/lib/qt-6/plugins"; qtWrapperArgs = [ "--prefix" "LD_LIBRARY_PATH" ":" qtLibPath diff --git a/nix/default.nix b/nix/default.nix index 96cc00b..838f23b 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -14,6 +14,7 @@ buildInputs = [ pkgs.qt6.qtbase + pkgs.qt6.qtsvg ]; cmakeFlags = [ diff --git a/src/ExplorerWidget.cpp b/src/ExplorerWidget.cpp index 7149ff9..91e002f 100644 --- a/src/ExplorerWidget.cpp +++ b/src/ExplorerWidget.cpp @@ -1,4 +1,5 @@ #include "ExplorerWidget.h" +#include "Style.h" #include "services/MockIndexerService.h" #include "widgets/NavigationBar.h" #include "widgets/SearchBar.h" @@ -15,9 +16,11 @@ ExplorerWidget::ExplorerWidget(QWidget* parent) : QWidget(parent) , m_indexer(std::make_unique()) { + setStyleSheet(Style::appBackground()); + auto* layout = new QVBoxLayout(this); - layout->setContentsMargins(8, 8, 8, 8); - layout->setSpacing(4); + layout->setContentsMargins(16, 12, 16, 12); + layout->setSpacing(8); // Navigation bar m_navBar = new NavigationBar(this); diff --git a/src/Style.h b/src/Style.h index 108c11e..e38d75c 100644 --- a/src/Style.h +++ b/src/Style.h @@ -4,41 +4,139 @@ namespace Style { +// --- Color palette --- + +namespace Color { + inline const char* bg() { return "#1e1e2e"; } + inline const char* surface() { return "#2a2a3c"; } + inline const char* surfaceHover() { return "#33334a"; } + inline const char* border() { return "#3b3b52"; } + inline const char* text() { return "#cdd6f4"; } + inline const char* subtext() { return "#7f849c"; } + inline const char* accent() { return "#89b4fa"; } + inline const char* green() { return "#a6e3a1"; } + inline const char* yellow() { return "#f9e2af"; } + inline const char* red() { return "#f38ba8"; } + inline const char* blue() { return "#89b4fa"; } + inline const char* mauve() { return "#cba6f7"; } + inline const char* peach() { return "#fab387"; } + inline const char* teal() { return "#94e2d5"; } +} + +// --- Base font size applied to the entire app --- + +inline int baseFontSize() { return 14; } + +// --- Widget styles --- + +inline QString appBackground() +{ + return QString("background-color: %1; color: %2; font-size: %3px;") + .arg(Color::bg(), Color::text()) + .arg(baseFontSize()); +} + inline QString cardFrame() { - return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px;"; + return QString( + "background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px;" + ).arg(Color::surface(), Color::border()); } inline QString cardFrameWithLabels() { - return "QFrame { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 12px; }" - " QFrame QLabel { color: #212529; }"; -} - -inline QString clickableRow() -{ - return "background: #f8f9fa; color: #212529; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin: 2px 0;"; + return QString( + "QFrame { background: %1; border: 1px solid %2; border-radius: 8px; padding: 18px; }" + " QFrame QLabel { color: %3; font-size: %4px; }" + ).arg(Color::surface(), Color::border(), Color::text()) + .arg(baseFontSize()); } inline QString clickableRowWithLabels(const QString& selector) { - return QString("%1 { background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; padding: 8px; margin: 2px 0; }" - " %1 QLabel { color: #212529; }").arg(selector); -} - -inline QString monoText() -{ - return "font-family: 'Menlo', 'Courier New', 'DejaVu Sans Mono'; font-size: 11px;"; + return QString( + "%1 { background: %2; border: 1px solid %3; border-radius: 8px; padding: 12px 16px; margin: 3px 0; }" + " %1 QLabel { color: %4; font-size: %5px; }" + ).arg(selector, Color::surface(), Color::border(), Color::text()) + .arg(baseFontSize()); } inline QString mutedText() { - return "color: #6c757d;"; + return QString("color: %1;").arg(Color::subtext()); +} + +inline QString monoText() +{ + return QString("font-family: 'Menlo', 'Courier New'; font-size: %1px; color: %2;") + .arg(baseFontSize() - 1) + .arg(Color::accent()); } inline QString badge(const QString& bgColor) { - return QString("color: white; background: %1; border-radius: 4px; padding: 2px 8px;").arg(bgColor); + return QString("color: %1; background: %2; border-radius: 4px; padding: 3px 12px; font-weight: bold; font-size: %3px;") + .arg(Color::bg(), bgColor) + .arg(baseFontSize() - 1); +} + +inline QString sectionHeader() +{ + return QString("color: %1;").arg(Color::text()); +} + +inline QString navButton() +{ + return QString( + "QPushButton { background: %1; color: %2; border: 1px solid %3; border-radius: 6px; padding: 6px 14px; font-size: %7px; }" + " QPushButton:hover { background: %4; border-color: %5; }" + " QPushButton:disabled { color: %6; background: %1; border-color: %3; opacity: 0.4; }" + ).arg(Color::surface(), Color::text(), Color::border(), Color::surfaceHover(), Color::accent(), Color::subtext()) + .arg(baseFontSize() + 2); +} + +inline QString searchInput() +{ + return QString( + "QLineEdit { background: %1; color: %2; border: 1px solid %3; border-radius: 6px; padding: 10px 14px; font-size: %6px; }" + " QLineEdit:focus { border-color: %4; }" + " QLineEdit::placeholder { color: %5; }" + ).arg(Color::surface(), Color::text(), Color::border(), Color::accent(), Color::subtext()) + .arg(baseFontSize()); +} + +inline QString searchButton() +{ + return QString( + "QPushButton { background: %1; color: %2; border: none; border-radius: 6px; padding: 10px 20px; font-size: %4px; font-weight: bold; }" + " QPushButton:hover { background: %3; }" + ).arg(Color::accent(), Color::bg(), Color::mauve()) + .arg(baseFontSize()); +} + +inline QString healthLabel() +{ + return QString("color: %1; font-weight: bold; font-size: %2px;") + .arg(Color::green()) + .arg(baseFontSize()); +} + +// --- Status colors --- + +inline QString statusColor(const QString& status) +{ + if (status == "Finalized") return Color::green(); + if (status == "Safe") return Color::yellow(); + return Color::subtext(); // Pending +} + +// --- Transaction type colors --- + +inline QString txTypeColor(const QString& type) +{ + if (type == "Public") return Color::blue(); + if (type == "Privacy-Preserving") return Color::mauve(); + return Color::peach(); // Program Deployment } } // namespace Style diff --git a/src/explorer_resources.qrc b/src/explorer_resources.qrc new file mode 100644 index 0000000..9060833 --- /dev/null +++ b/src/explorer_resources.qrc @@ -0,0 +1,12 @@ + + + icons/arrow-left.svg + icons/arrow-right.svg + icons/home.svg + icons/search.svg + icons/box.svg + icons/file-text.svg + icons/user.svg + icons/activity.svg + + diff --git a/src/icons/activity.svg b/src/icons/activity.svg new file mode 100644 index 0000000..a0cca09 --- /dev/null +++ b/src/icons/activity.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/icons/arrow-left.svg b/src/icons/arrow-left.svg new file mode 100644 index 0000000..3847265 --- /dev/null +++ b/src/icons/arrow-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/arrow-right.svg b/src/icons/arrow-right.svg new file mode 100644 index 0000000..782123f --- /dev/null +++ b/src/icons/arrow-right.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/box.svg b/src/icons/box.svg new file mode 100644 index 0000000..29c5fcf --- /dev/null +++ b/src/icons/box.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/icons/file-text.svg b/src/icons/file-text.svg new file mode 100644 index 0000000..064dd36 --- /dev/null +++ b/src/icons/file-text.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/src/icons/home.svg b/src/icons/home.svg new file mode 100644 index 0000000..e351604 --- /dev/null +++ b/src/icons/home.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/search.svg b/src/icons/search.svg new file mode 100644 index 0000000..526470d --- /dev/null +++ b/src/icons/search.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/icons/user.svg b/src/icons/user.svg new file mode 100644 index 0000000..53b88cc --- /dev/null +++ b/src/icons/user.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/pages/AccountPage.cpp b/src/pages/AccountPage.cpp index af37159..c8afb76 100644 --- a/src/pages/AccountPage.cpp +++ b/src/pages/AccountPage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace { @@ -32,30 +33,45 @@ AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidge : QWidget(parent) { auto* outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 0); auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }"); auto* scrollContent = new QWidget(); + scrollContent->setObjectName("scrollContent"); auto* layout = new QVBoxLayout(scrollContent); layout->setAlignment(Qt::AlignTop); + layout->setContentsMargins(0, 0, 0, 0); - // Title + // Title with icon + auto* titleRow = new QHBoxLayout(); + titleRow->setSpacing(10); + auto* titleIcon = new QLabel(); + titleIcon->setPixmap(QIcon(":/icons/user.svg").pixmap(30, 30)); auto* title = new QLabel("Account Details"); QFont titleFont = title->font(); - titleFont.setPointSize(20); + titleFont.setPointSize(24); titleFont.setBold(true); title->setFont(titleFont); - layout->addWidget(title); + title->setStyleSheet(QString("color: %1;").arg(Style::Color::text())); + titleRow->addWidget(titleIcon); + titleRow->addWidget(title); + titleRow->addStretch(); + layout->addLayout(titleRow); + layout->addSpacing(8); // Account info grid auto* infoFrame = new QFrame(); - infoFrame->setFrameShape(QFrame::StyledPanel); + infoFrame->setFrameShape(QFrame::NoFrame); infoFrame->setStyleSheet(Style::cardFrameWithLabels()); auto* grid = new QGridLayout(infoFrame); grid->setColumnStretch(1, 1); + grid->setVerticalSpacing(10); + grid->setHorizontalSpacing(20); int row = 0; grid->addWidget(makeFieldLabel("Account ID"), row, 0); @@ -64,7 +80,9 @@ AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidge grid->addWidget(idVal, row++, 1); grid->addWidget(makeFieldLabel("Balance"), row, 0); - grid->addWidget(makeValueLabel(account.balance), row++, 1); + auto* balVal = makeValueLabel(account.balance); + balVal->setStyleSheet(QString("color: %1; font-weight: bold;").arg(Style::Color::green())); + grid->addWidget(balVal, row++, 1); grid->addWidget(makeFieldLabel("Program Owner"), row, 0); auto* ownerVal = makeValueLabel(account.programOwner); @@ -83,34 +101,42 @@ AccountPage::AccountPage(const Account& account, IndexerService* indexer, QWidge auto transactions = indexer->getTransactionsByAccount(account.accountId, 0, 10); if (!transactions.isEmpty()) { + auto* headerRow = new QHBoxLayout(); + headerRow->setContentsMargins(0, 16, 0, 6); + headerRow->setSpacing(8); + auto* headerIcon = new QLabel(); + headerIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(24, 24)); auto* txHeader = new QLabel("Transaction History"); QFont headerFont = txHeader->font(); - headerFont.setPointSize(16); + headerFont.setPointSize(20); headerFont.setBold(true); txHeader->setFont(headerFont); - txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;"); - layout->addWidget(txHeader); + txHeader->setStyleSheet(Style::sectionHeader()); + headerRow->addWidget(headerIcon); + headerRow->addWidget(txHeader); + headerRow->addStretch(); + layout->addLayout(headerRow); for (const auto& tx : transactions) { auto* frame = new ClickableFrame(); - frame->setFrameShape(QFrame::StyledPanel); + frame->setFrameShape(QFrame::NoFrame); frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame")); auto* txRow = new QHBoxLayout(frame); + txRow->setSpacing(10); - auto* hashLabel = new QLabel(tx.hash.left(16) + "..."); + auto* icon = new QLabel(); + icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20)); + txRow->addWidget(icon); + + auto* hashLabel = new QLabel(tx.hash); QFont boldFont = hashLabel->font(); boldFont.setBold(true); hashLabel->setFont(boldFont); - QString typeColor; - switch (tx.type) { - case TransactionType::Public: typeColor = "#007bff"; break; - case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break; - case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break; - } - auto* typeLabel = new QLabel(transactionTypeToString(tx.type)); - typeLabel->setStyleSheet(Style::badge(typeColor)); + QString typeStr = transactionTypeToString(tx.type); + auto* typeLabel = new QLabel(typeStr); + typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr))); txRow->addWidget(hashLabel); txRow->addWidget(typeLabel); diff --git a/src/pages/BlockPage.cpp b/src/pages/BlockPage.cpp index 43cc449..d9396e9 100644 --- a/src/pages/BlockPage.cpp +++ b/src/pages/BlockPage.cpp @@ -8,9 +8,34 @@ #include #include #include +#include +#include +#include namespace { +class PrevHashClickFilter : public QObject { +public: + PrevHashClickFilter(quint64 blockId, BlockPage* page, QObject* parent) + : QObject(parent), m_blockId(blockId), m_page(page) {} + +protected: + bool eventFilter(QObject* obj, QEvent* event) override { + if (event->type() == QEvent::MouseButtonRelease) { + auto* me = static_cast(event); + if (me->button() == Qt::LeftButton) { + emit m_page->blockClicked(m_blockId); + return true; + } + } + return QObject::eventFilter(obj, event); + } + +private: + quint64 m_blockId; + BlockPage* m_page; +}; + QLabel* makeFieldLabel(const QString& text) { auto* label = new QLabel(text); @@ -32,66 +57,89 @@ BlockPage::BlockPage(const Block& block, QWidget* parent) : QWidget(parent) { auto* outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 0); auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }"); auto* scrollContent = new QWidget(); + scrollContent->setObjectName("scrollContent"); auto* layout = new QVBoxLayout(scrollContent); layout->setAlignment(Qt::AlignTop); + layout->setContentsMargins(0, 0, 0, 0); - // Title + // Title with icon + auto* titleRow = new QHBoxLayout(); + titleRow->setSpacing(10); + auto* titleIcon = new QLabel(); + titleIcon->setPixmap(QIcon(":/icons/box.svg").pixmap(30, 30)); auto* title = new QLabel(QString("Block #%1").arg(block.blockId)); QFont titleFont = title->font(); - titleFont.setPointSize(20); + titleFont.setPointSize(24); titleFont.setBold(true); title->setFont(titleFont); - layout->addWidget(title); + title->setStyleSheet(QString("color: %1;").arg(Style::Color::text())); + titleRow->addWidget(titleIcon); + titleRow->addWidget(title); + titleRow->addStretch(); + layout->addLayout(titleRow); + layout->addSpacing(8); // Block info grid auto* infoFrame = new QFrame(); - infoFrame->setFrameShape(QFrame::StyledPanel); + infoFrame->setFrameShape(QFrame::NoFrame); infoFrame->setStyleSheet(Style::cardFrameWithLabels()); auto* grid = new QGridLayout(infoFrame); grid->setColumnStretch(1, 1); + grid->setVerticalSpacing(10); + grid->setHorizontalSpacing(20); int row = 0; grid->addWidget(makeFieldLabel("Block ID"), row, 0); grid->addWidget(makeValueLabel(QString::number(block.blockId)), row++, 1); grid->addWidget(makeFieldLabel("Hash"), row, 0); - grid->addWidget(makeValueLabel(block.hash), row++, 1); + auto* hashVal = makeValueLabel(block.hash); + hashVal->setStyleSheet(Style::monoText()); + grid->addWidget(hashVal, row++, 1); grid->addWidget(makeFieldLabel("Previous Hash"), row, 0); - auto* prevHashLabel = new QLabel(QString("%1").arg(block.prevBlockHash)); - prevHashLabel->setTextInteractionFlags(Qt::TextBrowserInteraction); if (block.blockId > 1) { + auto* prevHashLabel = new QLabel(block.prevBlockHash); + prevHashLabel->setStyleSheet(QString("color: %1; text-decoration: underline;").arg(Style::Color::accent())); + prevHashLabel->setCursor(Qt::PointingHandCursor); + prevHashLabel->setWordWrap(true); + prevHashLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); quint64 prevBlockId = block.blockId - 1; - connect(prevHashLabel, &QLabel::linkActivated, this, [this, prevBlockId]() { - emit blockClicked(prevBlockId); - }); + prevHashLabel->installEventFilter(new PrevHashClickFilter(prevBlockId, this, this)); + grid->addWidget(prevHashLabel, row++, 1); + } else { + grid->addWidget(makeValueLabel(block.prevBlockHash), row++, 1); } - grid->addWidget(prevHashLabel, row++, 1); grid->addWidget(makeFieldLabel("Timestamp"), row, 0); grid->addWidget(makeValueLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC")), row++, 1); grid->addWidget(makeFieldLabel("Status"), row, 0); - QString statusColor; - switch (block.bedrockStatus) { - case BedrockStatus::Finalized: statusColor = "#28a745"; break; - case BedrockStatus::Safe: statusColor = "#ffc107"; break; - case BedrockStatus::Pending: statusColor = "#6c757d"; break; - } - auto* statusLabel = new QLabel(bedrockStatusToString(block.bedrockStatus)); - statusLabel->setStyleSheet(Style::badge(statusColor) + " max-width: 100px;"); + QString statusStr = bedrockStatusToString(block.bedrockStatus); + auto* statusLabel = new QLabel(statusStr); + statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr))); + statusLabel->setMaximumWidth(120); grid->addWidget(statusLabel, row++, 1); grid->addWidget(makeFieldLabel("Signature"), row, 0); - auto* sigLabel = makeValueLabel(block.signature); + // Insert zero-width spaces every 32 chars so QLabel can word-wrap the long hex string + QString wrappableSig = block.signature; + for (int i = 32; i < wrappableSig.size(); i += 33) { + wrappableSig.insert(i, QChar(0x200B)); + } + auto* sigLabel = new QLabel(wrappableSig); sigLabel->setStyleSheet(Style::monoText()); + sigLabel->setTextInteractionFlags(Qt::TextSelectableByMouse); + sigLabel->setWordWrap(true); grid->addWidget(sigLabel, row++, 1); grid->addWidget(makeFieldLabel("Transactions"), row, 0); @@ -101,34 +149,42 @@ BlockPage::BlockPage(const Block& block, QWidget* parent) // Transactions section if (!block.transactions.isEmpty()) { + auto* headerRow = new QHBoxLayout(); + headerRow->setContentsMargins(0, 16, 0, 6); + headerRow->setSpacing(8); + auto* headerIcon = new QLabel(); + headerIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(24, 24)); auto* txHeader = new QLabel("Transactions"); QFont headerFont = txHeader->font(); - headerFont.setPointSize(16); + headerFont.setPointSize(20); headerFont.setBold(true); txHeader->setFont(headerFont); - txHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;"); - layout->addWidget(txHeader); + txHeader->setStyleSheet(Style::sectionHeader()); + headerRow->addWidget(headerIcon); + headerRow->addWidget(txHeader); + headerRow->addStretch(); + layout->addLayout(headerRow); for (const auto& tx : block.transactions) { auto* frame = new ClickableFrame(); - frame->setFrameShape(QFrame::StyledPanel); + frame->setFrameShape(QFrame::NoFrame); frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame")); auto* txRow = new QHBoxLayout(frame); + txRow->setSpacing(10); - auto* hashLabel = new QLabel(tx.hash.left(16) + "..."); + auto* icon = new QLabel(); + icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20)); + txRow->addWidget(icon); + + auto* hashLabel = new QLabel(tx.hash); QFont boldFont = hashLabel->font(); boldFont.setBold(true); hashLabel->setFont(boldFont); - QString typeColor; - switch (tx.type) { - case TransactionType::Public: typeColor = "#007bff"; break; - case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break; - case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break; - } - auto* typeLabel = new QLabel(transactionTypeToString(tx.type)); - typeLabel->setStyleSheet(Style::badge(typeColor)); + QString typeStr = transactionTypeToString(tx.type); + auto* typeLabel = new QLabel(typeStr); + typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr))); auto* metaLabel = new QLabel(); switch (tx.type) { diff --git a/src/pages/MainPage.cpp b/src/pages/MainPage.cpp index f9e8c37..516eac9 100644 --- a/src/pages/MainPage.cpp +++ b/src/pages/MainPage.cpp @@ -7,16 +7,22 @@ #include #include #include +#include +#include MainPage::MainPage(IndexerService* indexer, QWidget* parent) : QWidget(parent) , m_indexer(indexer) { auto* outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 0); // Health indicator auto* healthRow = new QHBoxLayout(); + auto* healthIcon = new QLabel(this); + healthIcon->setPixmap(QIcon(":/icons/activity.svg").pixmap(20, 20)); m_healthLabel = new QLabel(this); + healthRow->addWidget(healthIcon); healthRow->addWidget(m_healthLabel); healthRow->addStretch(); outerLayout->addLayout(healthRow); @@ -25,10 +31,13 @@ MainPage::MainPage(IndexerService* indexer, QWidget* parent) auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }"); auto* scrollContent = new QWidget(); + scrollContent->setObjectName("scrollContent"); m_contentLayout = new QVBoxLayout(scrollContent); m_contentLayout->setAlignment(Qt::AlignTop); + m_contentLayout->setContentsMargins(0, 0, 0, 0); scrollArea->setWidget(scrollContent); outerLayout->addWidget(scrollArea); @@ -36,52 +45,77 @@ MainPage::MainPage(IndexerService* indexer, QWidget* parent) refresh(); } -QWidget* MainPage::createSectionHeader(const QString& title) +QWidget* MainPage::createSectionHeader(const QString& title, const QString& iconPath) { + auto* container = new QWidget(); + auto* layout = new QHBoxLayout(container); + layout->setContentsMargins(0, 12, 0, 6); + layout->setSpacing(8); + layout->setAlignment(Qt::AlignVCenter); + + if (!iconPath.isEmpty()) { + auto* icon = new QLabel(); + icon->setPixmap(QIcon(iconPath).pixmap(24, 24)); + icon->setFixedSize(24, 24); + layout->addWidget(icon); + } + auto* label = new QLabel(title); QFont font = label->font(); - font.setPointSize(16); + font.setPointSize(20); font.setBold(true); label->setFont(font); - label->setStyleSheet("margin-top: 12px; margin-bottom: 4px;"); - return label; + label->setStyleSheet(Style::sectionHeader()); + layout->addWidget(label); + layout->addStretch(); + + return container; } void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block) { auto* frame = new ClickableFrame(); - frame->setFrameShape(QFrame::StyledPanel); + frame->setFrameShape(QFrame::NoFrame); frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame")); auto* row = new QHBoxLayout(frame); + row->setSpacing(10); + + auto* icon = new QLabel(); + icon->setPixmap(QIcon(":/icons/box.svg").pixmap(20, 20)); + row->addWidget(icon); auto* idLabel = new QLabel(QString("Block #%1").arg(block.blockId)); QFont boldFont = idLabel->font(); boldFont.setBold(true); idLabel->setFont(boldFont); - QString statusColor; - switch (block.bedrockStatus) { - case BedrockStatus::Finalized: statusColor = "#28a745"; break; - case BedrockStatus::Safe: statusColor = "#ffc107"; break; - case BedrockStatus::Pending: statusColor = "#6c757d"; break; - } - auto* statusLabel = new QLabel(bedrockStatusToString(block.bedrockStatus)); - statusLabel->setStyleSheet(Style::badge(statusColor)); + QString statusStr = bedrockStatusToString(block.bedrockStatus); + auto* statusLabel = new QLabel(statusStr); + statusLabel->setStyleSheet(Style::badge(Style::statusColor(statusStr))); - auto* hashLabel = new QLabel(block.hash.left(16) + "..."); - hashLabel->setStyleSheet(Style::mutedText()); + auto* hashLabel = new QLabel(block.hash); + hashLabel->setStyleSheet(Style::mutedText() + " font-family: 'Menlo', 'Courier New'; font-size: 11px;"); + hashLabel->setFixedWidth(470); - auto* txCount = new QLabel(QString("%1 tx").arg(block.transactions.size())); + auto* txIcon = new QLabel(); + txIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(18, 18)); + + auto* txCount = new QLabel(QString::number(block.transactions.size())); auto* timeLabel = new QLabel(block.timestamp.toString("yyyy-MM-dd hh:mm:ss UTC")); timeLabel->setStyleSheet(Style::mutedText()); + statusLabel->setFixedWidth(90); + statusLabel->setAlignment(Qt::AlignCenter); + row->addWidget(idLabel); - row->addWidget(statusLabel); - row->addWidget(hashLabel, 1); + row->addWidget(hashLabel); + row->addStretch(1); + row->addWidget(txIcon); row->addWidget(txCount); row->addWidget(timeLabel); + row->addWidget(statusLabel); quint64 blockId = block.blockId; connect(frame, &ClickableFrame::clicked, this, [this, blockId]() { @@ -94,24 +128,24 @@ void MainPage::addBlockRow(QVBoxLayout* layout, const Block& block) void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx) { auto* frame = new ClickableFrame(); - frame->setFrameShape(QFrame::StyledPanel); + frame->setFrameShape(QFrame::NoFrame); frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame")); auto* row = new QHBoxLayout(frame); + row->setSpacing(10); - auto* hashLabel = new QLabel(tx.hash.left(16) + "..."); + auto* icon = new QLabel(); + icon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(20, 20)); + row->addWidget(icon); + + auto* hashLabel = new QLabel(tx.hash); QFont boldFont = hashLabel->font(); boldFont.setBold(true); hashLabel->setFont(boldFont); - QString typeColor; - switch (tx.type) { - case TransactionType::Public: typeColor = "#007bff"; break; - case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break; - case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break; - } - auto* typeLabel = new QLabel(transactionTypeToString(tx.type)); - typeLabel->setStyleSheet(Style::badge(typeColor)); + QString typeStr = transactionTypeToString(tx.type); + auto* typeLabel = new QLabel(typeStr); + typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr))); auto* metaLabel = new QLabel(); switch (tx.type) { @@ -142,12 +176,17 @@ void MainPage::addTransactionRow(QVBoxLayout* layout, const Transaction& tx) void MainPage::addAccountRow(QVBoxLayout* layout, const Account& account) { auto* frame = new ClickableFrame(); - frame->setFrameShape(QFrame::StyledPanel); + frame->setFrameShape(QFrame::NoFrame); frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame")); auto* row = new QHBoxLayout(frame); + row->setSpacing(10); - auto* idLabel = new QLabel(account.accountId.left(16) + "..."); + auto* icon = new QLabel(); + icon->setPixmap(QIcon(":/icons/user.svg").pixmap(20, 20)); + row->addWidget(icon); + + auto* idLabel = new QLabel(account.accountId); QFont boldFont = idLabel->font(); boldFont.setBold(true); idLabel->setFont(boldFont); @@ -174,27 +213,60 @@ void MainPage::refresh() quint64 latestId = m_indexer->getLatestBlockId(); m_healthLabel->setText(QString("Chain height: %1").arg(latestId)); - m_healthLabel->setStyleSheet("color: #28a745; font-weight: bold; font-size: 14px;"); + m_healthLabel->setStyleSheet(Style::healthLabel()); if (m_recentBlocksWidget) { m_contentLayout->removeWidget(m_recentBlocksWidget); delete m_recentBlocksWidget; + m_recentBlocksWidget = nullptr; + m_blocksLayout = nullptr; + m_loadMoreBtn = nullptr; } - m_recentBlocksWidget = new QWidget(); - auto* blocksLayout = new QVBoxLayout(m_recentBlocksWidget); - blocksLayout->setContentsMargins(0, 0, 0, 0); + m_oldestLoadedBlockId = std::nullopt; - blocksLayout->addWidget(createSectionHeader("Recent Blocks")); + m_recentBlocksWidget = new QWidget(); + auto* outerLayout = new QVBoxLayout(m_recentBlocksWidget); + outerLayout->setContentsMargins(0, 0, 0, 0); + outerLayout->addWidget(createSectionHeader("Recent Blocks", ":/icons/box.svg")); + + // Dedicated layout for block rows — button is appended after this in outerLayout + auto* blockRowsWidget = new QWidget(); + m_blocksLayout = new QVBoxLayout(blockRowsWidget); + m_blocksLayout->setContentsMargins(0, 0, 0, 0); + outerLayout->addWidget(blockRowsWidget); auto blocks = m_indexer->getBlocks(std::nullopt, 10); for (const auto& block : blocks) { - addBlockRow(blocksLayout, block); + addBlockRow(m_blocksLayout, block); + if (!m_oldestLoadedBlockId || block.blockId < *m_oldestLoadedBlockId) + m_oldestLoadedBlockId = block.blockId; } + m_loadMoreBtn = new QPushButton("Load more"); + m_loadMoreBtn->setStyleSheet(Style::searchButton()); + m_loadMoreBtn->setVisible(m_oldestLoadedBlockId && *m_oldestLoadedBlockId > 1); + connect(m_loadMoreBtn, &QPushButton::clicked, this, &MainPage::loadMoreBlocks); + outerLayout->addWidget(m_loadMoreBtn, 0, Qt::AlignHCenter); + m_contentLayout->addWidget(m_recentBlocksWidget); } +void MainPage::loadMoreBlocks() +{ + if (!m_oldestLoadedBlockId || *m_oldestLoadedBlockId <= 1) + return; + + auto blocks = m_indexer->getBlocks(m_oldestLoadedBlockId, 10); + for (const auto& block : blocks) { + addBlockRow(m_blocksLayout, block); + if (block.blockId < *m_oldestLoadedBlockId) + m_oldestLoadedBlockId = block.blockId; + } + + m_loadMoreBtn->setVisible(*m_oldestLoadedBlockId > 1 && !blocks.isEmpty()); +} + void MainPage::showSearchResults(const SearchResults& results) { clearSearchResults(); @@ -210,7 +282,7 @@ void MainPage::showSearchResults(const SearchResults& results) bool hasResults = false; if (!results.blocks.isEmpty()) { - layout->addWidget(createSectionHeader("Blocks")); + layout->addWidget(createSectionHeader("Blocks", ":/icons/box.svg")); for (const auto& block : results.blocks) { addBlockRow(layout, block); } @@ -218,7 +290,7 @@ void MainPage::showSearchResults(const SearchResults& results) } if (!results.transactions.isEmpty()) { - layout->addWidget(createSectionHeader("Transactions")); + layout->addWidget(createSectionHeader("Transactions", ":/icons/file-text.svg")); for (const auto& tx : results.transactions) { addTransactionRow(layout, tx); } @@ -226,7 +298,7 @@ void MainPage::showSearchResults(const SearchResults& results) } if (!results.accounts.isEmpty()) { - layout->addWidget(createSectionHeader("Accounts")); + layout->addWidget(createSectionHeader("Accounts", ":/icons/user.svg")); for (const auto& acc : results.accounts) { addAccountRow(layout, acc); } diff --git a/src/pages/MainPage.h b/src/pages/MainPage.h index 7086d6f..92504e7 100644 --- a/src/pages/MainPage.h +++ b/src/pages/MainPage.h @@ -3,9 +3,11 @@ #include "services/IndexerService.h" #include +#include class QVBoxLayout; class QLabel; +class QPushButton; class MainPage : public QWidget { Q_OBJECT @@ -26,11 +28,15 @@ private: void addBlockRow(QVBoxLayout* layout, const Block& block); void addTransactionRow(QVBoxLayout* layout, const Transaction& tx); void addAccountRow(QVBoxLayout* layout, const Account& account); - QWidget* createSectionHeader(const QString& title); + QWidget* createSectionHeader(const QString& title, const QString& iconPath = {}); + void loadMoreBlocks(); IndexerService* m_indexer = nullptr; QVBoxLayout* m_contentLayout = nullptr; QWidget* m_searchResultsWidget = nullptr; QWidget* m_recentBlocksWidget = nullptr; + QVBoxLayout* m_blocksLayout = nullptr; + QPushButton* m_loadMoreBtn = nullptr; QLabel* m_healthLabel = nullptr; + std::optional m_oldestLoadedBlockId; }; diff --git a/src/pages/TransactionPage.cpp b/src/pages/TransactionPage.cpp index 58b3109..9e514bc 100644 --- a/src/pages/TransactionPage.cpp +++ b/src/pages/TransactionPage.cpp @@ -8,6 +8,7 @@ #include #include #include +#include namespace { @@ -32,30 +33,45 @@ TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent) : QWidget(parent) { auto* outerLayout = new QVBoxLayout(this); + outerLayout->setContentsMargins(0, 0, 0, 0); auto* scrollArea = new QScrollArea(this); scrollArea->setWidgetResizable(true); scrollArea->setFrameShape(QFrame::NoFrame); + scrollArea->setStyleSheet("QScrollArea { background: transparent; border: none; } QWidget#scrollContent { background: transparent; }"); auto* scrollContent = new QWidget(); + scrollContent->setObjectName("scrollContent"); auto* layout = new QVBoxLayout(scrollContent); layout->setAlignment(Qt::AlignTop); + layout->setContentsMargins(0, 0, 0, 0); - // Title + // Title with icon + auto* titleRow = new QHBoxLayout(); + titleRow->setSpacing(10); + auto* titleIcon = new QLabel(); + titleIcon->setPixmap(QIcon(":/icons/file-text.svg").pixmap(30, 30)); auto* title = new QLabel("Transaction Details"); QFont titleFont = title->font(); - titleFont.setPointSize(20); + titleFont.setPointSize(24); titleFont.setBold(true); title->setFont(titleFont); - layout->addWidget(title); + title->setStyleSheet(QString("color: %1;").arg(Style::Color::text())); + titleRow->addWidget(titleIcon); + titleRow->addWidget(title); + titleRow->addStretch(); + layout->addLayout(titleRow); + layout->addSpacing(8); // Transaction info grid auto* infoFrame = new QFrame(); - infoFrame->setFrameShape(QFrame::StyledPanel); + infoFrame->setFrameShape(QFrame::NoFrame); infoFrame->setStyleSheet(Style::cardFrameWithLabels()); auto* grid = new QGridLayout(infoFrame); grid->setColumnStretch(1, 1); + grid->setVerticalSpacing(10); + grid->setHorizontalSpacing(20); int row = 0; grid->addWidget(makeFieldLabel("Hash"), row, 0); @@ -64,21 +80,17 @@ TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent) grid->addWidget(hashVal, row++, 1); grid->addWidget(makeFieldLabel("Type"), row, 0); - QString typeColor; - switch (tx.type) { - case TransactionType::Public: typeColor = "#007bff"; break; - case TransactionType::PrivacyPreserving: typeColor = "#6f42c1"; break; - case TransactionType::ProgramDeployment: typeColor = "#fd7e14"; break; - } - auto* typeLabel = new QLabel(transactionTypeToString(tx.type)); - typeLabel->setStyleSheet(Style::badge(typeColor) + " max-width: 160px;"); + QString typeStr = transactionTypeToString(tx.type); + auto* typeLabel = new QLabel(typeStr); + typeLabel->setStyleSheet(Style::badge(Style::txTypeColor(typeStr))); + typeLabel->setMaximumWidth(180); grid->addWidget(typeLabel, row++, 1); // Type-specific fields switch (tx.type) { case TransactionType::Public: grid->addWidget(makeFieldLabel("Program ID"), row, 0); - grid->addWidget(makeValueLabel(tx.programId), row++, 1); + { auto* v = makeValueLabel(tx.programId); v->setStyleSheet(Style::monoText()); grid->addWidget(v, row++, 1); } grid->addWidget(makeFieldLabel("Instruction Data"), row, 0); grid->addWidget(makeValueLabel(QString("%1 items").arg(tx.instructionData.size())), row++, 1); @@ -120,22 +132,35 @@ TransactionPage::TransactionPage(const Transaction& tx, QWidget* parent) // Accounts section if (!tx.accounts.isEmpty()) { + auto* headerRow = new QHBoxLayout(); + headerRow->setContentsMargins(0, 16, 0, 6); + headerRow->setSpacing(8); + auto* headerIcon = new QLabel(); + headerIcon->setPixmap(QIcon(":/icons/user.svg").pixmap(24, 24)); auto* accHeader = new QLabel("Accounts"); QFont headerFont = accHeader->font(); - headerFont.setPointSize(16); + headerFont.setPointSize(20); headerFont.setBold(true); accHeader->setFont(headerFont); - accHeader->setStyleSheet("margin-top: 16px; margin-bottom: 4px;"); - layout->addWidget(accHeader); + accHeader->setStyleSheet(Style::sectionHeader()); + headerRow->addWidget(headerIcon); + headerRow->addWidget(accHeader); + headerRow->addStretch(); + layout->addLayout(headerRow); for (const auto& accRef : tx.accounts) { auto* frame = new ClickableFrame(); - frame->setFrameShape(QFrame::StyledPanel); + frame->setFrameShape(QFrame::NoFrame); frame->setStyleSheet(Style::clickableRowWithLabels("ClickableFrame")); auto* accRow = new QHBoxLayout(frame); + accRow->setSpacing(10); - auto* idLabel = new QLabel(accRef.accountId.left(20) + "..."); + auto* icon = new QLabel(); + icon->setPixmap(QIcon(":/icons/user.svg").pixmap(20, 20)); + accRow->addWidget(icon); + + auto* idLabel = new QLabel(accRef.accountId); QFont boldFont = idLabel->font(); boldFont.setBold(true); idLabel->setFont(boldFont); diff --git a/src/widgets/ClickableFrame.h b/src/widgets/ClickableFrame.h index 38b4661..c250672 100644 --- a/src/widgets/ClickableFrame.h +++ b/src/widgets/ClickableFrame.h @@ -1,5 +1,7 @@ #pragma once +#include "Style.h" + #include #include @@ -24,4 +26,22 @@ protected: } QFrame::mousePressEvent(event); } + + void enterEvent(QEnterEvent* event) override + { + m_baseStyleSheet = styleSheet(); + setStyleSheet(m_baseStyleSheet + + QString(" ClickableFrame { background: %1 !important; border-color: %2 !important; }") + .arg(Style::Color::surfaceHover(), Style::Color::accent())); + QFrame::enterEvent(event); + } + + void leaveEvent(QEvent* event) override + { + setStyleSheet(m_baseStyleSheet); + QFrame::leaveEvent(event); + } + +private: + QString m_baseStyleSheet; }; diff --git a/src/widgets/NavigationBar.cpp b/src/widgets/NavigationBar.cpp index 44c62ab..c45045e 100644 --- a/src/widgets/NavigationBar.cpp +++ b/src/widgets/NavigationBar.cpp @@ -1,32 +1,50 @@ #include "NavigationBar.h" +#include "Style.h" #include #include +#include +#include NavigationBar::NavigationBar(QWidget* parent) : QWidget(parent) { auto* layout = new QHBoxLayout(this); layout->setContentsMargins(0, 0, 0, 0); + layout->setSpacing(6); - m_backBtn = new QPushButton("<", this); - m_backBtn->setFixedWidth(40); - m_backBtn->setMinimumHeight(32); + const QSize iconSize(22, 22); + + m_backBtn = new QPushButton(this); + m_backBtn->setIcon(QIcon(":/icons/arrow-left.svg")); + m_backBtn->setIconSize(iconSize); + m_backBtn->setFixedSize(42, 42); m_backBtn->setEnabled(false); m_backBtn->setToolTip("Back"); + m_backBtn->setStyleSheet(Style::navButton()); - m_forwardBtn = new QPushButton(">", this); - m_forwardBtn->setFixedWidth(40); - m_forwardBtn->setMinimumHeight(32); + m_forwardBtn = new QPushButton(this); + m_forwardBtn->setIcon(QIcon(":/icons/arrow-right.svg")); + m_forwardBtn->setIconSize(iconSize); + m_forwardBtn->setFixedSize(42, 42); m_forwardBtn->setEnabled(false); m_forwardBtn->setToolTip("Forward"); + m_forwardBtn->setStyleSheet(Style::navButton()); - auto* homeBtn = new QPushButton("Home", this); - homeBtn->setMinimumHeight(32); + auto* homeBtn = new QPushButton(this); + homeBtn->setIcon(QIcon(":/icons/home.svg")); + homeBtn->setIconSize(iconSize); + homeBtn->setFixedSize(42, 42); + homeBtn->setToolTip("Home"); + homeBtn->setStyleSheet(Style::navButton()); + + auto* titleLabel = new QLabel("LEZ Explorer"); + titleLabel->setStyleSheet(QString("color: %1; font-size: 14px; font-weight: bold; margin-left: 8px;").arg(Style::Color::accent())); layout->addWidget(m_backBtn); layout->addWidget(m_forwardBtn); layout->addWidget(homeBtn); + layout->addWidget(titleLabel); layout->addStretch(); connect(m_backBtn, &QPushButton::clicked, this, &NavigationBar::backClicked); diff --git a/src/widgets/SearchBar.cpp b/src/widgets/SearchBar.cpp index 1aff9ab..3f9aece 100644 --- a/src/widgets/SearchBar.cpp +++ b/src/widgets/SearchBar.cpp @@ -1,4 +1,5 @@ #include "SearchBar.h" +#include "Style.h" #include #include @@ -8,14 +9,17 @@ SearchBar::SearchBar(QWidget* parent) : QWidget(parent) { auto* layout = new QHBoxLayout(this); - layout->setContentsMargins(0, 0, 0, 0); + layout->setContentsMargins(0, 4, 0, 4); + layout->setSpacing(8); m_input = new QLineEdit(this); m_input->setPlaceholderText("Search by block ID / block hash / tx hash / account ID..."); - m_input->setMinimumHeight(32); + m_input->setMinimumHeight(38); + m_input->setStyleSheet(Style::searchInput()); auto* searchBtn = new QPushButton("Search", this); - searchBtn->setMinimumHeight(32); + searchBtn->setMinimumHeight(38); + searchBtn->setStyleSheet(Style::searchButton()); layout->addWidget(m_input, 1); layout->addWidget(searchBtn);