44f8e7bf1609

feat: Unify vault contextual bottom surfaces (v2.14.20)

time
2026-08-14 16:01 UTC · 2 months ago
machine
koh2
agent
gpt-5.6-sol
session
contextual bottom chrome
parent
f7dad03c0489
12 files VERSION+1 −1app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+9 −2app/src/main/java/com/peachpasswords/ui/PeachNav.kt+1 −0app/src/main/java/com/peachpasswords/ui/vault/AdditionalUrlsDetail.kt+2 −1app/src/main/java/com/peachpasswords/ui/vault/DetailTagRow.kt+25 −65app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+4 −3app/src/main/java/com/peachpasswords/ui/vault/EntryDeckChrome.kt+102 −0app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+26 −64app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+33 −38app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+19 −39app/src/main/java/com/peachpasswords/ui/vault/NotesModeViewModel.kt+2 −0app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+56 −78
VERSION+1 −1
@@ -1 +1 @@1-2.14.191+2.14.20

view file

app/src/main/java/com/peachpasswords/ui/PeachAppNavHost.kt+9 −2
@@ -241,10 +241,17 @@241241                 onBack = if (hasOrigin) ({ navController.popBackStack() }) else null,242242             )243243         }244+        composable(PeachNav.VaultSearch.route) {245+            VaultListScreen(246+                onNavigate = nav,247+                onBack = { navController.popBackStack() },248+                initialSearchActive = true,249+            )250+        }244251         // No {id}: NotesModeScreen opens on its table of contents, which is the245252         // actual notes surface. The notes filter on the vault list is a filter,246253         // not this.247-        composable(PeachNav.Notes.route) { NotesModeScreen(onBack = back) }254+        composable(PeachNav.Notes.route) { NotesModeScreen(onBack = back, onNavigate = nav) }248255 249256         composable(250257             route = PeachNav.EntryDetail.ROUTE,@@ -297,7 +304,7 @@297304             route = PeachNav.NotesMode.ROUTE,298305             arguments = listOf(navArgument("id") { type = NavType.StringType }),299306         ) {300-            NotesModeScreen(onBack = back)307+            NotesModeScreen(onBack = back, onNavigate = nav)301308         }302309         composable(PeachNav.Settings.route) { SettingsScreen(onBack = back, onNavigate = nav) }303310         composable(PeachNav.CodexGenerate.route) {

view file

app/src/main/java/com/peachpasswords/ui/PeachNav.kt+1 −0
@@ -9,6 +9,7 @@99     data object Onboarding : PeachNav("onboarding")1010     data object Lock : PeachNav("lock")1111     data object VaultList : PeachNav("vault")12+    data object VaultSearch : PeachNav("vault/search")1213 1314     /** The notes surface itself — [NotesMode]'s table of contents, with no note1415      * open yet. Reached from the entry-deck chrome's "Notes" action. */

view file

app/src/main/java/com/peachpasswords/ui/vault/AdditionalUrlsDetail.kt+2 −1
@@ -68,6 +68,7 @@6868     onCopy: (key: String, value: String) -> Unit,6969     onSave: (index: Int, value: String) -> Unit = { _, _ -> },7070     onAdd: (value: String) -> Unit = {},71+    showAdd: Boolean = false,7172 ) {7273     val context = LocalContext.current7374     urls.forEachIndexed { index, url ->@@ -103,7 +104,7 @@103104             }104105         }105106     }106-    AddAdditionalUrlRow(onAdd = onAdd)107+    if (showAdd) AddAdditionalUrlRow(onAdd = onAdd)107108 }108109 109110 @Composable

view file

app/src/main/java/com/peachpasswords/ui/vault/DetailTagRow.kt+25 −65
@@ -40,74 +40,34 @@4040 ) {4141     val accent = LocalPeachAccent.current4242     var editing by remember(tags) { mutableStateOf(false) }43-    if (editing) {44-        Column(Modifier.fillMaxWidth().padding(vertical = 10.dp)) {45-            TagChipEditor(46-                tags = tags,47-                allTags = allTags,48-                onAdd = onAddTag,49-                onRemove = onRemoveTag,50-            )51-            Text(52-                "done",53-                color = accent,54-                style = PeachType.labelMono,55-                modifier = Modifier56-                    .clip(RoundedCornerShape(6.dp))57-                    .clickable { editing = false }58-                    .padding(vertical = 4.dp),59-            )60-        }61-        return62-    }63-    if (tags.isEmpty()) {64-        Text(65-            "+ tags",66-            color = accent,67-            style = PeachType.labelMono,68-            modifier = Modifier69-                .clip(RoundedCornerShape(6.dp))70-                .clickable { editing = true }71-                .padding(vertical = 6.dp),72-        )73-        return74-    }75-    Column(76-        Modifier.fillMaxWidth().padding(vertical = 10.dp),77-    ) {78-        Text(79-            "TAGS",80-            color = peachColors.T2,81-            style = PeachType.labelMono.copy(letterSpacing = 0.08.em),82-        )83-        Spacer(Modifier.height(8.dp))84-        FlowRow(85-            modifier = Modifier.fillMaxWidth(),86-            horizontalArrangement = Arrangement.spacedBy(6.dp),87-            verticalArrangement = Arrangement.spacedBy(6.dp),88-        ) {89-            tags.forEach { tag ->43+    FieldShell("tags") {44+        if (editing) {45+            Column(Modifier.weight(1f)) {46+                TagChipEditor(tags, allTags, onAddTag, onRemoveTag)9047                 Text(91-                    text = tag,92-                    color = accent,93-                    style = PeachType.labelMono,94-                    modifier = Modifier95-                        .clip(RoundedCornerShape(6.dp))96-                        .background(accent.copy(alpha = 0.12f))97-                        .border(1.dp, accent.copy(alpha = 0.28f), RoundedCornerShape(6.dp))98-                        .padding(horizontal = 8.dp, vertical = 4.dp),48+                    "done", color = accent, style = PeachType.labelMono,49+                    modifier = Modifier.clickable { editing = false }.padding(vertical = 8.dp),9950                 )10051             }101-            Text(102-                text = "+",103-                color = accent,104-                style = PeachType.labelMono,105-                modifier = Modifier106-                    .clip(RoundedCornerShape(6.dp))107-                    .border(1.dp, accent.copy(alpha = 0.28f), RoundedCornerShape(6.dp))108-                    .clickable { editing = true }109-                    .padding(horizontal = 10.dp, vertical = 4.dp),110-            )52+        } else if (tags.isEmpty()) {53+            Text("none", color = peachColors.T2, style = PeachType.bodyMono, modifier = Modifier.weight(1f))54+            Text("add", color = accent, style = PeachType.labelMono, modifier = Modifier.clickable { editing = true }.padding(8.dp))55+        } else {56+            FlowRow(57+                modifier = Modifier.weight(1f),58+                horizontalArrangement = Arrangement.spacedBy(6.dp),59+                verticalArrangement = Arrangement.spacedBy(6.dp),60+            ) {61+                tags.forEach { tag ->62+                    Text(63+                        tag, color = accent, style = PeachType.labelMono,64+                        modifier = Modifier.background(accent.copy(alpha = 0.12f), RoundedCornerShape(6.dp))65+                            .border(1.dp, accent.copy(alpha = 0.28f), RoundedCornerShape(6.dp))66+                            .padding(horizontal = 8.dp, vertical = 4.dp),67+                    )68+                }69+            }70+            Text("edit", color = accent, style = PeachType.labelMono, modifier = Modifier.clickable { editing = true }.padding(8.dp))11171         }11272     }11373 }

view file

app/src/main/java/com/peachpasswords/ui/vault/EditableFieldRow.kt+4 −3
@@ -98,6 +98,7 @@9898     placeholder: String = "",9999     displayText: AnnotatedString? = null,100100     draftText: String? = null,101+    onEditingChange: (Boolean) -> Unit = {},101102 ) {102103     val editable = draftText ?: value103104     var editing by remember(value) { mutableStateOf(false) }@@ -112,15 +113,15 @@112113                 style = if (mono) PeachType.bodyMono else PeachType.body,113114                 modifier = Modifier114115                     .weight(1f)115-                    .clickable { draft = editable; editing = true },116+                    .clickable { draft = editable; editing = true; onEditingChange(true) },116117             )117118             if (!isEmpty) CopyButton(copied, onCopy)118119         } else {119120             InlineEditor(120121                 draft = draft,121122                 onDraftChange = { draft = it },122-                onSave = { onSave(draft.trim()); editing = false },123-                onCancel = { draft = editable; editing = false },123+                onSave = { onSave(draft.trim()); editing = false; onEditingChange(false) },124+                onCancel = { draft = editable; editing = false; onEditingChange(false) },124125                 suggestions = suggestions,125126                 onPickSuggestion = { draft = it },126127                 singleLine = singleLine,

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryDeckChrome.kt+102 −0
@@ -0,0 +1,102 @@1+package com.peachpasswords.ui.vault2+3+import androidx.compose.foundation.background4+import androidx.compose.foundation.border5+import androidx.compose.foundation.clickable6+import androidx.compose.foundation.gestures.detectVerticalDragGestures7+import androidx.compose.foundation.layout.Arrangement8+import androidx.compose.foundation.layout.Box9+import androidx.compose.foundation.layout.Row10+import androidx.compose.foundation.layout.Spacer11+import androidx.compose.foundation.layout.fillMaxWidth12+import androidx.compose.foundation.layout.height13+import androidx.compose.foundation.layout.padding14+import androidx.compose.foundation.layout.size15+import androidx.compose.foundation.layout.width16+import androidx.compose.foundation.shape.RoundedCornerShape17+import androidx.compose.material.icons.Icons18+import androidx.compose.material.icons.outlined.Description19+import androidx.compose.material.icons.outlined.Lock20+import androidx.compose.material.icons.outlined.Settings21+import androidx.compose.material.icons.rounded.Add22+import androidx.compose.material.icons.rounded.Search23+import androidx.compose.material3.Icon24+import androidx.compose.material3.Text25+import androidx.compose.runtime.Composable26+import androidx.compose.ui.Alignment27+import androidx.compose.ui.Modifier28+import androidx.compose.ui.graphics.Color29+import androidx.compose.ui.graphics.vector.ImageVector30+import androidx.compose.ui.input.pointer.pointerInput31+import androidx.compose.ui.unit.dp32+import androidx.compose.ui.unit.em33+import com.peachpasswords.ui.components.PeachWordmark34+import com.peachpasswords.ui.theme.LocalPeachAccent35+import com.peachpasswords.ui.theme.PeachRadius36+import com.peachpasswords.ui.theme.PeachSpacing37+import com.peachpasswords.ui.theme.PeachType38+import com.peachpasswords.ui.theme.peachColors39+40+@Composable41+internal fun PeachVaultIdentity(section: String) {42+    Row(43+        Modifier.fillMaxWidth().height(64.dp),44+        verticalAlignment = Alignment.CenterVertically,45+    ) {46+        PeachWordmark()47+        Spacer(Modifier.width(PeachSpacing.md))48+        Text(section.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))49+    }50+}51+52+@Composable53+internal fun PeachDeckBottomBar(54+    onSearch: () -> Unit,55+    onAdd: () -> Unit,56+    onNotes: () -> Unit,57+    onSettings: () -> Unit,58+    onLock: () -> Unit,59+    onSwipeUp: () -> Unit,60+    notesSelected: Boolean = false,61+    modifier: Modifier = Modifier,62+) {63+    val accent = LocalPeachAccent.current64+    Row(65+        modifier.fillMaxWidth()66+            .padding(horizontal = PeachSpacing.xl, vertical = PeachSpacing.sm)67+            .height(64.dp)68+            .background(peachColors.S1, RoundedCornerShape(PeachRadius.Pill))69+            .border(1.dp, peachColors.T3, RoundedCornerShape(PeachRadius.Pill))70+            .pointerInput(onSwipeUp) {71+                var opened = false72+                detectVerticalDragGestures(73+                    onDragEnd = { opened = false },74+                    onDragCancel = { opened = false },75+                ) { _, dragAmount ->76+                    if (!opened && dragAmount < -4f) {77+                        opened = true78+                        onSwipeUp()79+                    }80+                }81+            }82+            .padding(horizontal = PeachSpacing.md),83+        horizontalArrangement = Arrangement.SpaceBetween,84+        verticalAlignment = Alignment.CenterVertically,85+    ) {86+        DeckAction(Icons.Rounded.Search, "search", accent, onSearch)87+        DeckAction(Icons.Rounded.Add, "new", peachColors.T0, onAdd)88+        DeckAction(Icons.Outlined.Description, "Notes", if (notesSelected) accent else peachColors.T0, onNotes)89+        DeckAction(Icons.Outlined.Settings, "settings", peachColors.T0, onSettings)90+        DeckAction(Icons.Outlined.Lock, "Lock vault", accent, onLock)91+    }92+}93+94+@Composable95+private fun DeckAction(icon: ImageVector, label: String, tint: Color, onClick: () -> Unit) {96+    Box(97+        Modifier.size(48.dp).clickable(onClick = onClick),98+        contentAlignment = Alignment.Center,99+    ) {100+        Icon(icon, contentDescription = label, tint = tint, modifier = Modifier.size(22.dp))101+    }102+}

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryDetailScreen.kt+26 −64
@@ -29,18 +29,12 @@2929 import androidx.compose.foundation.rememberScrollState3030 import androidx.compose.foundation.shape.RoundedCornerShape3131 import androidx.compose.foundation.text.BasicTextField32-import androidx.compose.material.icons.rounded.Add3332 import androidx.compose.material.icons.rounded.Check3433 import androidx.compose.material.icons.rounded.ContentCopy35-import androidx.compose.material.icons.rounded.Search36-import androidx.compose.material.icons.rounded.Settings3734 import androidx.compose.foundation.verticalScroll3835 import androidx.compose.material.icons.Icons3936 import androidx.compose.material.icons.automirrored.rounded.ArrowBack4037 import androidx.compose.material.icons.automirrored.rounded.ArrowForward41-import androidx.compose.material.icons.outlined.Description42-import androidx.compose.material.icons.outlined.Settings43-import androidx.compose.material.icons.outlined.Lock4438 import androidx.compose.material3.Icon4539 import androidx.compose.material3.Text4640 import androidx.compose.runtime.Composable@@ -76,7 +70,6 @@7670 import com.peachpasswords.ui.components.CharEnterText7771 import com.peachpasswords.ui.components.CopyButton7872 import com.peachpasswords.ui.components.PeachEmptyOrEndState79-import com.peachpasswords.ui.components.PeachWordmark8073 import com.peachpasswords.ui.components.SecureFlagEffect8174 import com.peachpasswords.ui.components.TOTPRing8275 import com.peachpasswords.ui.lock.Biometrics@@ -123,6 +116,7 @@123116     var confirmingDelete by remember { mutableStateOf(false) }124117     var generatorOpen by remember { mutableStateOf(false) }125118     var historyExpanded by remember { mutableStateOf(false) }119+    var urlEditing by remember { mutableStateOf(false) }126120 127121     val e = entry128122     ShellBackground(modifier) {@@ -153,7 +147,7 @@153147                     .verticalScroll(rememberScrollState())154148                     .padding(horizontal = PeachSpacing.xl),155149             ) {156-                EntryDetailIdentity()150+                PeachVaultIdentity("vault")157151                 Spacer(Modifier.height(PeachSpacing.xxl))158152 159153                 if (e == null) {@@ -241,6 +235,7 @@241235                             onCopy = { vm.copy("url", e.url) },242236                             onSave = { vm.updateField("url", it) },243237                             placeholder = "add url",238+                            onEditingChange = { urlEditing = it },244239                         )245240                     }246241                     // Browser distinguishes USERNAME and EMAIL as separate@@ -298,6 +293,7 @@298293                         onCopy = { key, value -> vm.copy(key, value) },299294                         onSave = { index, value -> vm.updateAdditionalUrl(index, value) },300295                         onAdd = vm::addAdditionalUrl,296+                        showAdd = urlEditing,301297                     )302298                     if (e.hasPasskey()) {303299                         PasskeyDetailSection(e)@@ -391,15 +387,18 @@391387                             onTest = vm::testCanary,392388                         )393389                     } else {394-                        Text(395-                            "+ advanced",396-                            color = LocalPeachAccent.current,397-                            style = PeachType.labelMono,398-                            modifier = Modifier399-                                .clip(RoundedCornerShape(6.dp))400-                                .clickable { advancedExpanded = true }401-                                .padding(vertical = 6.dp),402-                        )390+                        FieldShell("advanced") {391+                            Text(392+                                "Canary alert and protection",393+                                color = peachColors.T2,394+                                style = PeachType.bodyMono,395+                                modifier = Modifier.weight(1f),396+                            )397+                            Text(398+                                "open", color = LocalPeachAccent.current, style = PeachType.labelMono,399+                                modifier = Modifier.clickable { advancedExpanded = true }.padding(8.dp),400+                            )401+                        }403402                     }404403                 }405404 @@ -411,6 +410,7 @@411410                     currentIndex = activeEntries.indexOfFirst { it.id == e.id },412411                     totalEntries = activeEntries.size,413412                     onOpenList = { onNavigate(PeachNav.VaultList) },413+                    onSearch = { onNavigate(PeachNav.VaultSearch) },414414                     onAddEntry = { onNavigate(PeachNav.EntryEdit(null)) },415415                     onOpenNotes = { onNavigate(PeachNav.Notes) },416416                     onSettings = { onNavigate(PeachNav.Settings) },@@ -434,19 +434,6 @@434434 }435435 436436 @Composable437-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-@Composable450437 private fun EntryDetailActions(451438     confirmingDelete: Boolean,452439     onEdit: () -> Unit,@@ -702,6 +689,7 @@702689     currentIndex: Int,703690     totalEntries: Int,704691     onOpenList: () -> Unit,692+    onSearch: () -> Unit,705693     onAddEntry: () -> Unit,706694     onOpenNotes: () -> Unit,707695     onSettings: () -> Unit,@@ -759,41 +747,15 @@759747                         .padding(8.dp),760748                 )761749             }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-            }750+            PeachDeckBottomBar(751+                onSearch = onSearch,752+                onAdd = onAddEntry,753+                onNotes = onOpenNotes,754+                onSettings = onSettings,755+                onLock = onLock,756+                onSwipeUp = onOpenList,757+            )779758         }780-    }781-}782-783-@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))797759     }798760 }799761 

view file

app/src/main/java/com/peachpasswords/ui/vault/EntryEditScreen.kt+33 −38
@@ -1,5 +1,6 @@11 package com.peachpasswords.ui.vault22 3+import androidx.compose.foundation.BorderStroke34 import androidx.compose.animation.core.animateDpAsState45 import androidx.compose.animation.core.tween56 import androidx.compose.foundation.background@@ -14,6 +15,7 @@1415 import androidx.compose.foundation.layout.fillMaxWidth1516 import androidx.compose.foundation.layout.height1617 import androidx.compose.foundation.layout.offset18+import androidx.compose.foundation.layout.navigationBarsPadding1719 import androidx.compose.foundation.layout.padding1820 import androidx.compose.foundation.layout.statusBarsPadding1921 import androidx.compose.foundation.layout.width@@ -22,6 +24,7 @@2224 import androidx.compose.foundation.verticalScroll2325 import androidx.compose.material3.Slider2426 import androidx.compose.material3.SliderDefaults27+import androidx.compose.material3.Surface2528 import androidx.compose.material3.Text2629 import androidx.compose.runtime.Composable2730 import androidx.compose.runtime.getValue@@ -37,6 +40,7 @@3740 import androidx.compose.ui.text.font.FontWeight3841 import androidx.compose.ui.text.input.PasswordVisualTransformation3942 import androidx.compose.ui.text.input.VisualTransformation43+import androidx.compose.ui.text.style.TextAlign4044 import androidx.compose.ui.text.style.TextOverflow4145 import androidx.compose.ui.unit.dp4246 import androidx.compose.ui.unit.em@@ -48,7 +52,6 @@4852 import com.peachpasswords.ui.components.PeachSegment4953 import com.peachpasswords.ui.components.PeachSegmentedControl5054 import com.peachpasswords.ui.components.SecureFlagEffect51-import com.peachpasswords.ui.components.PeachTopBar5255 import com.peachpasswords.ui.theme.LocalPeachAccent5356 import com.peachpasswords.ui.theme.PeachMotion5457 import com.peachpasswords.ui.theme.PeachRadius@@ -84,17 +87,11 @@8487                 .statusBarsPadding()8588                 .padding(horizontal = PeachSpacing.xl),8689         ) {87-            EntryEditTopBar(88-                title = editTitleBarLabel(vm.isNew, f.type),89-                saveLabel = entryEditPrimaryActionLabel(vm.isNew, f.saving),90-                canSave = canSaveEditForm(f) && !f.saving,91-                onCancel = onBack,92-                onSave = { vm.save(onBack) },93-            )90+            PeachVaultIdentity(editTitleBarLabel(vm.isNew, f.type))9491 9592             Spacer(Modifier.height(PeachSpacing.lg))9693 97-            Box(Modifier.fillMaxSize().clipToBounds()) {94+            Box(Modifier.weight(1f).fillMaxWidth().clipToBounds()) {9895                 // Ghost PeachScript watermark behind the form (matches the detail hero):9996                 // the full title, fading in as the user types.10097                 if (watermarkAlpha > 0f) {@@ -212,46 +209,44 @@212209                     Spacer(Modifier.height(PeachSpacing.xl))213210                 }214211             }212+            EntryEditBottomActions(213+                saveLabel = entryEditPrimaryActionLabel(vm.isNew, f.saving),214+                canSave = canSaveEditForm(f) && !f.saving,215+                onCancel = onBack,216+                onSave = { vm.save(onBack) },217+            )215218         }216219     }217220 }218221 219222 @Composable220-private fun EntryEditTopBar(221-    title: String,223+private fun EntryEditBottomActions(222224     saveLabel: String,223225     canSave: Boolean,224226     onCancel: () -> Unit,225227     onSave: () -> Unit,226228 ) {227229     val accent = LocalPeachAccent.current228-    PeachTopBar(229-        title = title,230-        leading = {231-            Text(232-                "cancel",233-                color = peachColors.T2,234-                fontFamily = PeachType.Mono,235-                fontSize = 11.sp,236-                modifier = Modifier237-                    .clip(RoundedCornerShape(8.dp))238-                    .clickable(onClick = onCancel)239-                    .padding(PeachSpacing.sm),240-            )241-        },242-        actions = {243-            Text(244-                saveLabel,245-                color = if (canSave) accent else peachColors.T2,246-                fontFamily = PeachType.Mono,247-                fontSize = 11.sp,248-                modifier = Modifier249-                    .clip(RoundedCornerShape(8.dp))250-                    .clickable(enabled = canSave, onClick = onSave)251-                    .padding(PeachSpacing.sm),252-            )253-        },254-    )230+    Row(231+        Modifier.fillMaxWidth().navigationBarsPadding().padding(vertical = PeachSpacing.md),232+        horizontalArrangement = Arrangement.spacedBy(PeachSpacing.md),233+    ) {234+        Surface(235+            modifier = Modifier.weight(1f).clickable(onClick = onCancel),236+            color = peachColors.S1,237+            shape = RoundedCornerShape(12.dp),238+            border = BorderStroke(1.dp, peachColors.T3),239+        ) {240+            Text("cancel", color = peachColors.T1, style = PeachType.bodyMono, modifier = Modifier.padding(18.dp), textAlign = TextAlign.Center)241+        }242+        Surface(243+            modifier = Modifier.weight(1f).clickable(enabled = canSave, onClick = onSave),244+            color = if (canSave) accent else peachColors.S2,245+            shape = RoundedCornerShape(12.dp),246+        ) {247+            Text(saveLabel, color = if (canSave) peachColors.Bg else peachColors.T2, style = PeachType.bodyMono, modifier = Modifier.padding(18.dp), textAlign = TextAlign.Center)248+        }249+    }255250 }256251 257252 /** Centered title-bar label: "new entry" / "new note" / "new card", or "edit". */

view file

app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+19 −39
@@ -57,7 +57,7 @@5757 import androidx.hilt.navigation.compose.hiltViewModel5858 import com.peachpasswords.haptics.PeachHaptics5959 import com.peachpasswords.ui.components.PeachEmptyOrEndState60-import com.peachpasswords.ui.components.PeachTopBar60+import com.peachpasswords.ui.PeachNav6161 import com.peachpasswords.ui.components.SecureFlagEffect6262 import com.peachpasswords.ui.theme.PeachSpacing6363 import com.peachpasswords.vault.VaultEntry@@ -80,6 +80,7 @@8080 @Composable8181 fun NotesModeScreen(8282     onBack: () -> Unit,83+    onNavigate: (PeachNav) -> Unit,8384     vm: NotesModeViewModel = hiltViewModel(),8485 ) {8586     val entry by vm.entry.collectAsState()@@ -118,6 +119,8 @@118119             onToggleSelected = vm::toggleSelected,119120             onDeleteSelected = vm::batchDelete,120121             onCancelMultiSelect = vm::exitMultiSelect,122+            onNavigate = onNavigate,123+            onLock = vm::lock,121124         )122125         return123126     }@@ -271,6 +274,8 @@271274     onToggleSelected: (String) -> Unit,272275     onDeleteSelected: () -> Unit,273276     onCancelMultiSelect: () -> Unit,277+    onNavigate: (PeachNav) -> Unit,278+    onLock: () -> Unit,274279 ) {275280     var searchVisible by remember { mutableStateOf(false) }276281     var query by remember { mutableStateOf("") }@@ -339,44 +344,7 @@339344                         )340345                     }341346                 }342-            } else {343-                PeachTopBar(344-                    title = "notes.",345-                    leading = {346-                        Icon(347-                            Icons.AutoMirrored.Rounded.ArrowBack,348-                            contentDescription = "back to vault",349-                            tint = peachColors.T0,350-                            modifier = Modifier.size(22.dp).clip(RoundedCornerShape(PeachSpacing.sm))351-                                .clickable(onClick = onBack),352-                        )353-                    },354-                    actions = {355-                        Icon(356-                            Icons.Outlined.Search,357-                            contentDescription = "search notes",358-                            tint = peachColors.T0,359-                            modifier = Modifier.size(28.dp).clip(CircleShape)360-                                .clickable { searchVisible = true }361-                                .padding(5.dp),362-                        )363-                        // New note lives in the top bar next to Search so it is364-                        // reachable without scrolling through a long notes list.365-                        // Hidden in multi-select so the bulk-delete toolbar owns366-                        // the action slot.367-                        if (!isMultiSelect) {368-                            Icon(369-                                Icons.Outlined.Add,370-                                contentDescription = "new note",371-                                tint = peachColors.T0,372-                                modifier = Modifier.size(28.dp).clip(CircleShape)373-                                    .clickable(onClick = onNewNote)374-                                    .padding(5.dp),375-                            )376-                        }377-                    },378-                )379-            }347+            } else PeachVaultIdentity("notes")380348             Spacer(Modifier.size(PeachSpacing.lg))381349 382350             LazyColumn(@@ -409,6 +377,18 @@409377                         )410378                     }411379                 }380+            }381+382+            if (!isMultiSelect) {383+                PeachDeckBottomBar(384+                    onSearch = { searchVisible = true },385+                    onAdd = onNewNote,386+                    onNotes = {},387+                    onSettings = { onNavigate(PeachNav.Settings) },388+                    onLock = onLock,389+                    onSwipeUp = onBack,390+                    notesSelected = true,391+                )412392             }413393 414394         }

view file

app/src/main/java/com/peachpasswords/ui/vault/NotesModeViewModel.kt+2 −0
@@ -152,6 +152,8 @@152152         }153153     }154154 155+    fun lock() = vault.lock()156+155157     private fun scheduleSave(patch: (VaultEntry) -> VaultEntry) {156158         saveJob?.cancel()157159         saveJob = viewModelScope.launch {

view file

app/src/main/java/com/peachpasswords/ui/vault/VaultListScreen.kt+56 −78
@@ -44,9 +44,7 @@4444 import androidx.compose.material.icons.rounded.Close4545 import androidx.compose.material.icons.rounded.Search4646 import androidx.compose.material.icons.rounded.Settings47-import androidx.compose.material3.SmallFloatingActionButton4847 import androidx.compose.material.icons.outlined.Lock49-import androidx.compose.material3.FloatingActionButton5048 import androidx.compose.material3.Icon5149 import androidx.compose.material3.TextButton5250 import androidx.compose.material3.Snackbar@@ -69,6 +67,7 @@6967 import androidx.compose.ui.geometry.Offset7068 import androidx.compose.ui.geometry.Size7169 import androidx.compose.ui.graphics.Color70+import androidx.compose.ui.graphics.Brush7271 import androidx.compose.ui.graphics.SolidColor7372 import androidx.compose.ui.graphics.StrokeCap7473 import androidx.compose.ui.graphics.drawscope.Stroke@@ -79,6 +78,8 @@7978 import androidx.compose.ui.input.nestedscroll.nestedScroll8079 import androidx.compose.ui.input.pointer.pointerInput8180 import androidx.compose.ui.input.pointer.positionChange81+import androidx.compose.ui.focus.FocusRequester82+import androidx.compose.ui.focus.focusRequester8283 import androidx.compose.ui.platform.LocalContext8384 import androidx.compose.ui.platform.LocalDensity8485 import androidx.compose.ui.text.TextStyle@@ -124,6 +125,7 @@124125     // search strip — slide back down to collapse to that entry. Null when125126     // the list itself is the deck root (e.g. empty vault, nothing to open).126127     onBack: (() -> Unit)? = null,128+    initialSearchActive: Boolean = false,127129     modifier: Modifier = Modifier,128130     vm: VaultListViewModel = hiltViewModel(),129131 ) {@@ -222,21 +224,6 @@222224                 },223225                 onDismiss = vm::dismissProNotice,224226             )225-            SearchBar(226-                value = search,227-                onChange = vm::setSearch,228-                count = if (search.isNotBlank()) "${entries.size}/$total" else pluralEntries(total),229-                onCollapse = onBack,230-                // The list is a sheet reached by sliding up from the entry-231-                // deck root now (C1) — it no longer carries its own persistent232-                // peach/lock/add/settings bar (that chrome moved to the deck233-                // root, see EntryDetailScreen's top bar + FAB), so this search234-                // row is the topmost element and needs the status-bar inset235-                // that bar used to provide.236-                topInset = !isMultiSelect,237-                syncStatus = syncStatus,238-                pairedCount = pairedDevices.size,239-            )240227             if (allTags.isNotEmpty()) {241228                 TagFilterStrip(242229                     allTags = allTags,@@ -246,13 +233,7 @@246233             }247234             SortBar(248235                 active = sort,249-                noteOnly = noteOnly,250-                // With an empty vault there's no entry-detail top bar, so251-                // without this the user can't reach settings at all.252-                onSettings = { onNavigate(PeachNav.Settings) },253236                 onSort = vm::setSort,254-                onToggleNote = vm::toggleNoteFilter,255-                onTrash = { onNavigate(PeachNav.Trash) },256237             )257238             Box(Modifier.fillMaxWidth().weight(1f)) {258239                 PeachPullToRefresh(@@ -299,52 +280,44 @@299280                                 modifier = Modifier.animateItem(),300281                             )301282                         }302-                        // Trailing space so the last row can clear the FAB.303-                        item { Spacer(Modifier.height(88.dp)) }283+                        // Trailing space so the last row clears the contextual284+                        // action tier and search surface.285+                        item { Spacer(Modifier.height(148.dp)) }304286                     }305287                 }306-                // Standard Android list+FAB convention, replacing the old307-                // "+ new entry" row that floated mid-list (centered in308-                // whatever dead space a short list left below its rows).309-                Column(310-                    modifier = Modifier311-                        .align(Alignment.BottomEnd)312-                        .padding(PeachSpacing.lg)313-                        .navigationBarsPadding(),314-                    horizontalAlignment = Alignment.CenterHorizontally,315-                    verticalArrangement = Arrangement.spacedBy(PeachSpacing.md),316-                ) {317-                    // Small and quiet above the accent FAB: locking is a318-                    // frequent action but not the screen's primary one, and319-                    // from an empty vault there was previously no way to lock320-                    // at all (the lock lives on the entry-detail top bar,321-                    // which does not exist when there are no entries).322-                    SmallFloatingActionButton(323-                        onClick = vm::lock,324-                        containerColor = peachColors.S1,325-                        contentColor = LocalPeachAccent.current,326-                    ) {327-                        Icon(Icons.Outlined.Lock, contentDescription = "Lock vault")328-                    }329-                    FloatingActionButton(330-                        onClick = {331-                            if (noteOnly) {332-                                vm.createNote { id -> onNavigate(PeachNav.NotesMode(id)) }333-                            } else {334-                                onNavigate(PeachNav.EntryEdit(null))335-                            }336-                        },337-                        containerColor = LocalPeachAccent.current,338-                        contentColor = peachColors.Bg,339-                    ) {340-                        Icon(Icons.Rounded.Add, contentDescription = if (noteOnly) "new note" else "new entry")341-                    }342-                }343288             }344289         }290+        Column(291+            Modifier.align(Alignment.BottomCenter)292+                .fillMaxWidth()293+                .background(Brush.verticalGradient(listOf(Color.Transparent, peachColors.Bg), startY = 0f))294+                .padding(top = PeachSpacing.lg)295+                .navigationBarsPadding(),296+        ) {297+            VaultActionCluster(298+                noteOnly = noteOnly,299+                onToggleNote = vm::toggleNoteFilter,300+                onTrash = { onNavigate(PeachNav.Trash) },301+                onSettings = { onNavigate(PeachNav.Settings) },302+                onAdd = {303+                    if (noteOnly) vm.createNote { id -> onNavigate(PeachNav.NotesMode(id)) }304+                    else onNavigate(PeachNav.EntryEdit(null))305+                },306+                onLock = vm::lock,307+            )308+            SearchBar(309+                value = search,310+                onChange = vm::setSearch,311+                count = if (search.isNotBlank()) "${entries.size}/$total" else pluralEntries(total),312+                onCollapse = onBack,313+                requestFocus = initialSearchActive,314+                syncStatus = syncStatus,315+                pairedCount = pairedDevices.size,316+            )317+        }345318         SnackbarHost(346319             hostState = snackbarHostState,347-            modifier = Modifier.align(Alignment.BottomCenter).navigationBarsPadding(),320+            modifier = Modifier.align(Alignment.BottomCenter).padding(bottom = 148.dp).navigationBarsPadding(),348321         ) { data ->349322             Snackbar(350323                 containerColor = peachColors.SurfaceElevated,@@ -407,12 +380,16 @@407380     onChange: (String) -> Unit,408381     count: String,409382     onCollapse: (() -> Unit)? = null,410-    topInset: Boolean = false,383+    requestFocus: Boolean = false,411384     syncStatus: SyncOutcomeSummary? = null,412385     pairedCount: Int = 0,413386 ) {414387     val accent = LocalPeachAccent.current415-    Column(Modifier.fillMaxWidth().then(if (topInset) Modifier.statusBarsPadding() else Modifier)) {388+    val focusRequester = remember { FocusRequester() }389+    LaunchedEffect(requestFocus) {390+        if (requestFocus) focusRequester.requestFocus()391+    }392+    Column(Modifier.fillMaxWidth()) {416393         Row(417394             Modifier418395                 .fillMaxWidth()@@ -455,7 +432,7 @@455432                     singleLine = true,456433                     textStyle = PeachType.searchInput.copy(color = peachColors.T0),457434                     cursorBrush = SolidColor(accent),458-                    modifier = Modifier.fillMaxWidth(),435+                    modifier = Modifier.fillMaxWidth().focusRequester(focusRequester),459436                 )460437             }461438             if (syncStatus != null) {@@ -474,11 +451,7 @@474451 @Composable475452 private fun SortBar(476453     active: SortOrder,477-    noteOnly: Boolean,478-    onSettings: () -> Unit,479454     onSort: (SortOrder) -> Unit,480-    onToggleNote: () -> Unit,481-    onTrash: () -> Unit,482455 ) {483456     val hairline = peachColors.T3484457     // One row, matching the browser's single combined sort+actions bar — the@@ -505,26 +478,23 @@505478             onSelected = { key ->506479                 SortOrder.entries.firstOrNull { it.name == key }?.let(onSort)507480             },508-            modifier = Modifier.weight(1f),481+            modifier = Modifier.fillMaxWidth(),509482         )510-        ActionCluster(511-            noteOnly = noteOnly,512-            onSettings = onSettings,513-            onToggleNote = onToggleNote,514-            onTrash = onTrash,515-        )516483     }517484 }518485 519486 @Composable520-private fun ActionCluster(487+private fun VaultActionCluster(521488     noteOnly: Boolean,522489     onSettings: () -> Unit,523490     onToggleNote: () -> Unit,524491     onTrash: () -> Unit,492+    onAdd: () -> Unit,493+    onLock: () -> Unit,525494 ) {526495     val accent = LocalPeachAccent.current527496     Surface(497+        modifier = Modifier.padding(horizontal = PeachSpacing.xl),528498         color = peachColors.S1,529499         contentColor = peachColors.T1,530500         shape = RoundedCornerShape(999.dp),@@ -555,11 +525,19 @@555525                 icon = { Icon(Icons.Outlined.Delete, contentDescription = "trash") },556526                 onClick = onTrash,557527             )528+            ClusterIcon(529+                icon = { Icon(Icons.Rounded.Add, contentDescription = if (noteOnly) "new note" else "new entry") },530+                onClick = onAdd,531+            )558532             // Settings last: it is the only app-level action in the cluster,559533             // and the extension likewise keeps it out of the content actions.560534             ClusterIcon(561535                 icon = { Icon(Icons.Outlined.Settings, contentDescription = "settings") },562536                 onClick = onSettings,537+            )538+            ClusterIcon(539+                icon = { Icon(Icons.Outlined.Lock, contentDescription = "Lock vault", tint = accent) },540+                onClick = onLock,563541             )564542         }565543     }

view file