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:
2026-08-18 17:54:15 -05:00
co-authored by Claude Opus 4.8
parent 0de44569b7
commit e24ca015d1
24 changed files with 599 additions and 148 deletions
+50 -13
View File
@@ -665,14 +665,26 @@ void RenderPeersTab(App* app)
dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol);
float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
// Reserve the line-1 trailing zone (ping + direction pill live on the far right,
// the nearest being the ping at innerW - pingW - spacingXl*3). A long IPv6+port
// addr must clip before it so it can't run under those. Mirror the ping formula
// conservatively and leave a gap.
float line1RightLimit = rowPos.x + innerW - Layout::spacingXl() * 3 -
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "0000ms").x -
Layout::spacingSm();
dl->PushClipRect(ImVec2(addrX, rowPos.y), ImVec2(line1RightLimit, rowEnd.y), true);
dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str());
dl->PopClipRect();
// Seed node icon — rendered right after the IP address
// Seed node icon — rendered right after the IP address, but never past the
// reserved trailing zone (a long addr would otherwise push it into the ping text).
if (IsSeedNode(peer.addr)) {
ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str());
ImFont* iconFont = Type().iconSmall();
float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f;
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
float seedIconX = std::min(addrX + addrSz.x + Layout::spacingSm(),
line1RightLimit - iconFont->LegacySize);
dl->AddText(iconFont, iconFont->LegacySize, ImVec2(seedIconX, iconY), WithAlpha(Success(), 200), ICON_MD_GRASS);
}
{
@@ -696,30 +708,55 @@ void RenderPeersTab(App* app)
}
float cy2 = cy + body2->LegacySize + Layout::spacingXs();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy2),
OnSurfaceDisabled(), peer.subver.c_str());
float subverX = cx + S.drawElement("tabs.peers", "address-x-offset").size;
// Reserve the line-2 trailing widths up front so an untrusted, arbitrarily long
// remote subver cannot push the TLS badge / ban-score off the row. Order right→left:
// ban-score pinned far right, then the TLS/no-TLS badge, then the clipped subver text.
float line2RightLimit = rowPos.x + innerW - Layout::spacingLg();
float banScoreLeftX = line2RightLimit;
char banBuf[16];
ImU32 banCol = 0;
bool haveBanScore = peer.banscore > 0;
if (haveBanScore) {
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
banCol = peer.banscore > 50 ? Error() : Warning();
float banW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf).x;
banScoreLeftX = line2RightLimit - banW;
// The TLS/no-TLS badge sits to the LEFT of the ban score with a gap.
line2RightLimit = banScoreLeftX - Layout::spacingLg();
}
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
float noTlsW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("peers_no_tls")).x;
float badgeW = peer.tls_cipher.empty() ? noTlsW : tlsBadgeW;
// Hard right boundary for the subver text column, leaving room for the badge.
float subverMaxX = line2RightLimit - badgeW - Layout::spacingSm();
if (subverMaxX < subverX) subverMaxX = subverX;
float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x;
float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs);
dl->PushClipRect(ImVec2(subverX, rowPos.y), ImVec2(subverMaxX, rowEnd.y), true);
dl->AddText(capFont, capFont->LegacySize, ImVec2(subverX, cy2),
OnSurfaceDisabled(), peer.subver.c_str());
dl->PopClipRect();
// Pin the badge just after the (clipped) subver, but never past the reserved zone.
float badgeX = std::min(subverX + verW + Layout::spacingSm(), subverMaxX + Layout::spacingSm());
if (!peer.tls_cipher.empty()) {
ImU32 tlsBg = WithAlpha(Success(), 25);
ImU32 tlsFg = WithAlpha(Success(), 200);
ImVec2 tlsMin(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2);
ImVec2 tlsMin(badgeX, cy2);
ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2);
dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size);
dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS");
} else {
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2),
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, cy2),
WithAlpha(Error(), 140), TR("peers_no_tls"));
}
if (peer.banscore > 0) {
char banBuf[16];
snprintf(banBuf, sizeof(banBuf), TR("peers_ban_score"), peer.banscore);
ImU32 banCol = peer.banscore > 50 ? Error() : Warning();
ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf);
if (haveBanScore) {
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingLg(), cy2), banCol, banBuf);
ImVec2(banScoreLeftX, cy2), banCol, banBuf);
}
ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH));