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:
Claude
2026-04-24 15:55:57 +00:00
parent 3b9ef980a6
commit 7da69fbaee
4 changed files with 70 additions and 52 deletions
@@ -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(
@@ -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
@@ -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
},