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
+35 -21
View File
@@ -83,7 +83,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
// over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%).
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap;
float innerW = estControlsW - pad * 2;
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size * dp, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f) * dp);
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
int cols = std::max(1, std::min(nOpts, (int)(innerW / (cellSz + cellGap))));
int rows = (nOpts + cols - 1) / cols;
@@ -162,14 +162,35 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
const float sy = curY;
ImVec2 savedCur = ImGui::GetCursorScreenPos();
material::IconButtonStyle sideStyle;
sideStyle.color = OnSurfaceMedium();
sideStyle.hoverBg = StateHover();
// -/+ buttons drawn in the same style as the thread tiles (rounded rect + border + centered
// glyph, same fill/border/rounding). Hit-tested with a real InvisibleButton so they stay
// popup-safe.
const float stepRound = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
// atBound: the stepper can't move further (already at 1 for "-" or max for "+"). When
// at the bound the control is a no-op, so render it disabled: dim glyph/border, no hand
// cursor, no tooltip — and swallow the click so it reads as inert.
auto rectStepBtn = [&](const char* id, const char* glyph, float x, const char* tip, bool atBound) -> bool {
ImGui::SetCursorScreenPos(ImVec2(x, sy));
ImGui::InvisibleButton(id, ImVec2(sideW, fieldH));
const bool hov = ImGui::IsItemHovered() && !atBound;
const bool clk = ImGui::IsItemClicked() && !atBound;
const ImVec2 mn(x, sy), mx(x + sideW, sy + fieldH);
dl->AddRectFilled(mn, mx, hov ? WithAlpha(OnSurface(), 25) : WithAlpha(OnSurface(), 8), stepRound);
dl->AddRect(mn, mx, WithAlpha(OnSurface(), atBound ? 15 : (hov ? 80 : 35)), stepRound);
const ImVec2 gsz = stepFont->CalcTextSizeA(stepFont->LegacySize, FLT_MAX, 0, glyph);
dl->AddText(stepFont, stepFont->LegacySize,
ImVec2(x + (sideW - gsz.x) * 0.5f, sy + (fieldH - gsz.y) * 0.5f),
WithAlpha(OnSurface(), atBound ? 30 : (hov ? 160 : 80)), glyph);
if (hov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (tip && tip[0]) material::Tooltip("%s", tip);
}
return clk;
};
// "-" button
sideStyle.tooltip = TR("mining_threads_minus_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
if (material::IconButton("##ThreadMinus", ICON_MD_REMOVE, stepFont, ImVec2(sideW, fieldH), sideStyle))
// "-" button (disabled at 1 thread)
if (rectStepBtn("##ThreadMinus", ICON_MD_REMOVE, sx, TR("mining_threads_minus_tooltip"),
s_selected_threads <= 1))
applyThreads(s_selected_threads - 1);
sx += sideW + g;
@@ -188,10 +209,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
ImGui::PopStyleVar();
sx += fieldW + g;
// "+" button
sideStyle.tooltip = TR("mining_threads_plus_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
if (material::IconButton("##ThreadPlus", ICON_MD_ADD, stepFont, ImVec2(sideW, fieldH), sideStyle))
// "+" button (disabled at max_threads)
if (rectStepBtn("##ThreadPlus", ICON_MD_ADD, sx, TR("mining_threads_plus_tooltip"),
s_selected_threads >= max_threads))
applyThreads(s_selected_threads + 1);
ImGui::SetCursorScreenPos(savedCur);
@@ -738,15 +758,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
} else if (active) {
// Active but not mining: solid primary fill
ImU32 pri = Primary();
int priR = (pri >> 0) & 0xFF;
int priG = (pri >> 8) & 0xFF;
int priB = (pri >> 16) & 0xFF;
ImU32 fillCol = hovered
? IM_COL32(priR, priG, priB, 220)
: IM_COL32(priR, priG, priB, 180);
ImU32 fillCol = material::WithAlpha(Primary(), hovered ? 220 : 180);
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
dl->AddRect(cMin, cMax, material::WithAlpha(Primary(), 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
} else {
// Inactive: dim outline
ImU32 fillCol = hovered
@@ -938,7 +952,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
else if (poolBlockedBySolo)
material::Tooltip("%s", TR("mining_stop_solo_for_pool"));
else if (poolNeedsPayout)
material::Tooltip("%s", "Enter a payout address first (generate a Z address)");
material::Tooltip("%s", TR("mining_pool_needs_payout_tooltip"));
else
material::Tooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
}