605e3de62e10

feat: Recompose pairing and recovery surfaces with design primitives (v2.3.65)

time
2026-07-08 21:46 UTC · 3 months ago
machine
koh2
parent
48faeaa8206e
5 files app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/pairing/ChirpPairScreen.kt+38 −29app/src/main/java/com/peachpasswords/ui/recovery/RecoveryWordEntryScreen.kt+67 −98app/src/main/java/com/peachpasswords/ui/settings/PanicScreen.kt+57 −44app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+24 −26
app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616         applicationId "com.peachpasswords"1717         minSdk 261818         targetSdk 3519-        versionCode 23640020-        versionName "2.3.64"19+        versionCode 23650020+        versionName "2.3.65"2121 2222         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323         vectorDrawables {

view file

app/src/main/java/com/peachpasswords/ui/pairing/ChirpPairScreen.kt+38 −29
@@ -8,7 +8,6 @@88 import androidx.compose.foundation.background99 import androidx.compose.foundation.border1010 import androidx.compose.foundation.clickable11-import androidx.compose.foundation.layout.Arrangement1211 import androidx.compose.foundation.layout.Box1312 import androidx.compose.foundation.layout.Column1413 import androidx.compose.foundation.layout.Row@@ -19,6 +18,9 @@1918 import androidx.compose.foundation.layout.padding2019 import androidx.compose.foundation.layout.size2120 import androidx.compose.foundation.layout.systemBarsPadding21+import androidx.compose.material.icons.Icons22+import androidx.compose.material.icons.automirrored.rounded.ArrowBack23+import androidx.compose.material3.Icon2224 import androidx.compose.foundation.shape.CircleShape2325 import androidx.compose.foundation.shape.RoundedCornerShape2426 import androidx.compose.material3.Text@@ -39,7 +41,9 @@3941 import com.peachpasswords.sync.PairingPhase4042 import com.peachpasswords.ui.components.BirdAnimation4143 import com.peachpasswords.ui.components.PairingAnimState44+import com.peachpasswords.ui.components.PeachTopBar4245 import com.peachpasswords.ui.theme.LocalPeachAccent46+import com.peachpasswords.ui.theme.PeachSpacing4347 import com.peachpasswords.ui.theme.peachColors4448 import com.peachpasswords.ui.theme.PeachType4549 import com.peachpasswords.ui.theme.ShellBackground@@ -55,24 +59,29 @@5559     val phase = progress.phase5660 5761     ShellBackground {58-        Column(modifier = Modifier.fillMaxSize().systemBarsPadding().padding(horizontal = 24.dp)) {59-            // Header60-            Row(61-                modifier = Modifier.fillMaxWidth().height(56.dp),62-                verticalAlignment = Alignment.CenterVertically,63-            ) {64-                Text(65-                    text = "‹",66-                    color = peachColors.T1,67-                    fontSize = 26.sp,68-                    modifier = Modifier.clip(CircleShape).clickable(onClick = onBack).padding(horizontal = 8.dp),69-                )70-                Spacer(Modifier.size(8.dp))71-                Text("Sync", color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)72-            }62+        Column(63+            modifier = Modifier64+                .fillMaxSize()65+                .systemBarsPadding()66+                .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),67+        ) {68+            PeachTopBar(69+                title = "Sync",70+                leading = {71+                    Icon(72+                        Icons.AutoMirrored.Rounded.ArrowBack,73+                        contentDescription = "back",74+                        tint = peachColors.T0,75+                        modifier = Modifier76+                            .size(22.dp)77+                            .clip(RoundedCornerShape(PeachSpacing.sm))78+                            .clickable(onClick = onBack),79+                    )80+                },81+                actions = { StatusChip(phase, accent) },82+            )7383 74-            Spacer(Modifier.height(4.dp))75-            StatusChip(phase, accent)84+            Spacer(Modifier.height(PeachSpacing.sm))7685 7786             Box(7887                 modifier = Modifier.fillMaxWidth().weight(1f),@@ -86,9 +95,9 @@8695 8796             Copy(phase, progress.peerDeviceName, progress.errorMessage)8897 89-            Spacer(Modifier.height(20.dp))98+            Spacer(Modifier.height(PeachSpacing.lg))9099             PrimaryAction(phase, vm)91-            Spacer(Modifier.height(28.dp))100+            Spacer(Modifier.height(PeachSpacing.md))92101         }93102 94103         if (sas != null) {@@ -132,11 +141,11 @@132141             .clip(CircleShape)133142             .background(accent.copy(alpha = 0.12f))134143             .border(1.dp, accent.copy(alpha = 0.28f), CircleShape)135-            .padding(horizontal = 10.dp, vertical = 5.dp),144+            .padding(horizontal = 10.dp, vertical = PeachSpacing.xs),136145         verticalAlignment = Alignment.CenterVertically,137146     ) {138147         Box(Modifier.size(4.dp).graphicsLayer { scaleX = dotScale; scaleY = dotScale }.clip(CircleShape).background(dot))139-        Spacer(Modifier.size(6.dp))148+        Spacer(Modifier.size(PeachSpacing.xs))140149         Text(label, color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 9.sp, letterSpacing = 0.08.em)141150     }142151 }@@ -155,7 +164,7 @@155164     }156165     Column(modifier = Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {157166         Text(headline, color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 26.sp, textAlign = TextAlign.Center)158-        Spacer(Modifier.height(8.dp))167+        Spacer(Modifier.height(PeachSpacing.sm))159168         Text(sub, color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp, textAlign = TextAlign.Center)160169     }161170 }@@ -171,7 +180,7 @@171180         else -> PillButton("Try again", accent, filled = true) { vm.startPairing() }172181     }173182     if (vm.isDebug && phase == PairingPhase.IDLE) {174-        Spacer(Modifier.height(10.dp))183+        Spacer(Modifier.height(PeachSpacing.sm))175184         Text(176185             "demo flow",177186             color = peachColors.T3,@@ -192,7 +201,7 @@192201             .clip(RoundedCornerShape(14.dp))193202             .background(if (filled) accent.copy(alpha = 0.14f) else Color.Transparent)194203             .clickable(onClick = onClick)195-            .padding(vertical = 15.dp),204+            .padding(vertical = PeachSpacing.md),196205         contentAlignment = Alignment.Center,197206     ) {198207         Text(label, color = if (filled) accent else color, fontFamily = PeachType.Mono, fontSize = 13.sp)@@ -210,9 +219,9 @@210219             horizontalAlignment = Alignment.CenterHorizontally,211220         ) {212221             Text("Verify the code", color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp)213-            Spacer(Modifier.height(6.dp))222+            Spacer(Modifier.height(PeachSpacing.xs))214223             Text("It must match on both devices.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp, textAlign = TextAlign.Center)215-            Spacer(Modifier.height(28.dp))224+            Spacer(Modifier.height(PeachSpacing.xl))216225             Text(217226                 text = code,218227                 color = accent,@@ -220,9 +229,9 @@220229                 fontSize = 38.sp,221230                 letterSpacing = 6.sp,222231             )223-            Spacer(Modifier.height(34.dp))232+            Spacer(Modifier.height(PeachSpacing.xxl))224233             PillButton("They match — pair", accent, filled = true, onClick = onConfirm)225-            Spacer(Modifier.height(10.dp))234+            Spacer(Modifier.height(PeachSpacing.sm))226235             PillButton("Cancel", peachColors.T2, filled = false, onClick = onCancel)227236         }228237     }

view file

app/src/main/java/com/peachpasswords/ui/recovery/RecoveryWordEntryScreen.kt+67 −98
@@ -23,6 +23,9 @@2323 import androidx.compose.foundation.text.KeyboardActions2424 import androidx.compose.foundation.text.KeyboardOptions2525 import androidx.compose.foundation.verticalScroll26+import androidx.compose.material.icons.Icons27+import androidx.compose.material.icons.automirrored.rounded.ArrowBack28+import androidx.compose.material3.Icon2629 import androidx.compose.material3.Text2730 import androidx.compose.runtime.Composable2831 import androidx.compose.runtime.collectAsState@@ -41,7 +44,6 @@4144 import androidx.compose.ui.geometry.Size4245 import androidx.compose.ui.graphics.Color4346 import androidx.compose.ui.graphics.SolidColor44-import androidx.compose.ui.text.TextStyle4547 import androidx.compose.ui.text.font.FontStyle4648 import androidx.compose.ui.text.input.ImeAction4749 import androidx.compose.ui.text.input.KeyboardType@@ -49,18 +51,21 @@4951 import androidx.compose.ui.text.input.VisualTransformation5052 import androidx.compose.ui.text.style.TextAlign5153 import androidx.compose.ui.unit.dp52-import androidx.compose.ui.unit.em5354 import androidx.compose.ui.unit.sp5455 import androidx.hilt.navigation.compose.hiltViewModel5556 import androidx.lifecycle.ViewModel5657 import androidx.lifecycle.viewModelScope5758 import com.peachpasswords.ui.components.SecureFlagEffect59+import com.peachpasswords.ui.components.PeachEmptyOrEndState60+import com.peachpasswords.ui.components.PeachField61+import com.peachpasswords.ui.components.PeachTopBar5862 import com.peachpasswords.ui.lock.MIN_PASSWORD_LENGTH5963 import com.peachpasswords.ui.security.StrengthBar6064 import com.peachpasswords.ui.security.scorePassword6165 import com.peachpasswords.ui.security.strengthColor6266 import com.peachpasswords.ui.security.strengthLabel6367 import com.peachpasswords.ui.theme.LocalPeachAccent68+import com.peachpasswords.ui.theme.PeachSpacing6469 import com.peachpasswords.ui.theme.peachColors6570 import com.peachpasswords.ui.theme.PeachType6671 import com.peachpasswords.ui.theme.ShellBackground@@ -181,9 +186,24 @@181186             modifier = Modifier182187                 .fillMaxSize()183188                 .systemBarsPadding()184-                .padding(horizontal = 22.dp, vertical = 18.dp)189+                .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md)185190                 .verticalScroll(rememberScrollState()),186191         ) {192+            PeachTopBar(193+                title = "Recovery",194+                leading = {195+                    Icon(196+                        Icons.AutoMirrored.Rounded.ArrowBack,197+                        contentDescription = "back",198+                        tint = peachColors.T0,199+                        modifier = Modifier200+                            .size(22.dp)201+                            .clip(RoundedCornerShape(PeachSpacing.sm))202+                            .clickable(onClick = onBack),203+                    )204+                },205+            )206+            Spacer(Modifier.height(PeachSpacing.lg))187207             when (state.step) {188208                 RecoveryStep.WORDS -> WordStep(state, vm, onBack)189209                 RecoveryStep.PASSWORD -> PasswordStep(state, vm, onSuccess)@@ -208,14 +228,14 @@208228     }209229 210230     Eyebrow("VAULT RECOVERY")211-    Spacer(Modifier.height(10.dp))231+    Spacer(Modifier.height(PeachSpacing.sm))212232     Headline("enter your\nrecovery words.")213-    Spacer(Modifier.height(18.dp))233+    Spacer(Modifier.height(PeachSpacing.lg))214234 215235     GlassPanel {216236         // 3 columns × 4 rows.217237         for (row in 0 until 4) {218-            Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {238+            Row(Modifier.fillMaxWidth().padding(vertical = 6.dp), horizontalArrangement = Arrangement.spacedBy(PeachSpacing.sm)) {219239                 for (col in 0 until 3) {220240                     val i = row * 3 + col221241                     WordSlot(@@ -237,7 +257,7 @@237257         }238258 239259         if (state.wordsError != null) {240-            Spacer(Modifier.height(10.dp))260+            Spacer(Modifier.height(PeachSpacing.sm))241261             Text(242262                 state.wordsError,243263                 color = peachColors.Breach,@@ -246,9 +266,9 @@246266         }247267     }248268 249-    Spacer(Modifier.height(18.dp))269+    Spacer(Modifier.height(PeachSpacing.lg))250270     AccentButton("Continue", enabled = state.checksumValid, onClick = vm::continueToPassword)251-    Spacer(Modifier.height(10.dp))271+    Spacer(Modifier.height(PeachSpacing.sm))252272     GhostButton("Back to login", peachColors.T1, onBack)253273 }254274 @@ -281,7 +301,7 @@281301                 textAlign = TextAlign.End,282302                 modifier = Modifier.width(20.dp),283303             )284-            Spacer(Modifier.width(4.dp))304+            Spacer(Modifier.width(PeachSpacing.xs))285305             BasicTextField(286306                 value = value,287307                 onValueChange = onValueChange,@@ -342,25 +362,31 @@342362     val canSubmit = score >= 2 && password.isNotEmpty() && password == confirm && !state.working343363 344364     Eyebrow("VAULT RECOVERY")345-    Spacer(Modifier.height(10.dp))365+    Spacer(Modifier.height(PeachSpacing.sm))346366     Headline("set a new\npassword.")347-    Spacer(Modifier.height(18.dp))367+    Spacer(Modifier.height(PeachSpacing.lg))348368 349369     GlassPanel {350-        FieldLabel("new password")351-        Spacer(Modifier.height(4.dp))352-        PasswordField(370+        PeachField(371+            label = "new password",353372             value = password,354-            onChange = { password = it },355-            reveal = reveal,356-            onToggleReveal = { reveal = !reveal },357-            imeAction = ImeAction.Next,373+            onValueChange = { password = it },374+            keyboardType = KeyboardType.Password,375+            visualTransformation = if (reveal) VisualTransformation.None else PasswordVisualTransformation(),376+            trailing = {377+                Text(378+                    if (reveal) "hide" else "show",379+                    color = LocalPeachAccent.current,380+                    style = PeachType.labelMono,381+                    modifier = Modifier.clickable(onClick = { reveal = !reveal }).padding(start = PeachSpacing.sm, top = 6.dp, bottom = 6.dp),382+                )383+            },358384         )359385 360386         if (password.isNotEmpty()) {361-            Spacer(Modifier.height(14.dp))387+            Spacer(Modifier.height(PeachSpacing.md))362388             StrengthBar(score = score)363-            Spacer(Modifier.height(6.dp))389+            Spacer(Modifier.height(PeachSpacing.xs))364390             Text(365391                 "strength: ${strengthLabel(score)}",366392                 color = strengthColor(score),@@ -370,15 +396,13 @@370396             )371397         }372398 373-        Spacer(Modifier.height(16.dp))374-        FieldLabel("confirm password")375-        Spacer(Modifier.height(4.dp))376-        PasswordField(399+        Spacer(Modifier.height(PeachSpacing.md))400+        PeachField(401+            label = "confirm password",377402             value = confirm,378-            onChange = { confirm = it },379-            reveal = reveal,380-            onToggleReveal = { reveal = !reveal },381-            imeAction = ImeAction.Done,403+            onValueChange = { confirm = it },404+            keyboardType = KeyboardType.Password,405+            visualTransformation = if (reveal) VisualTransformation.None else PasswordVisualTransformation(),382406         )383407 384408         Text(@@ -390,7 +414,7 @@390414         )391415     }392416 393-    Spacer(Modifier.height(18.dp))417+    Spacer(Modifier.height(PeachSpacing.lg))394418     AccentButton(395419         label = if (state.working) "recovering…" else "Recover vault",396420         enabled = canSubmit,@@ -400,25 +424,16 @@400424 401425 @Composable402426 private fun NoEscrowStep(onRestore: () -> Unit, onBack: () -> Unit) {403-    GlassPanel {404-        Text("⊘", color = peachColors.T2, style = TextStyle(fontFamily = PeachType.Display, fontSize = 40.sp))405-        Spacer(Modifier.height(12.dp))406-        Text(407-            "recovery not available",408-            color = peachColors.T1,409-            style = TextStyle(fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 26.sp),410-        )411-        Spacer(Modifier.height(10.dp))412-        Text(413-            "Recovery words were not set up for this vault. You can restore from a backup instead.",414-            color = peachColors.T2,415-            style = PeachType.labelMono.copy(fontSize = 10.sp),416-        )417-        Spacer(Modifier.height(18.dp))418-        GhostButton("Restore from backup", LocalPeachAccent.current, onRestore)419-        Spacer(Modifier.height(8.dp))420-        GhostButton("Back to login", peachColors.T1, onBack)421-    }427+    PeachEmptyOrEndState(428+        message = "recovery not available",429+        detail = "Recovery words were not set up for this vault. You can restore from a backup instead.",430+        action = {431+            Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm)) {432+                GhostButton("Restore from backup", LocalPeachAccent.current, onRestore)433+                GhostButton("Back to login", peachColors.T1, onBack)434+            }435+        },436+    )422437 }423438 424439 // ── Shared local primitives (match LockScreen / RecoveryWordsScreen idiom) ──@@ -436,52 +451,6 @@436451     )437452 438453 @Composable439-private fun FieldLabel(text: String) =440-    Text(text.uppercase(), color = peachColors.T2, style = PeachType.labelMono.copy(letterSpacing = 0.08.em))441-442-@Composable443-private fun PasswordField(444-    value: String,445-    onChange: (String) -> Unit,446-    reveal: Boolean,447-    onToggleReveal: () -> Unit,448-    imeAction: ImeAction,449-) {450-    val accent = LocalPeachAccent.current451-    val interaction = remember { MutableInteractionSource() }452-    val focused by interaction.collectIsFocusedAsState()453-    val hairline = peachColors.T3454-    Row(verticalAlignment = Alignment.CenterVertically) {455-        BasicTextField(456-            value = value,457-            onValueChange = onChange,458-            singleLine = true,459-            textStyle = PeachType.unlockInput.copy(color = peachColors.T0),460-            cursorBrush = SolidColor(accent),461-            visualTransformation = if (reveal) VisualTransformation.None else PasswordVisualTransformation(),462-            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password, imeAction = imeAction),463-            interactionSource = interaction,464-            modifier = Modifier465-                .weight(1f)466-                .drawBehind {467-                    drawRect(468-                        color = if (focused) accent else hairline,469-                        topLeft = Offset(0f, size.height - 1.dp.toPx()),470-                        size = Size(size.width, 1.dp.toPx()),471-                    )472-                }473-                .padding(vertical = 8.dp),474-        )475-        Text(476-            if (reveal) "hide" else "show",477-            color = accent,478-            style = PeachType.labelMono,479-            modifier = Modifier.clickable(onClick = onToggleReveal).padding(start = 8.dp, top = 6.dp, bottom = 6.dp),480-        )481-    }482-}483-484-@Composable485454 private fun GlassPanel(content: @Composable androidx.compose.foundation.layout.ColumnScope.() -> Unit) {486455     Column(487456         modifier = Modifier@@ -489,7 +458,7 @@489458             .clip(RoundedCornerShape(18.dp))490459             .background(Color(0xE0131109))491460             .border(1.dp, peachColors.T3, RoundedCornerShape(18.dp))492-            .padding(20.dp),461+            .padding(PeachSpacing.lg),493462         content = content,494463     )495464 }@@ -503,7 +472,7 @@503472             .clip(RoundedCornerShape(12.dp))504473             .background(if (enabled) accent else accent.copy(alpha = 0.3f))505474             .clickable(enabled = enabled, onClick = onClick)506-            .padding(vertical = 14.dp),475+            .padding(vertical = PeachSpacing.md),507476         contentAlignment = Alignment.Center,508477     ) {509478         Text(label, color = Color(0xFF0C0A06), style = PeachType.bodyMono)@@ -518,7 +487,7 @@518487             .clip(RoundedCornerShape(12.dp))519488             .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(12.dp))520489             .clickable(onClick = onClick)521-            .padding(vertical = 12.dp),490+            .padding(vertical = PeachSpacing.md),522491         contentAlignment = Alignment.Center,523492     ) {524493         Text(label, color = color, style = PeachType.bodyMono)

view file

app/src/main/java/com/peachpasswords/ui/settings/PanicScreen.kt+57 −44
@@ -6,16 +6,17 @@66 import androidx.compose.foundation.layout.Arrangement77 import androidx.compose.foundation.layout.Box88 import androidx.compose.foundation.layout.Column9-import androidx.compose.foundation.layout.Row109 import androidx.compose.foundation.layout.Spacer1110 import androidx.compose.foundation.layout.fillMaxSize1211 import androidx.compose.foundation.layout.fillMaxWidth12+import androidx.compose.foundation.layout.height1313 import androidx.compose.foundation.layout.padding1414 import androidx.compose.foundation.layout.size1515 import androidx.compose.foundation.layout.systemBarsPadding16-import androidx.compose.foundation.layout.width1716 import androidx.compose.foundation.shape.RoundedCornerShape18-import androidx.compose.foundation.text.BasicTextField17+import androidx.compose.material.icons.Icons18+import androidx.compose.material.icons.automirrored.rounded.ArrowBack19+import androidx.compose.material3.Icon1920 import androidx.compose.material3.Text2021 import androidx.compose.runtime.Composable2122 import androidx.compose.runtime.CompositionLocalProvider@@ -27,18 +28,18 @@2728 import androidx.compose.ui.Modifier2829 import androidx.compose.ui.draw.clip2930 import androidx.compose.ui.graphics.Color30-import androidx.compose.ui.graphics.SolidColor3131 import androidx.compose.ui.text.TextStyle3232 import androidx.compose.ui.text.font.FontStyle33-import androidx.compose.ui.text.input.TextFieldValue34-import androidx.compose.ui.unit.em3533 import androidx.compose.ui.unit.sp3634 import androidx.compose.ui.unit.dp3735 import androidx.hilt.navigation.compose.hiltViewModel3836 import androidx.lifecycle.ViewModel3937 import androidx.lifecycle.viewModelScope38+import com.peachpasswords.ui.components.PeachField39+import com.peachpasswords.ui.components.PeachTopBar4040 import com.peachpasswords.ui.theme.LocalPeachAccent4141 import com.peachpasswords.ui.theme.LocalPeachColors42+import com.peachpasswords.ui.theme.PeachSpacing4243 import com.peachpasswords.ui.theme.peachColors4344 import com.peachpasswords.ui.theme.peachDark4445 import com.peachpasswords.ui.theme.PeachType@@ -75,13 +76,33 @@7576             modifier = Modifier7677                 .fillMaxSize()7778                 .systemBarsPadding()78-                .padding(horizontal = 28.dp, vertical = 32.dp),79-            verticalArrangement = Arrangement.Center,79+                .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.md),8080         ) {81-            when (step) {82-                1 -> Step1(onCancel = onBack, onContinue = { step = 2 })83-                2 -> Step2(entryCount = entryCount, onBack = { step = 1 }, onContinue = { step = 3 })84-                else -> Step3(onDestroy = { vm.destroy(onDestroyed) })81+            PeachTopBar(82+                title = "Panic",83+                leading = {84+                    Icon(85+                        Icons.AutoMirrored.Rounded.ArrowBack,86+                        contentDescription = "back",87+                        tint = peachColors.T0,88+                        modifier = Modifier89+                            .size(22.dp)90+                            .clip(RoundedCornerShape(PeachSpacing.sm))91+                            .clickable(onClick = onBack),92+                    )93+                },94+            )95+            Box(96+                modifier = Modifier.fillMaxSize().padding(horizontal = PeachSpacing.sm),97+                contentAlignment = Alignment.Center,98+            ) {99+                Column(verticalArrangement = Arrangement.Center) {100+                    when (step) {101+                        1 -> Step1(onCancel = onBack, onContinue = { step = 2 })102+                        2 -> Step2(entryCount = entryCount, onBack = { step = 1 }, onContinue = { step = 3 })103+                        else -> Step3(onDestroy = { vm.destroy(onDestroyed) })104+                    }105+                }85106             }86107         }87108     }@@ -91,16 +112,18 @@91112 @Composable92113 private fun Step1(onCancel: () -> Unit, onContinue: () -> Unit) {93114     Headline("destroy vault", 44.sp, peachColors.Breach)94-    Spacer(Modifier.size(20.dp))115+    Spacer(Modifier.height(PeachSpacing.lg))95116     Text(96117         "This will permanently delete your vault and all saved passwords from this device. This cannot be undone.",97118         color = peachColors.T1,98119         style = PeachType.body,99120     )100-    Spacer(Modifier.size(28.dp))101-    Row(modifier = Modifier.fillMaxWidth()) {121+    Spacer(Modifier.height(PeachSpacing.xl))122+    Column(123+        modifier = Modifier.fillMaxWidth(),124+        verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),125+    ) {102126         GhostButton("Cancel", peachColors.T1, onCancel)103-        Spacer(Modifier.width(12.dp))104127         DangerButton("I understand, continue", enabled = true, onClick = onContinue)105128     }106129 }@@ -108,49 +131,35 @@108131 @Composable109132 private fun Step2(entryCount: Int, onBack: () -> Unit, onContinue: () -> Unit) {110133     Headline("are you sure?", 32.sp, peachColors.Breach)111-    Spacer(Modifier.size(18.dp))134+    Spacer(Modifier.height(PeachSpacing.lg))112135     Text(113136         "You will lose access to all $entryCount saved passwords.",114137         color = peachColors.T1,115138         style = PeachType.body,116139     )117-    Spacer(Modifier.size(28.dp))118-    Row(modifier = Modifier.fillMaxWidth()) {140+    Spacer(Modifier.height(PeachSpacing.xl))141+    Column(142+        modifier = Modifier.fillMaxWidth(),143+        verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm),144+    ) {119145         GhostButton("Go back", peachColors.T1, onBack)120-        Spacer(Modifier.width(12.dp))121146         DangerButton("Yes, destroy it", enabled = true, onClick = onContinue)122147     }123148 }124149 125150 @Composable126151 private fun Step3(onDestroy: () -> Unit) {127-    var typed by remember { mutableStateOf(TextFieldValue("")) }128-    val accent = LocalPeachAccent.current129-    val confirmed = typed.text == "DESTROY"152+    var typed by remember { mutableStateOf("") }153+    val confirmed = typed == "DESTROY"130154 131155     Headline("type DESTROY to confirm", 28.sp, peachColors.T0)132-    Spacer(Modifier.size(22.dp))133-    BasicTextField(156+    Spacer(Modifier.height(PeachSpacing.lg))157+    PeachField(158+        label = "confirmation",134159         value = typed,135160         onValueChange = { typed = it },136-        singleLine = true,137-        textStyle = TextStyle(138-            fontFamily = PeachType.Mono,139-            fontSize = 18.sp,140-            letterSpacing = 0.18.em,141-            color = peachColors.T0,142-        ),143-        cursorBrush = SolidColor(accent),144-        modifier = Modifier.fillMaxWidth(),145161     )146-    Spacer(Modifier.size(6.dp))147-    Box(148-        modifier = Modifier149-            .fillMaxWidth()150-            .size(1.dp)151-            .background(peachColors.T3),152-    )153-    Spacer(Modifier.size(28.dp))162+    Spacer(Modifier.height(PeachSpacing.xl))154163     DangerButton("Destroy vault", enabled = confirmed, onClick = onDestroy)155164 }156165 @@ -171,10 +180,12 @@171180 private fun GhostButton(label: String, color: Color, onClick: () -> Unit) {172181     Box(173182         modifier = Modifier183+            .fillMaxWidth()174184             .clip(RoundedCornerShape(999.dp))175185             .border(1.dp, color.copy(alpha = 0.4f), RoundedCornerShape(999.dp))176186             .clickable(onClick = onClick)177-            .padding(horizontal = 18.dp, vertical = 11.dp),187+            .padding(horizontal = PeachSpacing.lg, vertical = 11.dp),188+        contentAlignment = Alignment.Center,178189     ) {179190         Text(label, color = color, style = PeachType.sortPill.copy(fontSize = 11.sp))180191     }@@ -185,11 +196,13 @@185196     val alpha = if (enabled) 1f else 0.4f186197     Box(187198         modifier = Modifier199+            .fillMaxWidth()188200             .clip(RoundedCornerShape(999.dp))189201             .background(peachColors.BreachBg.copy(alpha = peachColors.BreachBg.alpha * alpha))190202             .border(1.dp, peachColors.Breach.copy(alpha = 0.55f * alpha), RoundedCornerShape(999.dp))191203             .clickable(enabled = enabled, onClick = onClick)192-            .padding(horizontal = 18.dp, vertical = 11.dp),204+            .padding(horizontal = PeachSpacing.lg, vertical = 11.dp),205+        contentAlignment = Alignment.Center,193206     ) {194207         Text(195208             label,

view file

app/src/main/java/com/peachpasswords/ui/settings/SettingsCardBodies.kt+24 −26
@@ -54,8 +54,11 @@5454 import androidx.compose.material3.Text5555 import com.peachpasswords.BuildConfig5656 import com.peachpasswords.ui.PeachNav57+import com.peachpasswords.ui.components.PeachEmptyOrEndState58+import com.peachpasswords.ui.components.PeachField5759 import com.peachpasswords.ui.sync.SyncViewModel5860 import com.peachpasswords.ui.theme.ACCENT_COLORS61+import com.peachpasswords.ui.theme.PeachSpacing5962 import com.peachpasswords.ui.theme.PeachType6063 import androidx.hilt.navigation.compose.hiltViewModel6164 import com.peachpasswords.vault.VaultSettings@@ -291,7 +294,14 @@291294     }292295     SubLabel("RECOVERY", fg)293296     RowLine(fg) {294-        Name("View recovery words", fg)297+        Column(Modifier.weight(1f)) {298+            Text("View recovery words", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)299+            Text(300+                "Keep the paper copy current before rotating sync identity or replacing devices.",301+                color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,302+                modifier = Modifier.padding(top = 2.dp),303+            )304+        }295305         SubtleButton("view", fg) { onNavigate(PeachNav.RecoveryView) }296306     }297307     SubLabel("BREACH PROTECTION", fg)@@ -345,7 +355,7 @@345355         SubtleButton(if (open) "cancel" else "change", fg) { open = !open; error = null; done = false }346356     }347357     AnimatedVisibility(open) {348-        Column(Modifier.fillMaxWidth().padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {358+        Column(Modifier.fillMaxWidth().padding(top = PeachSpacing.sm), verticalArrangement = Arrangement.spacedBy(PeachSpacing.sm)) {349359             PwField("current password", current, fg) { current = it }350360             PwField("new password (min 12 chars)", next, fg) { next = it }351361             PwField("confirm new password", confirm, fg) { confirm = it }@@ -370,26 +380,14 @@370380 }371381 372382 @Composable373-private fun PwField(placeholder: String, value: String, fg: Color, onChange: (String) -> Unit) {374-    Box(375-        Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp))376-            .background(Color(0x2E000000))377-            .border(1.dp, fg.copy(alpha = 0.22f), RoundedCornerShape(8.dp))378-            .padding(horizontal = 10.dp, vertical = 10.dp),379-    ) {380-        if (value.isEmpty()) {381-            Text(placeholder, color = fg.copy(alpha = 0.42f), fontFamily = PeachType.Mono, fontSize = 12.sp)382-        }383-        BasicTextField(384-            value = value,385-            onValueChange = onChange,386-            singleLine = true,387-            textStyle = TextStyle(color = fg, fontFamily = PeachType.Mono, fontSize = 12.sp),388-            cursorBrush = SolidColor(fg),389-            visualTransformation = PasswordVisualTransformation(),390-            modifier = Modifier.fillMaxWidth(),391-        )392-    }383+private fun PwField(placeholder: String, value: String, @Suppress("UNUSED_PARAMETER") fg: Color, onChange: (String) -> Unit) {384+    PeachField(385+        label = placeholder,386+        value = value,387+        onValueChange = onChange,388+        keyboardType = androidx.compose.ui.text.input.KeyboardType.Password,389+        visualTransformation = PasswordVisualTransformation(),390+    )393391 }394392 395393 /* ────────────────────────────────  Vault  ───────────────────────────────── */@@ -614,10 +612,10 @@614612 615613     SubLabel("PAIRED DEVICES", fg)616614     if (pairedDevices.isEmpty()) {617-        Text(618-            "No paired devices yet.",619-            color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 12.sp,620-            modifier = Modifier.padding(top = 2.dp),615+        PeachEmptyOrEndState(616+            message = "No paired devices yet.",617+            detail = "Pair a nearby phone, browser, or desktop helper when you're ready to sync.",618+            modifier = Modifier.padding(top = PeachSpacing.xs),621619         )622620     } else {623621         pairedDevices.forEach { d ->

view file