1package com.peachpasswords.ui.settings23import android.content.Intent4import android.net.Uri5import androidx.compose.animation.AnimatedVisibility6import androidx.compose.animation.core.animateDpAsState7import androidx.compose.foundation.background8import androidx.compose.foundation.border9import androidx.compose.foundation.clickable10import androidx.compose.foundation.layout.Arrangement11import androidx.compose.foundation.layout.Box12import androidx.compose.foundation.layout.Column13import androidx.compose.foundation.layout.FlowRow14import androidx.compose.foundation.layout.ExperimentalLayoutApi15import androidx.compose.foundation.layout.Row16import androidx.compose.foundation.layout.RowScope17import androidx.compose.foundation.layout.Spacer18import androidx.compose.foundation.layout.fillMaxWidth19import androidx.compose.foundation.layout.height20import androidx.compose.foundation.layout.padding21import androidx.compose.foundation.layout.size22import androidx.compose.foundation.layout.width23import androidx.compose.foundation.shape.CircleShape24import androidx.compose.foundation.shape.RoundedCornerShape25import androidx.compose.foundation.text.BasicTextField26import androidx.compose.runtime.Composable27import androidx.compose.material3.CircularProgressIndicator28import androidx.compose.runtime.DisposableEffect29import androidx.compose.runtime.collectAsState30import androidx.compose.runtime.getValue31import com.peachpasswords.feedback.FeedbackState32import com.peachpasswords.sync.SyncStatus33import com.peachpasswords.ui.theme.LocalPeachAccent34import com.peachpasswords.vault.DuressAction35import com.peachpasswords.ui.theme.peachColors36import androidx.compose.runtime.mutableStateOf37import androidx.compose.runtime.remember38import androidx.compose.runtime.rememberCoroutineScope39import androidx.compose.runtime.setValue40import androidx.compose.ui.Alignment41import androidx.compose.ui.Modifier42import androidx.compose.ui.draw.clip43import androidx.compose.ui.graphics.Color44import androidx.compose.ui.graphics.SolidColor45import androidx.compose.ui.platform.LocalContext46import androidx.compose.ui.text.TextStyle47import androidx.compose.ui.text.font.FontStyle48import androidx.compose.ui.text.input.PasswordVisualTransformation49import androidx.compose.ui.unit.Dp50import androidx.compose.ui.unit.dp51import androidx.compose.ui.unit.em52import androidx.compose.ui.unit.sp53import androidx.compose.material3.Text54import com.peachpasswords.ui.PeachNav55import com.peachpasswords.ui.sync.SyncViewModel56import com.peachpasswords.ui.theme.ACCENT_COLORS57import com.peachpasswords.ui.theme.PeachType58import androidx.hilt.navigation.compose.hiltViewModel59import com.peachpasswords.vault.VaultSettings60import com.peachpasswords.vault.VaultSnapshot61import kotlinx.coroutines.launch62import java.text.SimpleDateFormat63import java.util.Date64import java.util.Locale6566private const val PRO_URL = "https://buy.polar.sh/polar_cl_IXSFIPojx67rcOTksn8LkH5mnU3osPHtYqBIl1eyuIz"676869707172737475@Composable76fun CardBody(77 id: CardId,78 fg: Color,79 vm: SettingsViewModel,80 settings: VaultSettings,81 entriesCount: Int,82 onNavigate: (PeachNav) -> Unit,83) {84 when (id) {85 CardId.Appearance -> AppearanceBody(fg, vm, settings)86 CardId.Protection -> ProtectionBody(fg, vm, settings, onNavigate)87 CardId.Vault -> VaultBody(fg, vm, settings, entriesCount, onNavigate)88 CardId.Sync -> SyncBody(fg, onNavigate)89 CardId.Shortcuts -> ShortcutsBody(fg)90 CardId.Pro -> ProBody(fg, vm)91 CardId.Feedback -> FeedbackBody(fg, vm)92 }93}94959697@Composable98private fun SubLabel(text: String, fg: Color, top: Dp = 14.dp) {99 Text(100 text,101 modifier = Modifier.padding(top = top, bottom = 4.dp),102 color = fg.copy(alpha = 0.58f),103 fontFamily = PeachType.Mono,104 fontSize = 8.sp,105 letterSpacing = 0.22.em,106 )107}108109@Composable110private fun RowLine(fg: Color, modifier: Modifier = Modifier, content: @Composable RowScope.() -> Unit) {111 Row(112 Modifier.fillMaxWidth().then(modifier).padding(vertical = 9.dp),113 verticalAlignment = Alignment.CenterVertically,114 horizontalArrangement = Arrangement.SpaceBetween,115 content = content,116 )117 Box(Modifier.fillMaxWidth().height(1.dp).background(fg.copy(alpha = 0.12f)))118}119120@Composable121private fun RowScope.Name(text: String, fg: Color) {122 Text(text, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp, modifier = Modifier.weight(1f))123}124125@Composable126private fun ValueText(text: String, fg: Color) {127 Text(text, color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Mono, fontSize = 11.sp)128}129130@Composable131private fun SubtleButton(label: String, fg: Color, onClick: () -> Unit) {132 Box(133 Modifier.clip(RoundedCornerShape(8.dp))134 .border(1.dp, fg.copy(alpha = 0.28f), RoundedCornerShape(8.dp))135 .background(fg.copy(alpha = 0.08f))136 .clickable(onClick = onClick)137 .padding(horizontal = 12.dp, vertical = 6.dp),138 ) {139 Text(label, color = fg, fontFamily = PeachType.Mono, fontSize = 11.sp)140 }141}142143@Composable144private fun Toggle(on: Boolean, fg: Color, onToggle: (Boolean) -> Unit) {145 val knob by animateDpAsState(if (on) 16.dp else 2.dp, label = "knob")146 Box(147 Modifier.size(width = 34.dp, height = 20.dp).clip(RoundedCornerShape(999.dp))148 .background(if (on) fg.copy(alpha = 0.28f) else fg.copy(alpha = 0.08f))149 .border(1.dp, fg.copy(alpha = if (on) 0.5f else 0.32f), RoundedCornerShape(999.dp))150 .clickable { onToggle(!on) },151 ) {152 Box(153 Modifier.padding(top = 2.dp).width(16.dp).fillMaxWidth().height(16.dp)154 .padding(start = knob - 2.dp)155 .size(14.dp).clip(CircleShape)156 .background(if (on) fg else fg.copy(alpha = 0.58f)),157 )158 }159}160161@Composable162private fun Pill(label: String, active: Boolean, fg: Color, onClick: () -> Unit) {163 Box(164 Modifier.clip(RoundedCornerShape(999.dp))165 .border(1.dp, fg.copy(alpha = if (active) 0.55f else 0.32f), RoundedCornerShape(999.dp))166 .background(if (active) fg.copy(alpha = 0.18f) else Color.Transparent)167 .clickable(onClick = onClick)168 .padding(horizontal = 12.dp, vertical = 5.dp),169 ) {170 Text(label, color = fg, fontFamily = PeachType.Mono, fontSize = 10.sp)171 }172}173174@Composable175private fun ProBadge(fg: Color) {176 Box(177 Modifier.padding(start = 6.dp).clip(RoundedCornerShape(4.dp))178 .background(fg.copy(alpha = 0.16f)).padding(horizontal = 5.dp, vertical = 1.dp),179 ) {180 Text("Pro", color = fg.copy(alpha = 0.85f), fontFamily = PeachType.Mono, fontSize = 8.sp)181 }182}183184@Composable185private fun Hint(text: String, fg: Color) {186 Text(187 text,188 modifier = Modifier.padding(top = 6.dp),189 color = fg.copy(alpha = 0.6f),190 fontFamily = PeachType.Display,191 fontStyle = FontStyle.Italic,192 fontSize = 11.5.sp,193 lineHeight = 16.sp,194 )195}196197198199@OptIn(ExperimentalLayoutApi::class)200@Composable201private fun AppearanceBody(fg: Color, vm: SettingsViewModel, s: VaultSettings) {202 SubLabel("THEME", fg, top = 0.dp)203 RowLine(fg) {204 Name("Theme", fg)205 val (label, next) = when (s.theme) {206 "dark" -> "Switch to light" to "light"207 else -> "Switch to dark" to "dark"208 }209 SubtleButton(label, fg) { vm.setTheme(next) }210 }211 SubLabel("ACCENT", fg)212 FlowRow(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {213 ACCENT_COLORS.forEach { (name, color) ->214 Box(215 Modifier.size(22.dp).clip(CircleShape).background(color)216 .border(217 if (s.accentColor == name) 2.dp else 1.dp,218 if (s.accentColor == name) fg else fg.copy(alpha = 0.2f),219 CircleShape,220 )221 .clickable { vm.setAccent(name) },222 )223 }224 }225 SubLabel("TEXT SIZE", fg)226 RowLine(fg) {227 Name("Size", fg)228 Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {229 Pill("Default", s.textSize == "medium", fg) { vm.setTextSize("medium") }230 Pill("Large", s.textSize == "large", fg) { vm.setTextSize("large") }231 }232 }233 SubLabel("MOTION", fg)234 RowLine(fg) {235 Name("Reduce motion", fg)236 SubtleButton(if (s.reduceMotion) "On" else "Off", fg) { vm.setReduceMotion(!s.reduceMotion) }237 }238}239240241242@Composable243private fun ProtectionBody(fg: Color, vm: SettingsViewModel, s: VaultSettings, onNavigate: (PeachNav) -> Unit) {244 val lockOptions = listOf(1 to "1 minute", 5 to "5 minutes", 15 to "15 minutes", 30 to "30 minutes", 0 to "Never")245 val current = s.autoLockMinutes ?: 0246247 SubLabel("LOCKING", fg, top = 0.dp)248 RowLine(fg) {249 Name("Auto-lock after", fg)250 val idx = lockOptions.indexOfFirst { it.first == current }.coerceAtLeast(0)251 SubtleButton(lockOptions[idx].second, fg) {252 val next = lockOptions[(idx + 1) % lockOptions.size].first253 vm.setAutoLockMinutes(if (next == 0) 0 else next)254 }255 }256 SubLabel("BIOMETRICS", fg)257 val bioActivity = LocalContext.current as? androidx.fragment.app.FragmentActivity258 RowLine(fg) {259 Name("Unlock with biometrics", fg)260 Toggle(s.biometricsEnabled, fg) { on ->261 if (on && bioActivity != null) vm.enableBiometrics(bioActivity) else vm.disableBiometrics()262 }263 }264 val bioError = com.peachpasswords.ui.lock.Biometrics.statusMessage(LocalContext.current)265 if (bioError != null) {266 Text(267 bioError,268 color = com.peachpasswords.ui.theme.peachColors.Breach,269 fontFamily = PeachType.Mono, fontSize = 10.sp,270 modifier = Modifier.padding(start = 2.dp, top = 2.dp, bottom = 4.dp),271 )272 }273 SubLabel("PASSKEYS", fg)274 RowLine(fg) {275 Column(Modifier.weight(1f)) {276 Text("Credential provider", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)277 Text(278 "Peach can handle passkey sign-in and creation through Android Credential Manager.",279 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,280 modifier = Modifier.padding(top = 2.dp),281 )282 }283 Toggle(s.passkeyProviderEnabled, fg) { vm.setPasskeyProviderEnabled(it) }284 }285 RowLine(fg) {286 Name("Status", fg)287 ValueText(if (s.passkeyProviderEnabled) "enabled" else "off", fg)288 }289 SubLabel("RECOVERY", fg)290 RowLine(fg) {291 Name("View recovery words", fg)292 SubtleButton("view", fg) { onNavigate(PeachNav.RecoveryView) }293 }294 SubLabel("BREACH PROTECTION", fg)295 val checkingBreaches by vm.checkingBreaches.collectAsState()296 val breachResult by vm.breachResult.collectAsState()297 RowLine(fg) {298 Name("Breach monitoring", fg)299 Toggle(s.breachMonitoring, fg) { vm.setBreachMonitoring(it) }300 }301 RowLine(fg) {302 Name("Check all passwords", fg)303 SubtleButton(if (checkingBreaches) "checking..." else "run", fg) { vm.checkAllBreaches() }304 }305 breachResult?.let { result ->306 RowLine(fg) {307 Name("Last check", fg)308 ValueText(result, fg)309 }310 }311312 SubLabel("MASTER PASSWORD", fg)313 ChangePasswordRow(fg, vm)314315 SubLabel("DURESS PASSWORD", fg)316 val duressIsPro by vm.isPro.collectAsState()317 if (!duressIsPro) {318 RowLine(fg) {319 Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {320 Name("Duress password", fg); ProBadge(fg)321 }322 ValueText("off", fg)323 }324 Hint("If entered under coercion, a decoy empty vault opens; your real vault stays hidden. Pro feature.", fg)325 } else {326 DuressSection(fg, vm)327 }328}329330@Composable331private fun ChangePasswordRow(fg: Color, vm: SettingsViewModel) {332 var open by remember { mutableStateOf(false) }333 var current by remember { mutableStateOf("") }334 var next by remember { mutableStateOf("") }335 var confirm by remember { mutableStateOf("") }336 var error by remember { mutableStateOf<String?>(null) }337 var done by remember { mutableStateOf(false) }338 val scope = rememberCoroutineScope()339340 RowLine(fg) {341 Name("Master password", fg)342 SubtleButton(if (open) "cancel" else "change", fg) { open = !open; error = null; done = false }343 }344 AnimatedVisibility(open) {345 Column(Modifier.fillMaxWidth().padding(top = 8.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {346 PwField("current password", current, fg) { current = it }347 PwField("new password (min 12 chars)", next, fg) { next = it }348 PwField("confirm new password", confirm, fg) { confirm = it }349 error?.let { Text(it, color = Color(0xFFE5805C), fontFamily = PeachType.Mono, fontSize = 10.sp) }350 if (done) Text("password changed ✓", color = fg, fontFamily = PeachType.Mono, fontSize = 10.sp)351 SubtleButton("save new password", fg) {352 error = when {353 current.isBlank() -> "enter your current password"354 next.length < 12 -> "new password must be at least 12 characters"355 next != confirm -> "new passwords do not match"356 next == current -> "new password must differ from current"357 else -> null358 }359 if (error == null) scope.launch {360 if (vm.changePassword(current, next)) {361 current = ""; next = ""; confirm = ""; open = false; done = true362 } else error = "incorrect current password"363 }364 }365 }366 }367}368369@Composable370private fun PwField(placeholder: String, value: String, fg: Color, onChange: (String) -> Unit) {371 Box(372 Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp))373 .background(Color(0x2E000000))374 .border(1.dp, fg.copy(alpha = 0.22f), RoundedCornerShape(8.dp))375 .padding(horizontal = 10.dp, vertical = 10.dp),376 ) {377 if (value.isEmpty()) {378 Text(placeholder, color = fg.copy(alpha = 0.42f), fontFamily = PeachType.Mono, fontSize = 12.sp)379 }380 BasicTextField(381 value = value,382 onValueChange = onChange,383 singleLine = true,384 textStyle = TextStyle(color = fg, fontFamily = PeachType.Mono, fontSize = 12.sp),385 cursorBrush = SolidColor(fg),386 visualTransformation = PasswordVisualTransformation(),387 modifier = Modifier.fillMaxWidth(),388 )389 }390}391392393394@Composable395private fun VaultBody(396 fg: Color,397 vm: SettingsViewModel,398 s: VaultSettings,399 entriesCount: Int,400 onNavigate: (PeachNav) -> Unit,401) {402 val trashOptions = listOf(7 to "7 days", 30 to "30 days", 90 to "90 days", 0 to "Never")403 val current = s.trashRetentionDays ?: 0404 val isPro by vm.isPro.collectAsState()405 val dedupCount by vm.dedupCount.collectAsState()406 val dedupRunning by vm.dedupRunning.collectAsState()407 val dedupDone by vm.dedupDone.collectAsState()408 val history by vm.vaultHistory.collectAsState()409 val restoringSnapshotId by vm.restoringSnapshotId.collectAsState()410411 SubLabel("ENTRIES", fg, top = 0.dp)412 RowLine(fg) { Name("Entries", fg); ValueText("$entriesCount", fg) }413 RowLine(fg) {414 Name("Duplicates", fg)415 val done = dedupDone416 if (done != null) {417 ValueText(if (done == 0) "already clean" else "removed $done", com.peachpasswords.ui.theme.peachColors.SyncOk)418 } else {419 val count = dedupCount420 val label = when {421 dedupRunning -> "scanning..."422 count == null -> "scan"423 count == 0 -> "none found"424 else -> "$count found"425 }426 SubtleButton(label, fg) {427 if (count == null || count == 0) vm.scanDuplicates()428 }429 }430 }431 if ((dedupCount ?: 0) > 0 && dedupDone == null) {432 Column(433 Modifier.fillMaxWidth()434 .padding(top = 2.dp, bottom = 8.dp)435 .clip(RoundedCornerShape(8.dp))436 .background(fg.copy(alpha = 0.08f))437 .border(1.dp, fg.copy(alpha = 0.18f), RoundedCornerShape(8.dp))438 .padding(10.dp),439 verticalArrangement = Arrangement.spacedBy(8.dp),440 ) {441 val count = dedupCount ?: 0442 Text(443 "$count duplicate ${if (count == 1) "entry" else "entries"} found. The newest copy of each will be kept; older duplicates will be moved to trash.",444 color = fg.copy(alpha = 0.82f),445 fontFamily = PeachType.Body,446 fontSize = 11.sp,447 lineHeight = 16.sp,448 )449 Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {450 SubtleButton("cancel", fg) { vm.cancelDuplicateRemoval() }451 SubtleButton(if (dedupRunning) "removing..." else "remove duplicates", fg) { vm.removeDuplicates() }452 }453 }454 }455456 SubLabel("MAINTENANCE", fg)457 RowLine(fg) {458 Name("Trash auto-delete", fg)459 val idx = trashOptions.indexOfFirst { it.first == current }.coerceAtLeast(0)460 SubtleButton(trashOptions[idx].second, fg) {461 vm.setTrashRetentionDays(trashOptions[(idx + 1) % trashOptions.size].first)462 }463 }464 RowLine(fg) { Name("Advanced Vault Health", fg); SubtleButton("open", fg) { onNavigate(PeachNav.Health) } }465 RowLine(fg) { Name("Trash", fg); SubtleButton("open", fg) { onNavigate(PeachNav.Trash) } }466467 SubLabel("DATA", fg)468 RowLine(fg) { Name("Import passwords", fg); SubtleButton("open", fg) { onNavigate(PeachNav.Import) } }469 RowLine(fg) { Name("Export backup", fg); SubtleButton("open", fg) { onNavigate(PeachNav.Export) } }470 RowLine(fg) {471 Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) { Name("Peach Codex", fg); if (!isPro) ProBadge(fg) }472 SubtleButton(if (isPro) "open" else "Pro", fg) { if (isPro) onNavigate(PeachNav.CodexGenerate) }473 }474 RowLine(fg) { Name("Restore from Codex", fg); SubtleButton("open", fg) { onNavigate(PeachNav.CodexRestore) } }475476 var historyShowAll by remember { mutableStateOf(false) }477 SubLabel("VAULT HISTORY", fg)478 if (history.isEmpty()) {479 Text(480 "No snapshots yet. Peach saves automatically before every change — saves, syncs, and imports.",481 color = fg.copy(alpha = 0.68f),482 fontFamily = PeachType.Body,483 fontSize = 11.sp,484 lineHeight = 16.sp,485 modifier = Modifier.padding(bottom = 6.dp),486 )487 } else {488 Text(489 "Restoring locks your vault. Unlock with the password that encrypted that snapshot to confirm.",490 color = fg.copy(alpha = 0.68f),491 fontFamily = PeachType.Body,492 fontSize = 11.sp,493 lineHeight = 16.sp,494 modifier = Modifier.padding(bottom = 4.dp),495 )496 history.take(if (historyShowAll) history.size else 3).forEach { snap ->497 RowLine(fg) {498 Column(Modifier.weight(1f)) {499 Text(snapshotTimeLabel(snap.savedAt), color = fg, fontFamily = PeachType.Body, fontSize = 12.sp)500 Text(snapshotSubLabel(snap), color = fg.copy(alpha = 0.62f), fontFamily = PeachType.Mono, fontSize = 10.sp)501 }502 SubtleButton(if (restoringSnapshotId == snap.id) "..." else "restore", fg) {503 vm.restoreVaultSnapshot(snap.id)504 }505 }506 }507 if (history.size > 3) {508 RowLine(fg) {509 Name(if (historyShowAll) "show less" else "${history.size - 3} older", fg)510 SubtleButton(if (historyShowAll) "hide" else "show", fg) { historyShowAll = !historyShowAll }511 }512 }513 Text(514 "${history.size} of 5 snapshots",515 color = fg.copy(alpha = 0.58f),516 fontFamily = PeachType.Mono,517 fontSize = 10.sp,518 modifier = Modifier.padding(top = 2.dp, bottom = 6.dp),519 )520 }521522 var advancedOpen by remember { mutableStateOf(false) }523 SubLabel("ADVANCED", fg)524 RowLine(fg) {525 Name("Advanced", fg)526 SubtleButton(if (advancedOpen) "hide" else "show", fg) { advancedOpen = !advancedOpen }527 }528 if (advancedOpen) {529 RowLine(fg) {530 Name("Replace vault from backup", com.peachpasswords.ui.theme.peachColors.SyncBusy)531 SubtleButton("open", com.peachpasswords.ui.theme.peachColors.SyncBusy) { onNavigate(PeachNav.Import) }532 }533 RowLine(fg) {534 Name("Destroy vault", com.peachpasswords.ui.theme.peachColors.Breach)535 SubtleButton("destroy", com.peachpasswords.ui.theme.peachColors.Breach) { onNavigate(PeachNav.Panic) }536 }537 }538}539540private fun snapshotSubLabel(snapshot: VaultSnapshot): String {541 val count = snapshot.entryCount?.let { "up to $it ${if (it == 1) "entry" else "entries"}" }542 return listOfNotNull(count, snapshotReasonLabel(snapshot.reason)).joinToString(" · ")543}544545private fun snapshotReasonLabel(reason: String): String = when (reason) {546 "local_save" -> "saved"547 "sync_received" -> "synced"548 "import" -> "imported"549 "restore" -> "restored"550 else -> reason551}552553private fun snapshotTimeLabel(savedAt: Long): String {554 val diff = System.currentTimeMillis() - savedAt555 if (diff < 60_000) return "Just now"556 if (diff < 3_600_000) return "${diff / 60_000}m ago"557 val date = Date(savedAt)558 val today = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date())559 val thenDay = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(date)560 val time = SimpleDateFormat("h:mm a", Locale.getDefault()).format(date)561 if (today == thenDay) return "Today $time"562 val yesterday = SimpleDateFormat("yyyyMMdd", Locale.getDefault()).format(Date(System.currentTimeMillis() - 86_400_000))563 if (yesterday == thenDay) return "Yesterday $time"564 return SimpleDateFormat("MMM d h:mm a", Locale.getDefault()).format(date)565}566567568569@Composable570private fun SyncBody(fg: Color, onNavigate: (PeachNav) -> Unit) {571 val syncVm: SyncViewModel = hiltViewModel()572 val pairedDevices by syncVm.pairedDevices.collectAsState()573 val pendingVouched by syncVm.pendingVouched.collectAsState()574 val syncStatus by syncVm.status.collectAsState()575 val scope = rememberCoroutineScope()576 var confirmUnpair by remember { mutableStateOf<String?>(null) }577 var rotateConfirmOpen by remember { mutableStateOf(false) }578 var rotating by remember { mutableStateOf(false) }579 var rotateError by remember { mutableStateOf<String?>(null) }580581 SubLabel("PAIRING", fg, top = 0.dp)582 RowLine(fg) {583 Column(Modifier.weight(1f)) {584 Row(verticalAlignment = Alignment.CenterVertically) {585 Text("Peach Sync", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp); ProBadge(fg)586 }587 Text(588 "Pair nearby devices by chirp and keep encrypted vault blobs in step without a central account.",589 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,590 modifier = Modifier.padding(top = 2.dp),591 )592 }593 }594 595 596 RowLine(fg, Modifier.clickable { onNavigate(PeachNav.ChirpPair) }) {597 Name("Sync hub", fg)598 Text("›", color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 15.sp)599 }600 RowLine(fg) {601 Column(Modifier.weight(1f)) {602 Text("Pair new device", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)603 Text(604 "Add a phone, another browser, or your desktop sync helper.",605 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,606 modifier = Modifier.padding(top = 2.dp),607 )608 }609 SubtleButton("pair", fg) { onNavigate(PeachNav.ChirpPair) }610 }611612 SubLabel("PAIRED DEVICES", fg)613 if (pairedDevices.isEmpty()) {614 Text(615 "No paired devices yet.",616 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 12.sp,617 modifier = Modifier.padding(top = 2.dp),618 )619 } else {620 pairedDevices.forEach { d ->621 RowLine(fg) {622 Column(Modifier.weight(1f)) {623 Text(d.deviceName ?: d.deviceId, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)624 Text(625 "${deviceTypeLabel(d.deviceType)} · ${syncTimeLabel(d.lastSyncedAt)}${if (isSyncStale(d.lastSyncedAt)) " · not seen recently" else ""}",626 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,627 modifier = Modifier.padding(top = 2.dp),628 )629 }630 if (confirmUnpair == d.deviceId) {631 Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {632 SubtleButton("cancel", fg) { confirmUnpair = null }633 SubtleButton("remove", peachColors.Breach) { syncVm.unpairDevice(d.deviceId); confirmUnpair = null }634 }635 } else {636 SubtleButton("remove", fg) { confirmUnpair = d.deviceId }637 }638 }639 }640 RowLine(fg) {641 ValueText(syncFlashLabel(syncStatus), fg)642 SubtleButton(if (syncStatus == SyncStatus.Syncing) "syncing..." else "sync now", fg) {643 if (syncStatus != SyncStatus.Syncing) syncVm.syncNow()644 }645 }646 }647648 if (pendingVouched.isNotEmpty()) {649 SubLabel("PENDING DEVICES", fg)650 pendingVouched.forEach { dev ->651 RowLine(fg) {652 Column(Modifier.weight(1f)) {653 Text(dev.deviceName, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)654 Text(655 "Was added by another of your devices. Allow it to sync with this vault?",656 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,657 modifier = Modifier.padding(top = 2.dp),658 )659 }660 Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {661 SubtleButton("Allow", fg) { syncVm.confirmVouchedDevice(dev.deviceId) }662 SubtleButton("Reject", fg) { syncVm.rejectVouchedDevice(dev.deviceId) }663 }664 }665 }666 }667668 SubLabel("RECOVERY", fg)669 if (rotateConfirmOpen) {670 Hint("This will disconnect all paired devices and your 12 recovery words will change. You'll be shown the new words next.", peachColors.Breach)671 rotateError?.let {672 Text(it, color = peachColors.Breach, fontFamily = PeachType.Body, fontSize = 11.sp, modifier = Modifier.padding(top = 6.dp))673 }674 Row(Modifier.padding(top = 10.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {675 SubtleButton("cancel", fg) { if (!rotating) rotateConfirmOpen = false }676 SubtleButton(if (rotating) "rotating..." else "Rotate sync identity", peachColors.Breach) {677 if (rotating) return@SubtleButton678 rotating = true679 rotateError = null680 scope.launch {681 val ok = syncVm.rotateSyncIdentity()682 rotating = false683 if (ok) {684 rotateConfirmOpen = false685 onNavigate(PeachNav.RecoverySetup)686 } else {687 rotateError = "Rotation failed. Unlock your real vault and try again."688 }689 }690 }691 }692 } else {693 RowLine(fg) {694 Column(Modifier.weight(1f)) {695 Text("Rotate sync identity", color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)696 Text(697 "Use this if a paired device was lost or stolen. Re-keys the vault so the old device's key is useless.",698 color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 11.sp, lineHeight = 16.sp,699 modifier = Modifier.padding(top = 2.dp),700 )701 }702 SubtleButton("rotate", peachColors.Breach) { rotateError = null; rotateConfirmOpen = true }703 }704 }705706 Hint("Sync uses the same zero-knowledge vault format as backup and restore; pairing authorizes a device, not a cloud account.", fg)707}708709private fun deviceTypeLabel(type: String?): String = when (type) {710 "android" -> "Android"711 "phone" -> "Phone"712 "tablet" -> "Tablet"713 "desktop" -> "Desktop"714 "browser" -> "Browser"715 else -> "Device"716}717718private fun syncTimeLabel(lastSyncedAt: Long?): String {719 if (lastSyncedAt == null) return "never synced"720 val diff = System.currentTimeMillis() - lastSyncedAt721 if (diff < 60_000) return "just now"722 if (diff < 3_600_000) return "${diff / 60_000}m ago"723 if (diff < 86_400_000) return "${diff / 3_600_000}h ago"724 return "${diff / 86_400_000}d ago"725}726727private fun isSyncStale(lastSyncedAt: Long?): Boolean =728 lastSyncedAt != null && System.currentTimeMillis() - lastSyncedAt > 7L * 24L * 60L * 60L * 1000L729730private fun syncFlashLabel(status: SyncStatus): String = when (status) {731 SyncStatus.Idle -> ""732 SyncStatus.Syncing -> "syncing..."733 SyncStatus.Offline -> "offline"734 is SyncStatus.Error -> status.message735 is SyncStatus.LastSynced -> "synced ${syncTimeLabel(status.timestamp)}"736}737738739740@Composable741private fun ShortcutsBody(fg: Color) {742 val bindings = listOf(743 "Copy password" to "⌘C", "Copy username" to "⌘⇧C", "Open entry" to "↵",744 "Edit entry" to "E", "New entry" to "⌘N", "Search" to "/",745 )746 SubLabel("KEYBINDINGS", fg, top = 0.dp)747 Hint("Shortcuts apply when a hardware keyboard is attached. Defaults shown below.", fg)748 Spacer(Modifier.height(8.dp))749 bindings.forEach { (label, key) ->750 RowLine(fg) {751 Name(label, fg)752 Box(753 Modifier.clip(RoundedCornerShape(4.dp)).background(fg.copy(alpha = 0.06f))754 .border(1.dp, fg.copy(alpha = 0.32f), RoundedCornerShape(4.dp))755 .padding(horizontal = 7.dp, vertical = 2.dp),756 ) { Text(key, color = fg, fontFamily = PeachType.Mono, fontSize = 10.sp) }757 }758 }759}760761762763@Composable764private fun ProBody(fg: Color, vm: SettingsViewModel) {765 val ctx = LocalContext.current766 val isPro by vm.isPro.collectAsState()767 if (isPro) {768 ProActiveBody(fg, vm)769 return770 }771 val features = listOf(772 "Peach Sync" to "Encrypted · peer-to-peer",773 "Peach Codex" to "Restore from paper",774 "Passkeys" to "Zero-knowledge WebAuthn",775 "Advanced Vault Health" to "Reuse · age · exposure",776 "Canary entries" to "Detects vault compromise",777 "Duress vault" to "Decoy under coercion",778 )779 Text("PEACH PRO · ONE-TIME", color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 8.sp, letterSpacing = 0.22.em)780 Text(781 "Everything, everywhere.",782 modifier = Modifier.padding(top = 8.dp),783 color = fg, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 24.sp,784 )785 Text(786 "One key unlocks every Pro feature — for the lifetime of the app.",787 modifier = Modifier.padding(top = 6.dp),788 color = fg.copy(alpha = 0.72f), fontFamily = PeachType.Body, fontSize = 12.sp, lineHeight = 17.sp,789 )790 Spacer(Modifier.height(12.dp))791 features.forEachIndexed { i, (name, desc) ->792 RowLine(fg) {793 Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {794 Text("%02d".format(i + 1), color = fg.copy(alpha = 0.5f), fontFamily = PeachType.Mono, fontSize = 10.sp, modifier = Modifier.padding(end = 10.dp))795 Text(name, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)796 }797 Text(desc, color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 11.sp)798 }799 }800 Spacer(Modifier.height(14.dp))801 Text(802 "$39",803 color = fg, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 28.sp,804 )805 Text("once · yours forever", color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 9.sp, letterSpacing = 0.18.em)806 Spacer(Modifier.height(10.dp))807 Box(808 Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))809 .background(fg.copy(alpha = 0.16f))810 .border(1.dp, fg.copy(alpha = 0.4f), RoundedCornerShape(10.dp))811 .clickable {812 runCatching { ctx.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(PRO_URL))) }813 }814 .padding(vertical = 12.dp),815 contentAlignment = Alignment.Center,816 ) { Text("Get Peach Pro", color = fg, fontFamily = PeachType.Mono, fontSize = 12.sp) }817818 Spacer(Modifier.height(16.dp))819 Text("LICENSE KEY", color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 8.sp, letterSpacing = 0.22.em)820 var key by remember { mutableStateOf("") }821 val activating by vm.activating.collectAsState()822 val activationError by vm.activationError.collectAsState()823 Box(824 Modifier.fillMaxWidth().padding(top = 6.dp)825 .border(1.dp, fg.copy(alpha = 0.3f), RoundedCornerShape(8.dp))826 .padding(horizontal = 12.dp, vertical = 10.dp),827 ) {828 BasicTextField(829 value = key,830 onValueChange = { key = it.uppercase(); vm.clearActivationError() },831 singleLine = true,832 textStyle = TextStyle(fontFamily = PeachType.Mono, fontSize = 13.sp, letterSpacing = 0.12.em, color = fg),833 cursorBrush = SolidColor(fg),834 modifier = Modifier.fillMaxWidth(),835 decorationBox = { inner ->836 if (key.isEmpty()) Text("PPK-XXXX-XXXX-XXXX", color = fg.copy(alpha = 0.3f), fontFamily = PeachType.Mono, fontSize = 13.sp, letterSpacing = 0.12.em)837 inner()838 },839 )840 }841 if (activationError != null) {842 Text(activationError!!, color = com.peachpasswords.ui.theme.peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 10.sp, modifier = Modifier.padding(top = 6.dp))843 }844 Spacer(Modifier.height(8.dp))845 Box(846 Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))847 .background(fg.copy(alpha = if (activating || key.isBlank()) 0.08f else 0.16f))848 .border(1.dp, fg.copy(alpha = 0.4f), RoundedCornerShape(10.dp))849 .clickable(enabled = !activating && key.isNotBlank()) { vm.activateLicense(key) }850 .padding(vertical = 12.dp),851 contentAlignment = Alignment.Center,852 ) { Text(if (activating) "Activating…" else "Activate", color = fg, fontFamily = PeachType.Mono, fontSize = 12.sp) }853}854855@Composable856private fun ProActiveBody(fg: Color, vm: SettingsViewModel) {857 val features = listOf("Peach Sync", "Peach Codex", "Passkeys", "Advanced Vault Health", "Canary entries", "Duress vault")858 Box(Modifier.fillMaxWidth().padding(vertical = 8.dp), contentAlignment = Alignment.Center) {859 com.peachpasswords.ui.components.PeachMark(size = 60.dp)860 }861 Text(862 "Peach Pro",863 modifier = Modifier.fillMaxWidth(),864 color = fg, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 26.sp,865 textAlign = androidx.compose.ui.text.style.TextAlign.Center,866 )867 Text(868 "active · yours forever",869 modifier = Modifier.fillMaxWidth().padding(top = 4.dp),870 color = fg.copy(alpha = 0.6f), fontFamily = PeachType.Mono, fontSize = 9.sp, letterSpacing = 0.18.em,871 textAlign = androidx.compose.ui.text.style.TextAlign.Center,872 )873 Spacer(Modifier.height(12.dp))874 features.forEachIndexed { i, name ->875 RowLine(fg) {876 Row(Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically) {877 Text("%02d".format(i + 1), color = fg.copy(alpha = 0.5f), fontFamily = PeachType.Mono, fontSize = 10.sp, modifier = Modifier.padding(end = 10.dp))878 Text(name, color = fg, fontFamily = PeachType.Body, fontSize = 13.sp)879 }880 Text("unlocked", color = com.peachpasswords.ui.theme.peachColors.HealthOk, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 11.sp)881 }882 }883 Spacer(Modifier.height(12.dp))884 Box(885 Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp))886 .border(1.dp, com.peachpasswords.ui.theme.peachColors.Breach.copy(alpha = 0.4f), RoundedCornerShape(10.dp))887 .clickable { vm.deactivate() }888 .padding(vertical = 12.dp),889 contentAlignment = Alignment.Center,890 ) { Text("Deactivate", color = com.peachpasswords.ui.theme.peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 12.sp) }891}892893894895@OptIn(ExperimentalLayoutApi::class)896@Composable897private fun FeedbackBody(fg: Color, vm: SettingsViewModel) {898 val categories = listOf("Bug report", "Feature request", "Question", "Other")899 var category by remember { mutableStateOf(categories[0]) }900 var message by remember { mutableStateOf("") }901 val state by vm.feedbackState.collectAsState()902903 904 DisposableEffect(Unit) { onDispose { vm.resetFeedback() } }905906 if (state is FeedbackState.Sent) {907 Box(908 Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp)).background(fg.copy(alpha = 0.08f))909 .border(1.dp, fg.copy(alpha = 0.32f), RoundedCornerShape(10.dp)).padding(16.dp),910 ) {911 Column {912 Text("Thank you", color = fg, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp)913 Text("Your note is in the queue. Someone will process it soon.", color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 12.sp, modifier = Modifier.padding(top = 4.dp))914 }915 }916 return917 }918919 val sending = state is FeedbackState.Sending920921 Text("Help us improve Peach. Every report goes directly to our inbox.", color = fg.copy(alpha = 0.7f), fontFamily = PeachType.Body, fontSize = 12.sp, lineHeight = 17.sp)922 SubLabel("TYPE", fg)923 FlowRow(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {924 categories.forEach { cat -> Pill(cat, category == cat, fg) { if (!sending) category = cat } }925 }926 SubLabel("MESSAGE", fg)927 Box(928 Modifier.fillMaxWidth().height(110.dp).clip(RoundedCornerShape(8.dp))929 .background(Color(0x2E000000)).border(1.dp, fg.copy(alpha = 0.22f), RoundedCornerShape(8.dp))930 .padding(10.dp),931 ) {932 if (message.isEmpty()) Text("Describe the issue or idea...", color = fg.copy(alpha = 0.42f), fontFamily = PeachType.Body, fontSize = 12.sp)933 BasicTextField(934 value = message, onValueChange = { message = it },935 enabled = !sending,936 textStyle = TextStyle(color = fg, fontFamily = PeachType.Body, fontSize = 12.sp),937 cursorBrush = SolidColor(fg), modifier = Modifier.fillMaxWidth(),938 )939 }940 Spacer(Modifier.height(8.dp))941942 Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {943 val canSend = !sending && message.isNotBlank()944 Box(945 Modifier.clip(RoundedCornerShape(8.dp))946 .border(1.dp, fg.copy(alpha = if (canSend) 0.28f else 0.14f), RoundedCornerShape(8.dp))947 .background(fg.copy(alpha = if (canSend) 0.08f else 0.04f))948 .clickable(enabled = canSend) { vm.sendFeedback(category, message) }949 .padding(horizontal = 12.dp, vertical = 6.dp),950 ) {951 Text(952 if (state is FeedbackState.Error) "Retry" else "Send",953 color = fg.copy(alpha = if (canSend) 1f else 0.4f),954 fontFamily = PeachType.Mono, fontSize = 11.sp,955 )956 }957 if (sending) {958 CircularProgressIndicator(959 color = LocalPeachAccent.current,960 strokeWidth = 2.dp,961 modifier = Modifier.size(16.dp),962 )963 }964 }965966 (state as? FeedbackState.Error)?.let { err ->967 Text(968 err.message,969 modifier = Modifier.padding(top = 8.dp),970 color = peachColors.Breach,971 fontFamily = PeachType.Mono,972 fontSize = 11.sp,973 lineHeight = 15.sp,974 )975 }976977 Hint("Sent anonymously · no account required", fg)978}979980981982@Composable983internal fun DuressSection(fg: Color, vm: SettingsViewModel) {984 val configured by vm.duressConfigured.collectAsState()985 if (configured) DuressActiveState(fg, vm) else DuressForm(fg, vm)986}987988@Composable989private fun DuressActiveState(fg: Color, vm: SettingsViewModel) {990 Text(991 "Duress password is active",992 color = peachColors.HealthOk,993 fontFamily = PeachType.Mono,994 fontSize = 11.sp,995 modifier = Modifier.padding(top = 6.dp),996 )997 Spacer(Modifier.height(8.dp))998 DuressDangerGhostButton("Disable duress password", fg) { vm.disableDuress() }999 Hint("Entering your duress password instead of your real one triggers the action you chose.", fg)1000}10011002@Composable1003private fun DuressForm(fg: Color, vm: SettingsViewModel) {1004 var enabled by remember { mutableStateOf(false) }1005 var password by remember { mutableStateOf("") }1006 var confirm by remember { mutableStateOf("") }1007 var action by remember { mutableStateOf(DuressAction.DECOY) }1008 var error by remember { mutableStateOf<String?>(null) }1009 val scope = rememberCoroutineScope()10101011 RowLine(fg) {1012 Name("Duress password", fg)1013 Toggle(enabled, fg) { on ->1014 enabled = on1015 if (!on) { password = ""; confirm = ""; error = null }1016 }1017 }1018 Text(1019 "Entering your duress password instead of your real one opens a decoy vault or wipes the real one.",1020 color = fg.copy(alpha = 0.7f),1021 fontFamily = PeachType.Mono,1022 fontSize = 10.sp,1023 modifier = Modifier.padding(top = 6.dp),1024 )10251026 AnimatedVisibility(enabled) {1027 Column(Modifier.fillMaxWidth().padding(top = 10.dp), verticalArrangement = Arrangement.spacedBy(6.dp)) {1028 PwField("DURESS PASSPHRASE", password, fg) { password = it; error = null }1029 PwField("CONFIRM DURESS PASSPHRASE", confirm, fg) { confirm = it; error = null }10301031 Text(1032 "When your duress password is entered at the lock screen, Peach will:",1033 color = fg.copy(alpha = 0.7f),1034 fontFamily = PeachType.Mono,1035 fontSize = 10.sp,1036 modifier = Modifier.padding(top = 4.dp),1037 )10381039 DuressRadioOption("Show a decoy vault", action == DuressAction.DECOY, fg) { action = DuressAction.DECOY }1040 DuressRadioOption("Destroy the real vault", action == DuressAction.DESTROY, fg) { action = DuressAction.DESTROY }10411042 error?.let { Text(it, color = peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 10.sp) }10431044 val canSave = password.isNotBlank() && confirm.isNotBlank()1045 DuressSaveButton("Save duress password", fg, canSave) {1046 error = when {1047 password.isBlank() || confirm.isBlank() -> "Enter and confirm a duress password"1048 password != confirm -> "Passwords do not match"1049 else -> null1050 }1051 if (error == null) scope.launch {1052 if (vm.saveDuress(password, action)) {1053 password = ""; confirm = ""; enabled = false1054 } else {1055 error = "Couldn't save — the duress passphrase must differ from your real password"1056 }1057 }1058 }1059 }1060 }1061}10621063@Composable1064private fun DuressRadioOption(label: String, selected: Boolean, fg: Color, onClick: () -> Unit) {1065 val accent = LocalPeachAccent.current1066 Row(1067 Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp)).clickable(onClick = onClick).padding(vertical = 7.dp),1068 verticalAlignment = Alignment.CenterVertically,1069 horizontalArrangement = Arrangement.spacedBy(10.dp),1070 ) {1071 Box(1072 Modifier.size(16.dp).clip(CircleShape)1073 .border(1.dp, if (selected) accent else fg.copy(alpha = 0.4f), CircleShape),1074 contentAlignment = Alignment.Center,1075 ) {1076 if (selected) Box(Modifier.size(8.dp).clip(CircleShape).background(accent))1077 }1078 Text(label, color = if (selected) accent else fg, fontFamily = PeachType.Mono, fontSize = 11.sp)1079 }1080}10811082@Composable1083private fun DuressSaveButton(label: String, fg: Color, enabled: Boolean, onClick: () -> Unit) {1084 val accent = LocalPeachAccent.current1085 val tint = if (enabled) accent else fg.copy(alpha = 0.3f)1086 Box(1087 Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp))1088 .border(1.dp, tint.copy(alpha = if (enabled) 0.6f else 0.3f), RoundedCornerShape(8.dp))1089 .background(tint.copy(alpha = if (enabled) 0.16f else 0.06f))1090 .let { if (enabled) it.clickable(onClick = onClick) else it }1091 .padding(vertical = 11.dp),1092 contentAlignment = Alignment.Center,1093 ) {1094 Text(label, color = if (enabled) accent else fg.copy(alpha = 0.4f), fontFamily = PeachType.Mono, fontSize = 11.sp)1095 }1096}10971098@Composable1099private fun DuressDangerGhostButton(label: String, fg: Color, onClick: () -> Unit) {1100 Box(1101 Modifier.fillMaxWidth().clip(RoundedCornerShape(8.dp))1102 .border(1.dp, peachColors.Breach.copy(alpha = 0.5f), RoundedCornerShape(8.dp))1103 .clickable(onClick = onClick)1104 .padding(vertical = 11.dp),1105 contentAlignment = Alignment.Center,1106 ) {1107 Text(label, color = peachColors.Breach, fontFamily = PeachType.Mono, fontSize = 11.sp)1108 }1109}