Skip to content

fix(a11y): keep message-bubble metadata green legible on colored bubbles#5984

Closed
jamesarich wants to merge 2 commits into
mainfrom
feat/bubble-metadata-contrast
Closed

fix(a11y): keep message-bubble metadata green legible on colored bubbles#5984
jamesarich wants to merge 2 commits into
mainfrom
feat/bubble-metadata-contrast

Conversation

@jamesarich

Copy link
Copy Markdown
Collaborator

Follow-up to the XEdDSA signing UI (#5976, #5980), addressing accessibility review feedback on design#113.

Why

The green SNR/RSSI text and the signed-broadcast shield sit on a per-node-tinted message bubble. The shared "Status Good" green (StatusGreen, green-600) has poor contrast against those bubble colors — worst in light mode, where green-on-periwinkle is muddy and hard to read. The black message text is fine because it derives from the bubble's content color; the green metadata didn't.

Blast radius — why this is scoped, not a token change

StatusGreen is used in ~15 components (signal/battery indicators, node items, nav/security icons…), but almost all render on the neutral theme surface where green-600 is fine. The only place it sits on a colored background is the message-bubble metadata row. So changing the shared token globally would touch 15 healthy components to fix one — the wrong lever. This fix stays local to the bubble and leaves the token untouched.

🛠️ What changed

  • ensureContrastOn(background, minRatio) (new, in core/ui theme): nudges a color toward black/white just enough to meet WCAG AA (4.5:1) against a given background, preserving hue. It picks the higher-contrast direction, so a mid-tone bubble darkens the green (stays green, becomes legible) rather than washing it out to near-white. Unit-tested.
  • Snr / Rssi / SnrAndRssi gain an optional background: Color? = null param. Default null = current behavior, so every other call site is unaffected (no blast radius).
  • MessageItem passes the composited bubble background so SNR, RSSI, and the signed shield all stay legible; the shield is unified onto the same contrast-adjusted StatusGreen.

🧪 Testing performed

  • ./gradlew :core:ui:allTests --tests "*ColorContrastTest*" — passes (raises sub-AA pairs to AA, darkens rather than washes out on mid-tone bg, leaves already-contrasting colors untouched).
  • ./gradlew spotlessCheck detekt — clean.
  • ./gradlew :screenshot-tests:updateDebugScreenshotTest — all references pass; the signed-message light reference regenerated to the darker, legible green.
Before After
washed-out green-600 on the bubble darker, AA-legible green, still recognizably "good"/signed

🤖 Generated with Claude Code

jamesarich and others added 2 commits June 27, 2026 14:37
Review feedback (design#113): the green SNR/RSSI text and the signed
shield sit on a per-node-tinted message bubble, where the shared
"Status Good" green has poor contrast (worst in light mode) — an
accessibility/readability problem. The black message text is fine
because it derives from the bubble's content color; the green metadata
did not.

Fix is scoped to the bubble — the shared StatusGreen token is used in
~15 components, almost all on the neutral theme surface where it's fine,
so changing the token globally would be the wrong lever. Instead:

- Add ensureContrastOn(background): nudges a color toward black/white
  just enough to meet WCAG AA against a given background, preserving hue
  (picks the higher-contrast direction, so a mid-tone bubble darkens the
  green rather than washing it to white). Unit-tested.
- Snr/Rssi/SnrAndRssi gain an optional `background` param (default null =
  unchanged everywhere else — no blast radius).
- MessageItem passes the composited bubble background so SNR, RSSI, and
  the signed shield all stay legible; shield unified onto the same
  contrast-adjusted StatusGreen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends ColorContrastTest to sweep all four quality colors
(green/yellow/orange/red) across the full background-lightness range,
asserting WCAG AA (4.5:1) holds after ensureContrastOn — so the bubble
fix is verified for every status color and every node tint, not just
green on the one sample bubble.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

📄 Docs staleness check — advisory

This PR modifies user-facing UI source files but does not update any page under docs/en/user/ or docs/en/developer/.

⚠️ Doc changes propagate to 3 consumers: in-app docs browser, Jekyll site (GitHub Pages), and meshtastic.org (Docusaurus sync). Updating a page in docs/en/ automatically flows to all three.

Changed source files:

core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt
core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/theme/ColorContrast.kt
feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt

What to check:

Changed area Likely doc page
feature/messaging/ docs/en/user/messages-and-channels.md
feature/node/ docs/en/user/nodes.md or docs/en/user/node-metrics.md
feature/map/ docs/en/user/map-and-waypoints.md
feature/connections/ docs/en/user/connections.md
feature/settings/ docs/en/user/settings-radio-user.md or docs/en/user/settings-module-admin.md
feature/firmware/ docs/en/user/firmware.md
feature/intro/ docs/en/user/onboarding.md
feature/discovery/ docs/en/user/discovery.md
feature/docs/ Internal docs infrastructure
core/ui/ docs/en/developer/codebase.md or component-specific user pages

New page checklist (if adding a new doc page):

  1. Create the .md file in docs/en/user/ or docs/en/developer/ with last_updated frontmatter
  2. Register in DocBundleLoader.kt with string resources (in-app browser)
  3. Jekyll and Docusaurus sync pick up new pages automatically — no config change needed

If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the skip-docs-check label to dismiss this check.

Cross-platform note: This check is advisory while doc coverage matures. Both Android and Apple repos use the same skip-docs-check label and advisory severity. See meshtastic/design standards for shared conventions.

@github-actions

Copy link
Copy Markdown
Contributor

🖼️ Preview staleness check — advisory

This PR modifies UI composables but does not update any *Previews.kt files.

Previews power screenshot tests and in-app docs screenshots. Keeping them current ensures visual regression coverage stays accurate.

Changed UI files:

feature/messaging/src/commonMain/kotlin/org/meshtastic/feature/messaging/component/MessageItem.kt

What to check:

Pattern Preview file convention
feature/{name}/…/ui/ or component/ feature/{name}/…/*Previews.kt
core/ui/…/ core/ui/…/ (previews colocated)

Adding previews checklist:

  1. Create or update a *Previews.kt file in the same module with @PreviewLightDark
  2. Baseline the public preview in the module detekt-baseline.xml (or @Suppress("PreviewPublic")) if it is consumed cross-module by a screenshot wrapper
  3. Add a @PreviewTest wrapper: in screenshot-tests/src/screenshotTest/ for a regression-gated component, or docs-screenshots/src/screenshotTest/ for a doc-framed composition
  4. Run ./gradlew :screenshot-tests:updateDebugScreenshotTest (and :docs-screenshots:updateDebugScreenshotTest for doc compositions) to generate reference images

If this PR does not require preview updates (e.g., logic-only change, non-visual refactor), add the skip-preview-check label to dismiss.

@jamesarich

Copy link
Copy Markdown
Collaborator Author

Superseded by #5985, which takes the surface-backed approach (StatusSurface) instead of per-bubble lightness adjustment — keeps the status tokens at their true values and is reusable for the other status-color sites. Closing in favor of that direction.

@jamesarich jamesarich closed this Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant