48faeaa8206e
feat: Recompose remaining vault surfaces with design primitives (v2.3.64)
- time
- 2026-07-08 21:37 UTC · 3 months ago
- machine
- koh2
- parent
- 22164daef2c9
↓ peach-android-48faeaa8.tar.gzatlas at this save
files changed7 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/vault/ExportScreen.kt+61 −56app/src/main/java/com/peachpasswords/ui/vault/ImportScreen.kt+51 −54app/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+147 −109app/src/main/java/com/peachpasswords/ui/vault/RecoveryWordsScreen.kt+19 −32app/src/main/java/com/peachpasswords/ui/vault/TrashScreen.kt+67 −52app/src/main/java/com/peachpasswords/ui/vault/VaultHealthScreen.kt+27 −35app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23630020- versionName "2.3.63"19+ versionCode 23640020+ versionName "2.3.64"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/ui/vault/ExportScreen.kt+61 −56
@@ -47,8 +47,12 @@4747 import androidx.hilt.navigation.compose.hiltViewModel4848 import androidx.lifecycle.ViewModel4949 import com.peachpasswords.license.LicenseRepository50+import com.peachpasswords.ui.components.PeachEmptyOrEndState51+import com.peachpasswords.ui.components.PeachField as DesignPeachField52+import com.peachpasswords.ui.components.PeachTopBar5053 import com.peachpasswords.ui.components.SecureFlagEffect5154 import com.peachpasswords.ui.theme.LocalPeachAccent55+import com.peachpasswords.ui.theme.PeachSpacing5256 import com.peachpasswords.ui.theme.peachColors5357 import com.peachpasswords.ui.theme.PeachType5458 import com.peachpasswords.ui.security.StrengthBar@@ -151,29 +155,26 @@151155 modifier = Modifier152156 .fillMaxSize()153157 .systemBarsPadding()154- .padding(horizontal = 20.dp, vertical = 16.dp),158+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),155159 ) {156- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {157- Icon(158- Icons.AutoMirrored.Rounded.ArrowBack,159- contentDescription = "back",160- tint = peachColors.T0,161- modifier = Modifier162- .size(22.dp)163- .clip(RoundedCornerShape(8.dp))164- .clickable {165- if (route != ExportRoute.CHOOSE) route = ExportRoute.CHOOSE else onBack()166- },167- )168- Spacer(Modifier.width(14.dp))169- Text(170- "Export",171- color = peachColors.T0,172- style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp),173- )174- }160+ PeachTopBar(161+ title = "Export",162+ leading = {163+ Icon(164+ Icons.AutoMirrored.Rounded.ArrowBack,165+ contentDescription = "back",166+ tint = peachColors.T0,167+ modifier = Modifier168+ .size(22.dp)169+ .clip(RoundedCornerShape(PeachSpacing.sm))170+ .clickable {171+ if (route != ExportRoute.CHOOSE) route = ExportRoute.CHOOSE else onBack()172+ },173+ )174+ },175+ )175176 176- Spacer(Modifier.size(18.dp))177+ Spacer(Modifier.size(PeachSpacing.lg))177178 178179 Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {179180 when (route) {@@ -198,12 +199,16 @@198199 subtitle = "A backup encrypted with a separate password. Restores into Peach.",199200 onClick = onPeachScript,200201 )201- Spacer(Modifier.size(12.dp))202+ Spacer(Modifier.size(PeachSpacing.md))202203 OptionCard(203204 title = "CSV (unencrypted)",204205 subtitle = "Plain text for moving into another manager. Not encrypted.",205206 onClick = onCsv,206207 )208+ PeachEmptyOrEndState(209+ message = "Choose an export format.",210+ detail = "Encrypted PeachScript is safest for Peach-to-Peach backups.",211+ )207212 }208213 209214 @Composable@@ -265,30 +270,36 @@265270 fontSize = 24.sp, lineHeight = 26.sp,266271 ),267272 )268- Spacer(Modifier.size(10.dp))273+ Spacer(Modifier.size(PeachSpacing.sm))269274 Text(270275 "Your vault will be encrypted with a separate backup password.",271276 color = peachColors.T1, style = PeachType.body,272277 )273278 274279 if (isEmpty) {275- Spacer(Modifier.size(16.dp))280+ Spacer(Modifier.size(PeachSpacing.lg))276281 InlineError(ExportStrings.EMPTY_VAULT)277282 }278283 279- Spacer(Modifier.size(18.dp))280- FieldLabel("Backup password")281- Spacer(Modifier.size(6.dp))282- PeachUnderlineField(value = password, onValueChange = { password = it; output = null; status = null }, isPassword = true)284+ Spacer(Modifier.size(PeachSpacing.lg))285+ PeachUnderlineField(286+ label = "Backup password",287+ value = password,288+ onValueChange = { password = it; output = null; status = null },289+ isPassword = true,290+ )283291 284- Spacer(Modifier.size(16.dp))285- FieldLabel("Confirm backup password")286- Spacer(Modifier.size(6.dp))287- PeachUnderlineField(value = confirm, onValueChange = { confirm = it; output = null; status = null }, isPassword = true)292+ Spacer(Modifier.size(PeachSpacing.sm))293+ PeachUnderlineField(294+ label = "Confirm backup password",295+ value = confirm,296+ onValueChange = { confirm = it; output = null; status = null },297+ isPassword = true,298+ )288299 289- Spacer(Modifier.size(16.dp))300+ Spacer(Modifier.size(PeachSpacing.sm))290301 StrengthBar(score = score)291- Spacer(Modifier.size(6.dp))302+ Spacer(Modifier.size(PeachSpacing.xs))292303 Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {293304 Text(294305 "strength: ${strengthLabel(score)}",@@ -298,16 +309,16 @@298309 }299310 300311 if (password.isNotEmpty() && !strongEnough) {301- Spacer(Modifier.size(8.dp))312+ Spacer(Modifier.size(PeachSpacing.sm))302313 Text(ExportStrings.WEAK_PASSWORD, color = peachColors.Breach, style = PeachType.bodyMono)303314 }304315 if (confirm.isNotEmpty() && !matches) {305- Spacer(Modifier.size(8.dp))316+ Spacer(Modifier.size(PeachSpacing.sm))306317 Text(ExportStrings.MISMATCH, color = peachColors.Breach, style = PeachType.bodyMono)307318 }308319 }309320 310- Spacer(Modifier.size(18.dp))321+ Spacer(Modifier.size(PeachSpacing.lg))311322 312323 if (output == null) {313324 AccentButton(@@ -324,12 +335,12 @@324335 PeachClipboard.copy(context, "peach backup", text)325336 status = ExportStrings.COPIED326337 }327- Spacer(Modifier.size(8.dp))338+ Spacer(Modifier.size(PeachSpacing.sm))328339 GhostButton("Save to file", peachColors.T1) {329340 saveLauncher.launch(exportFileName("peach-backup", "peach"))330341 }331342 status?.let {332- Spacer(Modifier.size(12.dp))343+ Spacer(Modifier.size(PeachSpacing.md))333344 Text(334345 it,335346 color = if (it == ExportStrings.SAVE_FAILED) peachColors.Breach else accent,@@ -363,7 +374,7 @@363374 "⚠ CSV exports are not encrypted. Store this file securely and delete it after importing.",364375 color = peachColors.T0, style = PeachType.body,365376 )366- Spacer(Modifier.size(16.dp))377+ Spacer(Modifier.size(PeachSpacing.lg))367378 CheckboxRow(368379 label = "I understand this file is unencrypted",369380 checked = acknowledged,@@ -373,18 +384,18 @@373384 }374385 375386 if (isEmpty) {376- Spacer(Modifier.size(16.dp))387+ Spacer(Modifier.size(PeachSpacing.lg))377388 InlineError(ExportStrings.EMPTY_VAULT)378389 }379390 380- Spacer(Modifier.size(18.dp))391+ Spacer(Modifier.size(PeachSpacing.lg))381392 AccentButton(382393 label = "Export CSV",383394 enabled = acknowledged && !isEmpty,384395 onClick = { saveLauncher.launch(exportFileName("peach-export", "csv")) },385396 )386397 status?.let {387- Spacer(Modifier.size(12.dp))398+ Spacer(Modifier.size(PeachSpacing.md))388399 Text(389400 it,390401 color = if (it == ExportStrings.SAVE_FAILED) peachColors.Breach else accent,@@ -450,23 +461,17 @@450461 451462 @Composable452463 private fun PeachUnderlineField(464+ label: String,453465 value: String,454466 onValueChange: (String) -> Unit,455467 isPassword: Boolean = false,456468 ) {457- val accent = LocalPeachAccent.current458- Column(Modifier.fillMaxWidth()) {459- BasicTextField(460- value = value,461- onValueChange = onValueChange,462- singleLine = true,463- textStyle = PeachType.bodyMono.copy(color = peachColors.T0),464- cursorBrush = SolidColor(accent),465- visualTransformation = if (isPassword) PasswordVisualTransformation() else VisualTransformation.None,466- modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),467- )468- Box(Modifier.fillMaxWidth().height(1.dp).background(peachColors.T3))469- }469+ DesignPeachField(470+ label = label,471+ value = value,472+ onValueChange = onValueChange,473+ visualTransformation = if (isPassword) PasswordVisualTransformation() else VisualTransformation.None,474+ )470475 }471476 472477 @Composableapp/src/main/java/com/peachpasswords/ui/vault/ImportScreen.kt+51 −54
@@ -47,7 +47,11 @@4747 import androidx.hilt.navigation.compose.hiltViewModel4848 import androidx.lifecycle.ViewModel4949 import androidx.lifecycle.viewModelScope50+import com.peachpasswords.ui.components.PeachEmptyOrEndState51+import com.peachpasswords.ui.components.PeachField as DesignPeachField52+import com.peachpasswords.ui.components.PeachTopBar5053 import com.peachpasswords.ui.theme.LocalPeachAccent54+import com.peachpasswords.ui.theme.PeachSpacing5155 import com.peachpasswords.ui.theme.peachColors5256 import com.peachpasswords.ui.theme.PeachType5357 import com.peachpasswords.ui.theme.ShellBackground@@ -304,34 +308,30 @@304308 modifier = Modifier305309 .fillMaxSize()306310 .systemBarsPadding()307- .padding(horizontal = 20.dp, vertical = 16.dp),311+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),308312 ) {309- // Top bar: back · "Import"310- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {311- Icon(312- Icons.AutoMirrored.Rounded.ArrowBack,313- contentDescription = "back",314- tint = peachColors.T0,315- modifier = Modifier316- .size(22.dp)317- .clip(RoundedCornerShape(8.dp))318- .clickable {319- when {320- state !is ImportUiState.Idle -> vm.back(onBack)321- route != ImportRoute.CHOOSE -> route = ImportRoute.CHOOSE322- else -> onBack()323- }324- },325- )326- Spacer(Modifier.width(14.dp))327- Text(328- "Import",329- color = peachColors.T0,330- style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp),331- )332- }313+ PeachTopBar(314+ title = "Import",315+ leading = {316+ Icon(317+ Icons.AutoMirrored.Rounded.ArrowBack,318+ contentDescription = "back",319+ tint = peachColors.T0,320+ modifier = Modifier321+ .size(22.dp)322+ .clip(RoundedCornerShape(PeachSpacing.sm))323+ .clickable {324+ when {325+ state !is ImportUiState.Idle -> vm.back(onBack)326+ route != ImportRoute.CHOOSE -> route = ImportRoute.CHOOSE327+ else -> onBack()328+ }329+ },330+ )331+ },332+ )333333 334- Spacer(Modifier.size(18.dp))334+ Spacer(Modifier.size(PeachSpacing.lg))335335 336336 Column(Modifier.fillMaxSize().verticalScroll(rememberScrollState())) {337337 when (val s = state) {@@ -387,6 +387,10 @@387387 subtitle = "Bitwarden, Proton Pass, 1Password, Chrome, Safari, Edge CSV",388388 onClick = onFile,389389 )390+ PeachEmptyOrEndState(391+ message = "Choose an import source.",392+ detail = "Peach will preview everything before it changes your vault.",393+ )390394 }391395 392396 @Composable@@ -420,24 +424,23 @@420424 421425 GlassCard {422426 FieldLabel("Paste your PeachScript export")423- Spacer(Modifier.size(8.dp))427+ Spacer(Modifier.size(PeachSpacing.sm))424428 PeachMultilineField(425429 value = paste,426430 onValueChange = { paste = it },427431 minLines = 6,428432 )429433 if (needsPassword) {430- Spacer(Modifier.size(16.dp))431- FieldLabel("Backup password")432- Spacer(Modifier.size(6.dp))434+ Spacer(Modifier.size(PeachSpacing.lg))433435 PeachUnderlineField(436+ label = "Backup password",434437 value = backupPassword,435438 onValueChange = { backupPassword = it },436439 isPassword = true,437440 )438441 }439442 }440- Spacer(Modifier.size(18.dp))443+ Spacer(Modifier.size(PeachSpacing.lg))441444 AccentButton(442445 label = "Import",443446 enabled = paste.isNotBlank(),@@ -460,13 +463,13 @@460463 color = peachColors.T1,461464 style = PeachType.body,462465 )463- Spacer(Modifier.size(6.dp))466+ Spacer(Modifier.size(PeachSpacing.sm))464467 Text(465468 "Supports Bitwarden, Proton Pass, 1Password, Chrome/Edge, and Safari exports.",466469 color = peachColors.T2,467470 style = PeachType.hint,468471 )469- Spacer(Modifier.size(16.dp))472+ Spacer(Modifier.size(PeachSpacing.lg))470473 GhostButton("Choose file", LocalPeachAccent.current) {471474 // GetContent takes a single MIME type; the chooser still lists CSV472475 // and JSON. "*/*" is intentional so providers exporting odd MIME@@ -474,7 +477,7 @@474477 picker.launch("*/*")475478 }476479 if (detected != null) {477- Spacer(Modifier.size(12.dp))480+ Spacer(Modifier.size(PeachSpacing.md))478481 Text(479482 "Detected: $detected export",480483 color = LocalPeachAccent.current,@@ -500,13 +503,13 @@500503 color = LocalPeachAccent.current,501504 style = PeachType.labelMono,502505 )503- Spacer(Modifier.size(10.dp))506+ Spacer(Modifier.size(PeachSpacing.sm))504507 Text(505508 "Found ${preview.entries.size} ${if (preview.entries.size == 1) "entry" else "entries"}",506509 color = peachColors.T0,507510 style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp),508511 )509- Spacer(Modifier.size(12.dp))512+ Spacer(Modifier.size(PeachSpacing.md))510513 // Up to five entry names, scrollable if more.511514 Column(512515 modifier = Modifier@@ -533,12 +536,12 @@533536 }534537 Divider()535538 RadioRow("Merge with existing vault", selected = mode == ImportMode.MERGE) { mode = ImportMode.MERGE }536- Spacer(Modifier.size(8.dp))539+ Spacer(Modifier.size(PeachSpacing.sm))537540 RadioRow("Replace vault", selected = mode == ImportMode.REPLACE) { mode = ImportMode.REPLACE }538541 }539- Spacer(Modifier.size(18.dp))542+ Spacer(Modifier.size(PeachSpacing.lg))540543 AccentButton(label = "Confirm Import", onClick = { onConfirm(mode) })541- Spacer(Modifier.size(8.dp))544+ Spacer(Modifier.size(PeachSpacing.sm))542545 GhostButton("Cancel", peachColors.T1, onCancel)543546 }544547 @@ -575,14 +578,14 @@575578 color = peachColors.T0,576579 style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 24.sp),577580 )578- Spacer(Modifier.size(8.dp))581+ Spacer(Modifier.size(PeachSpacing.sm))579582 Text(580583 "$count ${if (count == 1) "entry" else "entries"} imported.",581584 color = peachColors.T1,582585 style = PeachType.body,583586 )584587 }585- Spacer(Modifier.size(18.dp))588+ Spacer(Modifier.size(PeachSpacing.lg))586589 AccentButton(label = "Done", onClick = onDone)587590 }588591 @@ -657,23 +660,17 @@657660 658661 @Composable659662 private fun PeachUnderlineField(663+ label: String,660664 value: String,661665 onValueChange: (String) -> Unit,662666 isPassword: Boolean = false,663667 ) {664- val accent = LocalPeachAccent.current665- Column(Modifier.fillMaxWidth()) {666- BasicTextField(667- value = value,668- onValueChange = onValueChange,669- singleLine = true,670- textStyle = PeachType.bodyMono.copy(color = peachColors.T0),671- cursorBrush = SolidColor(accent),672- visualTransformation = if (isPassword) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None,673- modifier = Modifier.fillMaxWidth().padding(vertical = 6.dp),674- )675- Box(Modifier.fillMaxWidth().size(1.dp).background(peachColors.T3))676- }668+ DesignPeachField(669+ label = label,670+ value = value,671+ onValueChange = onValueChange,672+ visualTransformation = if (isPassword) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None,673+ )677674 }678675 679676 @Composableapp/src/main/java/com/peachpasswords/ui/vault/NotesModeScreen.kt+147 −109
@@ -29,6 +29,7 @@2929 import androidx.compose.material.icons.outlined.DeleteOutline3030 import androidx.compose.material.icons.outlined.Search3131 import androidx.compose.material3.Icon32+import androidx.compose.material3.Surface3233 import androidx.compose.material3.Text3334 import androidx.compose.runtime.Composable3435 import androidx.compose.runtime.LaunchedEffect@@ -57,6 +58,9 @@5758 import androidx.compose.runtime.collectAsState5859 import androidx.fragment.app.FragmentActivity5960 import androidx.hilt.navigation.compose.hiltViewModel61+import com.peachpasswords.ui.components.PeachEmptyOrEndState62+import com.peachpasswords.ui.components.PeachTopBar63+import com.peachpasswords.ui.theme.PeachSpacing6064 import com.peachpasswords.vault.VaultEntry6165 import com.peachpasswords.ui.theme.LocalPeachAccent6266 import com.peachpasswords.ui.theme.peachColors@@ -121,53 +125,70 @@121125 var body by remember(e?.id) { mutableStateOf(TextFieldValue(e?.notes ?: "")) }122126 123127 ShellBackground {124- Column(Modifier.fillMaxSize().statusBarsPadding()) {125- // Top bar: back · editable title · "saved." indicator126- Row(127- Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 8.dp),128- verticalAlignment = Alignment.CenterVertically,128+ Column(129+ Modifier130+ .fillMaxSize()131+ .statusBarsPadding()132+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),133+ ) {134+ Surface(135+ modifier = Modifier.fillMaxWidth(),136+ color = peachColors.S1,137+ contentColor = peachColors.T0,138+ shape = RoundedCornerShape(PeachSpacing.lg),139+ tonalElevation = 1.dp,129140 ) {130- Icon(131- Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0,132- modifier = Modifier.size(40.dp).clip(CircleShape)133- .clickable { vm.saveNow(title, body.text); vm.showToc() }.padding(8.dp),134- )135- Box(Modifier.weight(1f).padding(horizontal = 4.dp)) {136- if (title.isEmpty()) {137- Text(138- "untitled note",139- color = peachColors.T2,140- style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp),141+ Row(142+ Modifier143+ .fillMaxWidth()144+ .height(56.dp)145+ .padding(horizontal = PeachSpacing.sm),146+ verticalAlignment = Alignment.CenterVertically,147+ ) {148+ Icon(149+ Icons.AutoMirrored.Rounded.ArrowBack,150+ contentDescription = "back",151+ tint = peachColors.T0,152+ modifier = Modifier.size(40.dp).clip(CircleShape)153+ .clickable { vm.saveNow(title, body.text); vm.showToc() }.padding(8.dp),154+ )155+ Box(Modifier.weight(1f).padding(horizontal = PeachSpacing.xs)) {156+ if (title.isEmpty()) {157+ Text(158+ "untitled note",159+ color = peachColors.T2,160+ style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp),161+ )162+ }163+ BasicTextField(164+ value = title,165+ onValueChange = { title = it; vm.onTitleChanged(it) },166+ singleLine = true,167+ textStyle = TextStyle(168+ color = peachColors.T0,169+ fontFamily = PeachType.Display,170+ fontStyle = FontStyle.Italic,171+ fontSize = 22.sp,172+ ),173+ cursorBrush = SolidColor(accent),174+ modifier = Modifier.fillMaxWidth(),141175 )142176 }143- BasicTextField(144- value = title,145- onValueChange = { title = it; vm.onTitleChanged(it) },146- singleLine = true,147- textStyle = TextStyle(148- color = peachColors.T0,149- fontFamily = PeachType.Display,150- fontStyle = FontStyle.Italic,151- fontSize = 22.sp,152- ),153- cursorBrush = SolidColor(accent),154- modifier = Modifier.fillMaxWidth(),177+ Text(178+ "saved.",179+ color = peachColors.T2,180+ style = TextStyle(fontFamily = PeachType.Mono, fontSize = 9.sp),181+ modifier = Modifier.padding(end = PeachSpacing.sm).alpha(savedAlpha),155182 )183+ Icon(184+ Icons.Outlined.DeleteOutline,185+ contentDescription = "delete note",186+ tint = peachColors.T2,187+ modifier = Modifier.size(36.dp).clip(CircleShape)188+ .clickable { vm.delete {} }189+ .padding(8.dp),190+ )156191 }157- Text(158- "saved.",159- color = peachColors.T2,160- style = TextStyle(fontFamily = PeachType.Mono, fontSize = 9.sp),161- modifier = Modifier.padding(end = 8.dp).alpha(savedAlpha),162- )163- Icon(164- Icons.Outlined.DeleteOutline,165- contentDescription = "delete note",166- tint = peachColors.T2,167- modifier = Modifier.size(36.dp).clip(CircleShape)168- .clickable { vm.delete {} }169- .padding(8.dp),170- )171192 }172193 173194 if (e == null) {@@ -179,8 +200,8 @@179200 180201 // Formatting toolbar: browser-compatible inline and block actions.181202 Row(182- Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 4.dp),183- horizontalArrangement = Arrangement.spacedBy(6.dp),203+ Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm),204+ horizontalArrangement = Arrangement.spacedBy(PeachSpacing.xs),184205 verticalAlignment = Alignment.CenterVertically,185206 ) {186207 FormatButton("B", FontWeight.Bold) { body = applySpan(body, SpanStyle(fontWeight = FontWeight.Bold)) }@@ -194,7 +215,7 @@194215 }195216 196217 // Content area.197- Box(Modifier.fillMaxSize().padding(horizontal = 16.dp, vertical = 12.dp)) {218+ Box(Modifier.fillMaxSize().padding(vertical = PeachSpacing.sm)) {198219 if (body.text.isEmpty()) {199220 Text(200221 "Start writing…",@@ -227,78 +248,95 @@227248 val accent = LocalPeachAccent.current228249 229250 ShellBackground {230- Column(Modifier.fillMaxSize().statusBarsPadding()) {231- Row(232- Modifier.fillMaxWidth().height(56.dp).padding(horizontal = 8.dp),233- verticalAlignment = Alignment.CenterVertically,234- ) {235- if (searchVisible) {236- BasicTextField(237- value = query,238- onValueChange = { query = it },239- singleLine = true,240- textStyle = TextStyle(241- color = peachColors.T0,242- fontFamily = PeachType.Mono,243- fontSize = 14.sp,244- ),245- cursorBrush = SolidColor(accent),246- modifier = Modifier.weight(1f).padding(horizontal = 12.dp),247- decorationBox = { inner ->248- Box {249- if (query.isEmpty()) {250- Text("search notes…", color = peachColors.T2, style = PeachType.searchInput)251+ Column(252+ Modifier253+ .fillMaxSize()254+ .statusBarsPadding()255+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),256+ ) {257+ if (searchVisible) {258+ Surface(259+ modifier = Modifier.fillMaxWidth(),260+ color = peachColors.S1,261+ shape = RoundedCornerShape(PeachSpacing.lg),262+ tonalElevation = 1.dp,263+ ) {264+ Row(265+ Modifier266+ .fillMaxWidth()267+ .height(56.dp)268+ .padding(horizontal = PeachSpacing.sm),269+ verticalAlignment = Alignment.CenterVertically,270+ ) {271+ BasicTextField(272+ value = query,273+ onValueChange = { query = it },274+ singleLine = true,275+ textStyle = TextStyle(276+ color = peachColors.T0,277+ fontFamily = PeachType.Mono,278+ fontSize = 14.sp,279+ ),280+ cursorBrush = SolidColor(accent),281+ modifier = Modifier.weight(1f).padding(horizontal = PeachSpacing.sm),282+ decorationBox = { inner ->283+ Box {284+ if (query.isEmpty()) {285+ Text("search notes…", color = peachColors.T2, style = PeachType.searchInput)286+ }287+ inner()251288 }252- inner()253- }254- },255- )256- Icon(257- Icons.Outlined.Close,258- contentDescription = "clear search",259- tint = peachColors.T0,260- modifier = Modifier.size(40.dp).clip(CircleShape)261- .clickable {262- query = ""263- searchVisible = false264- }265- .padding(9.dp),266- )267- } else {268- Icon(269- Icons.AutoMirrored.Rounded.ArrowBack,270- contentDescription = "back to vault",271- tint = peachColors.T0,272- modifier = Modifier.size(40.dp).clip(CircleShape).clickable(onClick = onBack).padding(8.dp),273- )274- Text(275- "notes.",276- color = peachColors.T0,277- style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 24.sp),278- modifier = Modifier.weight(1f).padding(horizontal = 8.dp),279- )280- Icon(281- Icons.Outlined.Search,282- contentDescription = "search notes",283- tint = peachColors.T0,284- modifier = Modifier.size(40.dp).clip(CircleShape)285- .clickable { searchVisible = true }286- .padding(9.dp),287- )289+ },290+ )291+ Icon(292+ Icons.Outlined.Close,293+ contentDescription = "clear search",294+ tint = peachColors.T0,295+ modifier = Modifier.size(40.dp).clip(CircleShape)296+ .clickable {297+ query = ""298+ searchVisible = false299+ }300+ .padding(9.dp),301+ )302+ }288303 }304+ } else {305+ PeachTopBar(306+ title = "notes.",307+ leading = {308+ Icon(309+ Icons.AutoMirrored.Rounded.ArrowBack,310+ contentDescription = "back to vault",311+ tint = peachColors.T0,312+ modifier = Modifier.size(22.dp).clip(RoundedCornerShape(PeachSpacing.sm))313+ .clickable(onClick = onBack),314+ )315+ },316+ actions = {317+ Icon(318+ Icons.Outlined.Search,319+ contentDescription = "search notes",320+ tint = peachColors.T0,321+ modifier = Modifier.size(28.dp).clip(CircleShape)322+ .clickable { searchVisible = true }323+ .padding(5.dp),324+ )325+ },326+ )289327 }328+ Spacer(Modifier.size(PeachSpacing.lg))290329 291330 LazyColumn(292- Modifier.weight(1f).padding(horizontal = 16.dp),293- verticalArrangement = Arrangement.spacedBy(8.dp),331+ Modifier.weight(1f),332+ verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),294333 ) {295334 if (filtered.isEmpty()) {296335 item {297336 Box(Modifier.fillParentMaxHeight(), contentAlignment = Alignment.Center) {298- Text(299- if (query.isBlank()) "No notes yet.\nStart writing." else "No notes match.",300- color = peachColors.T1,301- style = PeachType.bodyMono,337+ PeachEmptyOrEndState(338+ message = if (query.isBlank()) "No notes yet." else "No notes match.",339+ detail = if (query.isBlank()) "Start writing when you are ready." else "Try a different search.",302340 )303341 }304342 }@@ -310,7 +348,7 @@310348 }311349 312350 Row(313- Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 14.dp)351+ Modifier.fillMaxWidth().padding(vertical = PeachSpacing.md)314352 .clip(RoundedCornerShape(8.dp))315353 .background(peachColors.S2)316354 .clickable(onClick = onNewNote)app/src/main/java/com/peachpasswords/ui/vault/RecoveryWordsScreen.kt+19 −32
@@ -44,7 +44,10 @@4444 import androidx.lifecycle.ViewModel4545 import androidx.lifecycle.viewModelScope4646 import com.peachpasswords.ui.components.SecureFlagEffect47+import com.peachpasswords.ui.components.PeachField as DesignPeachField48+import com.peachpasswords.ui.components.PeachTopBar4749 import com.peachpasswords.ui.theme.LocalPeachAccent50+import com.peachpasswords.ui.theme.PeachSpacing4851 import com.peachpasswords.ui.theme.peachColors4952 import com.peachpasswords.ui.theme.PeachType5053 import com.peachpasswords.ui.theme.ShellBackground@@ -95,17 +98,11 @@9598 modifier = Modifier9699 .fillMaxSize()97100 .systemBarsPadding()98- .padding(horizontal = 22.dp, vertical = 18.dp)101+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md)99102 .verticalScroll(rememberScrollState()),100103 ) {101- Text(102- "recovery words",103- color = peachColors.T1,104- style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp),105- textAlign = TextAlign.Center,106- modifier = Modifier.fillMaxWidth(),107- )108- Spacer(Modifier.size(18.dp))104+ PeachTopBar(title = "recovery words")105+ Spacer(Modifier.size(PeachSpacing.lg))109106 when (phase) {110107 Phase.SHOW -> ShowPhase(111108 words = words,@@ -148,7 +145,7 @@148145 "These 12 words can restore your vault if you forget your master password. Write them down somewhere safe.",149146 color = peachColors.T1, style = PeachType.body,150147 )151- Spacer(Modifier.size(16.dp))148+ Spacer(Modifier.size(PeachSpacing.lg))152149 // 12-word grid, two columns.153150 Column {154151 for (rowStart in 0 until words.size step 2) {@@ -166,11 +163,11 @@166163 GhostButton("copy all words", accent) {167164 PeachClipboard.copy(context, "peach recovery", joined)168165 }169- Spacer(Modifier.size(8.dp))166+ Spacer(Modifier.size(PeachSpacing.sm))170167 GhostButton("save as text file", peachColors.T1) { saveLauncher.launch("peach-recovery.txt") }171168 }172169 173- Spacer(Modifier.size(18.dp))170+ Spacer(Modifier.size(PeachSpacing.lg))174171 if (requireConfirm) {175172 var remaining by remember { mutableStateOf(5) }176173 LaunchedEffect(Unit) {@@ -206,30 +203,20 @@206203 )207204 Divider()208205 Text("Enter the words at positions 3, 7, and 11.", color = peachColors.T1, style = PeachType.body)209- Spacer(Modifier.size(14.dp))206+ Spacer(Modifier.size(PeachSpacing.md))210207 positions.forEachIndexed { i, pos ->211208 val ok = correct(i)212- val tint = when {213- inputs[i].isBlank() -> peachColors.T2214- ok -> accent215- else -> peachColors.Breach216- }217- Text("Word #$pos", color = peachColors.T2, style = PeachType.labelMono, modifier = Modifier.padding(top = 12.dp))218- Row(verticalAlignment = Alignment.CenterVertically) {219- BasicTextField(220- value = inputs[i],221- onValueChange = { inputs[i] = it },222- singleLine = true,223- textStyle = PeachType.bodyMono.copy(color = peachColors.T0),224- cursorBrush = SolidColor(accent),225- modifier = Modifier.weight(1f),226- )227- if (ok) Text("✓", color = accent, style = PeachType.bodyMono)228- }229- Box(Modifier.fillMaxWidth().padding(top = 4.dp).size(1.dp).background(tint.copy(alpha = 0.5f)))209+ DesignPeachField(210+ label = "Word #$pos",211+ value = inputs[i],212+ onValueChange = { inputs[i] = it },213+ trailing = {214+ if (ok) Text("✓", color = accent, style = PeachType.bodyMono)215+ },216+ )230217 }231218 }232- Spacer(Modifier.size(18.dp))219+ Spacer(Modifier.size(PeachSpacing.lg))233220 AccentButton("Confirm", onConfirm, enabled = allCorrect)234221 }235222 app/src/main/java/com/peachpasswords/ui/vault/TrashScreen.kt+67 −52
@@ -31,7 +31,6 @@3131 import androidx.compose.ui.Alignment3232 import androidx.compose.ui.Modifier3333 import androidx.compose.ui.draw.clip34-import androidx.compose.ui.text.font.FontStyle3534 import androidx.compose.ui.text.style.TextAlign3635 import androidx.compose.ui.text.style.TextDecoration3736 import androidx.compose.ui.unit.dp@@ -40,10 +39,13 @@4039 import androidx.lifecycle.ViewModel4140 import androidx.lifecycle.viewModelScope4241 import com.peachpasswords.ui.components.FaviconInitials42+import com.peachpasswords.ui.components.PeachEmptyOrEndState43+import com.peachpasswords.ui.components.PeachTopBar4344 import com.peachpasswords.ui.theme.LocalPeachAccent44-import com.peachpasswords.ui.theme.peachColors45+import com.peachpasswords.ui.theme.PeachSpacing4546 import com.peachpasswords.ui.theme.PeachType4647 import com.peachpasswords.ui.theme.ShellBackground48+import com.peachpasswords.ui.theme.peachColors4749 import com.peachpasswords.vault.VaultEntry4850 import com.peachpasswords.vault.VaultRepository4951 import com.peachpasswords.vault.domainKey@@ -64,7 +66,9 @@6466 /** Retention window in days, or null when set to "Never". */6567 val retentionDays: Int? get() = vault.settings.value.trashRetentionDays?.takeIf { it > 0 }6668 67- private fun refresh() { _deleted.value = vault.getDeletedEntries() }69+ private fun refresh() {70+ _deleted.value = vault.getDeletedEntries()71+ }6872 6973 fun restore(id: String) = viewModelScope.launch { vault.restoreEntry(id); refresh() }7074 fun purge(id: String) = viewModelScope.launch { vault.purgeEntry(id); refresh() }@@ -85,40 +89,35 @@8589 modifier = Modifier8690 .fillMaxSize()8791 .systemBarsPadding()88- .padding(horizontal = 20.dp, vertical = 16.dp),92+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),8993 ) {90- // Top bar: back · "Trash" · "Empty trash"91- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {92- Icon(93- Icons.AutoMirrored.Rounded.ArrowBack, contentDescription = "back", tint = peachColors.T0,94- modifier = Modifier.size(22.dp).clip(RoundedCornerShape(8.dp)).clickable(onClick = onBack),95- )96- Spacer(Modifier.width(14.dp))97- Text(98- "Trash",99- color = peachColors.T0,100- style = PeachType.Display.let {101- androidx.compose.ui.text.TextStyle(102- fontFamily = it, fontStyle = FontStyle.Italic, fontSize = 22.sp,103- )104- },105- )106- Spacer(Modifier.weight(1f))107- if (deleted.isNotEmpty()) {108- GhostPill("Empty trash", peachColors.Breach) { confirmEmptyAll = true }109- }110- }94+ PeachTopBar(95+ title = "Trash",96+ leading = {97+ Icon(98+ Icons.AutoMirrored.Rounded.ArrowBack,99+ contentDescription = "back",100+ tint = peachColors.T0,101+ modifier = Modifier102+ .size(22.dp)103+ .clip(RoundedCornerShape(PeachSpacing.sm))104+ .clickable(onClick = onBack),105+ )106+ },107+ actions = {108+ if (deleted.isNotEmpty()) {109+ GhostPill("Empty trash", peachColors.Breach) { confirmEmptyAll = true }110+ }111+ },112+ )111113 112- Spacer(Modifier.size(18.dp))114+ Spacer(Modifier.size(PeachSpacing.lg))113115 114116 if (deleted.isEmpty()) {115117 Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {116- Text(117- "Trash is empty.",118- color = peachColors.T1,119- style = androidx.compose.ui.text.TextStyle(120- fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp,121- ),118+ PeachEmptyOrEndState(119+ message = "Trash is empty.",120+ detail = "Deleted entries will wait here before they are gone for good.",122121 )123122 }124123 } else {@@ -130,17 +129,26 @@130129 onPurge = { confirmPurge = entry },131130 )132131 }132+ item {133+ val days = vm.retentionDays134+ if (days != null) {135+ Text(136+ "Entries are permanently deleted after $days days",137+ color = peachColors.T2,138+ style = PeachType.labelMono,139+ textAlign = TextAlign.Center,140+ modifier = Modifier141+ .fillMaxWidth()142+ .padding(vertical = PeachSpacing.md),143+ )144+ } else {145+ PeachEmptyOrEndState(146+ message = "End of trash.",147+ detail = "Nothing below this point will be removed automatically.",148+ )149+ }150+ }133151 }134- val days = vm.retentionDays135- if (days != null) {136- Text(137- "Entries are permanently deleted after $days days",138- color = peachColors.T2,139- style = PeachType.labelMono,140- textAlign = TextAlign.Center,141- modifier = Modifier.fillMaxWidth().padding(vertical = 10.dp),142- )143- }144152 }145153 }146154 }@@ -167,10 +175,12 @@167175 private fun DeletedRow(entry: VaultEntry, onRestore: () -> Unit, onPurge: () -> Unit) {168176 Row(169177 verticalAlignment = Alignment.CenterVertically,170- modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp),178+ modifier = Modifier179+ .fillMaxWidth()180+ .padding(vertical = PeachSpacing.sm),171181 ) {172182 FaviconInitials(source = entry.url ?: entry.title, size = 26.dp)173- Spacer(Modifier.width(12.dp))183+ Spacer(Modifier.width(PeachSpacing.sm))174184 Column(Modifier.weight(1f)) {175185 Text(176186 entry.title,@@ -183,10 +193,15 @@183193 }184194 }185195 GhostPill("Restore", LocalPeachAccent.current, onRestore)186- Spacer(Modifier.width(8.dp))196+ Spacer(Modifier.width(PeachSpacing.sm))187197 Icon(188- Icons.Outlined.DeleteOutline, contentDescription = "delete forever", tint = peachColors.Breach,189- modifier = Modifier.size(20.dp).clip(RoundedCornerShape(6.dp)).clickable(onClick = onPurge),198+ Icons.Outlined.DeleteOutline,199+ contentDescription = "delete forever",200+ tint = peachColors.Breach,201+ modifier = Modifier202+ .size(20.dp)203+ .clip(RoundedCornerShape(6.dp))204+ .clickable(onClick = onPurge),190205 )191206 }192207 }@@ -220,18 +235,18 @@220235 ) {221236 Column(222237 modifier = Modifier223- .padding(32.dp)224- .clip(RoundedCornerShape(18.dp))238+ .padding(PeachSpacing.xxl)239+ .clip(RoundedCornerShape(PeachSpacing.lg))225240 .background(peachColors.S2)226- .border(1.dp, peachColors.T3, RoundedCornerShape(18.dp))241+ .border(1.dp, peachColors.T3, RoundedCornerShape(PeachSpacing.lg))227242 .clickable(enabled = false) {}228243 .padding(22.dp),229244 ) {230245 Text(message, color = peachColors.T0, style = PeachType.body)231- Spacer(Modifier.size(18.dp))246+ Spacer(Modifier.size(PeachSpacing.lg))232247 Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {233248 GhostPill("Cancel", peachColors.T1, onCancel)234- Spacer(Modifier.width(10.dp))249+ Spacer(Modifier.width(PeachSpacing.sm))235250 GhostPill(confirmLabel, peachColors.Breach, onConfirm)236251 }237252 }app/src/main/java/com/peachpasswords/ui/vault/VaultHealthScreen.kt+27 −35
@@ -41,8 +41,11 @@4141 import androidx.hilt.navigation.compose.hiltViewModel4242 import androidx.lifecycle.ViewModel4343 import com.peachpasswords.ui.components.FaviconInitials44+import com.peachpasswords.ui.components.PeachEmptyOrEndState4445 import com.peachpasswords.ui.components.PeachMark46+import com.peachpasswords.ui.components.PeachTopBar4547 import com.peachpasswords.ui.theme.LocalPeachAccent48+import com.peachpasswords.ui.theme.PeachSpacing4649 import com.peachpasswords.ui.theme.peachColors4750 import com.peachpasswords.ui.theme.PeachType4851 import com.peachpasswords.ui.theme.ShellBackground@@ -110,33 +113,28 @@110113 modifier = Modifier111114 .fillMaxSize()112115 .systemBarsPadding()113- .padding(horizontal = 20.dp, vertical = 16.dp),116+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),114117 ) {115- // Top bar: back · "vault health" · "check now"116- Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth()) {117- Icon(118- Icons.AutoMirrored.Rounded.ArrowBack,119- contentDescription = "back",120- tint = peachColors.T0,121- modifier = Modifier.size(22.dp).clip(RoundedCornerShape(8.dp)).clickable(onClick = onBack),122- )123- Spacer(Modifier.width(14.dp))124- Text(125- "vault health",126- color = peachColors.T0,127- style = TextStyle(128- fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp,129- ),130- )131- Spacer(Modifier.weight(1f))132- GhostPill("check now", accent) { vm.recompute() }133- }118+ PeachTopBar(119+ title = "vault health",120+ leading = {121+ Icon(122+ Icons.AutoMirrored.Rounded.ArrowBack,123+ contentDescription = "back",124+ tint = peachColors.T0,125+ modifier = Modifier.size(22.dp).clip(RoundedCornerShape(PeachSpacing.sm)).clickable(onClick = onBack),126+ )127+ },128+ actions = {129+ GhostPill("check now", accent) { vm.recompute() }130+ },131+ )134132 135- Spacer(Modifier.size(22.dp))133+ Spacer(Modifier.size(PeachSpacing.lg))136134 137135 ScoreRing(score = report.vaultScore, accent = accent)138136 139- Spacer(Modifier.size(24.dp))137+ Spacer(Modifier.size(PeachSpacing.xl))140138 141139 if (allHealthy) {142140 Column(@@ -144,15 +142,9 @@144142 horizontalAlignment = Alignment.CenterHorizontally,145143 ) {146144 Spacer(Modifier.weight(1f))147- PeachMark(size = 60.dp)148- Spacer(Modifier.size(18.dp))149- Text(150- "Your vault looks healthy.",151- color = peachColors.T1,152- textAlign = TextAlign.Center,153- style = TextStyle(154- fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 20.sp,155- ),145+ PeachEmptyOrEndState(146+ message = "Your vault looks healthy.",147+ detail = "No breached, reused, weak, old, or passkey-free logins need attention.",156148 )157149 Spacer(Modifier.weight(1f))158150 }@@ -208,7 +200,7 @@208200 .fillMaxWidth()209201 .clip(RoundedCornerShape(10.dp))210202 .clickable(onClick = onToggle)211- .padding(vertical = 12.dp),203+ .padding(vertical = PeachSpacing.sm),212204 ) {213205 Text(214206 title,@@ -217,7 +209,7 @@217209 fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp,218210 ),219211 )220- Spacer(Modifier.width(10.dp))212+ Spacer(Modifier.width(PeachSpacing.sm))221213 CountBadge(count, badgeColor)222214 }223215 }@@ -226,10 +218,10 @@226218 private fun HealthEntryRow(entry: VaultEntry, onChange: () -> Unit) {227219 Row(228220 verticalAlignment = Alignment.CenterVertically,229- modifier = Modifier.fillMaxWidth().padding(start = 6.dp, top = 8.dp, bottom = 8.dp),221+ modifier = Modifier.fillMaxWidth().padding(start = PeachSpacing.xs, top = PeachSpacing.sm, bottom = PeachSpacing.sm),230222 ) {231223 FaviconInitials(source = entry.url ?: entry.title, size = 26.dp)232- Spacer(Modifier.width(12.dp))224+ Spacer(Modifier.width(PeachSpacing.sm))233225 Column(Modifier.weight(1f)) {234226 Text(entry.title, color = peachColors.T0, style = PeachType.entryNameList)235227 val domain = entry.domainKey()