PeachSegmentedControl.kt

4.8 KB · raw

1package com.peachpasswords.ui.components23import androidx.compose.animation.animateColorAsState4import androidx.compose.animation.core.tween5import androidx.compose.foundation.background6import androidx.compose.foundation.clickable7import androidx.compose.foundation.layout.Arrangement8import androidx.compose.foundation.layout.Row9import androidx.compose.foundation.layout.defaultMinSize10import androidx.compose.foundation.layout.fillMaxWidth11import androidx.compose.foundation.layout.padding12import androidx.compose.foundation.shape.RoundedCornerShape13import androidx.compose.material3.Surface14import androidx.compose.material3.Text15import androidx.compose.runtime.Composable16import androidx.compose.runtime.getValue17import androidx.compose.ui.Alignment18import androidx.compose.ui.Modifier19import androidx.compose.ui.draw.clip20import androidx.compose.ui.graphics.Color21import androidx.compose.ui.unit.dp22import com.peachpasswords.ui.theme.LocalPeachAccent23import com.peachpasswords.ui.theme.PeachSpacing24import com.peachpasswords.ui.theme.PeachType25import com.peachpasswords.ui.theme.peachColors2627data class PeachSegment(28    val key: String,29    val label: String,30)3132@Composable33fun PeachSegmentedControl(34    segments: List<PeachSegment>,35    selectedKey: String,36    onSelected: (String) -> Unit,37    modifier: Modifier = Modifier,38) {39    val accent = LocalPeachAccent.current40    Surface(41        modifier = modifier.fillMaxWidth(),42        color = peachColors.S1,43        shape = RoundedCornerShape(999.dp),44        tonalElevation = 1.dp,45    ) {46        Row(47            modifier = Modifier.padding(PeachSpacing.xs),48            verticalAlignment = Alignment.CenterVertically,49            horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),50        ) {51            segments.forEach { segment ->52                val selected = segment.key == selectedKey53                Surface(54                    modifier = Modifier55                        .weight(1f)56                        .defaultMinSize(minHeight = 34.dp)57                        .clickable { onSelected(segment.key) },58                    color = if (selected) accent else peachColors.S2,59                    contentColor = if (selected) peachColors.Bg else peachColors.T1,60                    shape = RoundedCornerShape(999.dp),61                ) {62                    Row(63                        modifier = Modifier.padding(horizontal = PeachSpacing.sm, vertical = PeachSpacing.sm),64                        horizontalArrangement = Arrangement.Center,65                        verticalAlignment = Alignment.CenterVertically,66                    ) {67                        Text(segment.label.uppercase(), style = PeachType.sortPill)68                    }69                }70            }71        }72    }73}7475/**76 * Browser-matched quiet filter row: the active option gets one small,77 * content-sized accent pill; every other option is bare text (no box,78 * no fill) sitting quietly beside it. Distinct from [PeachSegmentedControl],79 * which renders every option as an equal-weight full-width button — right for80 * a real multi-way choice (entry type), wrong for a filter/sort row, where81 * giving every option the same loud pill treatment reads as several accent82 * moments stacked instead of one (SMART/A-Z/Z-A/RECENT in the browser).83 */84@Composable85fun PeachFilterPills(86    segments: List<PeachSegment>,87    selectedKey: String,88    onSelected: (String) -> Unit,89    modifier: Modifier = Modifier,90) {91    val accent = LocalPeachAccent.current92    Row(93        modifier = modifier,94        verticalAlignment = Alignment.CenterVertically,95        horizontalArrangement = Arrangement.spacedBy(PeachSpacing.md),96    ) {97        segments.forEach { segment ->98            val selected = segment.key == selectedKey99            // One shape, always mounted — animating its own background/content100            // color between segments reads as a smooth crossfade instead of101            // the old hard swap between two entirely different composables102            // (a bare Text vs. a Surface-wrapped one) with no transition.103            val bg by animateColorAsState(104                if (selected) accent else Color.Transparent,105                animationSpec = tween(180),106                label = "pill-bg",107            )108            val fg by animateColorAsState(109                if (selected) peachColors.Bg else peachColors.T2,110                animationSpec = tween(180),111                label = "pill-fg",112            )113            Text(114                segment.label.uppercase(),115                color = fg,116                style = PeachType.sortPill,117                modifier = Modifier118                    .clip(RoundedCornerShape(999.dp))119                    .background(bg)120                    .clickable { onSelected(segment.key) }121                    .padding(horizontal = PeachSpacing.sm, vertical = PeachSpacing.xs),122            )123        }124    }125}