feat: Full UI internationalization, pool hashrate stats, and layout caching

- Replace all hardcoded English strings with TR() translation keys across
  every tab, dialog, and component (~20 UI files)
- Expand all 8 language files (de, es, fr, ja, ko, pt, ru, zh) with
  complete translations (~37k lines added)
- Improve i18n loader with exe-relative path fallback and English base
  fallback for missing keys
- Add pool-side hashrate polling via pool stats API in xmrig_manager
- Introduce Layout::beginFrame() per-frame caching and refresh balance
  layout config only on schema generation change
- Offload daemon output parsing to worker thread
- Add CJK subset fallback font for Chinese/Japanese/Korean glyphs
This commit is contained in:
dan_s
2026-03-11 00:40:50 -05:00
parent cc617dd5be
commit 96c27bb949
71 changed files with 43567 additions and 5267 deletions

View File

@@ -214,7 +214,7 @@ static void RenderSourceDropdown(App* app, float width) {
auto& S = schema::UI();
char buf[256];
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "SENDING FROM");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_sending_from"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Auto-select the address with the largest balance on first load
@@ -237,7 +237,7 @@ static void RenderSourceDropdown(App* app, float width) {
// Build preview string for selected address
if (!app->isConnected()) {
s_source_preview = "Not connected to daemon";
s_source_preview = TR("not_connected");
} else if (s_selected_from_idx >= 0 &&
s_selected_from_idx < (int)state.addresses.size()) {
const auto& addr = state.addresses[s_selected_from_idx];
@@ -249,7 +249,7 @@ static void RenderSourceDropdown(App* app, float width) {
tag, trunc.c_str(), addr.balance, DRAGONX_TICKER);
s_source_preview = buf;
} else {
s_source_preview = "Select a source address...";
s_source_preview = TR("send_select_source");
}
ImGui::SetNextItemWidth(width);
@@ -257,7 +257,7 @@ static void RenderSourceDropdown(App* app, float width) {
ImGui::PushFont(Type().getFont(TypeStyle::Body2));
if (ImGui::BeginCombo("##SendFrom", s_source_preview.c_str())) {
if (!app->isConnected() || state.addresses.empty()) {
ImGui::TextDisabled("No addresses available");
ImGui::TextDisabled("%s", TR("no_addresses_available"));
} else {
// Sort by balance descending, only show addresses with balance
std::vector<size_t> sortedIdx;
@@ -272,7 +272,7 @@ static void RenderSourceDropdown(App* app, float width) {
});
if (sortedIdx.empty()) {
ImGui::TextDisabled("No addresses with balance");
ImGui::TextDisabled("%s", TR("send_no_balance"));
} else {
size_t addrTruncLen = static_cast<size_t>(std::max(S.drawElement("tabs.send", "addr-dropdown-trunc-min").size, width / S.drawElement("tabs.send", "addr-dropdown-trunc-divisor").size));
@@ -352,7 +352,7 @@ static void RenderAddressSuggestions(const WalletState& state, float width, cons
// ============================================================================
static void RenderFeeTierSelector(const char* suffix = "") {
auto& S = schema::UI();
const char* feeLabels[] = { "Low", "Normal", "High" };
const char* feeLabels[] = { TR("send_fee_low"), TR("send_fee_normal"), TR("send_fee_high") };
const double feeValues[] = { FEE_LOW, FEE_NORMAL, FEE_HIGH };
ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0));
@@ -569,12 +569,12 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-bg-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.send", "error-btn-hover-alpha").size)));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.send", "error-btn-rounding").size);
if (TactileSmallButton("Copy Error##txErr", schema::UI().resolveFont("button"))) {
if (TactileSmallButton(TR("send_copy_error"), schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(s_tx_status.c_str());
Notifications::instance().info("Error copied to clipboard");
Notifications::instance().info(TR("send_error_copied"));
}
ImGui::SameLine();
if (TactileSmallButton("Dismiss##txErr", schema::UI().resolveFont("button"))) {
if (TactileSmallButton(TR("send_dismiss"), schema::UI().resolveFont("button"))) {
s_tx_status.clear();
s_result_txid.clear();
s_status_success = false;
@@ -615,7 +615,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(ix, iy), Primary(), spinIcon);
double elapsed = ImGui::GetTime() - s_send_start_time;
snprintf(buf, sizeof(buf), "Submitting transaction... (%.0fs)", elapsed);
snprintf(buf, sizeof(buf), TR("send_submitting"), elapsed);
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), OnSurface(), buf);
} else {
// Success checkmark
@@ -624,7 +624,7 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, checkIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(ix, iy), Success(), checkIcon);
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), "Transaction sent!");
dl->AddText(body2, body2->LegacySize, ImVec2(ix + iSz.x + schema::UI().drawElement("tabs.send", "progress-icon-text-gap").size, iy), Success(), TR("send_tx_sent"));
if (!s_result_txid.empty()) {
float txY = iy + body2->LegacySize + schema::UI().drawElement("tabs.send", "txid-y-offset").size;
@@ -633,13 +633,13 @@ static void RenderTxProgress(ImDrawList* dl, float x, float y, float w,
std::string dispTxid = (int)s_result_txid.length() > txidThreshold
? s_result_txid.substr(0, txidTruncLen) + "..." + s_result_txid.substr(s_result_txid.length() - txidTruncLen)
: s_result_txid;
snprintf(buf, sizeof(buf), "TxID: %s", dispTxid.c_str());
snprintf(buf, sizeof(buf), TR("send_txid_label"), dispTxid.c_str());
dl->AddText(capFont, capFont->LegacySize, ImVec2(ix + schema::UI().drawElement("tabs.send", "txid-label-x-offset").size, txY),
OnSurfaceDisabled(), buf);
ImGui::SetCursorScreenPos(ImVec2(pMax.x - schema::UI().drawElement("tabs.send", "txid-copy-btn-right-offset").size, txY - schema::UI().drawElement("tabs.send", "txid-copy-btn-y-offset").size));
if (TactileSmallButton("Copy##TxID", schema::UI().resolveFont("button"))) {
if (TactileSmallButton(TR("copy"), schema::UI().resolveFont("button"))) {
ImGui::SetClipboardText(s_result_txid.c_str());
Notifications::instance().info("TxID copied to clipboard");
Notifications::instance().info(TR("send_txid_copied"));
}
}
}
@@ -688,7 +688,7 @@ void RenderSendConfirmPopup(App* app) {
// FROM card
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "FROM");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("from_upper"));
ImVec2 cMin = ImGui::GetCursorScreenPos();
ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
GlassPanelSpec gs; gs.rounding = popGlassRound;
@@ -702,7 +702,7 @@ void RenderSendConfirmPopup(App* app) {
// TO card
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("to_upper"));
ImVec2 cMin = ImGui::GetCursorScreenPos();
ImVec2 cMax(cMin.x + popW, cMin.y + addrCardH);
GlassPanelSpec gs; gs.rounding = popGlassRound;
@@ -716,7 +716,7 @@ void RenderSendConfirmPopup(App* app) {
// Fee tier selector
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "NETWORK FEE");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_network_fee"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
RenderFeeTierSelector("##confirm");
// Recalculate total after potential fee change
@@ -729,7 +729,7 @@ void RenderSendConfirmPopup(App* app) {
float valX = std::max(schema::UI().drawElement("tabs.send", "confirm-val-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-val-col-x").size * popVs);
float usdX = popW - std::max(schema::UI().drawElement("tabs.send", "confirm-usd-col-min-x").size, schema::UI().drawElement("tabs.send", "confirm-usd-col-x").size * popVs);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT DETAILS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_details"));
ImVec2 cMin = ImGui::GetCursorScreenPos();
float cH = std::max(schema::UI().drawElement("tabs.send", "confirm-amount-card-min-height").size, schema::UI().drawElement("tabs.send", "confirm-amount-card-height").size * popVs);
ImVec2 cMax(cMin.x + popW, cMin.y + cH);
@@ -740,7 +740,7 @@ void RenderSendConfirmPopup(App* app) {
float cy = cMin.y + Layout::spacingSm() + Layout::spacingXs();
float rowStep = std::max(schema::UI().drawElement("tabs.send", "confirm-row-step-min").size, schema::UI().drawElement("tabs.send", "confirm-row-step").size * popVs);
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Amount");
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_amount"));
snprintf(buf, sizeof(buf), "%.8f %s", s_amount, DRAGONX_TICKER);
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
if (market.price_usd > 0) {
@@ -749,7 +749,7 @@ void RenderSendConfirmPopup(App* app) {
}
cy += rowStep;
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Fee");
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_fee"));
snprintf(buf, sizeof(buf), "%.8f %s", s_fee, DRAGONX_TICKER);
popDl->AddText(capFont, capFont->LegacySize, ImVec2(cx + valX, cy), OnSurface(), buf);
if (market.price_usd > 0) {
@@ -762,7 +762,7 @@ void RenderSendConfirmPopup(App* app) {
ImGui::GetColorU32(Divider()), S.drawElement("tabs.send", "confirm-divider-thickness").size);
cy += rowStep;
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "Total");
popDl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("send_total"));
snprintf(buf, sizeof(buf), "%.8f %s", total, DRAGONX_TICKER);
DrawTextShadow(popDl, sub1, sub1->LegacySize, ImVec2(cx + valX, cy), Primary(), buf);
if (market.price_usd > 0) {
@@ -774,7 +774,7 @@ void RenderSendConfirmPopup(App* app) {
}
if (s_memo[0] != '\0' && is_valid_z) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_upper"));
Type().textColored(TypeStyle::Caption, OnSurface(), s_memo);
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
@@ -782,7 +782,7 @@ void RenderSendConfirmPopup(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (s_sending) {
Type().text(TypeStyle::Body2, "Sending...");
Type().text(TypeStyle::Body2, TR("sending"));
} else {
if (TactileButton(TR("confirm_and_send"), ImVec2(S.button("tabs.send", "confirm-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "confirm-button").font))) {
s_sending = true;
@@ -801,26 +801,26 @@ void RenderSendConfirmPopup(App* app) {
s_sending = false;
s_status_timestamp = ImGui::GetTime();
if (success) {
s_tx_status = "Transaction sent!";
s_tx_status = TR("send_tx_sent");
s_result_txid = result;
s_status_success = true;
Notifications::instance().success("Transaction sent successfully!");
Notifications::instance().success(TR("send_tx_success"));
s_to_address[0] = '\0';
s_amount = 0.0;
s_memo[0] = '\0';
s_send_max = false;
} else {
s_tx_status = "Error: " + result;
s_tx_status = std::string(TR("send_error_prefix")) + result;
s_result_txid.clear();
s_status_success = false;
Notifications::instance().error("Transaction failed: " + result);
Notifications::instance().error(std::string(TR("send_tx_failed")) + result);
}
}
);
s_show_confirm = false;
}
ImGui::SameLine();
if (TactileButton("Cancel", ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
if (TactileButton(TR("cancel"), ImVec2(S.button("tabs.send", "cancel-button").width, std::max(schema::UI().drawElement("tabs.send", "confirm-btn-min-height").size, schema::UI().drawElement("tabs.send", "confirm-btn-base-height").size * popVs)), S.resolveFont(S.button("tabs.send", "cancel-button").font))) {
s_show_confirm = false;
}
}
@@ -851,13 +851,13 @@ static bool RenderZeroBalanceCTA(App* app, ImDrawList* dl, float width) {
float cx = ctaMin.x + Layout::spacingXl();
float cy = ctaMin.y + Layout::spacingLg();
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), "Your wallet is empty");
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), TR("send_wallet_empty"));
cy += sub1->LegacySize + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(),
"Switch to Receive to get your address and start receiving funds.");
TR("send_switch_to_receive"));
cy += capFont->LegacySize + Layout::spacingMd();
ImGui::SetCursorScreenPos(ImVec2(cx, cy));
if (TactileButton("Go to Receive", ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
if (TactileButton(TR("send_go_to_receive"), ImVec2(schema::UI().drawElement("tabs.send", "cta-button-width").size, schema::UI().drawElement("tabs.send", "cta-button-height").size), schema::UI().resolveFont("button"))) {
app->setCurrentPage(NavPage::Receive);
}
ImGui::SetCursorScreenPos(ImVec2(ctaMin.x, ctaMax.y + Layout::spacingLg()));
@@ -904,19 +904,19 @@ static void RenderActionButtons(App* app, float width, float vScale,
if (!can_send && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
if (!app->isConnected())
ImGui::SetTooltip("Not connected to daemon");
ImGui::SetTooltip("%s", TR("send_tooltip_not_connected"));
else if (state.sync.syncing)
ImGui::SetTooltip("Blockchain is still syncing");
ImGui::SetTooltip("%s", TR("send_tooltip_syncing"));
else if (s_from_address[0] == '\0')
ImGui::SetTooltip("Select a source address above");
ImGui::SetTooltip("%s", TR("send_tooltip_select_source"));
else if (!is_valid_address)
ImGui::SetTooltip("Enter a valid recipient address");
ImGui::SetTooltip("%s", TR("send_tooltip_invalid_address"));
else if (s_amount <= 0)
ImGui::SetTooltip("Enter an amount to send");
ImGui::SetTooltip("%s", TR("send_tooltip_enter_amount"));
else if (total > available)
ImGui::SetTooltip("Amount exceeds available balance");
ImGui::SetTooltip("%s", TR("send_tooltip_exceeds_balance"));
else if (s_sending)
ImGui::SetTooltip("Transaction in progress...");
ImGui::SetTooltip("%s", TR("send_tooltip_in_progress"));
}
if (!can_send) ImGui::PopStyleColor(3);
@@ -946,14 +946,14 @@ static void RenderActionButtons(App* app, float width, float vScale,
s_clear_confirm_pending = false;
}
if (ImGui::BeginPopup(confirmClearId)) {
ImGui::Text("Clear all form fields?");
ImGui::Text("%s", TR("send_clear_fields"));
ImGui::Spacing();
if (TactileButton("Yes, Clear", ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_yes_clear"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-yes-width").size, 0), S.resolveFont("button"))) {
ClearFormWithUndo();
ImGui::CloseCurrentPopup();
}
ImGui::SameLine();
if (TactileButton("Keep", ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
if (TactileButton(TR("send_keep"), ImVec2(schema::UI().drawElement("tabs.send", "clear-confirm-keep-width").size, 0), S.resolveFont("button"))) {
ImGui::CloseCurrentPopup();
}
ImGui::EndPopup();
@@ -972,7 +972,7 @@ static void RenderActionButtons(App* app, float width, float vScale,
snprintf(undoId, sizeof(undoId), "Undo Clear%s", suffix);
if (TactileButton(undoId, ImVec2(width, btnH), S.resolveFont("button"))) {
RestoreFormSnapshot();
Notifications::instance().info("Form restored");
Notifications::instance().info(TR("send_form_restored"));
}
ImGui::PopStyleColor(2);
ImGui::PopStyleVar();
@@ -989,7 +989,7 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
float width, ImFont* capFont, App* app) {
auto& S = schema::UI();
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT SENDS");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recent_sends"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImVec2 avail = ImGui::GetContentRegionAvail();
@@ -1012,7 +1012,7 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
}
if (sends.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent sends");
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("send_no_recent"));
return;
}
@@ -1055,11 +1055,11 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
// Type label (first line)
float labelX = cx + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, "Sent");
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), sendCol, TR("sent_upper"));
// Time (next to type)
std::string ago = timeAgo(tx.timestamp);
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Sent").x;
float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, TR("sent_upper")).x;
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy),
OnSurfaceDisabled(), ago.c_str());
@@ -1096,12 +1096,12 @@ static void RenderRecentSends(ImDrawList* dl, const WalletState& state,
const char* statusStr;
ImU32 statusCol;
if (tx.confirmations == 0) {
statusStr = "Pending"; statusCol = Warning();
statusStr = TR("pending"); statusCol = Warning();
} else if (tx.confirmations < (int)S.drawElement("tabs.send", "confirmed-threshold").size) {
snprintf(buf, sizeof(buf), "%d conf", tx.confirmations);
snprintf(buf, sizeof(buf), TR("conf_count"), tx.confirmations);
statusStr = buf; statusCol = Warning();
} else {
statusStr = "Confirmed"; statusCol = greenCol;
statusStr = TR("confirmed"); statusCol = greenCol;
}
ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr);
float statusX = amtX - sSz.x - Layout::spacingXxl();
@@ -1238,7 +1238,7 @@ void RenderSendTab(App* app)
static bool s_paste_previewing = false;
static std::string s_preview_text;
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECIPIENT");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_recipient"));
// Validation indicator — inline next to title (no height change)
// Check the preview text during hover, otherwise check actual address
@@ -1254,9 +1254,9 @@ void RenderSendTab(App* app)
if (vz || vt) {
ImGui::SameLine();
if (vz)
Type().textColored(TypeStyle::Caption, Success(), "Valid shielded address");
Type().textColored(TypeStyle::Caption, Success(), TR("send_valid_shielded"));
else
Type().textColored(TypeStyle::Caption, Warning(), "Valid transparent address");
Type().textColored(TypeStyle::Caption, Warning(), TR("send_valid_transparent"));
} else if (!checkPreview) {
ImGui::SameLine();
Type().textColored(TypeStyle::Caption, Error(), TR("invalid_address"));
@@ -1341,7 +1341,7 @@ void RenderSendTab(App* app)
// ---- AMOUNT ----
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "AMOUNT");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("send_amount_upper"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Toggle between DRGX and USD input
@@ -1439,7 +1439,7 @@ void RenderSendTab(App* app)
// Amount error
if (s_amount > 0 && s_amount + s_fee > available && available > 0) {
snprintf(buf, sizeof(buf), "Exceeds available (%.8f)", available - s_fee);
snprintf(buf, sizeof(buf), TR("send_exceeds_available"), available - s_fee);
Type().textColored(TypeStyle::Caption, Error(), buf);
}
}
@@ -1455,7 +1455,7 @@ void RenderSendTab(App* app)
}
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)");
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("memo_optional"));
ImGui::Dummy(ImVec2(0, S.drawElement("tabs.send", "memo-label-gap").size));
float memoInputH = std::max(S.drawElement("tabs.send", "memo-min-height").size, S.drawElement("tabs.send", "memo-base-height").size * vScale);
@@ -1502,7 +1502,7 @@ void RenderSendTab(App* app)
ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y));
ImGui::Dummy(ImVec2(formW, 0));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Pass card bottom Y so error overlay can anchor to it
float cardBottom = containerMax.y;