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
7 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 −35
app/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 {

view file

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 @Composable

view file

app/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 @Composable

view file

app/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)

view file

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 

view file

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         }

view file

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()

view file