From 3fb99f86ac48714a553201ac4282c6615cdc4da2 Mon Sep 17 00:00:00 2001 From: m57 <41645758+Hy4ri@users.noreply.github.com> Date: Wed, 30 Sep 2026 14:33:08 +0300 Subject: [PATCH] fix(chat): tint composer send/stop/mic button with theme primary (#1410) --- .../java/com/m57/hermescontrol/theme/Color.kt | 3 ++ .../ui/chat/components/ComposerPalette.kt | 29 +++++++++++++++++-- .../ui/chat/ComposerPaletteTest.kt | 15 ++++++++++ 3 files changed, 44 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/m57/hermescontrol/theme/Color.kt b/app/src/main/java/com/m57/hermescontrol/theme/Color.kt index 3cb10951..89c05360 100644 --- a/app/src/main/java/com/m57/hermescontrol/theme/Color.kt +++ b/app/src/main/java/com/m57/hermescontrol/theme/Color.kt @@ -91,3 +91,6 @@ fun parseProjectColor(raw: String?): Color? { if (h !in 0f..360f || s !in 0f..1f || l !in 0f..1f) return null return Color.hsl(h % 360f, s, l) } + +/** Pure white glyph for filled accent controls (composer action button) when it is legible on the preset primary. */ +val GlyphWhite = Color(0xFFFFFFFF) diff --git a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerPalette.kt b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerPalette.kt index 531ba3f5..a1f2752f 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerPalette.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/chat/components/ComposerPalette.kt @@ -6,6 +6,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.ReadOnlyComposable import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver +import androidx.compose.ui.graphics.luminance +import com.m57.hermescontrol.theme.GlyphWhite /** * Resolved colors for the chat composer card and its controls. @@ -15,7 +17,8 @@ import androidx.compose.ui.graphics.compositeOver * (Nord's High and Highest are the same color), which made the flat controls * vanish into the card. A fixed onSurface tint keeps them visible in every * preset and under dynamic color. Text and icon contrast is gated per preset - * in ComposerPaletteTest. + * in ComposerPaletteTest. The send / stop / mic action button is the one + * themed control: it takes the preset's primary so it follows the theme tint. */ internal data class ComposerPalette( val card: Color, @@ -30,10 +33,30 @@ internal data class ComposerPalette( ) private const val CONTROL_TINT_ALPHA = 0.12f +private const val ACTION_MIN_CONTRAST = 3f + +private fun contrastRatio( + a: Color, + b: Color, +): Float { + val l1 = a.luminance() + val l2 = b.luminance() + return (maxOf(l1, l2) + 0.05f) / (minOf(l1, l2) + 0.05f) +} internal fun composerPalette(scheme: ColorScheme): ComposerPalette { val card = scheme.surfaceContainer val tint = scheme.onSurface.copy(alpha = CONTROL_TINT_ALPHA) + // Pastel primaries (Nord light) vanish on the card; keep the neutral fill there. + val tinted = contrastRatio(scheme.primary, card) >= ACTION_MIN_CONTRAST + val action = if (tinted) scheme.primary else scheme.onSurface + // White glyph wherever it clears 3:1 on the primary; pastel primaries keep their own dark onPrimary. + val onAction = + when { + !tinted -> scheme.surface + contrastRatio(GlyphWhite, scheme.primary) >= ACTION_MIN_CONTRAST -> GlyphWhite + else -> scheme.onPrimary + } return ComposerPalette( card = card, cardBorder = tint.compositeOver(scheme.background), @@ -42,8 +65,8 @@ internal fun composerPalette(scheme: ColorScheme): ComposerPalette { control = tint.compositeOver(card), onControl = scheme.onSurface, onControlVariant = scheme.onSurfaceVariant, - action = scheme.onSurface, - onAction = scheme.surface, + action = action, + onAction = onAction, ) } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerPaletteTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerPaletteTest.kt index 93a76b07..6e9fefb0 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerPaletteTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/chat/ComposerPaletteTest.kt @@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.chat.components import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.luminance +import com.m57.hermescontrol.theme.GlyphWhite import com.m57.hermescontrol.theme.ThemePreset import com.m57.hermescontrol.theme.resolveColorScheme import org.junit.Assert.assertTrue @@ -66,6 +67,20 @@ class ComposerPaletteTest { } } + @Test + fun actionButtonFollowsThePresetPrimaryWhenItIsLegible() { + forEveryPresetMode { mode, p, _ -> + val scheme = resolveColorScheme(ThemePreset.valueOf(mode.substringBefore(" ")), mode.endsWith("true")) + if (contrast(scheme.primary, p.card) >= 3f) { + assertTrue("$mode action must be primary", p.action == scheme.primary) + assertTrue( + "$mode glyph must be white or onPrimary", + p.onAction == scheme.onPrimary || p.onAction == GlyphWhite, + ) + } + } + } + @Test fun controlsAndCardEdgeStayVisibleInEveryPresetMode() { forEveryPresetMode { mode, p, background ->