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
+27 -4
View File
@@ -137,6 +137,12 @@ struct PfEditState {
};
static PfEditState s_pfEdit;
// Two-stage delete arm/confirm for the master-list per-row delete icon (mirrors the
// contacts tab's s_confirm_delete_idx): first click on a row's trash arms it (icon turns
// red); a second click on the SAME row within the loop confirms the erase. Hovering/clicking
// a different row, or clicking off the icons, disarms. -1 = nothing armed.
static int s_pfConfirmDelIdx = -1;
// The pure price-series math lives in data/market_series.h; the selected chart range is
// s_mkt.chartInterval (0=Live 1=1H 2=1D 3=1W 4=1M, session-scoped, default 1M).
@@ -837,6 +843,7 @@ static void RenderPortfolioEditor(App* app)
float rowH = 46.0f * dp;
float delSlot = 36.0f * dp; // reserved trailing space for the delete icon + margin
int clickedSel = -999, delRow = -1; // deferred: don't mutate `entries` mid-loop
bool armedThisFrame = false; // an arm-click also fires the row Selectable; don't let it disarm
for (int vi = 0; vi < (int)vis.size(); vi++) {
int i = vis[vi];
ImGui::PushID(i);
@@ -877,6 +884,7 @@ static void RenderPortfolioEditor(App* app)
// Per-row delete icon (larger, inset from the edge). Hit-tested manually so it is
// NOT an overlapping ImGui item over the row Selectable (which asserted on hover).
if (rowHov || selRow) {
bool armed = (s_pfConfirmDelIdx == i); // this row is arm-confirmed
ImFont* delFont = Type().iconMed();
float ds = delFont->LegacySize;
ImVec2 dc(rmx.x - Layout::spacingMd() - ds * 0.5f, rmn.y + rowH * 0.5f);
@@ -884,11 +892,20 @@ static void RenderPortfolioEditor(App* app)
bool dhov = ImGui::IsMouseHoveringRect(dmn, dmx);
if (dhov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) delRow = i;
// Hovering a *different* row's trash disarms the previously armed one.
if (!armed && s_pfConfirmDelIdx >= 0) s_pfConfirmDelIdx = -1;
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (armed) delRow = i; // 2nd click on the armed row erases
else { s_pfConfirmDelIdx = i; // 1st click arms this row...
armedThisFrame = true; } // ...and must survive the row-select this same click triggers
}
}
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, ICON_MD_DELETE_OUTLINE);
// A filled trash glyph while armed (vs outline) reinforces the recolor.
const char* delGlyph = armed ? ICON_MD_DELETE : ICON_MD_DELETE_OUTLINE;
ImU32 delCol = armed ? ReadableError() : (dhov ? Error() : OnSurfaceMedium());
ImVec2 isz = delFont->CalcTextSizeA(ds, FLT_MAX, 0, delGlyph);
mdl->AddText(delFont, ds, ImVec2(dc.x - isz.x * 0.5f, dc.y - isz.y * 0.5f),
dhov ? Error() : OnSurfaceMedium(), ICON_MD_DELETE_OUTLINE);
delCol, delGlyph);
}
ImGui::PopID();
}
@@ -905,10 +922,16 @@ static void RenderPortfolioEditor(App* app)
else if (delRow == s_pfEdit.sel) s_pfEdit.sel = std::min(delRow, (int)es.size() - 1);
PortfolioBeginEdit(app, s_pfEdit.sel);
}
} else if (clickedSel != -999) {
s_pfConfirmDelIdx = -1; // erase done — storage indices shifted, drop the arm
} else if (clickedSel != -999 && !armedThisFrame) {
pfCommitIfNeeded(app); // auto-save the current group before switching
PortfolioBeginEdit(app, clickedSel);
s_pfConfirmDelIdx = -1; // switching rows disarms any pending delete
}
// A left-click that hit no row/action (empty list space) disarms a pending delete.
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left) &&
!ImGui::IsAnyItemHovered())
s_pfConfirmDelIdx = -1;
}
ImGui::EndChild();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));