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
↓ peach-android-44f8e7bf.tar.gzatlas at this save
files changed12 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 −78app/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) {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. */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 @Composableapp/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 }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,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+}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 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". */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 }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 {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 }