fix(desktop): hover-state polish + Search width cap + reserve reaction-icon space
NoteCard hover (header+text inner clickable + avatar/name chip): - Clip(RoundedCornerShape(8.dp)) before .clickable on the header+text Column so the ripple rounds instead of cutting a hard rectangle. - Avatar+name chip gets a stadium clip (RoundedCornerShape(100.dp)) before its clickable — matches how Slack / Notion / Linear render user-pill hover states. Chat bubble hover (ChatBubbleLayout): - combinedClickable was applied outside the Surface's shape clipping, so the ripple ignored ChatBubbleShapeMe/Them. Moved the shape into a named val and added Modifier.clip(bubbleShape) ahead of combinedClickable. Hover now rounds with the bubble. Chat reaction icon (ChatPane detailRow): - On hover the "add reaction" icon used to conditionally appear and shift every other element in the detail row, causing the whole list to reflow up or down. Wrapped in a Box with Modifier.alpha that fades in/out — the icon is always laid out so the row stays fixed. Button is disabled when not hovered so it's click-through when invisible. Search text field: - Now padded by LocalReadingSidePadding so it stays inside the 720dp reading column on wide displays (previously the search Row's fillMaxWidth spanned the whole window after the ReadingColumn refactor). - Bumped height from 40dp → 44dp. M3 OutlinedTextField has ~16dp of internal vertical content padding that was clipping the bodyMedium placeholder at 40dp. Same change for the chat input. https://claude.ai/code/session_01NufduPfZvYQVYwLkbCjCUo
This commit is contained in:
@@ -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(
|
||||
|
||||
+35
-32
@@ -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
|
||||
|
||||
+10
-4
@@ -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
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user