IdentitySuggestDropdown.kt

5.5 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.heightIn11import androidx.compose.foundation.layout.padding12import androidx.compose.foundation.shape.RoundedCornerShape13import androidx.compose.foundation.text.BasicTextField14import androidx.compose.material3.DropdownMenu15import androidx.compose.material3.DropdownMenuItem16import androidx.compose.material3.Text17import androidx.compose.runtime.Composable18import androidx.compose.runtime.getValue19import androidx.compose.runtime.mutableStateOf20import androidx.compose.runtime.remember21import androidx.compose.runtime.setValue22import androidx.compose.ui.Modifier23import androidx.compose.ui.draw.clip24import androidx.compose.ui.focus.onFocusChanged25import androidx.compose.ui.graphics.SolidColor26import androidx.compose.ui.text.TextStyle27import androidx.compose.ui.unit.dp28import androidx.compose.ui.unit.sp29import androidx.compose.ui.window.PopupProperties30import com.peachpasswords.ui.theme.LocalPeachAccent31import com.peachpasswords.ui.theme.peachColors32import com.peachpasswords.ui.theme.PeachType3334/**35 * Anchored suggestion dropdown for identity-style fields (username/email).36 *37 * Self-contained for clean merge into EntryEditScreen.kt. The brief uses38 * `DropdownMenu(expanded = focused && suggestions.isNotEmpty())`; we keep that39 * shape but mark the popup non-focusable so it never steals focus from the40 * BasicTextField that drives `expanded` — otherwise the field blurs the instant41 * the menu opens and the dropdown flickers shut.42 *43 * Mirrors the editor's visual language: IBM Plex Mono rows on the S2 glass44 * surface with a hairline T3 border. The popup has a bounded viewport so its45 * own Material scroll surface consumes list gestures instead of the editor's46 * parent scroll container. Suggestions are capped at eight by the ViewModel.47 */48@Composable49fun IdentitySuggestDropdown(50    expanded: Boolean,51    suggestions: List<String>,52    onPick: (String) -> Unit,53    onDismiss: () -> Unit,54) {55    DropdownMenu(56        expanded = expanded && suggestions.isNotEmpty(),57        onDismissRequest = onDismiss,58        properties = PopupProperties(focusable = false),59        modifier = Modifier60            .heightIn(max = 240.dp)61            .background(peachColors.S2)62            .border(1.dp, peachColors.T3, RoundedCornerShape(10.dp))63            .clip(RoundedCornerShape(10.dp)),64    ) {65        suggestions.forEach { suggestion ->66            DropdownMenuItem(67                text = {68                    Text(69                        suggestion,70                        style = TextStyle(71                            color = peachColors.T1,72                            fontFamily = PeachType.Mono,73                            fontSize = 11.sp,74                        ),75                    )76                },77                onClick = { onPick(suggestion) },78                modifier = Modifier.fillMaxWidth(),79            )80        }81    }82}8384/**85 * Identity field + anchored suggest dropdown, wired to focus state.86 *87 * Drop-in replacement for the bare `Field(...)` calls for username/email in88 * EntryEditScreen. It owns the focus tracking the dropdown needs; the parent89 * supplies value, suggestions, and the change callback. The dropdown is wrapped90 * with the field so the popup anchors directly beneath it.91 *92 * `Field` is `private` in EntryEditScreen.kt, so this re-implements the same93 * underline input rather than calling it. Keep this composable in the same file94 * when merging, or make `Field` accept `onFocusChanged` and reuse it (see95 * INTEGRATION.md "Option A").96 */97@Composable98fun IdentitySuggestField(99    label: String,100    value: String,101    suggestions: List<String>,102    onChange: (String) -> Unit,103    joined: Boolean = false,104    divider: Boolean = false,105) {106    val accent = LocalPeachAccent.current107    var focused by remember { mutableStateOf(false) }108109    Column(110        Modifier.fillMaxWidth()111            .then(if (joined) Modifier else Modifier.padding(bottom = 14.dp))112            .then(113                if (joined) Modifier.padding(horizontal = 18.dp, vertical = 14.dp)114                else Modifier.border(1.dp, peachColors.T3, RoundedCornerShape(16.dp)).padding(horizontal = 18.dp, vertical = 14.dp),115            ),116    ) {117        Text(label.uppercase(), color = peachColors.T2, fontFamily = PeachType.Mono, fontSize = 9.sp)118        Spacer(Modifier.height(6.dp))119        Box(Modifier.fillMaxWidth()) {120            BasicTextField(121                value = value,122                onValueChange = onChange,123                singleLine = true,124                textStyle = TextStyle(color = peachColors.T0, fontFamily = PeachType.Mono, fontSize = 13.sp),125                cursorBrush = SolidColor(accent),126                modifier = Modifier127                    .fillMaxWidth()128                    .onFocusChanged { focused = it.isFocused },129            )130        }131        IdentitySuggestDropdown(132            expanded = focused,133            suggestions = suggestions.filter { it != value },134            onPick = { picked ->135                onChange(picked)136                focused = false137            },138            onDismiss = { focused = false },139        )140    }141    if (joined && divider) Box(Modifier.fillMaxWidth().height(1.dp).background(peachColors.T3))142}