feat(settings): add hover tooltips to the settings controls that lacked them

A per-card audit found 38 interactive controls with no tooltip. Add them, with
new tt_* i18n keys (English source; per-language JSONs fall back to English
until translated):
- Chat & Contacts (8): emoji/bubble style, accent, density, text size, poll
  rate, timestamps, enter-sends — the segmented/beginRow helpers added none.
- Node & Security (26): the whole lite wallet lifecycle / backup-keys /
  encryption panel, plus the full-node RPC-details toggle and daemon Refresh.
- Debug Options (4): the screenshot-sweep / full-UI-sweep / open-folder /
  seed-demo-chat buttons.

Theme, Wallet, Explorer, About were already fully covered.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-21 21:31:15 -05:00
parent 541a9e1fd5
commit 7cf0bb8bc7
3 changed files with 81 additions and 2 deletions

View File

@@ -1646,6 +1646,7 @@ void RenderSettingsPage(App* app) {
if (material::TactileButton(TrId("lite_wallet_request", "LiteLifecycleToggle").c_str(), ImVec2(liteInputW, 0))) {
s_settingsState.lite_lifecycle_expanded = !s_settingsState.lite_lifecycle_expanded;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_toggle"));
if (s_settingsState.lite_lifecycle_expanded) {
const char* lifecycleLabels[] = {TR("lite_op_create"), TR("lite_op_open"), TR("lite_op_restore")};
@@ -1667,6 +1668,7 @@ void RenderSettingsPage(App* app) {
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_op"));
if (s_settingsState.lite_lifecycle_operation == 1 ||
s_settingsState.lite_lifecycle_operation == 2) {
@@ -1677,6 +1679,7 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteWalletPath", s_settingsState.lite_wallet_path,
sizeof(s_settingsState.lite_wallet_path));
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_wallet_path"));
}
if (s_settingsState.lite_lifecycle_operation == 2) {
@@ -1688,6 +1691,7 @@ void RenderSettingsPage(App* app) {
ImGui::InputText("##LiteRestoreSeed", s_settingsState.lite_restore_seed,
sizeof(s_settingsState.lite_restore_seed),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_seed"));
// Live 24-word count so the user knows the seed is complete before restoring.
{
const int wc = liteSeedWordCount(s_settingsState.lite_restore_seed);
@@ -1703,6 +1707,7 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreBirthday", &s_settingsState.lite_restore_birthday);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_birthday"));
if (s_settingsState.lite_restore_birthday < 0) s_settingsState.lite_restore_birthday = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
@@ -1714,11 +1719,13 @@ void RenderSettingsPage(App* app) {
ImGui::SameLine(leftX - sectionOrigin.x + liteLabelW);
ImGui::SetNextItemWidth(std::min(160.0f, liteInputW));
ImGui::InputInt("##LiteRestoreAccount", &s_settingsState.lite_restore_account);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_account"));
if (s_settingsState.lite_restore_account < 0) s_settingsState.lite_restore_account = 0;
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::Checkbox(TrId("lite_overwrite", "LiteRestoreOverwrite").c_str(),
&s_settingsState.lite_restore_overwrite);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_restore_overwrite"));
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -1730,6 +1737,7 @@ void RenderSettingsPage(App* app) {
s_settingsState.lite_lifecycle_passphrase,
sizeof(s_settingsState.lite_lifecycle_passphrase),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lifecycle_pass"));
// Poll a completed async create/open/restore (driven by the App-owned
// controller; finalized on the main thread by App::update's
@@ -1760,6 +1768,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TrId("lite_validate", "LiteLifecycleValidate").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
evaluateLiteLifecycleRequestFromPageState(app);
}
if (ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) material::Tooltip("%s", TR("tt_lite_lifecycle_run"));
if (liteLifecycleBusy) ImGui::EndDisabled();
if (!s_settingsState.lite_lifecycle_status.empty()) {
@@ -1806,6 +1815,7 @@ void RenderSettingsPage(App* app) {
}
wallet::secureWipeLiteSecret(r.seedPhrase); // wipe the result copy
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_seed"));
ImGui::SameLine(0, Layout::spacingSm());
if (material::ActionButton("##LiteExportKeys", TR("lite_show_private_keys"), ICON_MD_KEY, material::ActionTier::Secondary)) {
auto r = app->liteWallet()->exportPrivateKeys();
@@ -1821,6 +1831,7 @@ void RenderSettingsPage(App* app) {
}
wallet::secureWipeLiteSecret(r.privateKeysJson);
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_show_keys"));
// Revealed secret: shown read-only (no extra copies), with copy + wipe.
if (!s_settingsState.lite_export_secret.empty()) {
@@ -1843,6 +1854,7 @@ void RenderSettingsPage(App* app) {
if (TactileButton(TrId("lite_copy", "LiteExportCopy").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
app->copySecretToClipboard(s_settingsState.lite_export_secret);
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_copy"));
ImGui::SameLine(0, Layout::spacingSm());
// Save the seed (+ birthday) to an owner-only (0600) file in the config dir.
if (s_settingsState.lite_export_is_seed &&
@@ -1857,12 +1869,16 @@ void RenderSettingsPage(App* app) {
? (std::string(TR("lite_saved_to")) + path)
: (std::string(TR("lite_could_not_write")) + path);
}
if (s_settingsState.lite_export_is_seed) ImGui::SameLine(0, Layout::spacingSm());
if (s_settingsState.lite_export_is_seed) {
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_save_seed_file"));
ImGui::SameLine(0, Layout::spacingSm());
}
if (TactileButton(TrId("lite_hide_wipe", "LiteExportHide").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
wallet::secureWipeLiteSecret(s_settingsState.lite_export_secret);
s_settingsState.lite_export_label.clear();
s_settingsState.lite_export_is_seed = false;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_hide_wipe"));
}
// Import a spending/viewing key (history appears after the next sync).
@@ -1874,6 +1890,7 @@ void RenderSettingsPage(App* app) {
ImGui::InputText("##LiteImportKey", s_settingsState.lite_import_key,
sizeof(s_settingsState.lite_import_key),
ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_import", "LiteImportKeyBtn").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
std::string liteKey(s_settingsState.lite_import_key);
@@ -1888,6 +1905,7 @@ void RenderSettingsPage(App* app) {
r.ok ? TR("lite_key_imported") : r.error;
}
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_import_key_btn"));
if (!s_settingsState.lite_backup_status.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -1909,12 +1927,14 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteEncryptPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_encrypt_wallet", "LiteEncrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->encryptWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = r.ok ? TR("lite_wallet_encrypted") : r.error;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_encrypt"));
} else {
if (wstate.isLocked()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -1924,12 +1944,14 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteUnlockPass", s_settingsState.lite_enc_pass,
sizeof(s_settingsState.lite_enc_pass), ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_unlock", "LiteUnlock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const bool ok = app->liteWallet()->unlockWallet(s_settingsState.lite_enc_pass);
sodium_memzero(s_settingsState.lite_enc_pass, sizeof(s_settingsState.lite_enc_pass));
s_settingsState.lite_encryption_status = ok ? TR("lite_wallet_unlocked") : TR("lite_unlock_failed");
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_unlock"));
} else {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_lock_now", "LiteLock").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
@@ -1937,6 +1959,7 @@ void RenderSettingsPage(App* app) {
s_settingsState.lite_encryption_status =
app->lockLiteWallet() ? TR("lite_wallet_locked") : TR("lite_lock_failed");
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_lock"));
}
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
ImGui::AlignTextToFramePadding();
@@ -1945,12 +1968,14 @@ void RenderSettingsPage(App* app) {
ImGui::SetNextItemWidth(liteInputW);
ImGui::InputText("##LiteDecryptPass", s_settingsState.lite_dec_pass,
sizeof(s_settingsState.lite_dec_pass), ImGuiInputTextFlags_Password);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_decrypt_pass"));
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
if (TactileButton(TrId("lite_remove_encryption", "LiteDecrypt").c_str(), ImVec2(0, 0), S.resolveFont("button"))) {
const auto r = app->liteWallet()->decryptWallet(s_settingsState.lite_dec_pass);
sodium_memzero(s_settingsState.lite_dec_pass, sizeof(s_settingsState.lite_dec_pass));
s_settingsState.lite_encryption_status = r.ok ? TR("lite_encryption_removed") : r.error;
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_lite_remove_encrypt"));
}
if (!s_settingsState.lite_encryption_status.empty()) {
ImGui::SetCursorScreenPos(ImVec2(leftX, ImGui::GetCursorScreenPos().y));
@@ -2096,7 +2121,7 @@ void RenderSettingsPage(App* app) {
ImVec2 hdrPos = ImGui::GetCursorScreenPos();
if (ImGui::Button("##RpcToggle", ImVec2(contentW, ImGui::GetFrameHeight())))
s_settingsState.rpc_expanded = !s_settingsState.rpc_expanded;
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); material::Tooltip("%s", TR("tt_rpc_toggle")); }
float textY = hdrPos.y + (ImGui::GetFrameHeight() - rpcHdrFont->LegacySize) * 0.5f;
dl->AddText(rpcHdrFont, rpcHdrFont->LegacySize, ImVec2(hdrPos.x, textY), OnSurfaceMedium(), TR("rpc_connection"));
ImFont* rpcIconFont = Type().iconSmall();
@@ -2288,6 +2313,7 @@ void RenderSettingsPage(App* app) {
uf.next(material::ActionButtonWidth(TR("refresh"), ICON_MD_REFRESH));
if (material::ActionButton("##drefresh", TR("refresh"), ICON_MD_REFRESH, AT::Secondary))
s_settingsState.daemon_info_loaded = false;
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_daemon_refresh"));
ImGui::BeginDisabled(!app->isUsingEmbeddedDaemon() || !bun.available);
uf.next(material::ActionButtonWidth(TR("daemon_install_bundled"), ICON_MD_DOWNLOAD));
if (material::ActionButton("##dinst", TR("daemon_install_bundled"), ICON_MD_DOWNLOAD, AT::Secondary))
@@ -2619,18 +2645,22 @@ void RenderSettingsPage(App* app) {
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
if (TactileButton(TR("screenshot_sweep"), ImVec2(0, 0), S.resolveFont("button")))
app->startScreenshotSweep();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep"));
ImGui::SameLine();
// Full UI sweep: also captures every modal / dialog / flow / state (demo data, offline).
if (TactileButton(TR("screenshot_sweep_full"), ImVec2(0, 0), S.resolveFont("button")))
app->startFullUiSweep();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_sweep_full"));
ImGui::SameLine();
if (TactileButton(TR("screenshot_open_dir"), ImVec2(0, 0), S.resolveFont("button")))
util::Platform::openFolder(app->screenshotDir());
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_screenshot_open_dir"));
if (chat::hushChatFeatureEnabledAtBuild()) {
// Populate the Chat tab with demo conversations so the sweep captures its real UI.
ImGui::SameLine();
if (TactileButton("Seed demo chat", ImVec2(0, 0), S.resolveFont("button")))
app->seedChatDemoData();
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_seed_demo_chat"));
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Separator();

View File

@@ -1946,6 +1946,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
int v = st->getChatEmojiColor() ? 1 : 0;
const char* items[] = { TR("chat_emoji_mono"), TR("chat_emoji_color") };
int nv = segmented(TR("chat_opt_emoji"), items, 2, v);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_emoji_style"));
if (nv != v) { st->setChatEmojiColor(nv == 1); st->save(); app->requestFontRebuild(); }
}
// Bubble style (segmented) + accent color (a 6-way dropdown — too many for a segmented control).
@@ -1953,6 +1954,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
const char* items[] = { TR("chat_bubble_rounded"), TR("chat_bubble_square"), TR("chat_bubble_minimal") };
int v = st->getChatBubbleStyle();
int nv = segmented(TR("chat_opt_bubble_style"), items, 3, v);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_bubble_style"));
if (nv != v) { st->setChatBubbleStyle(nv); st->save(); }
}
{
@@ -1961,12 +1963,14 @@ void RenderChatSettingsControls(App* app, float contentWidth)
const char* items[] = { TR("chat_accent_theme"), TR("chat_accent_blue"), TR("chat_accent_green"),
TR("chat_accent_purple"), TR("chat_accent_amber"), TR("chat_accent_pink") };
if (ImGui::Combo("##chat_baccent", &v, items, 6)) { st->setChatBubbleAccent(v); st->save(); }
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_bubble_accent"));
}
// Message density (segmented).
{
const char* items[] = { TR("chat_density_comfortable"), TR("chat_density_compact") };
int v = st->getChatDensity();
int nv = segmented(TR("chat_opt_density"), items, 2, v);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_density"));
if (nv != v) { st->setChatDensity(nv); st->save(); }
}
// Message text size (slider).
@@ -1976,6 +1980,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
if (ImGui::SliderFloat("##chat_font", &v, 0.8f, 1.5f, "%.2fx", ImGuiSliderFlags_AlwaysClamp)) {
st->setChatFontScale(v); st->save();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_font_size"));
}
// Move Messaging into the right column (float it with Indent so every row's
@@ -1996,12 +2001,14 @@ void RenderChatSettingsControls(App* app, float contentWidth)
if (ImGui::SliderFloat("##chat_poll", &v, 0.5f, 15.0f, "%.1f s", ImGuiSliderFlags_AlwaysClamp)) {
st->setChatPollRateSec(v); st->save();
}
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_poll_rate"));
}
// Chat timestamps (segmented) — overrides the app-wide clock (Settings → General) for this tab only.
{
const char* items[] = { TR("chat_ts_global_short"), TR("chat_ts_24h"), TR("chat_ts_12h") };
int v = st->getChatTimeFormat();
int nv = segmented(TR("chat_opt_timestamp"), items, 3, v);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_timestamp"));
if (nv != v) { st->setChatTimeFormat(nv); st->save(); }
}
// Enter-to-send (checkbox).
@@ -2009,6 +2016,7 @@ void RenderChatSettingsControls(App* app, float contentWidth)
ImGui::Dummy(ImVec2(0.0f, rowGap));
bool v = st->getChatEnterSends();
if (ImGui::Checkbox(TR("chat_opt_enter_sends"), &v)) { st->setChatEnterSends(v); st->save(); }
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("tt_chat_enter_sends"));
}
// Close the two-column band: un-indent and drop below the taller column.

View File

@@ -721,6 +721,47 @@ void I18n::loadBuiltinEnglish()
strings_["tt_idle_delay"] = "How long to wait before starting mining";
strings_["tt_wizard"] = "Re-run the initial setup wizard\nDaemon will be restarted";
strings_["tt_download_bootstrap"] = "Download blockchain bootstrap to speed up sync\nExisting block data will be replaced";
// --- Full-node Node & Security tooltips ---
strings_["tt_rpc_toggle"] = "Show or hide the read-only RPC connection details (host, port, user, password) for the daemon";
strings_["tt_daemon_refresh"] = "Re-read the installed and bundled dragonxd version, size, and date shown above";
// --- Lite wallet Node & Security tooltips ---
strings_["tt_lite_lifecycle_toggle"] = "Show or hide the create / open / restore controls for managing your lite wallet file";
strings_["tt_lite_lifecycle_op"] = "Choose whether to create a new wallet, open an existing one, or restore one from a seed phrase";
strings_["tt_lite_wallet_path"] = "Path or name of the wallet file to open or restore into";
strings_["tt_lite_restore_seed"] = "The 24-word recovery seed phrase to restore this wallet from; hidden as you type";
strings_["tt_lite_restore_birthday"] = "Block height the wallet was created at; scanning starts here. Use 0 or the earliest height if unsure";
strings_["tt_lite_restore_account"] = "HD account index to restore; leave 0 unless you used multiple accounts under this seed";
strings_["tt_lite_restore_overwrite"] = "Replace an existing wallet file with this restore. Warning: overwrites the current wallet data";
strings_["tt_lite_lifecycle_pass"] = "Passphrase to unlock or set on the wallet during this create / open / restore operation";
strings_["tt_lite_lifecycle_run"] = "Run the selected create / open / restore operation with the values above";
strings_["tt_lite_show_seed"] = "Reveal this wallet's recovery seed phrase and birthday. Anyone with the seed can spend your funds";
strings_["tt_lite_show_keys"] = "Reveal this wallet's private spending keys. Anyone with a key can spend the funds it controls";
strings_["tt_lite_copy"] = "Copy the revealed secret to the clipboard";
strings_["tt_lite_save_seed_file"] = "Write the seed and birthday to an owner-only file (lite-seed-backup.txt) in the config folder";
strings_["tt_lite_hide_wipe"] = "Hide the revealed secret and securely wipe it from memory";
strings_["tt_lite_import_key"] = "Paste a private spending or viewing key to import; its history appears after the next sync";
strings_["tt_lite_import_key_btn"] = "Import the entered private key into this wallet; funds and history appear after the next sync";
strings_["tt_lite_encrypt_pass"] = "Passphrase to encrypt the wallet with. If lost, the wallet cannot be unlocked or recovered";
strings_["tt_lite_encrypt"] = "Encrypt the wallet with the passphrase above; it locks immediately and requires the passphrase to unlock";
strings_["tt_lite_unlock_pass"] = "Enter your passphrase to unlock the encrypted wallet";
strings_["tt_lite_unlock"] = "Unlock the encrypted wallet using the passphrase above";
strings_["tt_lite_lock"] = "Lock the wallet now; a passphrase is required to unlock and any chat session is torn down";
strings_["tt_lite_decrypt_pass"] = "Enter your passphrase to remove encryption from the wallet";
strings_["tt_lite_remove_encrypt"] = "Remove encryption and store the wallet unprotected; no passphrase will be required to open it";
// --- Chat & Contacts tooltips ---
strings_["tt_chat_emoji_style"] = "Render emoji in monochrome outline or full color";
strings_["tt_chat_bubble_style"] = "Message bubble shape: rounded, square, or minimal (flat, borderless)";
strings_["tt_chat_bubble_accent"] = "Accent color for your outgoing message bubbles (or follow the current theme)";
strings_["tt_chat_density"] = "Spacing between messages: Comfortable adds more padding; Compact fits more on screen";
strings_["tt_chat_font_size"] = "Scale chat message text from 0.8x to 1.5x. Affects only the Chat tab, not the rest of the app";
strings_["tt_chat_poll_rate"] = "How often to check for new and 0-conf messages (0.5-15 s). Faster is more responsive but uses more CPU";
strings_["tt_chat_timestamp"] = "Timestamp format for this tab only: follow the app-wide clock, or force 24-hour or 12-hour";
strings_["tt_chat_enter_sends"] = "When on, Enter sends the message and Shift+Enter adds a newline; when off, Enter adds a newline";
// --- Debug Options tooltips ---
strings_["tt_screenshot_sweep"] = "Cycle every theme across every tab, saving a screenshot of each into the config screenshots folder (overwrites the last sweep)";
strings_["tt_screenshot_sweep_full"] = "Like the theme sweep but also captures every modal / dialog / flow using temporary offline demo wallet data";
strings_["tt_screenshot_open_dir"] = "Open the screenshots folder (under the config directory) in your file manager";
strings_["tt_seed_demo_chat"] = "Inject sample conversations into the Chat tab so a sweep captures its UI; in-memory only, gone on restart";
strings_["download_bootstrap"] = "Download Bootstrap";
strings_["download"] = "Download";
strings_["retry"] = "Retry";