package com.peachpasswords.ui.vault import androidx.compose.foundation.background import androidx.compose.foundation.clickable import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.material3.ExperimentalMaterial3Api 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.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawBehind import androidx.compose.ui.geometry.Offset import androidx.compose.ui.geometry.Size import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.font.FontStyle import androidx.compose.ui.text.style.TextAlign 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.VaultEntry import com.peachpasswords.vault.hasTOTP /** * D12 long-press context menu (spec §9). A glass ModalBottomSheet over an entry * row offering quick copy / edit / trash without opening the detail screen. * * Self-contained, drop-in. Triggered from VaultListScreen on a 500ms long-press * (see INTEGRATION.md for the pointerInput anchor). Callbacks are wired to the * VM in the host; this composable owns no business logic. */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun ContextMenuSheet( entry: VaultEntry, onCopyPassword: () -> Unit, onCopyUsername: () -> Unit, onCopyTotp: () -> Unit, onEdit: () -> Unit, onMoveToTrash: () -> Unit, onDismiss: () -> Unit, ) { val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) ModalBottomSheet( onDismissRequest = onDismiss, sheetState = sheetState, containerColor = peachColors.SurfaceElevated, dragHandle = null, ) { Column( Modifier .fillMaxWidth() .padding(top = 8.dp) .navigationBarsPadding(), ) { // Entry name header — non-interactive title. Text( text = entry.title, color = peachColors.T0, style = PeachType.entryNameList.copy(fontSize = 15.sp), modifier = Modifier.padding(horizontal = 20.dp, vertical = 12.dp), ) Divider() val username = entry.username ?: entry.email if (!entry.password.isNullOrEmpty()) { ActionRow(label = "Copy password", onClick = { onCopyPassword(); onDismiss() }) } if (!username.isNullOrEmpty()) { ActionRow(label = "Copy username", onClick = { onCopyUsername(); onDismiss() }) } if (entry.hasTOTP()) { ActionRow(label = "Copy TOTP code", onClick = { onCopyTotp(); onDismiss() }) } ActionRow(label = "Edit", onClick = { onEdit(); onDismiss() }) ActionRow( label = "Move to trash", textColor = peachColors.Breach, onClick = { onMoveToTrash(); onDismiss() }, ) Divider() CancelRow(onClick = onDismiss) Spacer(Modifier.height(4.dp)) } } } @Composable private fun Divider() { Box( Modifier .fillMaxWidth() .height(1.dp) .background(peachColors.T3), ) } /** * A single action row: 44dp min height, IBM Plex Mono 12sp, with a 2dp accent * left-border that fades in on press (the spec's "accent left-border on press"). */ @Composable private fun ActionRow( label: String, textColor: Color = peachColors.T0, onClick: () -> Unit, ) { val accent = LocalPeachAccent.current val interaction = remember { MutableInteractionSource() } val pressed by interaction.collectIsPressedAsState() Row( modifier = Modifier .fillMaxWidth() .heightIn(min = 44.dp) .clickable(interactionSource = interaction, indication = null, onClick = onClick) .background(if (pressed) peachColors.T4 else Color.Transparent) .drawBehind { if (pressed) { drawRect( color = accent, topLeft = Offset.Zero, size = Size(2.dp.toPx(), size.height), ) } } .padding(horizontal = 20.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( text = label, color = textColor, style = PeachType.bodyMono.copy(fontSize = 12.sp), ) } } @Composable private fun CancelRow(onClick: () -> Unit) { Row( modifier = Modifier .fillMaxWidth() .heightIn(min = 44.dp) .clickable(onClick = onClick) .padding(horizontal = 20.dp, vertical = 12.dp), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically, ) { Text( text = "Cancel", color = peachColors.T2, style = PeachType.labelMono.copy(fontSize = 11.sp), textAlign = TextAlign.Center, ) } }