feat: better styling

This commit is contained in:
Daniil Polyakov 2026-04-02 16:14:26 +03:00
parent 871765de2d
commit 578f0d2b15
22 changed files with 520 additions and 141 deletions

View File

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

View File

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

View File

@ -14,6 +14,7 @@
buildInputs = [
pkgs.qt6.qtbase
pkgs.qt6.qtsvg
];
cmakeFlags = [

View File

@ -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<MockIndexerService>())
{
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);

View File

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

View File

@ -0,0 +1,12 @@
<RCC>
<qresource prefix="/">
<file>icons/arrow-left.svg</file>
<file>icons/arrow-right.svg</file>
<file>icons/home.svg</file>
<file>icons/search.svg</file>
<file>icons/box.svg</file>
<file>icons/file-text.svg</file>
<file>icons/user.svg</file>
<file>icons/activity.svg</file>
</qresource>
</RCC>

3
src/icons/activity.svg Normal file
View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#a6e3a1" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>
</svg>

After

Width:  |  Height:  |  Size: 216 B

4
src/icons/arrow-left.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="19" y1="12" x2="5" y2="12"/>
<polyline points="12 19 5 12 12 5"/>
</svg>

After

Width:  |  Height:  |  Size: 241 B

View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="5" y1="12" x2="19" y2="12"/>
<polyline points="12 5 19 12 12 19"/>
</svg>

After

Width:  |  Height:  |  Size: 242 B

5
src/icons/box.svg Normal file
View File

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/>
<polyline points="3.27 6.96 12 12.01 20.73 6.96"/>
<line x1="12" y1="22.08" x2="12" y2="12"/>
</svg>

After

Width:  |  Height:  |  Size: 395 B

7
src/icons/file-text.svg Normal file
View File

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/>
<polyline points="14 2 14 8 20 8"/>
<line x1="16" y1="13" x2="8" y2="13"/>
<line x1="16" y1="17" x2="8" y2="17"/>
<polyline points="10 9 9 9 8 9"/>
</svg>

After

Width:  |  Height:  |  Size: 390 B

4
src/icons/home.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"/>
<polyline points="9 22 9 12 15 12 15 22"/>
</svg>

After

Width:  |  Height:  |  Size: 267 B

4
src/icons/search.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#cdd6f4" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="8"/>
<line x1="21" y1="21" x2="16.65" y2="16.65"/>
</svg>

After

Width:  |  Height:  |  Size: 243 B

4
src/icons/user.svg Normal file
View File

@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#89b4fa" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
<circle cx="12" cy="7" r="4"/>
</svg>

After

Width:  |  Height:  |  Size: 250 B

View File

@ -8,6 +8,7 @@
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
#include <QIcon>
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);

View File

@ -8,9 +8,34 @@
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
#include <QIcon>
#include <QEvent>
#include <QMouseEvent>
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<QMouseEvent*>(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("<a href='#' style='color: #007bff;'>%1</a>").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) {

View File

@ -7,16 +7,22 @@
#include <QLabel>
#include <QScrollArea>
#include <QFrame>
#include <QPixmap>
#include <QPushButton>
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);
}

View File

@ -3,9 +3,11 @@
#include "services/IndexerService.h"
#include <QWidget>
#include <optional>
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<quint64> m_oldestLoadedBlockId;
};

View File

@ -8,6 +8,7 @@
#include <QScrollArea>
#include <QFrame>
#include <QGridLayout>
#include <QIcon>
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);

View File

@ -1,5 +1,7 @@
#pragma once
#include "Style.h"
#include <QFrame>
#include <QMouseEvent>
@ -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;
};

View File

@ -1,32 +1,50 @@
#include "NavigationBar.h"
#include "Style.h"
#include <QHBoxLayout>
#include <QPushButton>
#include <QLabel>
#include <QIcon>
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);

View File

@ -1,4 +1,5 @@
#include "SearchBar.h"
#include "Style.h"
#include <QHBoxLayout>
#include <QLineEdit>
@ -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);