IdentitySuggestDropdown.kt

5.3 KB · raw

1package com.peachpasswords.ui.vault23import androidx.compose.foundation.background4import androidx.compose.foundation.border5import androidx.compose.foundation.layout.Box6import androidx.compose.foundation.layout.Column7import androidx.compose.foundation.layout.Spacer8import androidx.compose.foundation.layout.fillMaxWidth9import androidx.compose.foundation.layout.height10import androidx.compose.foundation.layout.padding11import androidx.compose.foundation.shape.RoundedCornerShape12import androidx.compose.foundation.text.BasicTextField13import androidx.compose.material3.DropdownMenu14import androidx.compose.material3.DropdownMenuItem15import androidx.compose.material3.Text16import androidx.compose.runtime.Composable17import androidx.compose.runtime.getValue18import androidx.compose.runtime.mutableStateOf19import androidx.compose.runtime.remember20import androidx.compose.runtime.setValue21import androidx.compose.ui.Modifier22import androidx.compose.ui.draw.clip23import androidx.compose.ui.focus.onFocusChanged24import androidx.compose.ui.graphics.SolidColor25import androidx.compose.ui.text.TextStyle26import androidx.compose.ui.unit.dp27import androidx.compose.ui.unit.sp28import androidx.compose.ui.window.PopupProperties29import com.peachpasswords.ui.theme.LocalPeachAccent30import com.peachpasswords.ui.theme.peachColors31import com.peachpasswords.ui.theme.PeachType3233/**34 * Anchored suggestion dropdown for identity-style fields (username/email).35 *36 * Self-contained for clean merge into EntryEditScreen.kt. The brief uses37 * `DropdownMenu(expanded = focused && suggestions.isNotEmpty())`; we keep that38 * shape but mark the popup non-focusable so it never steals focus from the39 * BasicTextField that drives `expanded` — otherwise the field blurs the instant40 * the menu opens and the dropdown flickers shut.41 *42 * Mirrors the editor's visual language: IBM Plex Mono rows on the S2 glass43 * surface with a hairline T3 border. Suggestions are expected pre-trimmed to44 * <=4 by the ViewModel (see usernameSuggestions in INTEGRATION.md).45 */46@Composable47fun IdentitySuggestDropdown(48    expanded: Boolean,49    suggestions: List<String>,50    onPick: (String) -> Unit,51    onDismiss: () -> Unit,52) {53    DropdownMenu(54        expanded = expanded && suggestions.isNotEmpty(),55        onDismissRequest = onDismiss,56        properties = PopupProperties(focusable = false),57        modifier = Modifier58            .background(peachColors.S2)59            .border(1.dp, peachColors.T3, RoundedCornerShape(10.dp))60            .clip(RoundedCornerShape(10.dp)),61    ) {62        suggestions.forEach { suggestion ->63            DropdownMenuItem(64                text = {65                    Text(66                        suggestion,67                        style = TextStyle(68                            color = peachColors.T1,69                            fontFamily = PeachType.Mono,70                            fontSize = 11.sp,71                        ),72                    )73                },74                onClick = { onPick(suggestion) },75                modifier = Modifier.fillMaxWidth(),76            )77        }78    }79}8081/**82 * Identity field + anchored suggest dropdown, wired to focus state.83 *84 * Drop-in replacement for the bare `Field(...)` calls for username/email in85 * EntryEditScreen. It owns the focus tracking the dropdown needs; the parent86 * supplies value, suggestions, and the change callback. The dropdown is wrapped87 * with the field so the popup anchors directly beneath it.88 *89 * `Field` is `private` in EntryEditScreen.kt, so this re-implements the same90 * underline input rather than calling it. Keep this composable in the same file91 * when merging, or make `Field` accept `onFocusChanged` and reuse it (see92 * INTEGRATION.md "Option A").93 */94@Composable95fun IdentitySuggestField(96    label: String,97    value: String,98    suggestions: List<String>,99    onChange: (String) -> Unit,100    joined: Boolean = false,101    divider: Boolean = false,102) {103    val accent = LocalPeachAccent.current104    var focused by remember { mutableStateOf(false) }105106    Column(107        Modifier.fillMaxWidth()108            .then(if (joined) Modifier else Modifier.padding(bottom = 14.dp))109            .then(110                if (joined) Modifier.padding(horizontal = 18.dp, vertical = 14.dp)111                else Modifier.border(1.dp, peachColors.T3, RoundedCornerShape(16.dp)).padding(horizontal = 18.dp, vertical = 14.dp),112            ),113    ) {114        Text(label.uppercase(), color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp)115        Spacer(Modifier.height(6.dp))116        Box(Modifier.fillMaxWidth()) {117            BasicTextField(118                value = value,119                onValueChange = onChange,120                singleLine = true,121                textStyle = TextStyle(color = peachColors.T0, fontFamily = PeachType.Mono, fontSize = 13.sp),122                cursorBrush = SolidColor(accent),123                modifier = Modifier124                    .fillMaxWidth()125                    .onFocusChanged { focused = it.isFocused },126            )127        }128        IdentitySuggestDropdown(129            expanded = focused,130            suggestions = suggestions.filter { it != value },131            onPick = { picked ->132                onChange(picked)133                focused = false134            },135            onDismiss = { focused = false },136        )137    }138    if (joined && divider) Box(Modifier.fillMaxWidth().height(1.dp).background(peachColors.T3))139}