f7dad03c0489

feat: Simplify entry detail for thumb reach (v2.14.19)

- Replace the dense top toolbar with Peach/Vault identity - Present credential values as quiet outlined cards - Add labeled Edit and Move to trash actions - Separate pagination from a five-action bottom bar with Notes - Route existing-entry Edit into the full editor

time
2026-08-14 15:48 UTC · 2 months ago
machine
koh2
agent
gpt-5.6-sol
session
simplify entry detail
parent
24bcf1918eae
4 files VERSION+1 −1app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+2 −2app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+0 −4app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+124 −170
VERSION+1 −1
@@ -1 +1 @@1-2.14.181+2.14.19

view file

app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+2 −2
@@ -285,10 +285,10 @@285285                     },286286                 )287287             } else {288-                EntryDetailScreen(288+                EntryEditScreen(289289                     id = editId,290-                    onNavigate = nav,291290                     onBack = back,291+                    onOpenNote = { noteId -> nav(PeachNav.NotesMode(noteId)) },292292                 )293293             }294294         }

view file

app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+0 −4
@@ -180,10 +180,6 @@180180                 style = PeachType.bodyMono,181181                 modifier = Modifier.weight(1f).clickable { beginEdit() },182182             )183-            Text(184-                "edit", color = accent, style = PeachType.labelMono,185-                modifier = Modifier.clip(RoundedCornerShape(6.dp)).clickable { beginEdit() }.padding(8.dp),186-            )187183             RevealButton(revealed = reveal, onClick = { reveal = !reveal })188184             CopyButton(copied, onCopy)189185         } else {

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+124 −170
@@ -12,14 +12,12 @@1212 import androidx.compose.foundation.border1313 import androidx.compose.foundation.clickable1414 import androidx.compose.foundation.gestures.detectHorizontalDragGestures15-import androidx.compose.foundation.gestures.detectVerticalDragGestures1615 import androidx.compose.foundation.layout.Arrangement1716 import androidx.compose.foundation.layout.Box1817 import androidx.compose.foundation.layout.Column1918 import androidx.compose.foundation.layout.Row2019 import androidx.compose.foundation.layout.Spacer2120 import androidx.compose.foundation.layout.fillMaxSize22-import androidx.compose.foundation.layout.fillMaxHeight2321 import androidx.compose.foundation.layout.fillMaxWidth2422 import androidx.compose.foundation.layout.height2523 import androidx.compose.foundation.layout.offset@@ -29,7 +27,6 @@2927 import androidx.compose.foundation.layout.statusBarsPadding3028 import androidx.compose.foundation.layout.width3129 import androidx.compose.foundation.rememberScrollState32-import androidx.compose.foundation.shape.CircleShape3330 import androidx.compose.foundation.shape.RoundedCornerShape3431 import androidx.compose.foundation.text.BasicTextField3532 import androidx.compose.material.icons.rounded.Add@@ -41,8 +38,6 @@4138 import androidx.compose.material.icons.Icons4239 import androidx.compose.material.icons.automirrored.rounded.ArrowBack4340 import androidx.compose.material.icons.automirrored.rounded.ArrowForward44-import androidx.compose.material.icons.outlined.Add45-import androidx.compose.material.icons.outlined.Delete4641 import androidx.compose.material.icons.outlined.Description4742 import androidx.compose.material.icons.outlined.Settings4843 import androidx.compose.material.icons.outlined.Lock@@ -81,14 +76,14 @@8176 import com.peachpasswords.ui.components.CharEnterText8277 import com.peachpasswords.ui.components.CopyButton8378 import com.peachpasswords.ui.components.PeachEmptyOrEndState84-import com.peachpasswords.ui.components.PeachMark85-import com.peachpasswords.ui.components.PeachTopBar79+import com.peachpasswords.ui.components.PeachWordmark8680 import com.peachpasswords.ui.components.SecureFlagEffect8781 import com.peachpasswords.ui.components.TOTPRing8882 import com.peachpasswords.ui.lock.Biometrics8983 import com.peachpasswords.ui.theme.LocalPeachAccent9084 import com.peachpasswords.ui.theme.LocalReduceMotion9185 import com.peachpasswords.ui.theme.PeachSpacing86+import com.peachpasswords.ui.theme.PeachRadius9287 import com.peachpasswords.ui.theme.peachColors9388 import com.peachpasswords.ui.theme.PeachType9489 import com.peachpasswords.ui.theme.ShellBackground@@ -154,27 +149,12 @@154149         ) {155150             Column(156151                 Modifier.fillMaxSize()157-                    .padding(bottom = 100.dp)152+                    .padding(bottom = 172.dp)158153                     .verticalScroll(rememberScrollState())159154                     .padding(horizontal = PeachSpacing.xl),160155             ) {161-                EntryDetailTopBar(162-                    entry = e,163-                    confirmingDelete = confirmingDelete,164-                    onBack = onBack,165-                    onLock = vm::lock,166-                    onDelete = {167-                        if (confirmingDelete) {168-                            vm.delete(onDone = onBack ?: { onNavigate(PeachNav.VaultList) })169-                        } else {170-                            confirmingDelete = true171-                        }172-                    },173-                    onAddEntry = { onNavigate(PeachNav.EntryEdit(null)) },174-                    onSettings = { onNavigate(PeachNav.Settings) },175-                    onOpenNotes = { onNavigate(PeachNav.Notes) },176-                )177-                Spacer(Modifier.height(PeachSpacing.lg))156+                EntryDetailIdentity()157+                Spacer(Modifier.height(PeachSpacing.xxl))178158 179159                 if (e == null) {180160                     PeachEmptyOrEndState(@@ -375,6 +355,18 @@375355                         draftText = noteEditorText(v),376356                     )377357                 }358+                EntryDetailActions(359+                    confirmingDelete = confirmingDelete,360+                    onEdit = { onNavigate(PeachNav.EntryEdit(e.id)) },361+                    onDelete = {362+                        if (confirmingDelete) {363+                            vm.delete(onDone = onBack ?: { onNavigate(PeachNav.VaultList) })364+                        } else {365+                            confirmingDelete = true366+                        }367+                    },368+                )369+                Spacer(Modifier.height(PeachSpacing.md))378370                 DetailTagRow(379371                     tags = e.tags.orEmpty(),380372                     allTags = allTags,@@ -415,10 +407,14 @@415407             }416408 417409             if (e != null) {418-                EntryDetailSearchStrip(410+                EntryDetailBottomControls(419411                     currentIndex = activeEntries.indexOfFirst { it.id == e.id },420412                     totalEntries = activeEntries.size,421413                     onOpenList = { onNavigate(PeachNav.VaultList) },414+                    onAddEntry = { onNavigate(PeachNav.EntryEdit(null)) },415+                    onOpenNotes = { onNavigate(PeachNav.Notes) },416+                    onSettings = { onNavigate(PeachNav.Settings) },417+                    onLock = vm::lock,422418                     onPrevious = { vm.adjacentDestination(-1)?.let(onNavigate) },423419                     onNext = { vm.adjacentDestination(1)?.let(onNavigate) },424420                     modifier = Modifier.align(Alignment.BottomCenter),@@ -438,94 +434,67 @@438434 }439435 440436 @Composable441-private fun EntryDetailTopBar(442-    entry: com.peachpasswords.vault.VaultEntry?,437+private fun EntryDetailIdentity(438+) {439+    Row(440+        Modifier.fillMaxWidth().height(64.dp),441+        verticalAlignment = Alignment.CenterVertically,442+    ) {443+        PeachWordmark()444+        Spacer(Modifier.width(PeachSpacing.md))445+        Text("VAULT", color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))446+    }447+}448+449+@Composable450+private fun EntryDetailActions(443451     confirmingDelete: Boolean,444-    onBack: (() -> Unit)?,445-    onLock: () -> Unit,452+    onEdit: () -> Unit,446453     onDelete: () -> Unit,447-    onAddEntry: () -> Unit,448-    onSettings: () -> Unit,449-    onOpenNotes: () -> Unit,450454 ) {451-    val domain = remember(entry?.url, entry?.title) { entry?.displayDomain().orEmpty() }452455     val accent = LocalPeachAccent.current453-    PeachTopBar(454-        title = when {455-            confirmingDelete && entry != null -> "delete \"${entry.title}\"?"456-            // Deck root (no back target) mirrors the browser's persistent457-            // header, which always shows the "peach" wordmark — never the458-            // current entry's identity — leaving PeachMark in `leading` below459-            // to complete the wordmark pairing (see LockScreen's same pattern).460-            onBack == null -> "peach"461-            entry == null -> "entry"462-            domain.isNotEmpty() -> domain463-            else -> entry.title464-        },465-        leading = {466-            if (onBack != null) {467-                Icon(468-                    Icons.AutoMirrored.Rounded.ArrowBack,469-                    contentDescription = "back",470-                    tint = peachColors.T1,471-                    modifier = Modifier472-                        .size(40.dp)473-                        .clip(CircleShape)474-                        .clickable(onClick = onBack)475-                        .padding(PeachSpacing.sm),476-                )477-            } else {478-                PeachMark(modifier = Modifier.padding(start = PeachSpacing.sm, end = PeachSpacing.xs))479-            }480-            // The favicon chip used to sit here, but it's the one loud,481-            // colorful element in an otherwise quiet/muted bar (matching the482-            // browser's persistent header, which carries no per-entry color483-            // at all) — and it's redundant with the entry's own hero section484-            // just below, which already shows its identity clearly.485-        },486-        actions = {487-            // The chrome actions render on EVERY entry, not just the deck root.488-            // They used to be gated on `onBack == null`, so an entry opened from489-            // the vault list showed a bare back-arrow-and-title bar while the490-            // deck root showed the full set — the same surface with two491-            // different bars, which reads as the toolbar breaking mid-swipe.492-            // The browser's header is persistent; this now is too. Order and493-            // family mirror it: Notes, Settings, New entry, Delete, Lock, all494-            // Outlined at one optical size.495-            ChromeAction(Icons.Outlined.Description, "Notes", onClick = onOpenNotes)496-            ChromeAction(Icons.Outlined.Settings, "settings", onClick = onSettings)497-            ChromeAction(Icons.Outlined.Add, "new entry", onClick = onAddEntry)498-            if (entry != null) {499-                ChromeAction(500-                    Icons.Outlined.Delete,501-                    "delete",502-                    tint = if (confirmingDelete) peachColors.Breach else peachColors.T1,503-                    onClick = onDelete,504-                )505-            }506-            ChromeAction(Icons.Outlined.Lock, "Lock vault", tint = accent, onClick = onLock)507-        },508-    )456+    Row(457+        Modifier.fillMaxWidth().padding(top = PeachSpacing.lg),458+        horizontalArrangement = Arrangement.spacedBy(PeachSpacing.sm),459+    ) {460+        DetailActionButton(461+            label = "edit",462+            color = peachColors.Bg,463+            background = accent,464+            border = accent,465+            onClick = onEdit,466+            modifier = Modifier.weight(1f),467+        )468+        DetailActionButton(469+            label = if (confirmingDelete) "confirm trash" else "move to trash",470+            color = if (confirmingDelete) peachColors.Breach else accent,471+            background = Color.Transparent,472+            border = if (confirmingDelete) peachColors.BreachBorder else peachColors.T3,473+            onClick = onDelete,474+            modifier = Modifier.weight(1f),475+        )476+    }509477 }510478 511-/** One shape for every top-bar action so the cluster reads as a set. */512479 @Composable513-private fun ChromeAction(514-    icon: androidx.compose.ui.graphics.vector.ImageVector,480+private fun DetailActionButton(515481     label: String,516-    tint: Color = peachColors.T1,482+    color: Color,483+    background: Color,484+    border: Color,517485     onClick: () -> Unit,486+    modifier: Modifier = Modifier,518487 ) {519-    Icon(520-        icon,521-        contentDescription = label,522-        tint = tint,523-        modifier = Modifier524-            .size(38.dp)525-            .clip(CircleShape)526-            .clickable(onClick = onClick)527-            .padding(9.dp),528-    )488+    Box(489+        modifier.height(52.dp)490+            .clip(RoundedCornerShape(PeachRadius.Button))491+            .background(background)492+            .border(1.dp, border, RoundedCornerShape(PeachRadius.Button))493+            .clickable(onClick = onClick),494+        contentAlignment = Alignment.Center,495+    ) {496+        Text(label, color = color, style = PeachType.bodyMono)497+    }529498 }530499 531500 @Composable@@ -729,10 +698,14 @@729698 }730699 731700 @Composable732-private fun EntryDetailSearchStrip(701+private fun EntryDetailBottomControls(733702     currentIndex: Int,734703     totalEntries: Int,735704     onOpenList: () -> Unit,705+    onAddEntry: () -> Unit,706+    onOpenNotes: () -> Unit,707+    onSettings: () -> Unit,708+    onLock: () -> Unit,736709     onPrevious: () -> Unit,737710     onNext: () -> Unit,738711     modifier: Modifier = Modifier,@@ -757,55 +730,10 @@757730                 .navigationBarsPadding(),758731         ) {759732             Row(760-                Modifier761-                    .fillMaxWidth()762-                    .padding(horizontal = 16.dp, vertical = 12.dp)763-                    .height(56.dp)764-                    .clip(RoundedCornerShape(16.dp))765-                    .background(peachColors.S1)766-                    .border(1.dp, peachColors.T3, RoundedCornerShape(16.dp))767-                    .padding(horizontal = 12.dp),733+                Modifier.fillMaxWidth().height(44.dp),734+                horizontalArrangement = Arrangement.Center,768735                 verticalAlignment = Alignment.CenterVertically,769736             ) {770-                Row(771-                    Modifier772-                        .weight(1f)773-                        .fillMaxHeight()774-                        .clickable(onClick = onOpenList)775-                        .pointerInput(onOpenList) {776-                            var opened = false777-                            detectVerticalDragGestures(778-                                onDragEnd = { opened = false },779-                                onDragCancel = { opened = false },780-                            ) { _, dragAmount ->781-                                if (!opened && dragAmount < -4f) {782-                                    opened = true783-                                    onOpenList()784-                                }785-                            }786-                        },787-                    verticalAlignment = Alignment.CenterVertically,788-                ) {789-                    Icon(790-                        Icons.Rounded.Search,791-                        contentDescription = null,792-                        tint = accent,793-                        modifier = Modifier.size(14.dp),794-                    )795-                    Spacer(Modifier.width(8.dp))796-                    Text(797-                        "search vault...",798-                        color = peachColors.T2,799-                        style = PeachType.searchInput,800-                        modifier = Modifier.weight(1f),801-                    )802-                    Text(803-                        counter,804-                        color = peachColors.T2,805-                        style = PeachType.labelMono,806-                    )807-                }808-                Spacer(Modifier.width(8.dp))809737                 Icon(810738                     Icons.AutoMirrored.Rounded.ArrowBack,811739                     contentDescription = "Previous entry",@@ -816,6 +744,9 @@816744                         .clickable(enabled = hasEntries, onClick = onPrevious)817745                         .padding(8.dp),818746                 )747+                Spacer(Modifier.width(PeachSpacing.md))748+                Text(counter, color = peachColors.T2, style = PeachType.labelMono)749+                Spacer(Modifier.width(PeachSpacing.md))819750                 Spacer(Modifier.width(4.dp))820751                 Icon(821752                     Icons.AutoMirrored.Rounded.ArrowForward,@@ -828,25 +759,57 @@828759                         .padding(8.dp),829760                 )830761             }762+            Row(763+                Modifier.fillMaxWidth()764+                    .padding(horizontal = PeachSpacing.xl, vertical = PeachSpacing.sm)765+                    .height(64.dp)766+                    .clip(RoundedCornerShape(PeachRadius.Pill))767+                    .background(peachColors.S1)768+                    .border(1.dp, peachColors.T3, RoundedCornerShape(PeachRadius.Pill))769+                    .padding(horizontal = PeachSpacing.md),770+                horizontalArrangement = Arrangement.SpaceBetween,771+                verticalAlignment = Alignment.CenterVertically,772+            ) {773+                BottomBarAction(Icons.Rounded.Search, "search vault", accent, onOpenList)774+                BottomBarAction(Icons.Rounded.Add, "new entry", peachColors.T0, onAddEntry)775+                BottomBarAction(Icons.Outlined.Description, "Notes", peachColors.T0, onOpenNotes)776+                BottomBarAction(Icons.Outlined.Settings, "settings", peachColors.T0, onSettings)777+                BottomBarAction(Icons.Outlined.Lock, "Lock vault", accent, onLock)778+            }831779         }832780     }833781 }834782 835-/** A field row: uppercase mono label over a value/action row, with a t3 hairline beneath (spec §5.4). */836783 @Composable784+private fun BottomBarAction(785+    icon: androidx.compose.ui.graphics.vector.ImageVector,786+    label: String,787+    tint: Color,788+    onClick: () -> Unit,789+) {790+    Box(791+        Modifier.size(48.dp)792+            .clip(RoundedCornerShape(PeachRadius.IconButton))793+            .clickable(onClick = onClick),794+        contentAlignment = Alignment.Center,795+    ) {796+        Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(22.dp))797+    }798+}799+800+/** A credential field card: uppercase mono label over its value and local actions. */801+@Composable837802 internal fun FieldShell(838803     label: String,839804     content: @Composable androidx.compose.foundation.layout.RowScope.() -> Unit,840805 ) {841-    val hairline = peachColors.T3842806     Column(843-        Modifier.fillMaxWidth().padding(vertical = 10.dp).drawBehind {844-            drawRect(845-                color = hairline,846-                topLeft = Offset(0f, size.height - 1.dp.toPx()),847-                size = Size(size.width, 1.dp.toPx()),848-            )849-        },807+        Modifier.fillMaxWidth()808+            .padding(vertical = PeachSpacing.xs)809+            .clip(RoundedCornerShape(PeachRadius.StackCard))810+            .background(peachColors.SurfaceCard)811+            .border(1.dp, peachColors.T3, RoundedCornerShape(PeachRadius.StackCard))812+            .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),850813     ) {851814         Text(label.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))852815         Spacer(Modifier.height(6.dp))@@ -875,13 +838,4 @@875838     val date = java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM)876839         .format(java.util.Date(lastPinged))877840     return "Last triggered: $date · $countLabel"878-}879-880-private fun com.peachpasswords.vault.VaultEntry.displayDomain(): String {881-    val raw = url882-    if (!raw.isNullOrBlank()) {883-        val cleaned = raw.replace(Regex("^https?://"), "").removePrefix("www.").substringBefore('/')884-        if (cleaned.isNotEmpty()) return cleaned885-    }886-    return ""887841 }

view file