From 42f633f06adf4e0c328277c040f05965eda43bb2 Mon Sep 17 00:00:00 2001 From: RedFox Date: Mon, 12 Oct 2026 16:56:19 -0400 Subject: [PATCH 1/2] fix(ui): readable text colors in dark mode (#340) The AFT tier cards in Settings -> Anti-Flood carried an inline `background: #fff`, which beats the stylesheet, so in dark mode the text followed the theme (near-white) while the card stayed white (1.1:1 contrast). Same pattern elsewhere inside .fm-app: - settings.rs: AFT usage cards, AFT tier picker and the identity switcher popover now use var(--bg-card) instead of #fff. - login.rs: import-contact "unverified" warning uses the new --danger-ink token (was #991b1b on the dark card, 2.1:1). - settings.css: contact "unknown" trust badge uses --caution; the verify-box tick uses the new --on-trust token (white on the dark theme's brighter green was 2.3:1). - tokens.css: add --danger-ink and --on-trust; light values equal the previous hard-coded colors, so light mode is unchanged. All changed pairs now meet WCAG AA in dark mode (>= 4.7:1 for text). Co-Authored-By: Claude Opus 5.5 --- ui/public/vendor/css/components/settings.css | 4 ++-- ui/public/vendor/css/components/tokens.css | 13 +++++++++++++ ui/src/app/login.rs | 2 +- ui/src/app/settings.rs | 6 +++--- 4 files changed, 19 insertions(+), 6 deletions(-) diff --git a/ui/public/vendor/css/components/settings.css b/ui/public/vendor/css/components/settings.css index 9023ad9..17f30cf 100644 --- a/ui/public/vendor/css/components/settings.css +++ b/ui/public/vendor/css/components/settings.css @@ -460,7 +460,7 @@ padding: 2px 6px; border-radius: 3px; border: 1px solid; } .fm-contact-trust.known { color: var(--trust); border-color: rgba(51, 153, 102, 0.3); background: var(--trust-bg); } - .fm-contact-trust.unknown { color: #a6731f; border-color: rgba(217, 119, 6, 0.3); background: rgba(217, 119, 6, 0.06); } + .fm-contact-trust.unknown { color: var(--caution); border-color: rgba(217, 119, 6, 0.3); background: rgba(217, 119, 6, 0.06); } .fm-contact-trust.unsigned { color: #a32f2f; border-color: rgba(163, 47, 47, 0.3); background: #fee2e2; } .fm-contact-tier { font-family: "Geist Mono", monospace; font-size: 10.5px; letter-spacing: 0.04em; @@ -746,7 +746,7 @@ background: var(--trust); border-color: var(--trust); } .verify-box .tick { - color: #fff; font-size: 13px; line-height: 1; opacity: 0; transform: scale(0.7); + color: var(--on-trust); font-size: 13px; line-height: 1; opacity: 0; transform: scale(0.7); transition: opacity .15s ease, transform .15s ease; } .verify-check.checked .verify-box .tick { opacity: 1; transform: scale(1); } diff --git a/ui/public/vendor/css/components/tokens.css b/ui/public/vendor/css/components/tokens.css index 62ea937..5da1038 100644 --- a/ui/public/vendor/css/components/tokens.css +++ b/ui/public/vendor/css/components/tokens.css @@ -64,6 +64,15 @@ --caution: #a6731f; --warn: #a32f2f; + /* Error/danger text on a card surface (e.g. the import-contact + unverified warning, #340). Dark theme remaps to a light red so it + stays readable on the dark card. */ + --danger-ink: #991b1b; + + /* Ink on a solid --trust surface (verify-box tick). White works on the + light-theme green but not on the brighter dark-theme green (#340). */ + --on-trust: #fff; + position: fixed; inset: 0; display: flex; @@ -124,6 +133,8 @@ --amber-ink: #fbbf24; --caution: #d6a44e; --warn: #e06c6c; + --danger-ink: #f87171; + --on-trust: #052e16; } @media (prefers-color-scheme: dark) { @@ -156,6 +167,8 @@ --amber-ink: #fbbf24; --caution: #d6a44e; --warn: #e06c6c; + --danger-ink: #f87171; + --on-trust: #052e16; } } diff --git a/ui/src/app/login.rs b/ui/src/app/login.rs index 76c794d..d377f81 100644 --- a/ui/src/app/login.rs +++ b/ui/src/app/login.rs @@ -2376,7 +2376,7 @@ pub(super) fn ImportContactForm() -> Element { } if has_verify_phrase && !*verified.read() { span { class: "verify-sub", - style: "color:#991b1b;", + style: "color: var(--danger-ink, #991b1b);", "The sender included a verify phrase. Importing unverified is allowed, but verified-only inboxes will reject your messages until you tick this once you've matched the six words out-of-band." } } diff --git a/ui/src/app/settings.rs b/ui/src/app/settings.rs index e277069..89d5a09 100644 --- a/ui/src/app/settings.rs +++ b/ui/src/app/settings.rs @@ -129,7 +129,7 @@ pub(crate) fn SettingsShell() -> Element { div { class: "fm-set-ident-pop", role: "listbox", - style: "border: 1px solid var(--line); border-radius: 10px; background: #fff; box-shadow: 0 8px 24px rgba(0,0,0,0.08); margin: 4px 0 12px; overflow: hidden;", + style: "border: 1px solid var(--line); border-radius: 10px; background: var(--bg-card); box-shadow: 0 8px 24px rgba(0,0,0,0.08); margin: 4px 0 12px; overflow: hidden;", for (uid, row_alias, fp) in switcher_rows.iter() { { let uid = *uid; @@ -962,7 +962,7 @@ fn ScrAft() -> Element { div { key: "{tier_name}", class: "fm-tier", - style: "display: flex; flex-direction: column; gap: 4px; border: 1px solid var(--line); border-radius: 11px; padding: 12px 12px 10px; background: #fff; {extra_style}", + style: "display: flex; flex-direction: column; gap: 4px; border: 1px solid var(--line); border-radius: 11px; padding: 12px 12px 10px; background: var(--bg-card); {extra_style}", "data-testid": "fm-aft-usage-tier", "data-tier": "{tier_name}", "data-used": "{used}", @@ -1007,7 +1007,7 @@ fn ScrAft() -> Element { key: "{id}", class: "{cls}", r#type: "button", - style: "all: unset; display: flex; flex-direction: column; gap: 4px; cursor: pointer; border: 1px solid var(--line); border-radius: 11px; padding: 12px 12px 10px; background: #fff;", + style: "all: unset; display: flex; flex-direction: column; gap: 4px; cursor: pointer; border: 1px solid var(--line); border-radius: 11px; padding: 12px 12px 10px; background: var(--bg-card);", "data-testid": "fm-aft-tier", "data-tier": "{id}", "data-active": "{active}", From e5c3087ade99dac3b9ed54eee3043d2ba8bcaf63 Mon Sep 17 00:00:00 2001 From: RedFox Date: Mon, 12 Oct 2026 18:25:53 -0400 Subject: [PATCH 2/2] fix(ui): identity-switcher rows readable in dark mode (#340) The switcher popover got var(--bg-card) in the previous commit, but each inactive row still set its own inline `background: #fff` (built via a `bg` variable, so the earlier search missed it). In dark / system-dark mode that left identity names at 1.23:1 and fingerprints at 2.56:1. Rows now use var(--bg-card): 14.07:1 and 6.76:1 in dark, unchanged in light mode. Add no_hardcoded_white_backgrounds_340, which scans the non-test part of settings.rs for #fff/#ffffff literals so a hard-coded white surface can't come back on any Settings screen (all render inside the themed .fm-app). Verified it fails on the old code, pointing at line 144. Co-Authored-By: Claude Opus 5.5 --- ui/src/app/settings.rs | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/ui/src/app/settings.rs b/ui/src/app/settings.rs index 89d5a09..5e006b1 100644 --- a/ui/src/app/settings.rs +++ b/ui/src/app/settings.rs @@ -141,7 +141,7 @@ pub(crate) fn SettingsShell() -> Element { .map(|c| c.to_string()) .unwrap_or_else(|| "·".into()); let row_fp = fp.clone(); - let bg = if is_active { "var(--bg-soft)" } else { "#fff" }; + let bg = if is_active { "var(--bg-soft)" } else { "var(--bg-card)" }; let style = format!( "all: unset; display: flex; gap: 10px; align-items: center; width: 100%; padding: 8px 12px; cursor: pointer; background: {bg};", ); @@ -1612,6 +1612,31 @@ mod tests { const FP: &str = "abandon-ability-able-about-above-absent"; + /// #340: every Settings screen renders inside `.fm-app`, which has a dark + /// theme. A hard-coded white inline background ignores the theme and puts + /// the light dark-mode ink on a white surface (AFT tier cards, identity + /// switcher rows). Use `var(--bg-card)` instead. + #[test] + fn no_hardcoded_white_backgrounds_340() { + let src = include_str!("settings.rs"); + let body = src.split("#[cfg(test)]").next().unwrap(); + let offenders: Vec = body + .match_indices('#') + .filter(|(i, _)| { + let hex: String = body[i + 1..] + .chars() + .take_while(|c| c.is_ascii_hexdigit()) + .collect(); + matches!(hex.to_ascii_lowercase().as_str(), "fff" | "ffffff") + }) + .map(|(i, _)| body[..i].matches('\n').count() + 1) + .collect(); + assert!( + offenders.is_empty(), + "hard-coded white in settings.rs at line(s) {offenders:?}; use var(--bg-card)" + ); + } + /// No saved decision + auto-accept OFF → `None` (show modal). #[test] fn no_policy_returns_none() {