fix(theme): complete color and component contrast contracts (#1417)

This commit is contained in:
m57 authored and GitHub committed 2026-09-30 21:14:56 +03:00
1 parent 9529a7e32c
commit 56bb3a88c2
22 files changed
+1013 -318

No files matched your search

@@ -40,6 +40,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.HermesStatusColors
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.theme.LocalSpacing
@@ -81,28 +82,8 @@ fun StatusBadge(
val spacing = LocalSpacing.current
val statusColors = LocalHermesStatusColors.current
val (bgColor, fgColor) =
when (status) {
StatusBadgeType.SUCCESS -> {
statusColors.successContainer to statusColors.success
}
StatusBadgeType.WARNING -> {
statusColors.warningContainer to statusColors.warning
}
StatusBadgeType.ERROR -> {
statusColors.errorContainer to statusColors.error
}
StatusBadgeType.INFO -> {
statusColors.infoContainer to statusColors.info
}
StatusBadgeType.NEUTRAL -> {
MaterialTheme.colorScheme.surfaceContainerHigh to
MaterialTheme.colorScheme.onSurfaceVariant
}
}
statusBadgeColors(status, statusColors)
?: (MaterialTheme.colorScheme.surfaceContainerHigh to MaterialTheme.colorScheme.onSurfaceVariant)
Surface(
modifier = modifier,
shape = RoundedCornerShape(8.dp),
@@ -135,6 +116,20 @@ fun StatusBadge(
enum class StatusBadgeType { SUCCESS, WARNING, ERROR, INFO, NEUTRAL }
/** Exact fill/text pair shared by status badges and live-session indicators. */
internal fun statusBadgeColors(
status: StatusBadgeType,
colors: HermesStatusColors,
): Pair<Color, Color>? =
// PR #1417: on-status tokens belong on the status fill, not its container.
when (status) {
StatusBadgeType.SUCCESS -> colors.success to colors.onSuccess
StatusBadgeType.WARNING -> colors.warning to colors.onWarning
StatusBadgeType.ERROR -> colors.error to colors.onError
StatusBadgeType.INFO -> colors.info to colors.onInfo
StatusBadgeType.NEUTRAL -> null // Neutral badges use the active Material scheme.
}
// ── ToggleRow — switch row with label + description ────────────────────
@Composable
@@ -25,6 +25,8 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.SessionLiveStatus
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.StatusBadgeType
import com.m57.hermescontrol.ui.common.statusBadgeColors
@Composable
fun SessionLiveStatusIndicator(
@@ -35,27 +37,26 @@ fun SessionLiveStatusIndicator(
val spacing = LocalSpacing.current
val statusColors = LocalHermesStatusColors.current
val (bgColor, fgColor, textRes, descRes) =
val (badgeType, textRes, descRes) =
when (liveStatus) {
SessionLiveStatus.WORKING -> {
Tuple4(
statusColors.successContainer,
statusColors.success,
Triple(
StatusBadgeType.SUCCESS,
R.string.sessions_live_status_running,
R.string.sessions_live_status_running_desc,
)
}
SessionLiveStatus.WAITING -> {
Tuple4(
statusColors.warningContainer,
statusColors.warning,
Triple(
StatusBadgeType.WARNING,
R.string.sessions_live_status_waiting,
R.string.sessions_live_status_waiting_desc,
)
}
}
val (bgColor, fgColor) = requireNotNull(statusBadgeColors(badgeType, statusColors))
val text = stringResource(textRes)
val description = stringResource(descRes)
@@ -103,10 +104,3 @@ fun SessionLiveStatusIndicator(
}
}
}
private data class Tuple4<A, B, C, D>(
val first: A,
val second: B,
val third: C,
val fourth: D,
)
@@ -14,7 +14,6 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -270,11 +269,6 @@ private fun ReleaseCandidateRow(
checked = checked,
onCheckedChange = onCheckedChange,
enabled = enabled,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
}
@@ -1,5 +1,6 @@
package com.m57.hermescontrol.ui.settings.components
import android.os.Build
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
@@ -21,7 +22,6 @@ import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Slider
import androidx.compose.material3.Surface
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
@@ -41,6 +41,7 @@ import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.model.BusySendMode
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.theme.ThemePreset
import com.m57.hermescontrol.theme.ThemeRegistry
import com.m57.hermescontrol.ui.chat.label
import com.m57.hermescontrol.ui.settings.SectionCard
import kotlin.math.abs
@@ -55,6 +56,7 @@ internal fun AppearanceSection(
themePreset: ThemePreset,
onThemePresetChange: (ThemePreset) -> Unit,
) {
val dynamicAvailable = useDynamicColors && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
SectionCard {
Text(
text = stringResource(R.string.settings_item_theme),
@@ -106,11 +108,6 @@ internal fun AppearanceSection(
Switch(
checked = useDynamicColors,
onCheckedChange = onUseDynamicColorsChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
@@ -122,72 +119,30 @@ internal fun AppearanceSection(
)
Spacer(modifier = Modifier.height(8.dp))
if (dynamicAvailable) {
Text(
text = stringResource(R.string.settings_desc_theme_preset_dynamic),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
var presetsExpanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.fillMaxWidth()) {
OutlinedButton(
onClick = { presetsExpanded = true },
enabled = !useDynamicColors,
modifier = Modifier.fillMaxWidth(),
) {
Text(
when (themePreset) {
ThemePreset.DEFAULT -> stringResource(R.string.theme_preset_default)
ThemePreset.MONOCHROME -> stringResource(R.string.theme_preset_monochrome)
ThemePreset.GRUVBOX -> stringResource(R.string.theme_preset_gruvbox)
ThemePreset.CATPPUCCIN -> stringResource(R.string.theme_preset_catppuccin)
ThemePreset.AMOLED -> stringResource(R.string.theme_preset_amoled)
ThemePreset.NORD -> stringResource(R.string.theme_preset_nord)
},
)
Text(themePreset.label())
}
DropdownMenu(
expanded = presetsExpanded,
onDismissRequest = { presetsExpanded = false },
modifier = Modifier.fillMaxWidth(0.85f),
) {
ThemePreset.entries.forEach { preset ->
ThemeRegistry.forEach { (preset, _) ->
DropdownMenuItem(
text = {
Text(
when (preset) {
ThemePreset.DEFAULT -> {
stringResource(
R.string.theme_preset_default,
)
}
ThemePreset.MONOCHROME -> {
stringResource(
R.string.theme_preset_monochrome,
)
}
ThemePreset.GRUVBOX -> {
stringResource(
R.string.theme_preset_gruvbox,
)
}
ThemePreset.CATPPUCCIN -> {
stringResource(
R.string.theme_preset_catppuccin,
)
}
ThemePreset.AMOLED -> {
stringResource(
R.string.theme_preset_amoled,
)
}
ThemePreset.NORD -> {
stringResource(
R.string.theme_preset_nord,
)
}
},
)
},
text = { Text(preset.label()) },
onClick = {
onThemePresetChange(preset)
presetsExpanded = false
@@ -199,6 +154,19 @@ internal fun AppearanceSection(
}
}
@Composable
private fun ThemePreset.label(): String =
stringResource(
when (this) {
ThemePreset.DEFAULT -> R.string.theme_preset_default
ThemePreset.MONOCHROME -> R.string.theme_preset_monochrome
ThemePreset.GRUVBOX -> R.string.theme_preset_gruvbox
ThemePreset.CATPPUCCIN -> R.string.theme_preset_catppuccin
ThemePreset.AMOLED -> R.string.theme_preset_amoled
ThemePreset.NORD -> R.string.theme_preset_nord
},
)
@Composable
internal fun ChatSection(
typingEffectEnabled: Boolean,
@@ -485,11 +453,6 @@ internal fun ChatSection(
checked = showModelProvider,
onCheckedChange = onShowModelProviderChange,
modifier = Modifier.testTag("settings_show_model_provider"),
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
@@ -516,11 +479,6 @@ internal fun ChatSection(
Switch(
checked = typingEffectEnabled,
onCheckedChange = onTypingEffectEnabledChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
@@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.height
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.SwitchDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
@@ -50,11 +49,6 @@ internal fun BehaviorSection(
Switch(
checked = autoReconnect,
onCheckedChange = onAutoReconnectChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
@@ -83,11 +77,6 @@ internal fun BehaviorSection(
Switch(
checked = keepConnectedInBackground,
onCheckedChange = onKeepConnectedInBackgroundChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
@@ -116,11 +105,6 @@ internal fun BehaviorSection(
Switch(
checked = restoreLastSession,
onCheckedChange = onRestoreLastSessionChange,
colors =
SwitchDefaults.colors(
checkedThumbColor = MaterialTheme.colorScheme.primary,
checkedTrackColor = MaterialTheme.colorScheme.primaryContainer,
),
)
}
}