package com.peachpasswords.ui.vault /* * D4 drafts — generator access from EntryDetailScreen. * * Provides: * - WrenchButton: a 18dp wrench icon to place beside the password copy button. * - GeneratorSheet: a ModalBottomSheet wrapping the shared GeneratorBody. It * holds LOCAL generator option + draft-password state, never auto-saves, and * exposes [Copy] (drain) + [Use this password] (accent). "Use this password" * calls onUse(draft) which the screen wires to the VM's setPassword path. * * This is a DRAFT. The orchestrator integrates per INTEGRATION.md: it must add a * `setPassword`/`updateEntry` method to EntryDetailViewModel (see INTEGRATION.md) * and a `copy(...)`-backed copied latch for the sheet's draft. */ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.Build import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.Text import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.peachpasswords.ui.theme.LocalPeachAccent import com.peachpasswords.ui.theme.peachColors import com.peachpasswords.ui.theme.PeachType import com.peachpasswords.vault.PasswordGenerator /** * Opens the generator sheet from the detail password row. Matches CopyButton's * icon-button shape (36dp, 6dp rounded, filled background) — this used to be a * bare icon on a plain CircleShape clip with no background or press feedback, * visibly inconsistent with the copy button sitting right next to it. */ @Composable internal fun WrenchButton(onClick: () -> Unit) { Box( Modifier .size(36.dp) .clip(RoundedCornerShape(6.dp)) .background(peachColors.S2) .clickable(onClick = onClick), contentAlignment = Alignment.Center, ) { Icon( Icons.Outlined.Build, contentDescription = "generate password", tint = peachColors.T2, modifier = Modifier.size(15.dp), ) } } /** * Generator bottom sheet for EntryDetailScreen. Local-only: generating populates * a draft password (never persisted). The user copies it (drain) or commits it * via [Use this password], which fires onUse(draft) and dismisses. * * @param copied latch for the [Copy] drain animation (driven by the screen/VM). * @param onCopy copy the current draft to clipboard (sets the latch). * @param onUse commit the draft password (VM updateEntry + history capture). * @param onDismiss close the sheet without committing. */ @OptIn(ExperimentalMaterial3Api::class) @Composable internal fun GeneratorSheet( copied: Boolean, onCopy: (String) -> Unit, onUse: (String) -> Unit, onDismiss: () -> Unit, ) { val accent = LocalPeachAccent.current val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) var mode by remember { mutableStateOf(GeneratorMode.Password) } var length by remember { mutableStateOf(20) } var wordCount by remember { mutableStateOf(PasswordGenerator.DEFAULT_PASSPHRASE_WORDS) } var separator by remember { mutableStateOf("-") } var upper by remember { mutableStateOf(true) } var lower by remember { mutableStateOf(true) } var numbers by remember { mutableStateOf(true) } var symbols by remember { mutableStateOf(true) } var draft by remember { mutableStateOf("") } fun gen() { draft = if (mode == GeneratorMode.Passphrase) { PasswordGenerator.generatePassphrase(wordCount, separator) } else { PasswordGenerator.generate( PasswordGenerator.Options( length = length, includeUppercase = upper, includeLowercase = lower, includeNumbers = numbers, includeSymbols = symbols, ), ) } } ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = peachColors.S1, ) { Column(Modifier.fillMaxWidth().padding(horizontal = 20.dp, vertical = 4.dp)) { Text( "generate password", color = peachColors.T0, fontFamily = PeachType.Display, fontStyle = FontStyle.Italic, fontSize = 18.sp, ) Spacer(Modifier.height(8.dp)) // Draft preview — empty until first generate. Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp)) .border(1.dp, peachColors.T3, RoundedCornerShape(10.dp)) .padding(horizontal = 12.dp, vertical = 14.dp), ) { Text( text = draft.ifEmpty { "tap generate" }, color = if (draft.isEmpty()) peachColors.T2 else peachColors.T0, fontFamily = PeachType.Mono, fontSize = 13.sp, ) } Spacer(Modifier.height(6.dp)) // Shared generator controls + [Copy] drain button on the generate row. GeneratorBody( mode = mode, length = length, wordCount = wordCount, separator = separator, upper = upper, lower = lower, numbers = numbers, symbols = symbols, password = draft, copied = copied, onMode = { mode = it; gen() }, onLength = { length = it }, onWordCount = { wordCount = it }, onSeparator = { separator = it; gen() }, onUpper = { upper = it }, onLower = { lower = it }, onNumbers = { numbers = it }, onSymbols = { symbols = it }, onGenerate = { gen() }, onCopy = { if (draft.isNotEmpty()) onCopy(draft) }, ) // [Use this password] — accent button, commits + closes. No-op while empty. Box( Modifier.fillMaxWidth().clip(RoundedCornerShape(10.dp)) .background(if (draft.isEmpty()) accent.copy(alpha = 0.10f) else accent.copy(alpha = 0.18f)) .border(1.dp, accent.copy(alpha = 0.5f), RoundedCornerShape(10.dp)) .clickable(enabled = draft.isNotEmpty()) { onUse(draft) } .padding(vertical = 14.dp), contentAlignment = Alignment.Center, ) { Text( "use this ${if (mode == GeneratorMode.Passphrase) "passphrase" else "password"}", color = if (draft.isEmpty()) accent.copy(alpha = 0.5f) else accent, fontFamily = PeachType.Mono, fontSize = 12.sp, ) } Spacer(Modifier.height(20.dp)) } } }