From bbe84c2bb0679a08c0834142109f1830aa7cc817 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 24 Apr 2026 16:02:00 +0000 Subject: [PATCH] fix(desktop): BasicTextField+DecorationBox for compact Search/Chat inputs M3 OutlinedTextField has ~32dp of vertical contentPadding baked in, so forcing .height(40-44dp) clipped the bodyMedium (14sp, 20dp line- height) placeholder vertically. Refactored both Search and Chat inputs to BasicTextField wrapped in OutlinedTextFieldDefaults.DecorationBox, which lets us override contentPadding to (12.dp horizontal, 8.dp vertical). The field now renders at a true 40dp tall with the placeholder centered and no clipping. Border, focus states, placeholder, and leading/trailing icons still come from M3 so the look stays consistent. https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo --- .../amethyst/desktop/ui/SearchScreen.kt | 96 ++++++++++++------- .../amethyst/desktop/ui/chats/ChatPane.kt | 61 +++++++++--- 2 files changed, 110 insertions(+), 47 deletions(-) diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index b0c5e4ecb..e0a18453a 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -49,7 +49,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable @@ -365,47 +364,80 @@ fun SearchScreen( verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - OutlinedTextField( + // Compact desktop search field. M3's OutlinedTextField has a hard + // ~32dp vertical contentPadding baked in, so forcing .height(40dp) + // clipped the placeholder. BasicTextField + DecorationBox lets us + // override contentPadding to 8dp vertical so the field can sit at + // a true 40dp tall without clipping the bodyMedium line-height. + val searchInteraction = + remember { + androidx.compose.foundation.interaction + .MutableInteractionSource() + } + androidx.compose.foundation.text.BasicTextField( value = textFieldValue, onValueChange = { textFieldValue = it state.updateFromText(it.text) }, - // .height(44.dp) overrides M3's 56dp min-height (intended for - // mobile touch) — desktop inputs should feel closer to Slack / - // Raycast / VS Code. 44dp (not 40) keeps the bodyMedium - // placeholder from clipping vertically inside M3's built-in - // content padding. - modifier = Modifier.weight(1f).height(44.dp).focusRequester(focusRequester), - textStyle = MaterialTheme.typography.bodyMedium, - placeholder = { - Text( - "Search people, tags, notes…", - style = MaterialTheme.typography.bodyMedium, - ) - }, - leadingIcon = { - Icon( - MaterialSymbols.Search, - contentDescription = "Search", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp), - ) - }, - trailingIcon = { - if (displayText.isNotEmpty()) { - IconButton(onClick = { state.clearSearch() }, modifier = Modifier.size(28.dp)) { + modifier = Modifier.weight(1f).height(40.dp).focusRequester(focusRequester), + textStyle = + MaterialTheme.typography.bodyMedium + .copy(color = MaterialTheme.colorScheme.onSurface), + cursorBrush = + androidx.compose.ui.graphics + .SolidColor(MaterialTheme.colorScheme.primary), + singleLine = true, + interactionSource = searchInteraction, + decorationBox = { innerTextField -> + androidx.compose.material3.OutlinedTextFieldDefaults.DecorationBox( + value = textFieldValue.text, + innerTextField = innerTextField, + enabled = true, + singleLine = true, + visualTransformation = androidx.compose.ui.text.input.VisualTransformation.None, + interactionSource = searchInteraction, + placeholder = { + Text( + "Search people, tags, notes…", + style = MaterialTheme.typography.bodyMedium, + ) + }, + leadingIcon = { Icon( - MaterialSymbols.Clear, - contentDescription = "Clear", + MaterialSymbols.Search, + contentDescription = "Search", tint = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.size(18.dp), ) - } - } + }, + trailingIcon = { + if (displayText.isNotEmpty()) { + IconButton(onClick = { state.clearSearch() }, modifier = Modifier.size(28.dp)) { + Icon( + MaterialSymbols.Clear, + contentDescription = "Clear", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } + } + }, + contentPadding = + androidx.compose.foundation.layout.PaddingValues( + horizontal = 12.dp, + vertical = 8.dp, + ), + container = { + androidx.compose.material3.OutlinedTextFieldDefaults.Container( + enabled = true, + isError = false, + interactionSource = searchInteraction, + shape = RoundedCornerShape(10.dp), + ) + }, + ) }, - singleLine = true, - shape = RoundedCornerShape(10.dp), ) if (account != null && !account.isReadOnly) { IconButton(onClick = { showRelayPicker = true }) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt index 85caf36ea..fb6b1b311 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/chats/ChatPane.kt @@ -43,7 +43,6 @@ import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.SnackbarHost import androidx.compose.material3.SnackbarHostState import androidx.compose.material3.Surface @@ -718,17 +717,22 @@ private fun MessageInput( ) } - OutlinedTextField( + // BasicTextField + DecorationBox with custom contentPadding lets the + // field sit at a compact ~40dp minimum on desktop (M3's default + // OutlinedTextField has ~32dp vertical contentPadding baked in, which + // would clip the bodyMedium placeholder at any height below ~52dp). + val messageInteraction = + remember { + androidx.compose.foundation.interaction + .MutableInteractionSource() + } + androidx.compose.foundation.text.BasicTextField( value = messageText, onValueChange = onMessageChange, - // heightIn(min = 44.dp) overrides M3's 56dp mobile-touch min so the - // chat input sits at a desktop-sensible ~44dp when empty; still grows - // to up to ~120dp with content via maxLines = 4. 44 (not 40) keeps the - // bodyMedium placeholder from clipping inside M3's internal padding. modifier = Modifier .weight(1f) - .heightIn(min = 44.dp) + .heightIn(min = 40.dp) .onPreviewKeyEvent { event -> if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false // Cmd+Enter (Mac) or Ctrl+Enter to send @@ -740,16 +744,43 @@ private fun MessageInput( false } }, - textStyle = MaterialTheme.typography.bodyMedium, - placeholder = { - Text( - "Message... (${if (isMacOS) "\u2318" else "Ctrl"}+Enter to send)", - style = MaterialTheme.typography.bodyMedium, + textStyle = + MaterialTheme.typography.bodyMedium + .copy(color = MaterialTheme.colorScheme.onSurface), + cursorBrush = + androidx.compose.ui.graphics + .SolidColor(MaterialTheme.colorScheme.primary), + maxLines = 4, + interactionSource = messageInteraction, + decorationBox = { innerTextField -> + androidx.compose.material3.OutlinedTextFieldDefaults.DecorationBox( + value = messageText, + innerTextField = innerTextField, + enabled = true, + singleLine = false, + visualTransformation = androidx.compose.ui.text.input.VisualTransformation.None, + interactionSource = messageInteraction, + placeholder = { + Text( + "Message\u2026 (${if (isMacOS) "\u2318" else "Ctrl"}+Enter to send)", + style = MaterialTheme.typography.bodyMedium, + ) + }, + contentPadding = + androidx.compose.foundation.layout.PaddingValues( + horizontal = 12.dp, + vertical = 8.dp, + ), + container = { + androidx.compose.material3.OutlinedTextFieldDefaults.Container( + enabled = true, + isError = false, + interactionSource = messageInteraction, + shape = RoundedCornerShape(10.dp), + ) + }, ) }, - singleLine = false, - maxLines = 4, - shape = RoundedCornerShape(10.dp), ) IconButton(