feat(chat): restyle the composer as a single card (#1114)

This commit is contained in:
Alfredo Ortegón Sepúlveda authored and GitHub committed 2026-09-15 16:37:46 +03:00
1 parent 528ec4f28f
commit e5adfcf571
7 files changed
+671 -303

No files matched your search

@@ -0,0 +1,76 @@
package com.m57.hermescontrol.ui.chat.components
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.luminance
import com.m57.hermescontrol.theme.ThemePreset
import com.m57.hermescontrol.theme.resolveColorScheme
import org.junit.Assert.assertTrue
import org.junit.Test
/**
* Contrast gate for the chat composer across every preset in both modes.
*
* - Primary text and control labels (onSurface) >= 4.5:1 — WCAG AA text.
* - Secondary text (placeholder, reasoning level) >= 3:1 — same bar as the
* full-bleed header gate in ThemePaletteTest.
* - Action button against the card, and its glyph against the button >= 3:1.
* - Flat controls and the card edge must stay visibly distinct (>= 1.15:1).
* Their labels and icons carry the WCAG 1.4.11 contrast; this floor catches
* presets whose surface tiers collapse (Nord's High == Highest rendered the
* controls invisible).
*/
class ComposerPaletteTest {
private fun contrast(
a: Color,
b: Color,
): Float {
val l1 = a.luminance()
val l2 = b.luminance()
return (maxOf(l1, l2) + 0.05f) / (minOf(l1, l2) + 0.05f)
}
private fun forEveryPresetMode(block: (String, ComposerPalette, Color) -> Unit) {
ThemePreset.entries.forEach { preset ->
listOf(true, false).forEach { dark ->
val scheme = resolveColorScheme(preset, darkTheme = dark)
block("$preset dark=$dark", composerPalette(scheme), scheme.background)
}
}
}
private fun assertContrast(
label: String,
a: Color,
b: Color,
min: Float,
) {
val ratio = contrast(a, b)
assertTrue("$label contrast %.2f must be >= $min".format(ratio), ratio >= min)
}
@Test
fun textMeetsContrastInEveryPresetMode() {
forEveryPresetMode { mode, p, _ ->
assertContrast("$mode input text/card", p.text, p.card, 4.5f)
assertContrast("$mode placeholder/card", p.placeholder, p.card, 3f)
assertContrast("$mode control label/control", p.onControl, p.control, 4.5f)
assertContrast("$mode reasoning level/control", p.onControlVariant, p.control, 3f)
}
}
@Test
fun actionButtonMeetsContrastInEveryPresetMode() {
forEveryPresetMode { mode, p, _ ->
assertContrast("$mode action/card", p.action, p.card, 3f)
assertContrast("$mode action glyph/action", p.onAction, p.action, 3f)
}
}
@Test
fun controlsAndCardEdgeStayVisibleInEveryPresetMode() {
forEveryPresetMode { mode, p, background ->
assertContrast("$mode control/card", p.control, p.card, 1.15f)
assertContrast("$mode card border/background", p.cardBorder, background, 1.15f)
}
}
}
@@ -0,0 +1,35 @@
package com.m57.hermescontrol.ui.chat.components
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Unit tests for [composerModelLabel] — the label shown in the composer's
* model/reasoning pill. Only the "custom:" provider marker is dropped; the
* provider stays so the same model from different providers never collides.
*/
class ComposerToolbarTest {
@Test
fun composerModelLabel_keepsProviderToDisambiguateSameModel() {
assertEquals("openai/gpt-5.5", composerModelLabel("openai/gpt-5.5"))
assertEquals("copilot/gpt-5.5", composerModelLabel("copilot/gpt-5.5"))
}
@Test
fun composerModelLabel_dropsOnlyTheCustomMarker() {
assertEquals("my-provider/glm-5.3", composerModelLabel("custom:my-provider/glm-5.3"))
assertEquals("openrouter/openai/gpt-5.5", composerModelLabel("openrouter/openai/gpt-5.5"))
}
@Test
fun composerModelLabel_leavesModelTagsAndPlainIdsAlone() {
assertEquals("ollama/llama3:8b", composerModelLabel("ollama/llama3:8b"))
assertEquals("llama3:8b", composerModelLabel("llama3:8b"))
assertEquals("gpt-5", composerModelLabel("gpt-5"))
}
@Test
fun composerModelLabel_bareCustomMarker_fallsBackToFullId() {
assertEquals("custom:/glm-5.3", composerModelLabel("custom:/glm-5.3"))
}
}