diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/chat/ChatBubbleLayout.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/chat/ChatBubbleLayout.kt index bd92d401a..fd479141e 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/chat/ChatBubbleLayout.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/ui/chat/ChatBubbleLayout.kt @@ -34,6 +34,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.compositeOver import com.vitorpamplona.amethyst.commons.ui.theme.ChatBubbleMaxSizeModifier @@ -128,18 +129,23 @@ fun ChatBubbleLayout( ) } + val bubbleShape = if (isLoggedInUser) ChatBubbleShapeMe else ChatBubbleShapeThem + // Clip the hover/ripple to the bubble shape — combinedClickable otherwise + // paints a rectangular ripple that ignores the Surface's rounded corners. val clickableModifier = - remember { - Modifier.combinedClickable( - onClick = { - if (!onClick()) { - if (!isComplete) { - showDetails.value = !showDetails.value + remember(bubbleShape) { + Modifier + .clip(bubbleShape) + .combinedClickable( + onClick = { + if (!onClick()) { + if (!isComplete) { + showDetails.value = !showDetails.value + } } - } - }, - onLongClick = { popupExpanded.value = true }, - ) + }, + onLongClick = { popupExpanded.value = true }, + ) } Row( @@ -148,7 +154,7 @@ fun ChatBubbleLayout( ) { Surface( color = bgColor.value, - shape = if (isLoggedInUser) ChatBubbleShapeMe else ChatBubbleShapeThem, + shape = bubbleShape, modifier = clickableModifier, ) { Column(modifier = MessageBubbleLimits, verticalArrangement = ChatRowColSpacing5dp) { 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 b895a80ed..b0c5e4ecb 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 @@ -358,9 +358,10 @@ fun SearchScreen( Spacer(Modifier.height(16.dp)) - // Search bar with advanced toggle + // Search bar with advanced toggle — honors the reading width cap so it + // stays centered with the rest of the screen's content on wide windows. Row( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp), ) { @@ -370,10 +371,12 @@ fun SearchScreen( textFieldValue = it state.updateFromText(it.text) }, - // .height(40.dp) overrides M3's 56dp min-height (intended for + // .height(44.dp) overrides M3's 56dp min-height (intended for // mobile touch) — desktop inputs should feel closer to Slack / - // Raycast / VS Code at ~40dp. - modifier = Modifier.weight(1f).height(40.dp).focusRequester(focusRequester), + // 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( 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 261934e09..85caf36ea 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 @@ -63,6 +63,7 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draganddrop.DragAndDropEvent import androidx.compose.ui.draganddrop.DragAndDropTarget +import androidx.compose.ui.draw.alpha import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType import androidx.compose.ui.input.key.isCtrlPressed @@ -560,35 +561,36 @@ private fun MessageWithReactions( } } - // AddReaction icon on hover - if (showIcon) { - Box { - IconButton( - onClick = { showPicker = !showPicker }, - modifier = Modifier.size(20.dp), - ) { - Icon( - symbol = MaterialSymbols.AddReaction, - contentDescription = "React", - modifier = Modifier.size(14.dp), - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + // AddReaction icon: always laid out to reserve space (so hover + // doesn't reflow the detail row and shift the whole list); + // alpha fades in/out based on hover. + Box(modifier = Modifier.alpha(if (showIcon) 1f else 0f)) { + IconButton( + onClick = { showPicker = !showPicker }, + enabled = showIcon, + modifier = Modifier.size(20.dp), + ) { + Icon( + symbol = MaterialSymbols.AddReaction, + contentDescription = "React", + modifier = Modifier.size(14.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } - if (showPicker) { - Popup( - alignment = Alignment.TopCenter, - offset = IntOffset(0, -44), - onDismissRequest = { showPicker = false }, - properties = PopupProperties(focusable = true), - ) { - ReactionBar( - onReaction = { emoji -> - onReaction(emoji) - showPicker = false - }, - ) - } + if (showPicker) { + Popup( + alignment = Alignment.TopCenter, + offset = IntOffset(0, -44), + onDismissRequest = { showPicker = false }, + properties = PopupProperties(focusable = true), + ) { + ReactionBar( + onReaction = { emoji -> + onReaction(emoji) + showPicker = false + }, + ) } } } @@ -719,13 +721,14 @@ private fun MessageInput( OutlinedTextField( value = messageText, onValueChange = onMessageChange, - // heightIn(min = 40.dp) overrides M3's 56dp mobile-touch min so the - // chat input sits at a desktop-sensible 40dp when empty; still grows - // to up to ~120dp with content via maxLines = 4. + // 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 = 40.dp) + .heightIn(min = 44.dp) .onPreviewKeyEvent { event -> if (event.type != KeyEventType.KeyDown) return@onPreviewKeyEvent false // Cmd+Enter (Mac) or Ctrl+Enter to send diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt index 4f87e08cd..8a966f695 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/note/NoteCard.kt @@ -165,11 +165,14 @@ fun NoteCard( elevation = CardDefaults.cardElevation(defaultElevation = 1.dp), ) { Column(modifier = Modifier.padding(12.dp)) { - // Header + text area — clickable to navigate to thread + // Header + text area — clickable to navigate to thread. Clip BEFORE + // clickable so the ripple/hover fill is rounded, not a hard rectangle. Column( modifier = if (onClick != null) { - Modifier.clickable { onClick() } + Modifier + .clip(RoundedCornerShape(8.dp)) + .clickable { onClick() } } else { Modifier }, @@ -179,12 +182,15 @@ fun NoteCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - // Author with avatar + // Author with avatar — stadium-shaped hover to match the + // avatar+name chip's visual shape. Row( verticalAlignment = Alignment.CenterVertically, modifier = if (onAuthorClick != null) { - Modifier.clickable { onAuthorClick(note.pubKeyHex) } + Modifier + .clip(RoundedCornerShape(100.dp)) + .clickable { onAuthorClick(note.pubKeyHex) } } else { Modifier },