b06edcaca0e7

begin task 21 motion pass: animate vault list reflow and filter pill selection - LazyColumn entry rows had no animateItem() modifier, so sort changes, deletes, and new entries snapped straight to their new position; added Modifier.animateItem() per row. PeachFilterPills switched between two entirely different composables (bare Text vs Surface-wrapped Text) on selection change with no transition at all - unified into one always-mounted Text with animateColorAsState on background/content color so switching segments now crossfades instead of hard-snapping. Live-verified both: A-Z sort reorders the list and reassigns the accent pill correctly.

time
2026-07-24 13:03 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
critical-corrections-c1-c7
parent
2f7b3c14b0ec
2 files app/src/main/java/com/peachpasswords/ui/components/PeachSegmentedControl.kt+29 −25app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+6 −1
app/src/main/java/com/peachpasswords/ui/components/PeachSegmentedControl.kt+29 −25
@@ -1,5 +1,8 @@11 package com.peachpasswords.ui.components22 3+import androidx.compose.animation.animateColorAsState4+import androidx.compose.animation.core.tween5+import androidx.compose.foundation.background36 import androidx.compose.foundation.clickable47 import androidx.compose.foundation.layout.Arrangement58 import androidx.compose.foundation.layout.Row@@ -10,9 +13,11 @@1013 import androidx.compose.material3.Surface1114 import androidx.compose.material3.Text1215 import androidx.compose.runtime.Composable16+import androidx.compose.runtime.getValue1317 import androidx.compose.ui.Alignment1418 import androidx.compose.ui.Modifier1519 import androidx.compose.ui.draw.clip20+import androidx.compose.ui.graphics.Color1621 import androidx.compose.ui.unit.dp1722 import com.peachpasswords.ui.theme.LocalPeachAccent1823 import com.peachpasswords.ui.theme.PeachSpacing@@ -91,31 +96,30 @@9196     ) {9297         segments.forEach { segment ->9398             val selected = segment.key == selectedKey94-            if (selected) {95-                Surface(96-                    color = accent,97-                    contentColor = peachColors.Bg,98-                    shape = RoundedCornerShape(999.dp),99-                ) {100-                    Text(101-                        segment.label.uppercase(),102-                        style = PeachType.sortPill,103-                        modifier = Modifier104-                            .clip(RoundedCornerShape(999.dp))105-                            .clickable { onSelected(segment.key) }106-                            .padding(horizontal = PeachSpacing.sm, vertical = PeachSpacing.xs),107-                    )108-                }109-            } else {110-                Text(111-                    segment.label.uppercase(),112-                    color = peachColors.T2,113-                    style = PeachType.sortPill,114-                    modifier = Modifier115-                        .clickable { onSelected(segment.key) }116-                        .padding(vertical = PeachSpacing.xs),117-                )118-            }99+            // 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+            )119123         }120124     }121125 }

view file

app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+6 −1
@@ -235,6 +235,10 @@235235                                     else menuEntry = entry236236                                 },237237                                 onSwipeCopy = { vm.copyPassword(entry) },238+                                // Sort changes, deletes, and new entries used to snap239+                                // rows straight to their new position — animate the240+                                // reflow instead.241+                                modifier = Modifier.animateItem(),238242                             )239243                         }240244                         item {@@ -600,6 +604,7 @@600604     onTap: () -> Unit,601605     onLongPress: () -> Unit,602606     onSwipeCopy: () -> Unit,607+    modifier: Modifier = Modifier,603608 ) {604609     val context = LocalContext.current605610     val accent = LocalPeachAccent.current@@ -616,7 +621,7 @@616621     val meta = remember(entry) { entry.domainKeyOrUser() }617622 618623     Row(619-        modifier = Modifier624+        modifier = modifier620625             .fillMaxWidth()621626             .clip(RoundedCornerShape(7.dp))622627             .background(rowBg)

view file