fix scroll buttons + restyle sidebar context menu

Scroll buttons: only one shown at a time based on direction
(up when scrolled down, down when scrolled up), hidden during
streaming. Mutually exclusive instead of both visible.

Sidebar menu: replaced default Material3 DropdownMenu/MenuItem
with custom styled menu — dark surface color (1A2030 dark,
F8F9FB light), KaizenShapes.md rounded corners, 12dp shadow
elevation, custom MenuRow composable with proper spacing.
This commit is contained in:
Bruno Deanoz 2026-06-22 01:05:35 +02:00
parent f62cc4b7ac
commit fb269731b7
2 changed files with 64 additions and 78 deletions

View file

@ -569,27 +569,24 @@ fun ChatScreen(
} }
} }
// Scroll buttons if (!isStreaming) {
val showScrollUp by remember { derivedStateOf { listState.firstVisibleItemIndex > 2 } } val atTop by remember { derivedStateOf { listState.firstVisibleItemIndex <= 1 } }
val showScrollDown by remember { derivedStateOf { val atBottom by remember { derivedStateOf {
val info = listState.layoutInfo val info = listState.layoutInfo
info.visibleItemsInfo.lastOrNull()?.index != info.totalItemsCount - 1 info.visibleItemsInfo.lastOrNull()?.index == info.totalItemsCount - 1
}} }}
val canScrollUp by remember { derivedStateOf { listState.canScrollBackward } }
val canScrollDown by remember { derivedStateOf { listState.canScrollForward } }
if (showScrollUp || showScrollDown) { if (!atTop && canScrollUp) {
Column(
modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
if (showScrollUp) {
GlassSurface( GlassSurface(
shape = KaizenShapes.circle, shape = KaizenShapes.circle,
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()), tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
shadowStack = KaizenShadows.level1, shadowStack = KaizenShadows.level1,
cornerRadius = 18.dp, cornerRadius = 18.dp,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp, bottom = 24.dp)
.size(36.dp) .size(36.dp)
.clickable { scope.launch { listState.animateScrollToItem(0) } }, .clickable { scope.launch { listState.animateScrollToItem(0) } },
) { ) {
@ -597,14 +594,15 @@ fun ChatScreen(
Icon(Icons.Rounded.KeyboardArrowUp, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp)) Icon(Icons.Rounded.KeyboardArrowUp, null, tint = MaterialTheme.colorScheme.onBackground, modifier = Modifier.size(20.dp))
} }
} }
} } else if (!atBottom && canScrollDown) {
if (showScrollDown) {
GlassSurface( GlassSurface(
shape = KaizenShapes.circle, shape = KaizenShapes.circle,
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()), tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
shadowStack = KaizenShadows.level1, shadowStack = KaizenShadows.level1,
cornerRadius = 18.dp, cornerRadius = 18.dp,
modifier = Modifier modifier = Modifier
.align(Alignment.CenterEnd)
.padding(end = 8.dp, top = 24.dp)
.size(36.dp) .size(36.dp)
.clickable { scope.launch { listState.animateScrollToItem(messages.size - 1) } }, .clickable { scope.launch { listState.animateScrollToItem(messages.size - 1) } },
) { ) {
@ -615,7 +613,6 @@ fun ChatScreen(
} }
} }
} }
}
// 2. Floating Top Menu Button // 2. Floating Top Menu Button
val scrimColor = MaterialTheme.colorScheme.background val scrimColor = MaterialTheme.colorScheme.background

View file

@ -34,8 +34,7 @@ import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Settings import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.AlertDialog import androidx.compose.material3.AlertDialog
import androidx.compose.material3.DropdownMenu import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -423,48 +422,38 @@ private fun HistoryItemRow(
} }
} }
DropdownMenu( androidx.compose.material3.DropdownMenu(
expanded = showMenu, expanded = showMenu,
onDismissRequest = { showMenu = false }, onDismissRequest = { showMenu = false },
containerColor = if (isDark) Color(0xFF1A2030) else Color(0xFFF8F9FB),
shadowElevation = 12.dp,
shape = KaizenShapes.md,
) { ) {
if (!item.locked) { if (!item.locked) {
DropdownMenuItem( MenuRow(Icons.Rounded.Edit, stringResource(R.string.sidebar_rename), cs.onBackground) { showMenu = false; onRename() }
text = { Text(stringResource(R.string.sidebar_rename), fontSize = 14.sp) },
onClick = { showMenu = false; onRename() },
leadingIcon = { Icon(Icons.Rounded.Edit, null, modifier = Modifier.size(18.dp)) },
)
} }
DropdownMenuItem( MenuRow(Icons.Rounded.PushPin, stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin), cs.onBackground) { showMenu = false; onTogglePin() }
text = { MenuRow(
Text(
stringResource(if (item.pinned) R.string.sidebar_unpin else R.string.sidebar_pin),
fontSize = 14.sp,
)
},
onClick = { showMenu = false; onTogglePin() },
leadingIcon = { Icon(Icons.Rounded.PushPin, null, modifier = Modifier.size(18.dp)) },
)
DropdownMenuItem(
text = {
Text(
stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock),
fontSize = 14.sp,
)
},
onClick = { showMenu = false; onToggleLock() },
leadingIcon = {
Icon(
if (item.locked) Icons.Rounded.LockOpen else Icons.Rounded.Lock, if (item.locked) Icons.Rounded.LockOpen else Icons.Rounded.Lock,
null, stringResource(if (item.locked) R.string.sidebar_unlock else R.string.sidebar_lock),
modifier = Modifier.size(18.dp), cs.onBackground,
) ) { showMenu = false; onToggleLock() }
}, MenuRow(Icons.Rounded.Delete, stringResource(R.string.sidebar_delete), Color(0xFFEF4444)) { showMenu = false; onDelete() }
)
DropdownMenuItem(
text = { Text(stringResource(R.string.sidebar_delete), fontSize = 14.sp, color = Color(0xFFEF4444)) },
onClick = { showMenu = false; onDelete() },
leadingIcon = { Icon(Icons.Rounded.Delete, null, tint = Color(0xFFEF4444), modifier = Modifier.size(18.dp)) },
)
} }
} }
} }
@Composable
private fun MenuRow(icon: ImageVector, label: String, tint: Color, onClick: () -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(icon, null, tint = tint.copy(alpha = 0.7f), modifier = Modifier.size(18.dp))
Spacer(Modifier.width(12.dp))
Text(label, color = tint, fontSize = 14.sp, fontWeight = FontWeight.Medium)
}
}