feat: redesign mode pills and dropdowns

Pills: extracted shared ModePill composable — thinner border (0.8dp gradient
instead of 1.2dp solid), subtler inactive background, smaller text (12.5sp),
compact padding (12x7 instead of 13x8). Active state uses accent gradient
border instead of solid color.

Reasoning dropdown: styled container (dark glass bg, 16dp shadow, squircle
shape), check icon on selected preset, cleaner row layout.

Sampling popover: same styled container, custom checkbox (squircle with accent
tint instead of Material Checkbox), disabled state dims labels and track,
wider panel (260dp), more spacing between parameters.
This commit is contained in:
Bruno Deanoz 2026-06-23 01:07:57 +02:00
parent fcac983201
commit 497f0a82f7

View file

@ -159,82 +159,83 @@ fun ModePillsRow(
var showReasoningMenu by remember { mutableStateOf(false) } var showReasoningMenu by remember { mutableStateOf(false) }
var showSamplingPopup by remember { mutableStateOf(false) } var showSamplingPopup by remember { mutableStateOf(false) }
val inactiveBg = if (isDark) cs.surface.copy(alpha = 0.35f) else cs.surface.copy(alpha = 0.55f)
val inactiveBorder = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f)
Row( Row(
modifier modifier
.fillMaxWidth() .fillMaxWidth()
.horizontalScroll(rememberScrollState()) .horizontalScroll(rememberScrollState())
.padding(horizontal = 14.dp), .padding(horizontal = 14.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(6.dp),
) { ) {
// Reasoning pill (dropdown) // Reasoning pill
Box { Box {
val isDefault = reasoningPreset == ReasoningPreset.Standard
ModePill(
icon = KaizenIcons.Brain,
label = stringResource(reasoningPreset.label),
active = !isDefault,
showChevron = true,
onClick = { showReasoningMenu = true },
)
DropdownMenu(
expanded = showReasoningMenu,
onDismissRequest = { showReasoningMenu = false },
containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB),
shadowElevation = 16.dp,
shape = KaizenShapes.md,
) {
ReasoningPreset.entries.forEach { preset ->
Row( Row(
Modifier Modifier
.clip(KaizenShapes.pill) .fillMaxWidth()
.background(inactiveBg) .clickable { onReasoningChange(preset); showReasoningMenu = false }
.border(1.2.dp, inactiveBorder, KaizenShapes.pill) .padding(horizontal = 16.dp, vertical = 10.dp),
.clickable { showReasoningMenu = true }
.padding(horizontal = 13.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(KaizenIcons.Brain, null, tint = cs.onSurfaceVariant, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(6.dp))
Text(stringResource(reasoningPreset.label), color = cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.width(4.dp))
Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp))
}
DropdownMenu(expanded = showReasoningMenu, onDismissRequest = { showReasoningMenu = false }) {
ReasoningPreset.entries.forEach { preset ->
DropdownMenuItem(
text = {
Text( Text(
stringResource(preset.label), stringResource(preset.label),
fontSize = 14.sp, fontSize = 13.5.sp,
color = if (preset == reasoningPreset) accent.primary else cs.onBackground, color = if (preset == reasoningPreset) accent.primary else cs.onBackground.copy(alpha = 0.85f),
fontWeight = if (preset == reasoningPreset) FontWeight.SemiBold else FontWeight.Normal, fontWeight = if (preset == reasoningPreset) FontWeight.SemiBold else FontWeight.Normal,
modifier = Modifier.weight(1f),
) )
}, if (preset == reasoningPreset) {
onClick = { onReasoningChange(preset); showReasoningMenu = false }, Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(15.dp))
) }
}
} }
} }
} }
// Sampling pill (popover) // Sampling pill
Box { Box {
val anyEnabled = samplingPrefs.temperature.enabled || samplingPrefs.topP.enabled || samplingPrefs.topK.enabled val anyEnabled = samplingPrefs.temperature.enabled || samplingPrefs.topP.enabled || samplingPrefs.topK.enabled
Row( ModePill(
Modifier icon = KaizenIcons.SlidersHorizontal,
.clip(KaizenShapes.pill) label = stringResource(R.string.mode_sampling),
.background(if (anyEnabled) accent.primary.copy(alpha = if (isDark) 0.18f else 0.14f) else inactiveBg) active = anyEnabled,
.border(1.2.dp, if (anyEnabled) accent.primary.copy(alpha = 0.4f) else inactiveBorder, KaizenShapes.pill) showChevron = true,
.clickable { showSamplingPopup = !showSamplingPopup } onClick = { showSamplingPopup = !showSamplingPopup },
.padding(horizontal = 13.dp, vertical = 8.dp), )
verticalAlignment = Alignment.CenterVertically, DropdownMenu(
expanded = showSamplingPopup,
onDismissRequest = { showSamplingPopup = false },
containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB),
shadowElevation = 16.dp,
shape = KaizenShapes.md,
) { ) {
Icon(KaizenIcons.SlidersHorizontal, null, tint = if (anyEnabled) accent.primary else cs.onSurfaceVariant, modifier = Modifier.size(16.dp)) Column(Modifier.padding(horizontal = 16.dp, vertical = 12.dp).width(260.dp)) {
Spacer(Modifier.width(6.dp))
Text(stringResource(R.string.mode_sampling), color = if (anyEnabled) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = FontWeight.Medium)
Spacer(Modifier.width(4.dp))
Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.5f), modifier = Modifier.size(14.dp))
}
DropdownMenu(expanded = showSamplingPopup, onDismissRequest = { showSamplingPopup = false }) {
Column(Modifier.padding(horizontal = 16.dp, vertical = 8.dp).width(240.dp)) {
SamplingRow( SamplingRow(
label = stringResource(R.string.sampling_temperature), label = stringResource(R.string.sampling_temperature),
param = samplingPrefs.temperature, min = 0f, max = 2f, steps = 200, param = samplingPrefs.temperature, min = 0f, max = 2f, steps = 200,
onUpdate = { onSamplingChange(samplingPrefs.copy(temperature = it)) }, onUpdate = { onSamplingChange(samplingPrefs.copy(temperature = it)) },
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(14.dp))
SamplingRow( SamplingRow(
label = stringResource(R.string.sampling_top_p), label = stringResource(R.string.sampling_top_p),
param = samplingPrefs.topP, min = 0f, max = 1f, steps = 100, param = samplingPrefs.topP, min = 0f, max = 1f, steps = 100,
onUpdate = { onSamplingChange(samplingPrefs.copy(topP = it)) }, onUpdate = { onSamplingChange(samplingPrefs.copy(topP = it)) },
) )
Spacer(Modifier.height(12.dp)) Spacer(Modifier.height(14.dp))
SamplingRow( SamplingRow(
label = stringResource(R.string.sampling_top_k), label = stringResource(R.string.sampling_top_k),
param = samplingPrefs.topK, min = 1f, max = 100f, steps = 99, param = samplingPrefs.topK, min = 1f, max = 100f, steps = 99,
@ -246,24 +247,62 @@ fun ModePillsRow(
// Mode pills (Bild, Suche, Video, Audio) // Mode pills (Bild, Suche, Video, Audio)
chatModes.filter { it.labelRes != R.string.mode_standard && it.labelRes != R.string.mode_sampling }.forEach { mode -> chatModes.filter { it.labelRes != R.string.mode_standard && it.labelRes != R.string.mode_sampling }.forEach { mode ->
val label = stringResource(mode.labelRes)
val active = mode.labelRes == activeMode val active = mode.labelRes == activeMode
val backgroundFill = if (active) accent.primary.copy(alpha = if (isDark) 0.18f else 0.14f) else inactiveBg ModePill(
val borderColor = if (active) accent.primary.copy(alpha = 0.4f) else inactiveBorder icon = mode.icon,
label = stringResource(mode.labelRes),
active = active,
onClick = { onToggle(mode.labelRes) },
)
}
}
}
@Composable
private fun ModePill(
icon: ImageVector,
label: String,
active: Boolean,
showChevron: Boolean = false,
onClick: () -> Unit,
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val bg = when {
active -> accent.primary.copy(alpha = if (isDark) 0.15f else 0.10f)
else -> if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)
}
val borderBrush = if (active) {
Brush.verticalGradient(listOf(
accent.primary.copy(alpha = if (isDark) 0.30f else 0.22f),
accent.primary.copy(alpha = if (isDark) 0.08f else 0.06f),
))
} else {
Brush.verticalGradient(listOf(
if (isDark) Color.White.copy(alpha = 0.08f) else Color.White.copy(alpha = 0.40f),
if (isDark) Color.White.copy(alpha = 0.02f) else Color.Black.copy(alpha = 0.03f),
))
}
val iconTint = if (active) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.60f)
val textColor = if (active) cs.onBackground else cs.onSurfaceVariant.copy(alpha = 0.70f)
Row( Row(
Modifier Modifier
.clip(KaizenShapes.pill) .clip(KaizenShapes.pill)
.background(backgroundFill) .background(bg)
.border(1.2.dp, borderColor, KaizenShapes.pill) .border(0.8.dp, borderBrush, KaizenShapes.pill)
.clickable { onToggle(mode.labelRes) } .clickable(onClick = onClick)
.padding(horizontal = 13.dp, vertical = 8.dp), .padding(horizontal = 12.dp, vertical = 7.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Icon(mode.icon, null, tint = if (active) accent.primary else cs.onSurfaceVariant, modifier = Modifier.size(16.dp)) Icon(icon, null, tint = iconTint, modifier = Modifier.size(15.dp))
Spacer(Modifier.width(6.dp)) Spacer(Modifier.width(5.dp))
Text(label, color = if (active) cs.onBackground else cs.onSurfaceVariant, fontSize = 13.sp, fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium) Text(label, color = textColor, fontSize = 12.5.sp, fontWeight = if (active) FontWeight.SemiBold else FontWeight.Medium)
} if (showChevron) {
Spacer(Modifier.width(3.dp))
Icon(KaizenIcons.ChevronDown, null, tint = cs.onSurfaceVariant.copy(alpha = 0.35f), modifier = Modifier.size(13.dp))
} }
} }
} }
@ -271,21 +310,48 @@ fun ModePillsRow(
@Composable @Composable
private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Float, steps: Int, onUpdate: (SamplingParam) -> Unit) { private fun SamplingRow(label: String, param: SamplingParam, min: Float, max: Float, steps: Int, onUpdate: (SamplingParam) -> Unit) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current val accent = LocalKaizenAccent.current
Row(verticalAlignment = Alignment.CenterVertically) { Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = param.enabled, onCheckedChange = { onUpdate(param.copy(enabled = it)) }, modifier = Modifier.size(20.dp)) Box(
Spacer(Modifier.width(8.dp)) Modifier
Text(label, color = cs.onBackground, fontSize = 13.sp, modifier = Modifier.weight(1f)) .size(20.dp)
.clip(KaizenShapes.xs)
.border(
1.dp,
if (param.enabled) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.25f),
KaizenShapes.xs,
)
.then(if (param.enabled) Modifier.background(accent.primary.copy(alpha = 0.15f)) else Modifier)
.clickable { onUpdate(param.copy(enabled = !param.enabled)) },
contentAlignment = Alignment.Center,
) {
if (param.enabled) {
Icon(KaizenIcons.Check, null, tint = accent.primary, modifier = Modifier.size(13.dp))
}
}
Spacer(Modifier.width(10.dp))
Text(label, color = cs.onBackground.copy(alpha = if (param.enabled) 0.90f else 0.50f), fontSize = 13.sp, modifier = Modifier.weight(1f))
Text( Text(
if (max > 2f) param.value.toInt().toString() else "%.2f".format(param.value), if (max > 2f) param.value.toInt().toString() else "%.2f".format(param.value),
color = cs.onSurfaceVariant, fontSize = 12.sp, color = if (param.enabled) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.40f),
fontSize = 12.sp, fontWeight = FontWeight.Medium,
) )
} }
Spacer(Modifier.height(4.dp))
Slider( Slider(
value = param.value, onValueChange = { onUpdate(param.copy(value = it)) }, value = param.value, onValueChange = { onUpdate(param.copy(value = it)) },
valueRange = min..max, steps = steps, valueRange = min..max, steps = steps,
enabled = param.enabled, enabled = param.enabled,
colors = SliderDefaults.colors(thumbColor = accent.primary, activeTrackColor = accent.primary), colors = SliderDefaults.colors(
thumbColor = accent.primary,
activeTrackColor = accent.primary,
inactiveTrackColor = if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.06f),
disabledThumbColor = cs.onSurfaceVariant.copy(alpha = 0.20f),
disabledActiveTrackColor = cs.onSurfaceVariant.copy(alpha = 0.15f),
disabledInactiveTrackColor = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.04f),
),
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
) )
} }