PeachSegmentedControl.kt

5.2 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.MaterialTheme14import androidx.compose.material3.Surface15import androidx.compose.material3.Text16import androidx.compose.runtime.Composable17import androidx.compose.runtime.getValue18import androidx.compose.ui.Alignment19import androidx.compose.ui.Modifier20import androidx.compose.ui.draw.clip21import androidx.compose.ui.graphics.Color22import androidx.compose.ui.unit.dp23import com.peachpasswords.ui.theme.LocalPeachAccent24import com.peachpasswords.ui.theme.PeachSpacing25import com.peachpasswords.ui.theme.PeachType26import com.peachpasswords.ui.theme.peachColors2728data class PeachSegment(29    val key: String,30    val label: String,31)3233@Composable34fun PeachSegmentedControl(35    segments: List<PeachSegment>,36    selectedKey: String,37    onSelected: (String) -> Unit,38    modifier: Modifier = Modifier,39) {40    val accent = LocalPeachAccent.current41    Surface(42        modifier = modifier.fillMaxWidth(),43        // Gate 1: colorScheme.surface is PeachTheme's exact mapping of S1.44        color = MaterialTheme.colorScheme.surface,45        shape = RoundedCornerShape(999.dp),46        tonalElevation = 1.dp,47    ) {48        Row(49            modifier = Modifier.padding(PeachSpacing.xs),50            verticalAlignment = Alignment.CenterVertically,51            horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),52        ) {53            segments.forEach { segment ->54                val selected = segment.key == selectedKey55                Surface(56                    modifier = Modifier57                        .weight(1f)58                        // MD3 minimum touch target (Gate 1 finding).59                        .defaultMinSize(minHeight = 48.dp)60                        .clickable { onSelected(segment.key) },61                    // Gate 1: colorScheme.surfaceVariant/onPrimary are PeachTheme's62                    // exact mappings of S2/Bg.63                    color = if (selected) accent else MaterialTheme.colorScheme.surfaceVariant,64                    contentColor = if (selected) MaterialTheme.colorScheme.onPrimary else peachColors.T1,65                    shape = RoundedCornerShape(999.dp),66                ) {67                    Row(68                        modifier = Modifier.padding(horizontal = PeachSpacing.sm, vertical = PeachSpacing.sm),69                        horizontalArrangement = Arrangement.Center,70                        verticalAlignment = Alignment.CenterVertically,71                    ) {72                        Text(segment.label.uppercase(), style = PeachType.sortPill)73                    }74                }75            }76        }77    }78}7980/**81 * Browser-matched quiet filter row: the active option gets one small,82 * content-sized accent pill; every other option is bare text (no box,83 * no fill) sitting quietly beside it. Distinct from [PeachSegmentedControl],84 * which renders every option as an equal-weight full-width button — right for85 * a real multi-way choice (entry type), wrong for a filter/sort row, where86 * giving every option the same loud pill treatment reads as several accent87 * moments stacked instead of one (SMART/A-Z/Z-A/RECENT in the browser).88 */89@Composable90fun PeachFilterPills(91    segments: List<PeachSegment>,92    selectedKey: String,93    onSelected: (String) -> Unit,94    modifier: Modifier = Modifier,95) {96    val accent = LocalPeachAccent.current97    Row(98        modifier = modifier,99        verticalAlignment = Alignment.CenterVertically,100        horizontalArrangement = Arrangement.spacedBy(PeachSpacing.md),101    ) {102        segments.forEach { segment ->103            val selected = segment.key == selectedKey104            // One shape, always mounted — animating its own background/content105            // color between segments reads as a smooth crossfade instead of106            // the old hard swap between two entirely different composables107            // (a bare Text vs. a Surface-wrapped one) with no transition.108            val bg by animateColorAsState(109                if (selected) accent else Color.Transparent,110                animationSpec = tween(180),111                label = "pill-bg",112            )113            val fg by animateColorAsState(114                if (selected) peachColors.Bg else peachColors.T2,115                animationSpec = tween(180),116                label = "pill-fg",117            )118            Text(119                segment.label.uppercase(),120                color = fg,121                style = PeachType.sortPill,122                modifier = Modifier123                    .clip(RoundedCornerShape(999.dp))124                    .background(bg)125                    .clickable { onSelected(segment.key) }126                    .padding(horizontal = PeachSpacing.sm, vertical = PeachSpacing.xs),127            )128        }129    }130}