a781101cdf46

fix vault list accent scatter (new-entry link now neutral, not competing with SMART/ALL pills) and compose end-of-list footer to center in remaining viewport height instead of trailing into dead space

time
2026-07-24 02:16 UTC · 3 months ago
machine
koh2
agent
claude-sonnet-5
session
gate1-gate2-verification-pass
parent
a9fab5504cb3
1 file app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+59 −37
app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+59 −37
@@ -17,12 +17,14 @@1717 import androidx.compose.foundation.interaction.collectIsPressedAsState1818 import androidx.compose.foundation.layout.Arrangement1919 import androidx.compose.foundation.layout.Box20+import androidx.compose.foundation.layout.BoxWithConstraints2021 import androidx.compose.foundation.layout.Column2122 import androidx.compose.foundation.layout.Row2223 import androidx.compose.foundation.layout.Spacer2324 import androidx.compose.foundation.layout.fillMaxSize2425 import androidx.compose.foundation.layout.fillMaxWidth2526 import androidx.compose.foundation.layout.height27+import androidx.compose.foundation.layout.heightIn2628 import androidx.compose.foundation.layout.padding2729 import androidx.compose.foundation.layout.size2830 import androidx.compose.foundation.layout.statusBarsPadding@@ -196,42 +198,56 @@196198                 onHealth = { onNavigate(PeachNav.Health) },197199                 onTrash = { onNavigate(PeachNav.Trash) },198200             )199-            PeachPullToRefresh(200-                refreshing = syncStatus is SyncStatus.Syncing,201-                onRefresh = { syncVm.syncNow() },202-                modifier = Modifier.fillMaxWidth().weight(1f),203-            ) {204-                LazyColumn(Modifier.fillMaxSize()) {205-                    if (entries.isEmpty()) {206-                        item { EmptyState(onSeed = vm::seedDemo) }207-                    }208-                    items(entries, key = { it.id }) { entry ->209-                        EntryRow(210-                            entry = entry,211-                            flashing = flashId == entry.id,212-                            canSwipeCopy = vm.canSwipeCopy(entry) && !isMultiSelect,213-                            multiSelect = isMultiSelect,214-                            selected = entry.id in selectedIds,215-                            onTap = {216-                                if (isMultiSelect) vm.toggleSelected(entry.id)217-                                else onNavigate(entryTapDestination(entry))218-                            },219-                            onLongPress = {220-                                if (isMultiSelect) vm.toggleSelected(entry.id)221-                                else menuEntry = entry222-                            },223-                            onSwipeCopy = { vm.copyPassword(entry) },224-                        )225-                    }226-                    item {227-                        NewEntryRow(onClick = { onNavigate(PeachNav.EntryEdit(null)) })228-                        if (entries.isNotEmpty()) {229-                            PeachEmptyOrEndState(230-                                message = "all caught up",231-                                modifier = Modifier.padding(top = PeachSpacing.sm),201+            BoxWithConstraints(Modifier.fillMaxWidth().weight(1f)) {202+                // Short lists (a handful of entries) would otherwise leave the203+                // "all caught up" footer stranded near the top with dead space204+                // trailing beneath it — center the footer in whatever viewport205+                // height the entries don't already occupy (principle: composed206+                // space, never leftover). Long lists get a remainder of ~0 and207+                // are unaffected.208+                val footerMinHeight = (maxHeight - ENTRY_ROW_HEIGHT * entries.size).coerceAtLeast(0.dp)209+                PeachPullToRefresh(210+                    refreshing = syncStatus is SyncStatus.Syncing,211+                    onRefresh = { syncVm.syncNow() },212+                    modifier = Modifier.fillMaxSize(),213+                ) {214+                    LazyColumn(Modifier.fillMaxSize()) {215+                        if (entries.isEmpty()) {216+                            item { EmptyState(onSeed = vm::seedDemo) }217+                        }218+                        items(entries, key = { it.id }) { entry ->219+                            EntryRow(220+                                entry = entry,221+                                flashing = flashId == entry.id,222+                                canSwipeCopy = vm.canSwipeCopy(entry) && !isMultiSelect,223+                                multiSelect = isMultiSelect,224+                                selected = entry.id in selectedIds,225+                                onTap = {226+                                    if (isMultiSelect) vm.toggleSelected(entry.id)227+                                    else onNavigate(entryTapDestination(entry))228+                                },229+                                onLongPress = {230+                                    if (isMultiSelect) vm.toggleSelected(entry.id)231+                                    else menuEntry = entry232+                                },233+                                onSwipeCopy = { vm.copyPassword(entry) },232234                             )233235                         }234-                        Spacer(Modifier.navigationBarsPadding())236+                        item {237+                            Column(238+                                modifier = Modifier.fillMaxWidth().heightIn(min = footerMinHeight),239+                                verticalArrangement = Arrangement.Center,240+                            ) {241+                                NewEntryRow(onClick = { onNavigate(PeachNav.EntryEdit(null)) })242+                                if (entries.isNotEmpty()) {243+                                    PeachEmptyOrEndState(244+                                        message = "all caught up",245+                                        modifier = Modifier.padding(top = PeachSpacing.sm),246+                                    )247+                                }248+                            }249+                            Spacer(Modifier.navigationBarsPadding())250+                        }235251                     }236252                 }237253             }@@ -664,9 +680,15 @@664680     }665681 }666682 683+/** Approximate on-screen height of one [EntryRow], used to size the trailing footer. */684+private val ENTRY_ROW_HEIGHT = 76.dp685+667686 @Composable668687 private fun NewEntryRow(onClick: () -> Unit) {669-    val accent = LocalPeachAccent.current688+    // Neutral, not accent: the persistent "+" in PeachTopBar already owns the689+    // add-entry accent moment. A second accent-colored add affordance here690+    // would compete with the SMART/ALL selection pills for the screen's one691+    // hero accent use.670692     Row(671693         modifier = Modifier672694             .fillMaxWidth()@@ -675,8 +697,8 @@675697         verticalAlignment = Alignment.CenterVertically,676698         horizontalArrangement = Arrangement.spacedBy(10.dp),677699     ) {678-        Icon(Icons.Rounded.Add, contentDescription = null, tint = accent, modifier = Modifier.size(16.dp))679-        Text("new entry", color = accent, style = PeachType.labelMono)700+        Icon(Icons.Rounded.Add, contentDescription = null, tint = peachColors.T1, modifier = Modifier.size(16.dp))701+        Text("new entry", color = peachColors.T1, style = PeachType.labelMono)680702     }681703 }682704 

view file