feat(ui): StatusSurface AA legibility + node-details signing/transport polish#5985
Conversation
…urface Review direction (design#113): the status-color tokens (StatusGreen/ Yellow/Orange/Red) are bright indicator colors that only clear WCAG AA against a near-black or near-white surface — never a mid-tone. On a per-node-tinted message bubble they're hard to read. Rather than mutate each color's lightness per surface, give status-colored content a constant dark backing so the tokens keep their TRUE values and stay legible anywhere: - StatusSurface: a reusable opaque near-black scrim chip (StatusScrim). Opaque by necessity — a translucent scrim composites with the surface behind it and can't guarantee the ratio. Unit test proves all four status tokens meet AA (4.5:1) on the scrim. - contrastRatio() helper (WCAG) in core/ui theme. - MessageItem wraps the bubble metadata cluster (SNR/RSSI/transport/ signed shield) in StatusSurface; colors render at their true token values inside. StatusSurface is reusable for the other status-color sites (signal/ battery/node indicators) — those can adopt it incrementally; this change only touches the message bubble. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
✅ Preview staleness check passedPreview and screenshot references are up to date. |
📄 Docs staleness check — advisoryThis PR modifies user-facing UI source files but does not update any page under
Changed source files: What to check:
New page checklist (if adding a new doc page):
If this PR does not require a doc update (e.g., internal refactor, bug fix, test change), add the
|
… node items Refine StatusSurface usage to the principle: the scrim chip wraps ONLY the elements actually drawn in a status color — neutral content (white transport/hop icons, distance, bearing, battery %, etc.) stays bare. - MessageItem bubble: SNR/RSSI in one chip, the signed shield in its own chip; the white transport/hop icons are no longer on the scrim. - NodeItem: SNR/RSSI/quality grouped into a single chip cell in the metrics grid; the green "online" last-heard backed only when online. - NodeItemCompact: signal-quality segment chipped; green last-heard backed only when online. Status-colored icons that are shape-redundant (battery, favorite star, key/lock) are intentionally left bare — color isn't the sole channel and a chip behind a lone icon reads wrong. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e, tighter padding Polish from review: the solid-black chip read harsh. Make the scrim slightly translucent so the surface tint shows through, switch to CircleShape (a pill for a row, a circle behind a solo icon like the signed shield), and tighten the padding. Translucency composites with the surface, so the strict contrast guarantee relaxes for the darkest token: green (the common signed/good case) still clears AA text contrast even over a light card; the worst case (red) holds the 3:1 graphical floor. Test updated to assert the composited guarantee. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…nt + border) Make the message bubble and node card use one shared surface treatment (new NodeSurface: nodeTintedContainer + nodeBorderStroke) — the more accessible of the two. The bubble previously used a 40%-saturated node fill with a thin opaque outline; it now uses the node card's faint 8% wash over the neutral surface plus a node-colored 1.5dp border. That keeps the surface/onSurface AA pairing for the message text while still reading as "this node", and makes the two surfaces visually consistent. Selection/filtered states map to tint strength (emphasized/muted) + border emphasis instead of fill opacity. Node cards are unchanged (refactored onto the shared helper, identical output). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… AA floor) The bright status tokens need a dark backing, so there isn't much room: over a pure-white worst case ~0xD4 is the floor before red drops under 3:1 / green under 4.5:1. 0xD6 keeps a small margin (green 4.68, red 3.11 over white) while reading more translucent than 0xDB. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… (0xCD) - NodeItem header: NodeKeyStatusIcon + the XEdDSA signed shield (when node.signsPackets) now share one StatusSurface chip, extracted into a NodeSecurityIcons composable. The key icon is always status-colored, so the scrim fixes its contrast on the light card too. - Bubble: signed shield leads the metadata row. - StatusScrim alpha 0xD4 → 0xCD — the floor over the surfaces the chip ACTUALLY sits on (light card surface at max node tint) rather than pure white, which it never sits on. Test now composites over that real backdrop; green still ≥4.5 (AA text), red ≥3.0 (AA graphical), zero margin by design. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…node items NodeItemSignedPreview renders a node with signsPackets=true; the green shield-check now shows beside the key-status icon in the header security chip. Registered as ScreenshotNodeItemSigned (light/dark) for regression coverage of the signed state. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
NodeItemSignedPreview now sets a public key so the security chip shows the fully-trusted combo: green lock (PKC) + green signed shield. Also corrects node screenshots that a prior run had regenerated with a stray status-icon size mutation; re-rendered from clean source. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s on activity) ThisNodeStatusBadge now uses AnimatedConnectionsNavIcon — the same blinking connection icon as the nav bar — instead of the static one. The mesh-activity stream reaches the badge via a new LocalMeshActivity CompositionLocal that holds the FLOW (stable ref), provided once at the app root from uiViewModel.meshActivity. Providing the flow (not a collected value) costs no recomposition; only this single local-node badge collects it, at the leaf, animating in the draw phase. Falls back to the static ConnectionsNavIcon in previews/tests (local unset). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- NodeItemCompact: show the key-status + signed shield in a StatusSurface chip (via NodeSecurityIcons, now internal with an iconSize param, reused at 18dp) — matching NodeItem. - MessageItem: the quoted-reply header now uses nodeTintedContainer (emphasized tint) instead of an 80%-saturated node fill, so its onSurface text keeps the AA pairing like the rest of the bubble. - NodeStatusIcons: connection badge ordered last; status badges 20dp. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The "online" last-heard chip (StatusGreen + scrim when online, plain otherwise) was duplicated in NodeItem and NodeItemCompact. Extract it to one internal StatusAwareLastHeard composable and call it from both. Pure refactor — identical render (0 screenshot changes). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…details - New NodeSignedStatusIcon: the XEdDSA shield abstracted into a reusable, tappable component (like NodeKeyStatusIcon) — tapping opens a plain-language explanation dialog (new security_signed_node_help string). - NodeSecurityIcons is now public, renders NodeSignedStatusIcon, and takes a modifier. - Node details: replaced the labeled "Signed node" row with the NodeSecurityIcons key+shield cluster (both icons now tap-for-detail); dropped SignedNodeRow + its imports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…cluster) Keep the labeled "Signed node / Verified automatically" row in node details; the NodeSecurityIcons key+shield cluster stays in the node-list rows only. NodeSignedStatusIcon (tappable shield) remains in use via the list cluster. Signed-detail preview given a key (green lock + shield). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
SignedNodeRow now shows the plain-language help beneath "Verified automatically" (new optional InfoItem supportingText slot), so node details is self-explanatory without a dialog — the tap-for-dialog affordance stays for the icon-only list/bubble surfaces. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Node details: the "Signed node" cell now sits in the verification row (left) alongside the manual-verify "Supported" cell (right), instead of a separate full-width row. Tapping it opens the plain-language explanation (SignedNodeDialog, extracted from NodeSignedStatusIcon and shared). - InfoItem gains an onClick (the supporting-text slot is removed). - SignedNodeDialog extracted in core/ui for reuse by icon + row. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Broadens the old MQTT-only cell into a general "Transport" row showing how the node was last heard, driven by Node.lastTransport (MeshPacket.TransportMechanism). Hidden for internally-generated packets. The verification row is now a clean signed | supported pair. Transport icon+label mapping is centralised in a shared `transportInfo` helper so TransportIcon (list badge) and the node-details row stay in sync. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
❌ 1 Tests Failed:
View the top 1 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
TransportIcon now uses the centralised transportInfo short labels, so the MQTT badge's content description changed from "via MQTT" to "MQTT". Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Supersedes #5984. Addresses the accessibility thread on design#113 (thebentern: status-color contrast; olm3c: it's the shared token), and carries the node-details/security UI polish that grew out of it.
Why
The status-color tokens (
StatusGreen/Yellow/Orange/Red) are bright indicator colors — they only clear WCAG AA against a near-black or near-white surface, and fail in the mid-tone range. As colored text they fail on both the per-node-tinted message bubble and the node-list card surface.Rather than mutate each color per surface (closed #5984), give status-colored content a constant dark backing so the tokens keep their true values and stay legible anywhere — then clean up the surrounding node-details/security UI while we're in there.
What it looks like
🌟 What changed
Legibility (
StatusSurface)StatusScrim, alpha at the AA floor) +contrastRatio()WCAG helper. Unit test proves all four status tokens meet AA (4.5:1) on the scrim.NodeSurface).NodeItem/NodeItemCompact): signal-quality grouped in one chip; green "online" last-heard backed only when online (sharedStatusAwareLastHeard).Node details / security
SignedNodeDialog); shares the verification row withSupported.Node.lastTransport. Icon+label mapping centralised in a sharedtransportInfohelper so the list badge (TransportIcon) and the details row can't drift. Hidden for internally-generated (own) packets.LocalMeshActivityprovided as a Flow, so per-packet activity doesn't recompose the tree).Scope / deliberate exclusions
StatusSurfaceis the reusable primitive for them when warranted.🧪 Testing performed
./gradlew :core:ui:allTests --tests "*StatusSurfaceTest*"— all four status tokens meet AA onStatusScrim../gradlew spotlessCheck detekt— clean../gradlew :screenshot-tests:updateDebugScreenshotTest— references regenerated (bubble, node-item, node-details signed; light/dark).🤖 Generated with Claude Code