bc64fde8cf75
feat: Recompose Codex surfaces with design primitives (v2.3.66)
- time
- 2026-07-08 21:52 UTC · 3 months ago
- machine
- koh2
- parent
- 605e3de62e10
↓ peach-android-bc64fde8.tar.gzatlas at this save
files changed4 files
app/build.gradle+2 −2app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt+70 −80app/src/main/java/com/peachpasswords/ui/codex/CodexRestoreChooseAndCamera.kt+23 −59app/src/main/java/com/peachpasswords/ui/codex/CodexRestoreScreen.kt+36 −30app/build.gradle+2 −2
@@ -16,8 +16,8 @@1616 applicationId "com.peachpasswords"1717 minSdk 261818 targetSdk 3519- versionCode 23650020- versionName "2.3.65"19+ versionCode 23660020+ versionName "2.3.66"2121 2222 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"2323 vectorDrawables {app/src/main/java/com/peachpasswords/ui/codex/CodexGenerateScreen.kt+70 −80
@@ -14,10 +14,7 @@1414 import androidx.compose.foundation.layout.fillMaxWidth1515 import androidx.compose.foundation.layout.height1616 import androidx.compose.foundation.layout.padding17-import androidx.compose.foundation.layout.size18-import androidx.compose.foundation.shape.CircleShape1917 import androidx.compose.foundation.shape.RoundedCornerShape20-import androidx.compose.foundation.text.BasicTextField2118 import androidx.compose.material3.LinearProgressIndicator2219 import androidx.compose.material3.Text2320 import androidx.compose.runtime.Composable@@ -29,7 +26,6 @@2926 import androidx.compose.ui.Modifier3027 import androidx.compose.ui.draw.clip3128 import androidx.compose.ui.graphics.Color32-import androidx.compose.ui.graphics.SolidColor3329 import androidx.compose.ui.platform.LocalContext3430 import androidx.compose.ui.text.font.FontStyle3531 import androidx.compose.ui.unit.dp@@ -37,11 +33,15 @@3733 import androidx.hilt.navigation.compose.hiltViewModel3834 import androidx.lifecycle.compose.collectAsStateWithLifecycle3935 import com.peachpasswords.codex.CodexStep36+import com.peachpasswords.ui.components.PeachEmptyOrEndState37+import com.peachpasswords.ui.components.PeachField38+import com.peachpasswords.ui.components.PeachTopBar4039 import com.peachpasswords.ui.components.SecureFlagEffect4140 import com.peachpasswords.ui.theme.LocalPeachAccent42-import com.peachpasswords.ui.theme.peachColors41+import com.peachpasswords.ui.theme.PeachSpacing4342 import com.peachpasswords.ui.theme.PeachType4443 import com.peachpasswords.ui.theme.ShellBackground44+import com.peachpasswords.ui.theme.peachColors4545 import java.time.LocalDate4646 4747 @Composable@@ -61,63 +61,64 @@6161 SecureFlagEffect() // MN-3: never screenshot the codex generation / verify code6262 6363 ShellBackground {64- Column(Modifier.fillMaxSize().padding(horizontal = 24.dp)) {65- CodexHeader("Codex backup", onBack)66- Spacer(Modifier.height(8.dp))64+ Column(Modifier.fillMaxSize().padding(horizontal = PeachSpacing.xl)) {65+ Spacer(Modifier.height(PeachSpacing.md))66+ CodexTopBar("Codex backup", onBack)67+ Spacer(Modifier.height(PeachSpacing.sm))6768 Text(6869 "A printable PDF of Aztec codes that encodes your encrypted vault. Keep it with your recovery words — together they restore everything, even offline.",6970 color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,7071 )71- Spacer(Modifier.height(28.dp))72+ Spacer(Modifier.height(PeachSpacing.xxl))7273 7374 when (val s = state) {7475 is GenerateState.Idle -> {7576 when (codexGenerateGate(vm.isPro, vm.recoveryWordsSet)) {7677 CodexGenerateGate.PRO_REQUIRED -> {77- Text("Codex generation is a Pro feature", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)78- Spacer(Modifier.height(6.dp))79- Text(80- "Restoring from an existing Codex is always free. Creating a new printable Codex backup requires Peach Pro.",81- color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,78+ PeachEmptyOrEndState(79+ message = "Codex generation is a Pro feature",80+ detail = "Restoring from an existing Codex is always free. Creating a new printable Codex backup requires Peach Pro.",81+ action = {82+ CodexButton("Open Pro", accent, filled = true, onClick = onUpgradePro)83+ },8284 )83- Spacer(Modifier.height(20.dp))84- CodexButton("Open Pro", accent, filled = true, onClick = onUpgradePro)8585 }8686 CodexGenerateGate.RECOVERY_REQUIRED -> {87- Text("Set up recovery words first", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)88- Spacer(Modifier.height(6.dp))89- Text(90- "A Codex backup only restores alongside your recovery words. Set them up before generating one.",91- color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,92- )93- Spacer(Modifier.height(20.dp))94- CodexButton("Set up recovery words", accent, filled = true, onClick = onSetupRecovery)87+ PeachEmptyOrEndState(88+ message = "Set up recovery words first",89+ detail = "A Codex backup only restores alongside your recovery words. Set them up before generating one.",90+ action = {91+ CodexButton("Set up recovery words", accent, filled = true, onClick = onSetupRecovery)92+ },93+ )9594 }9695 CodexGenerateGate.READY -> {97- var w3 by remember { mutableStateOf("") }98- var w7 by remember { mutableStateOf("") }99- var w11 by remember { mutableStateOf("") }100- val ok = vm.gateMatches(w3, w7, w11)101- Text("Confirm your recovery words", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)102- Spacer(Modifier.height(6.dp))103- Text("Enter the words at positions 3, 7, and 11.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)104- Spacer(Modifier.height(16.dp))105- GateField("Word #3", w3, accent) { w3 = it }106- GateField("Word #7", w7, accent) { w7 = it }107- GateField("Word #11", w11, accent) { w11 = it }108- Spacer(Modifier.height(20.dp))109- CodexButton(110- "Generate Codex PDF",111- if (ok) accent else peachColors.T2,112- filled = ok,113- ) { if (ok) vm.startGenerate(System.currentTimeMillis()) }96+ var w3 by remember { mutableStateOf("") }97+ var w7 by remember { mutableStateOf("") }98+ var w11 by remember { mutableStateOf("") }99+ val ok = vm.gateMatches(w3, w7, w11)100+ Text("Confirm your recovery words", color = peachColors.T1, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)101+ Spacer(Modifier.height(PeachSpacing.xs))102+ Text("Enter the words at positions 3, 7, and 11.", color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)103+ Spacer(Modifier.height(PeachSpacing.lg))104+ PeachField("Word #3", w3, { w3 = it })105+ Spacer(Modifier.height(PeachSpacing.md))106+ PeachField("Word #7", w7, { w7 = it })107+ Spacer(Modifier.height(PeachSpacing.md))108+ PeachField("Word #11", w11, { w11 = it })109+ Spacer(Modifier.height(PeachSpacing.xl))110+ CodexButton(111+ "Generate Codex PDF",112+ if (ok) accent else peachColors.T2,113+ filled = ok,114+ ) { if (ok) vm.startGenerate(System.currentTimeMillis()) }114115 }115116 }116117 }117118 118119 is GenerateState.Running -> Column {119120 Text(stepLabel(s.step), color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 11.sp)120- Spacer(Modifier.height(10.dp))121+ Spacer(Modifier.height(PeachSpacing.sm))121122 val frac = if (s.total > 0) s.current.toFloat() / s.total else 0f122123 LinearProgressIndicator(123124 progress = { if (s.step == CodexStep.AZTEC) frac else 0.5f },@@ -125,7 +126,7 @@125126 color = accent, trackColor = peachColors.S2,126127 )127128 if (s.step == CodexStep.AZTEC) {128- Spacer(Modifier.height(6.dp))129+ Spacer(Modifier.height(PeachSpacing.xs))129130 Text("${s.current} / ${s.total} codes", color = peachColors.T3, fontFamily = PeachType.Mono, fontSize = 9.sp)130131 }131132 }@@ -135,9 +136,9 @@135136 SummaryRow("entries", s.manifest.entryCount.toString(), accent)136137 SummaryRow("codes", "${s.manifest.totalChunks} (${s.manifest.parityChunks} recovery)", accent)137138 SummaryRow("pages", s.manifest.pageCount.toString(), accent)138- Spacer(Modifier.height(24.dp))139+ Spacer(Modifier.height(PeachSpacing.xl))139140 CodexButton("Save PDF…", accent, filled = true) { saveLauncher.launch(codexPdfFileName()) }140- Spacer(Modifier.height(10.dp))141+ Spacer(Modifier.height(PeachSpacing.sm))141142 CodexButton("Share PDF…", peachColors.T2, filled = false) {142143 val uri = vm.sharePdfUri() ?: return@CodexButton143144 val intent = Intent(Intent.ACTION_SEND).apply {@@ -147,17 +148,17 @@147148 }148149 context.startActivity(Intent.createChooser(intent, "Share Codex PDF"))149150 }150- Spacer(Modifier.height(10.dp))151+ Spacer(Modifier.height(PeachSpacing.sm))151152 CodexButton("Generate again", peachColors.T2, filled = false) { vm.startGenerate(System.currentTimeMillis()) }152153 }153154 154- is GenerateState.Error -> Column {155- Text("Couldn't generate", color = peachColors.Breach, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)156- Spacer(Modifier.height(6.dp))157- Text(s.message, color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)158- Spacer(Modifier.height(20.dp))159- CodexButton("Try again", accent, filled = true) { vm.startGenerate(System.currentTimeMillis()) }160- }155+ is GenerateState.Error -> PeachEmptyOrEndState(156+ message = "Couldn't generate",157+ detail = s.message,158+ action = {159+ CodexButton("Try again", accent, filled = true) { vm.startGenerate(System.currentTimeMillis()) }160+ },161+ )161162 }162163 }163164 }@@ -183,39 +184,28 @@183184 }184185 185186 @Composable186-internal fun CodexHeader(title: String, onBack: () -> Unit) {187- Row(188- modifier = Modifier.fillMaxWidth().padding(top = 12.dp).height(48.dp),189- verticalAlignment = Alignment.CenterVertically,190- ) {191- Text("‹", color = peachColors.T1, fontSize = 26.sp,192- modifier = Modifier.clip(CircleShape).clickable(onClick = onBack).padding(horizontal = 8.dp))193- Spacer(Modifier.size(8.dp))194- Text(title, color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)195- }187+internal fun CodexTopBar(title: String, onBack: () -> Unit) {188+ PeachTopBar(189+ title = title,190+ leading = {191+ Text(192+ "‹",193+ color = peachColors.T1,194+ fontSize = 26.sp,195+ modifier = Modifier196+ .clip(RoundedCornerShape(999.dp))197+ .clickable(onClick = onBack)198+ .padding(horizontal = PeachSpacing.sm),199+ )200+ },201+ )196202 }197203 198204 @Composable199205 private fun SummaryRow(label: String, value: String, accent: Color) {200- Row(Modifier.fillMaxWidth().padding(vertical = 7.dp), horizontalArrangement = Arrangement.SpaceBetween) {206+ Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {201207 Text(label, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)202208 Text(value, color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)203- }204-}205-206-@Composable207-private fun GateField(label: String, value: String, accent: Color, onChange: (String) -> Unit) {208- Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {209- Text(label, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)210- BasicTextField(211- value = value,212- onValueChange = { onChange(it) },213- singleLine = true,214- textStyle = PeachType.bodyMono.copy(color = peachColors.T0),215- cursorBrush = SolidColor(accent),216- modifier = Modifier.fillMaxWidth().padding(top = 4.dp),217- )218- Box(Modifier.fillMaxWidth().padding(top = 4.dp).height(1.dp).background(peachColors.T3))219209 }220210 }221211 app/src/main/java/com/peachpasswords/ui/codex/CodexRestoreChooseAndCamera.kt+23 −59
@@ -43,7 +43,6 @@4343 import androidx.compose.foundation.shape.RoundedCornerShape4444 import androidx.compose.material.icons.Icons4545 import androidx.compose.material.icons.outlined.Description46-import androidx.compose.material.icons.outlined.NoPhotography4746 import androidx.compose.material.icons.outlined.PhotoCamera4847 import androidx.compose.material3.Icon4948 import androidx.compose.material3.Text@@ -68,6 +67,8 @@6867 import androidx.compose.ui.viewinterop.AndroidView6968 import androidx.core.content.ContextCompat7069 import androidx.lifecycle.compose.LocalLifecycleOwner70+import com.peachpasswords.ui.components.PeachEmptyOrEndState71+import com.peachpasswords.ui.theme.PeachSpacing7172 import com.peachpasswords.ui.theme.peachColors7273 import com.peachpasswords.ui.theme.PeachType7374 import java.util.concurrent.Executors@@ -85,13 +86,12 @@8586 onScanCamera: () -> Unit,8687 onImportPdf: () -> Unit,8788 ) {88- Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp)) {89+ Column(Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(PeachSpacing.md)) {8990 ChooseCard(9091 icon = Icons.Outlined.PhotoCamera,9192 iconTint = accent,9293 title = "Scan with camera",9394 body = "Point your camera at the printed Aztec grid",94- accent = accent,9595 onClick = onScanCamera,9696 )9797 ChooseCard(@@ -99,7 +99,6 @@9999 iconTint = peachColors.T2,100100 title = "Import PDF",101101 body = "Import the PDF you saved when generating your Codex",102- accent = accent,103102 onClick = onImportPdf,104103 )105104 }@@ -111,7 +110,6 @@111110 iconTint: Color,112111 title: String,113112 body: String,114- accent: Color,115113 onClick: () -> Unit,116114 ) {117115 Row(@@ -121,9 +119,9 @@121119 .background(peachColors.SurfaceElevated)122120 .border(1.dp, peachColors.T3, RoundedCornerShape(18.dp))123121 .clickable(onClick = onClick)124- .padding(horizontal = 18.dp, vertical = 18.dp),122+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.lg),125123 verticalAlignment = Alignment.CenterVertically,126- horizontalArrangement = Arrangement.spacedBy(16.dp),124+ horizontalArrangement = Arrangement.spacedBy(PeachSpacing.lg),127125 ) {128126 Icon(icon, contentDescription = null, tint = iconTint, modifier = Modifier.height(24.dp))129127 Column(Modifier.fillMaxWidth()) {@@ -134,7 +132,7 @@134132 fontStyle = FontStyle.Italic,135133 fontSize = 19.sp,136134 )137- Spacer(Modifier.height(4.dp))135+ Spacer(Modifier.height(PeachSpacing.xs))138136 Text(body, color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)139137 }140138 }@@ -269,7 +267,7 @@269267 Box(270268 Modifier271269 .fillMaxSize()272- .padding(10.dp)270+ .padding(PeachSpacing.sm)273271 .drawBehind {274272 val arm = 20.dp.toPx()275273 val w = 2.dp.toPx()@@ -291,7 +289,7 @@291289 Modifier292290 .fillMaxSize()293291 .background(Color(0xCC0A0703))294- .padding(24.dp),292+ .padding(PeachSpacing.xl),295293 verticalArrangement = Arrangement.Center,296294 horizontalAlignment = Alignment.CenterHorizontally,297295 ) {@@ -301,13 +299,13 @@301299 fontFamily = PeachType.Mono,302300 fontSize = 11.sp,303301 )304- Spacer(Modifier.height(16.dp))302+ Spacer(Modifier.height(PeachSpacing.lg))305303 Box(306304 Modifier307305 .clip(RoundedCornerShape(8.dp))308306 .border(BorderStroke(1.dp, accent), RoundedCornerShape(8.dp))309307 .clickable(onClick = onRetry)310- .padding(horizontal = 18.dp, vertical = 10.dp),308+ .padding(horizontal = PeachSpacing.lg, vertical = PeachSpacing.sm),311309 ) {312310 Text("Try again", color = accent, fontFamily = PeachType.Mono, fontSize = 11.sp)313311 }@@ -317,51 +315,17 @@317315 @Composable318316 private fun CameraDeniedCard(accent: Color) {319317 val context = LocalContext.current320- Column(321- Modifier322- .fillMaxWidth()323- .clip(RoundedCornerShape(18.dp))324- .background(peachColors.SurfaceElevated)325- .border(1.dp, peachColors.T3, RoundedCornerShape(18.dp))326- .padding(20.dp),327- horizontalAlignment = Alignment.CenterHorizontally,328- ) {329- Icon(330- Icons.Outlined.NoPhotography,331- contentDescription = null,332- tint = peachColors.T2,333- modifier = Modifier.height(24.dp),334- )335- Spacer(Modifier.height(12.dp))336- Text(337- "Camera access needed",338- color = peachColors.T0,339- fontFamily = PeachType.Display,340- fontStyle = FontStyle.Italic,341- fontSize = 19.sp,342- )343- Spacer(Modifier.height(6.dp))344- Text(345- "Grant camera permission in Settings to scan your Codex",346- color = peachColors.T2,347- fontFamily = PeachType.Mono,348- fontSize = 10.sp,349- )350- Spacer(Modifier.height(18.dp))351- Box(352- Modifier353- .clip(RoundedCornerShape(8.dp))354- .border(BorderStroke(1.dp, accent), RoundedCornerShape(8.dp))355- .clickable {356- val intent = Intent(357- Settings.ACTION_APPLICATION_DETAILS_SETTINGS,358- Uri.fromParts("package", context.packageName, null),359- ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)360- context.startActivity(intent)361- }362- .padding(horizontal = 18.dp, vertical = 10.dp),363- ) {364- Text("Open Settings", color = accent, fontFamily = PeachType.Mono, fontSize = 11.sp)365- }366- }318+ PeachEmptyOrEndState(319+ message = "Camera access needed",320+ detail = "Grant camera permission in Settings to scan your Codex.",321+ action = {322+ CodexButton("Open Settings", accent, filled = true) {323+ val intent = Intent(324+ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,325+ Uri.fromParts("package", context.packageName, null),326+ ).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)327+ context.startActivity(intent)328+ }329+ },330+ )367331 }app/src/main/java/com/peachpasswords/ui/codex/CodexRestoreScreen.kt+36 −30
@@ -10,8 +10,6 @@1010 import androidx.compose.foundation.layout.fillMaxWidth1111 import androidx.compose.foundation.layout.height1212 import androidx.compose.foundation.layout.padding13-import androidx.compose.foundation.text.KeyboardOptions14-import androidx.compose.material3.OutlinedTextField1513 import androidx.compose.material3.Text1614 import androidx.compose.runtime.Composable1715 import androidx.compose.runtime.getValue@@ -21,15 +19,18 @@2119 import androidx.compose.ui.Modifier2220 import androidx.compose.ui.graphics.Color2321 import androidx.compose.ui.text.font.FontStyle22+import androidx.compose.ui.text.input.KeyboardType2423 import androidx.compose.ui.text.input.PasswordVisualTransformation25-import androidx.compose.ui.unit.dp2624 import androidx.compose.ui.unit.sp2725 import androidx.hilt.navigation.compose.hiltViewModel2826 import androidx.lifecycle.compose.collectAsStateWithLifecycle27+import com.peachpasswords.ui.components.PeachEmptyOrEndState28+import com.peachpasswords.ui.components.PeachField2929 import com.peachpasswords.ui.theme.LocalPeachAccent30-import com.peachpasswords.ui.theme.peachColors30+import com.peachpasswords.ui.theme.PeachSpacing3131 import com.peachpasswords.ui.theme.PeachType3232 import com.peachpasswords.ui.theme.ShellBackground33+import com.peachpasswords.ui.theme.peachColors3334 3435 @Composable3536 fun CodexRestoreScreen(onBack: () -> Unit, onRestored: () -> Unit, vm: CodexViewModel = hiltViewModel()) {@@ -41,14 +42,15 @@4142 ) { uri -> uri?.let { vm.scan(it) } }4243 4344 ShellBackground {44- Column(Modifier.fillMaxSize().padding(horizontal = 24.dp)) {45- CodexHeader("Codex restore", onBack)46- Spacer(Modifier.height(8.dp))45+ Column(Modifier.fillMaxSize().padding(horizontal = PeachSpacing.xl)) {46+ Spacer(Modifier.height(PeachSpacing.md))47+ CodexTopBar("Codex restore", onBack)48+ Spacer(Modifier.height(PeachSpacing.sm))4749 Text(4850 "Choose a Codex PDF. Peach reads its Aztec codes — even if some pages are missing or smudged — and rebuilds your encrypted vault. You'll need the master password.",4951 color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp,5052 )51- Spacer(Modifier.height(28.dp))53+ Spacer(Modifier.height(PeachSpacing.xxl))5254 5355 when (val s = state) {5456 is RestoreState.Idle -> when (val cs = cameraState) {@@ -92,16 +94,21 @@9294 is RestoreState.Restoring ->9395 Text("Restoring vault…", color = peachColors.T1, fontFamily = PeachType.Mono, fontSize = 12.sp)9496 95- is RestoreState.Restored ->96- Text("Vault restored.", color = accent, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 22.sp)97+ is RestoreState.Restored -> PeachEmptyOrEndState(98+ message = "Vault restored.",99+ detail = "Your restored vault is ready.",100+ )97101 98- is RestoreState.Error -> Column {99- Text("Couldn't restore", color = peachColors.Breach, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 19.sp)100- Spacer(Modifier.height(6.dp))101- Text(s.message, color = peachColors.T2, fontFamily = PeachType.Body, fontSize = 13.sp)102- Spacer(Modifier.height(20.dp))103- CodexButton("Choose another file", accent, filled = true) { vm.resetRestore(); pickLauncher.launch("application/pdf") }104- }102+ is RestoreState.Error -> PeachEmptyOrEndState(103+ message = "Couldn't restore",104+ detail = s.message,105+ action = {106+ CodexButton("Choose another file", accent, filled = true) {107+ vm.resetRestore()108+ pickLauncher.launch("application/pdf")109+ }110+ },111+ )105112 }106113 }107114 }@@ -122,7 +129,7 @@122129 fontStyle = FontStyle.Italic,123130 fontSize = 19.sp,124131 )125- Spacer(Modifier.height(6.dp))132+ Spacer(Modifier.height(PeachSpacing.xs))126133 Text(127134 "The PDF decoded ${s.found} codes, but ${s.missing} remained unreadable. " +128135 (if (s.ready == s.total) {@@ -134,16 +141,16 @@134141 fontFamily = PeachType.Body,135142 fontSize = 13.sp,136143 )137- Spacer(Modifier.height(8.dp))144+ Spacer(Modifier.height(PeachSpacing.sm))138145 Text(139146 "Try importing the PDF again to fill the gaps, or switch to camera scanning if it keeps failing.",140147 color = peachColors.T2,141148 fontFamily = PeachType.Body,142149 fontSize = 13.sp,143150 )144- Spacer(Modifier.height(20.dp))151+ Spacer(Modifier.height(PeachSpacing.xl))145152 CodexButton("Try PDF again", accent, filled = true, onClick = onRetryPdf)146- Spacer(Modifier.height(10.dp))153+ Spacer(Modifier.height(PeachSpacing.sm))147154 CodexButton("Use camera", peachColors.T2, filled = false, onClick = onUseCamera)148155 }149156 }@@ -152,29 +159,28 @@152159 private fun DecodedBody(s: RestoreState.Decoded, accent: Color, vm: CodexViewModel, onRestored: () -> Unit) {153160 var password by remember { mutableStateOf("") }154161 Column {155- Row(Modifier.fillMaxWidth().padding(vertical = 7.dp), horizontalArrangement = Arrangement.SpaceBetween) {162+ Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {156163 Text("verify code", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)157164 Text(s.payload.verifyCode, color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)158165 }159- Row(Modifier.fillMaxWidth().padding(vertical = 7.dp), horizontalArrangement = Arrangement.SpaceBetween) {166+ Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {160167 Text("entries", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)161168 Text(s.payload.entryCount.toString(), color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)162169 }163- Row(Modifier.fillMaxWidth().padding(vertical = 7.dp), horizontalArrangement = Arrangement.SpaceBetween) {170+ Row(Modifier.fillMaxWidth().padding(vertical = PeachSpacing.sm), horizontalArrangement = Arrangement.SpaceBetween) {164171 Text("codes read", color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 10.sp)165172 Text(s.codesFound.toString(), color = accent, fontFamily = PeachType.Mono, fontSize = 12.sp)166173 }167- Spacer(Modifier.height(20.dp))168- OutlinedTextField(174+ Spacer(Modifier.height(PeachSpacing.xl))175+ PeachField(176+ label = "Master password",169177 value = password,170178 onValueChange = { password = it },171- placeholder = { Text("master password", color = peachColors.T3, fontFamily = PeachType.Mono, fontSize = 13.sp) },172179 visualTransformation = PasswordVisualTransformation(),173- keyboardOptions = KeyboardOptions(keyboardType = androidx.compose.ui.text.input.KeyboardType.Password),180+ keyboardType = KeyboardType.Password,174181 singleLine = true,175- modifier = Modifier.fillMaxWidth(),176182 )177- Spacer(Modifier.height(16.dp))183+ Spacer(Modifier.height(PeachSpacing.lg))178184 CodexButton("Restore this vault", accent, filled = true) {179185 if (password.isNotEmpty()) vm.confirmRestore(password, onRestored)180186 }