feat(ui): implement UI/UX audit — i18n, HiDPI, theme, destructive-action & overflow fixes
Implements all 26 confirmed UI/UX audit findings plus the 4 dashboard/timeline tile labels. Verified across full-node + Lite + Windows builds (ctest green) and an adversarial diff review (one market-tab delete regression caught + fixed). i18n coverage: - Balance hero/quick-actions/toasts + dashboard & timeline tiles (Total Balance, Shielded, Transparent, Quick Send, Quick Receive, Click to open, Market) - Send: Review Send / Cancel / Paste / view-only tooltip / memo byte counter - Settings: Lite lifecycle errors, plaintext-RPC security warning, 8 toasts - Mining pool-payout tooltip - Resolve daemon_update_title double-assignment collision (new daemon_update_prompt_title) - 23 new keys translated into all 8 locales; CJK subset font rebuilt HiDPI: DPI-scale the Send amount bar, mining thread-tile clamp bounds, receive loading skeleton, and sidebar notification-badge insets. Light theme: theme-aware material::SurfaceOverlay() for balance bar tracks and row-hover highlights; contacts active-pill foreground uses OnPrimary(). Destructive actions: arm/confirm for portfolio-group delete, saved pool/worker remove, and avatar-image delete. Interaction/overflow: route balance star/eye buttons and the console fold-toggle through popup-safe guards; clip peer addr/subver; truncate the balance custom label; measure Settings tool-button widths; mining stepper disabled-state feedback; +/- stepper buttons restyled to match the thread tiles. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,7 @@
|
||||
#include "imgui.h"
|
||||
#include <toml++/toml.hpp>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <cstring>
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
@@ -196,7 +197,9 @@ void RenderBalanceTab(App* app)
|
||||
for (const auto& l : allLayouts) {
|
||||
if (l.id == layoutId) { displayName = l.name; break; }
|
||||
}
|
||||
Notifications::instance().info("Layout: " + displayName);
|
||||
char layoutToast[128];
|
||||
snprintf(layoutToast, sizeof(layoutToast), TR("balance_layout_switched"), displayName.c_str());
|
||||
Notifications::instance().info(layoutToast);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -359,8 +362,11 @@ static void RenderBalanceClassic(App* app)
|
||||
IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.classic", "logo-opacity").sizeOr(180.0f)));
|
||||
}
|
||||
|
||||
std::string totalLabelUpper = TR("total_balance_label");
|
||||
std::transform(totalLabelUpper.begin(), totalLabelUpper.end(), totalLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
OnSurfaceMedium(), totalLabelUpper.c_str());
|
||||
cy += ovFont->LegacySize + ovGap;
|
||||
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
@@ -387,7 +393,7 @@ static void RenderBalanceClassic(App* app)
|
||||
// Sync progress or mining indicator (whichever fits)
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
|
||||
snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct);
|
||||
snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy),
|
||||
Warning(), buf);
|
||||
|
||||
@@ -401,7 +407,7 @@ static void RenderBalanceClassic(App* app)
|
||||
dl->PushClipRect(ImVec2(cMin.x, barTop), cMax, true);
|
||||
// Background track
|
||||
dl->AddRectFilled(cMin, cMax,
|
||||
IM_COL32(255, 255, 255, 15), cardSpec.rounding);
|
||||
SurfaceOverlay(15), cardSpec.rounding);
|
||||
// Progress fill — additional horizontal clip
|
||||
float progRight = cMin.x + (cMax.x - cMin.x) * prog;
|
||||
dl->PushClipRect(ImVec2(cMin.x, barTop), ImVec2(progRight, cMax.y), true);
|
||||
@@ -418,7 +424,11 @@ static void RenderBalanceClassic(App* app)
|
||||
dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f),
|
||||
S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol);
|
||||
double hr = state.mining.localHashrate;
|
||||
snprintf(buf, sizeof(buf), " Mining %s", FormatHashrate(hr).c_str());
|
||||
// Leading indent clears the mining dot drawn at cx+4dp; the text
|
||||
// itself starts at cx+12dp so keep the two spaces for spacing parity.
|
||||
char mineFmt[64];
|
||||
snprintf(mineFmt, sizeof(mineFmt), " %s", TR("balance_mining_rate"));
|
||||
snprintf(buf, sizeof(buf), mineFmt, FormatHashrate(hr).c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(cx + 12 * dp, cy),
|
||||
WithAlpha(Success(), 200), buf);
|
||||
@@ -574,15 +584,18 @@ static void RenderBalanceClassic(App* app)
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "USD");
|
||||
|
||||
// Measure widest text line to determine sparkline left edge
|
||||
std::string marketLabel = TR("market");
|
||||
std::transform(marketLabel.begin(), marketLabel.end(), marketLabel.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
float textW = std::max(pSz.x + tickGap + usdSz.x,
|
||||
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, "MARKET").x);
|
||||
ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, marketLabel.c_str()).x);
|
||||
float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f);
|
||||
float sparkLeft = cx + textW + sparkGap;
|
||||
float sparkRight = cMax.x - cardPadLg;
|
||||
|
||||
// Left side: label + price + 24h change
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy),
|
||||
OnSurfaceMedium(), "MARKET");
|
||||
OnSurfaceMedium(), marketLabel.c_str());
|
||||
cy += ovFont->LegacySize + ovGap;
|
||||
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy),
|
||||
@@ -682,7 +695,7 @@ static void RenderBalanceDonut(App* app) {
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.donut", "hero-pad-ratio").sizeOr(8.0f) * vs));
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
|
||||
ImGui::Dummy(ImVec2(0, 2 * dp));
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImFont* heroFont = Type().h2();
|
||||
@@ -947,10 +960,13 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
|
||||
// Shielded bar
|
||||
float shieldX = cardMin.x + pad;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), "SHIELDED");
|
||||
std::string shieldLabelUpper = TR("shielded");
|
||||
std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), shieldLabelUpper.c_str());
|
||||
barY += ovFont->LegacySize + 4 * dp;
|
||||
dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW * shieldRatio, barY + barH),
|
||||
WithAlpha(Success(), 180), barH * 0.5f);
|
||||
barY += barH + 2 * dp;
|
||||
@@ -960,10 +976,13 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
// Transparent bar
|
||||
float transX = cardMin.x + pad * 2 + halfW;
|
||||
barY = divY + Layout::spacingSm();
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), "TRANSPARENT");
|
||||
std::string transLabelUpper = TR("transparent");
|
||||
std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), transLabelUpper.c_str());
|
||||
barY += ovFont->LegacySize + 4 * dp;
|
||||
dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW * transRatio, barY + barH),
|
||||
WithAlpha(Warning(), 180), barH * 0.5f);
|
||||
barY += barH + 2 * dp;
|
||||
@@ -982,7 +1001,7 @@ static void RenderBalanceConsolidated(App* app) {
|
||||
dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), cardMax, true);
|
||||
// Background track
|
||||
dl->AddRectFilled(cardMin, cardMax,
|
||||
IM_COL32(255, 255, 255, 15), glassRound);
|
||||
SurfaceOverlay(15), glassRound);
|
||||
// Progress fill — additional horizontal clip
|
||||
float progRight = cardMin.x + (cardMax.x - cardMin.x) * prog;
|
||||
dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), ImVec2(progRight, cardMax.y), true);
|
||||
@@ -1075,11 +1094,20 @@ static void RenderBalanceDashboard(App* app) {
|
||||
snprintf(shBuf, sizeof(shBuf), "%.8f", s_dispShielded);
|
||||
snprintf(trBuf, sizeof(trBuf), "%.8f", s_dispTransparent);
|
||||
|
||||
// Localized captions — raw dl->AddText (no auto-uppercase), so uppercase to preserve the caption look.
|
||||
auto upperTR = [](const char* key) {
|
||||
std::string s = TR(key);
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
|
||||
return s;
|
||||
};
|
||||
std::string lblShielded = upperTR("shielded"), lblTransparent = upperTR("transparent");
|
||||
std::string lblQuickSend = upperTR("quick_send"), lblQuickReceive = upperTR("quick_receive");
|
||||
|
||||
TileInfo tiles[4] = {
|
||||
{"SHIELDED", shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
|
||||
{"TRANSPARENT", trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
|
||||
{"QUICK SEND", "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
|
||||
{"QUICK RECEIVE", "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
|
||||
{lblShielded.c_str(), shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false},
|
||||
{lblTransparent.c_str(), trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false},
|
||||
{lblQuickSend.c_str(), "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true},
|
||||
{lblQuickReceive.c_str(), "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true},
|
||||
};
|
||||
|
||||
for (int i = 0; i < 4; i++) {
|
||||
@@ -1125,7 +1153,7 @@ static void RenderBalanceDashboard(App* app) {
|
||||
tiles[i].accent, tiles[i].value);
|
||||
} else {
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py),
|
||||
OnSurfaceMedium(), "Click to open");
|
||||
OnSurfaceMedium(), TR("tile_click_to_open"));
|
||||
}
|
||||
|
||||
// Click
|
||||
@@ -1279,7 +1307,7 @@ static void RenderBalanceVerticalStack(App* app) {
|
||||
float barW = barRight - barLeft;
|
||||
float barY = rowPos.y + (rowH - barH) * 0.5f;
|
||||
dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barRight, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(barLeft, barY),
|
||||
ImVec2(barLeft + barW * rowInfos[i].ratio, barY + barH),
|
||||
WithAlpha(rowInfos[i].accent, 180), barH * 0.5f);
|
||||
@@ -1473,7 +1501,7 @@ static void RenderBalanceVertical2x2(App* app) {
|
||||
float barX = cellMax.x - amtSz.x - rowPad - barW - Layout::spacingSm();
|
||||
float barY = cellMin.y + (rowH - barH) * 0.5f;
|
||||
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||
IM_COL32(255, 255, 255, 15), barH * 0.5f);
|
||||
SurfaceOverlay(15), barH * 0.5f);
|
||||
dl->AddRectFilled(ImVec2(barX, barY),
|
||||
ImVec2(barX + barW * cell.ratio, barY + barH),
|
||||
WithAlpha(cell.accent, 180), barH * 0.5f);
|
||||
@@ -1551,7 +1579,7 @@ static void RenderBalanceShield(App* app) {
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
|
||||
ImGui::Dummy(ImVec2(0, 2 * dp));
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImFont* heroFont = Type().h2();
|
||||
@@ -1665,13 +1693,19 @@ static void RenderBalanceShield(App* app) {
|
||||
float infoY = panelMin.y + shieldPad;
|
||||
ImFont* ovFont = Type().overline();
|
||||
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), "SHIELDED");
|
||||
std::string shieldLabelUpper = TR("shielded");
|
||||
std::transform(shieldLabelUpper.begin(), shieldLabelUpper.end(), shieldLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
std::string transLabelUpper = TR("transparent");
|
||||
std::transform(transLabelUpper.begin(), transLabelUpper.end(), transLabelUpper.begin(),
|
||||
[](unsigned char c){ return (char)std::toupper(c); });
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), shieldLabelUpper.c_str());
|
||||
infoY += ovFont->LegacySize + 2 * dp;
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispShielded);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Success(), buf);
|
||||
infoY += capFont->LegacySize + 6 * dp;
|
||||
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), "TRANSPARENT");
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), transLabelUpper.c_str());
|
||||
infoY += ovFont->LegacySize + 2 * dp;
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Warning(), buf);
|
||||
@@ -1727,7 +1761,7 @@ static void RenderBalanceTimeline(App* app) {
|
||||
else
|
||||
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs));
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE");
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("total_balance_label"));
|
||||
ImGui::Dummy(ImVec2(0, 2 * dp));
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImFont* heroFont = Type().h2();
|
||||
@@ -1816,10 +1850,16 @@ static void RenderBalanceTimeline(App* app) {
|
||||
spec.rounding = glassRound;
|
||||
|
||||
struct SumCard { const char* label; ImU32 col; double val; bool isMoney; };
|
||||
auto upperTR = [](const char* key) {
|
||||
std::string s = TR(key);
|
||||
std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c){ return (char)std::toupper(c); });
|
||||
return s;
|
||||
};
|
||||
std::string cShielded = upperTR("shielded"), cTransparent = upperTR("transparent"), cMarket = upperTR("market");
|
||||
SumCard cards[3] = {
|
||||
{"SHIELDED", Success(), s_dispShielded, false},
|
||||
{"TRANSPARENT", Warning(), s_dispTransparent, false},
|
||||
{"MARKET", Primary(), state.market.price_usd, true},
|
||||
{cShielded.c_str(), Success(), s_dispShielded, false},
|
||||
{cTransparent.c_str(), Warning(), s_dispTransparent, false},
|
||||
{cMarket.c_str(), Primary(), state.market.price_usd, true},
|
||||
};
|
||||
for (int i = 0; i < 3; i++) {
|
||||
ImVec2 cMin(origin.x + i * (cardW + cGap), origin.y);
|
||||
@@ -1887,13 +1927,13 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
ImFont* capFont = Type().caption();
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float pct = static_cast<float>(state.sync.verification_progress) * 100.0f;
|
||||
snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct);
|
||||
snprintf(buf, sizeof(buf), TR("balance_syncing_pct"), pct);
|
||||
Type().textColored(TypeStyle::Caption, Warning(), buf);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
if (state.mining.generate) {
|
||||
double hr = state.mining.localHashrate;
|
||||
snprintf(buf, sizeof(buf), "Mining %s", FormatHashrate(hr).c_str());
|
||||
snprintf(buf, sizeof(buf), TR("balance_mining_rate"), FormatHashrate(hr).c_str());
|
||||
Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf);
|
||||
ImGui::SameLine();
|
||||
}
|
||||
@@ -1902,11 +1942,15 @@ static void RenderBalanceTwoRow(App* app) {
|
||||
float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f);
|
||||
float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg();
|
||||
ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm());
|
||||
if (TactileButton("Send", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
// Stable ## ids keep the button identity fixed across translations.
|
||||
char sendBtn[64], recvBtn[64];
|
||||
snprintf(sendBtn, sizeof(sendBtn), "%s##tworow-send", TR("send"));
|
||||
snprintf(recvBtn, sizeof(recvBtn), "%s##tworow-receive", TR("receive"));
|
||||
if (TactileButton(sendBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton("Receive", ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
if (TactileButton(recvBtn, ImVec2(btnW, 0), S.resolveFont("button"))) {
|
||||
app->setCurrentPage(NavPage::Receive);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user