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
+18 -6
View File
@@ -40,6 +40,7 @@ static bool s_show_edit_dialog = false;
static bool s_show_contacts_settings = false; // Contacts customization modal (gear button)
static bool s_focus_edit_field = false; // focus the first field the frame the add/edit dialog opens
static int s_confirm_delete_idx = -1; // armed storage index; a 2nd Delete confirms
static int s_confirm_avatar_del_idx = -1; // armed avatar-library index; a 2nd badge click confirms the (irreversible) file delete
static char s_edit_label[128] = "";
static char s_edit_address[512] = "";
static char s_edit_notes[512] = "";
@@ -555,7 +556,7 @@ void RenderContactsTab(App* app)
sdl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp),
ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp),
material::WithAlpha(material::Primary(), 210), (segH - 4.0f * dp) * 0.5f);
ImU32 fg = active ? IM_COL32(255, 255, 255, 255) : (hov ? material::OnSurface() : material::OnSurfaceMedium());
ImU32 fg = active ? material::OnPrimary() : (hov ? material::OnSurface() : material::OnSurfaceMedium());
float igW = segIcoF->CalcTextSizeA(segIcoF->LegacySize, FLT_MAX, 0, segIco[i]).x;
float lbW = btnFont->CalcTextSizeA(btnFont->LegacySize, FLT_MAX, 0, segLbl[i]).x;
float gapI = 5.0f * dp;
@@ -747,12 +748,16 @@ void RenderContactsTab(App* app)
float dr = std::max(7.0f * dp, cell * 0.15f);
ImVec2 dcc(mx.x - dr - 3.0f * dp, mn.y + dr + 3.0f * dp);
bool dhov = ImGui::IsMouseHoveringRect(ImVec2(dcc.x-dr, dcc.y-dr), ImVec2(dcc.x+dr, dcc.y+dr));
const int libIdx = n - 1;
const bool delArmed = (s_confirm_avatar_del_idx == libIdx);
ImGui::PushID(n);
bool thumbClicked = ImGui::InvisibleButton("##avthumb", ImVec2(cell, cell));
bool thumbHov = ImGui::IsItemHovered();
ImGui::PopID();
if (thumbHov || sel || hov) { // draw the delete badge
gdl->AddCircleFilled(dcc, dr, dhov ? material::ReadableError() : IM_COL32(0, 0, 0, 175));
if (thumbHov || sel || hov || delArmed) { // draw the delete badge (persist while armed)
gdl->AddCircleFilled(dcc, dr, (dhov || delArmed) ? material::ReadableError() : IM_COL32(0, 0, 0, 175));
// While armed, ring the badge so the "click again to delete" state is unmistakable.
if (delArmed) gdl->AddCircle(dcc, dr + 1.5f * dp, material::ReadableError(), 0, 1.5f * dp);
ImFont* xf = material::Type().iconSmall();
float xsz = dr * 1.35f;
ImVec2 xs = xf->CalcTextSizeA(xsz, FLT_MAX, 0, ICON_MD_CLOSE);
@@ -760,11 +765,18 @@ void RenderContactsTab(App* app)
}
if (dhov) { // badge takes priority over selecting the thumbnail
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("delete"));
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) pendingDelete = n - 1;
// Two-stage confirm: the first click arms this badge; a second click on the SAME
// badge deletes the image file (fs::remove is irreversible — no undo).
material::Tooltip("%s", delArmed ? TR("address_book_confirm_delete") : TR("delete"));
if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
if (delArmed) { pendingDelete = libIdx; s_confirm_avatar_del_idx = -1; }
else s_confirm_avatar_del_idx = libIdx; // arm; requires a 2nd deliberate click
}
} else {
if (thumbHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (thumbClicked) s_edit_avatar = "img:" + path;
// Any click that lands off this badge (selecting the thumbnail or elsewhere)
// disarms it, so a stale armed badge can't be confirmed by an unrelated click.
if (thumbClicked) { s_edit_avatar = "img:" + path; s_confirm_avatar_del_idx = -1; }
}
}
col = (col + 1) % cols;