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
↓ peach-android-605e3de6.tar.gzatlas at this save
files changed5 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 −26app/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 {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 }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)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,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 ->