diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt index 5626ff06..b802ab1a 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/chat/ChatView.kt @@ -89,6 +89,7 @@ import io.askimo.ui.common.preferences.ApplicationPreferences import io.askimo.ui.common.theme.AppComponents import io.askimo.ui.common.theme.LocalBackgroundActive import io.askimo.ui.common.theme.ThemePreferences +import io.askimo.ui.common.ui.TooltipPlacement import io.askimo.ui.common.ui.themedTooltip import io.askimo.ui.service.AvatarService import io.askimo.ui.session.manageDirectivesDialog @@ -494,10 +495,10 @@ fun chatView( // Session title themedTooltip( - text = sessionTitle ?: "New Chat", + text = sessionTitle, ) { Text( - text = sessionTitle ?: "New Chat", + text = sessionTitle, style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, @@ -597,6 +598,7 @@ fun chatView( availableDirectives.forEach { directive -> themedTooltip( text = "${directive.name}\n${directive.content}", + placement = TooltipPlacement.LEFT, ) { DropdownMenuItem( text = { @@ -706,7 +708,7 @@ fun chatView( } } - if (sessionId != null) { + if (sessionId != null && messages.isNotEmpty()) { sessionActionsMenu( sessionId = sessionId, onRenameSession = onRenameSession, diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/common/ui/Tooltip.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/common/ui/Tooltip.kt index 3602fc1d..e6bc3519 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/common/ui/Tooltip.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/common/ui/Tooltip.kt @@ -37,6 +37,18 @@ import kotlinx.coroutines.delay /** Maximum number of characters shown in a tooltip before truncating with "…". */ const val TOOLTIP_MAX_CHARS = 1200 +/** Controls which side the tooltip appears on relative to its anchor. */ +enum class TooltipPlacement { + /** Default: above when in bottom half of screen, below otherwise. */ + AUTO, + + /** Always to the left of the anchor. */ + LEFT, + + /** Always to the right of the anchor. */ + RIGHT, +} + /** * A reusable tooltip component that follows the application theme. * Automatically positions itself to avoid overlapping with the component and screen edges. @@ -47,6 +59,7 @@ const val TOOLTIP_MAX_CHARS = 1200 * * @param text The text to display in the tooltip * @param maxChars Maximum characters to show before truncating (default [TOOLTIP_MAX_CHARS]) + * @param placement Controls where the tooltip appears relative to the anchor (default [TooltipPlacement.AUTO]) * @param modifier Optional modifier for the TooltipBox * @param content The composable content that the tooltip wraps */ @@ -55,6 +68,7 @@ const val TOOLTIP_MAX_CHARS = 1200 fun themedTooltip( text: String, maxChars: Int = TOOLTIP_MAX_CHARS, + placement: TooltipPlacement = TooltipPlacement.AUTO, modifier: Modifier = Modifier, content: @Composable () -> Unit, ) { @@ -84,8 +98,8 @@ fun themedTooltip( Box { TooltipBox( - positionProvider = remember(containerHeight) { - SmartTooltipPositionProvider(maxHeightPx = containerHeight) + positionProvider = remember(containerHeight, placement) { + SmartTooltipPositionProvider(maxHeightPx = containerHeight, placement = placement) }, tooltip = { Surface( @@ -130,6 +144,7 @@ fun themedTooltip( */ private class SmartTooltipPositionProvider( private val maxHeightPx: Float, + private val placement: TooltipPlacement = TooltipPlacement.AUTO, ) : PopupPositionProvider { override fun calculatePosition( anchorBounds: IntRect, @@ -139,21 +154,29 @@ private class SmartTooltipPositionProvider( ): IntOffset { val spacing = 8 - val isInBottomHalf = anchorBounds.top > maxHeightPx / 2 - - return when { - isInBottomHalf -> IntOffset( - x = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, - y = anchorBounds.top - popupContentSize.height - spacing, - ) - !isInBottomHalf -> IntOffset( - x = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, - y = anchorBounds.bottom + spacing, + return when (placement) { + TooltipPlacement.LEFT -> IntOffset( + x = anchorBounds.left - popupContentSize.width - spacing, + y = anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2, ) - else -> IntOffset( + TooltipPlacement.RIGHT -> IntOffset( x = anchorBounds.right + spacing, y = anchorBounds.top + (anchorBounds.height - popupContentSize.height) / 2, ) + TooltipPlacement.AUTO -> { + val isInBottomHalf = anchorBounds.top > maxHeightPx / 2 + if (isInBottomHalf) { + IntOffset( + x = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, + y = anchorBounds.top - popupContentSize.height - spacing, + ) + } else { + IntOffset( + x = anchorBounds.left + (anchorBounds.width - popupContentSize.width) / 2, + y = anchorBounds.bottom + spacing, + ) + } + } } } } diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectView.kt index 952c9677..934f38de 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectView.kt @@ -12,7 +12,9 @@ import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.ScrollbarStyle import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -176,14 +178,24 @@ fun projectView( .fillMaxWidth() .padding(bottom = 16.dp), horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + verticalAlignment = Alignment.Top, ) { - Text( - text = currentProject.name, - style = MaterialTheme.typography.headlineMedium, - fontWeight = FontWeight.Bold, - color = MaterialTheme.colorScheme.onSurface, - ) + Column(modifier = Modifier.weight(1f).padding(end = 8.dp)) { + Text( + text = currentProject.name, + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onSurface, + ) + currentProject.description?.takeIf { it.isNotBlank() }?.let { desc -> + Text( + text = desc, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp), + ) + } + } Box { themedTooltip(text = stringResource("project.menu.tooltip")) { @@ -227,16 +239,6 @@ fun projectView( } } - // Project Description (if exists) - currentProject.description?.let { desc -> - Text( - text = desc, - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.padding(bottom = 16.dp), - ) - } - // Knowledge Sources Panel knowledgeSourcesPanel( currentProject = currentProject, @@ -405,10 +407,13 @@ private fun sessionCard( var showMenu by remember { mutableStateOf(false) } var showNewProjectDialog by remember { mutableStateOf(false) } var sessionIdToMove by remember { mutableStateOf(null) } + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() Card( modifier = Modifier .fillMaxWidth() + .hoverable(interactionSource) .clickableCard(cornerRadius = 8.dp, onClick = onClick), shape = RoundedCornerShape(8.dp), colors = CardDefaults.cardColors( @@ -436,14 +441,16 @@ private fun sessionCard( Column( modifier = Modifier.weight(1f).padding(horizontal = 12.dp), ) { - Text( - text = session.title, - style = MaterialTheme.typography.bodyMedium, - fontWeight = FontWeight.Normal, - color = MaterialTheme.colorScheme.onSurface, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - ) + themedTooltip(text = session.title) { + Text( + text = session.title, + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Normal, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } Text( text = TimeUtil.formatDisplay(session.updatedAt), style = MaterialTheme.typography.bodySmall, @@ -451,53 +458,55 @@ private fun sessionCard( ) } - Box { - IconButton( - onClick = { showMenu = true }, - modifier = Modifier - .size(24.dp) - .pointerHoverIcon(PointerIcon.Hand), - ) { - Icon( - Icons.Default.MoreVert, - contentDescription = "More options", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(18.dp), - ) - } + if (isHovered || showMenu) { + Box { + IconButton( + onClick = { showMenu = true }, + modifier = Modifier + .size(24.dp) + .pointerHoverIcon(PointerIcon.Hand), + ) { + Icon( + Icons.Default.MoreVert, + contentDescription = "More options", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } - AppComponents.dropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }, - ) { - SessionActionMenu.projectViewMenu( - sessionId = session.id, - currentProjectId = currentProject.id, - currentProjectName = currentProject.name, - availableProjects = allProjects, - onExport = { onExportSession(session.id) }, - onRename = { onRenameSession(session.id, session.title) }, - onDelete = { onDeleteSession(session.id) }, - onMoveToNewProject = { - sessionIdToMove = session.id - showNewProjectDialog = true - }, - onMoveToExistingProject = { selectedProject -> - viewModel.moveSessionToProject(session.id, selectedProject.id) - }, - onRemoveFromProject = { - viewModel.removeSessionFromProject(session.id) - // Refresh global sessions list (session now appears in "All Sessions") - EventBus.post( - SessionsRefreshEvent( - reason = "Session ${session.id} removed from project", - ), - ) - }, - onDismiss = { showMenu = false }, - ) - } - } + AppComponents.dropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + SessionActionMenu.projectViewMenu( + sessionId = session.id, + currentProjectId = currentProject.id, + currentProjectName = currentProject.name, + availableProjects = allProjects, + onExport = { onExportSession(session.id) }, + onRename = { onRenameSession(session.id, session.title) }, + onDelete = { onDeleteSession(session.id) }, + onMoveToNewProject = { + sessionIdToMove = session.id + showNewProjectDialog = true + }, + onMoveToExistingProject = { selectedProject -> + viewModel.moveSessionToProject(session.id, selectedProject.id) + }, + onRemoveFromProject = { + viewModel.removeSessionFromProject(session.id) + // Refresh global sessions list (session now appears in "All Sessions") + EventBus.post( + SessionsRefreshEvent( + reason = "Session ${session.id} removed from project", + ), + ) + }, + onDismiss = { showMenu = false }, + ) + } + } // end Box + } // end if (isHovered || showMenu) } } diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectsView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectsView.kt index 51ce891f..2815de76 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectsView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/project/ProjectsView.kt @@ -11,7 +11,9 @@ import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.ScrollbarStyle import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -22,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter @@ -91,7 +94,6 @@ fun projectsView( .fillMaxWidth() .padding(start = 24.dp, end = 36.dp, top = 24.dp, bottom = 24.dp), ) { - // ── Header ──────────────────────────────────────────────────────── Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, @@ -156,84 +158,76 @@ fun projectsView( } } - // ── Scrollable content ──────────────────────────────────────────── - Column( - modifier = Modifier - .weight(1f) - .fillMaxWidth() - .verticalScroll(scrollState), - ) { - when { - viewModel.isLoading -> { - Box( - modifier = Modifier.fillMaxWidth().height(200.dp), - contentAlignment = Alignment.Center, - ) { - CircularProgressIndicator() - } + when { + viewModel.isLoading -> { + Box( + modifier = Modifier.fillMaxWidth().height(200.dp), + contentAlignment = Alignment.Center, + ) { + CircularProgressIndicator() } + } - viewModel.errorMessage != null -> { - Box( - modifier = Modifier.fillMaxWidth().height(200.dp), - contentAlignment = Alignment.Center, + viewModel.errorMessage != null -> { + Box( + modifier = Modifier.fillMaxWidth().height(200.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource("projects.error", viewModel.errorMessage ?: ""), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.error, - ) - TextButton(onClick = { - viewModel.clearError() - viewModel.refresh() - }) { - Text(stringResource("action.retry")) - } + Text( + text = stringResource("projects.error", viewModel.errorMessage ?: ""), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.error, + ) + TextButton(onClick = { + viewModel.clearError() + viewModel.refresh() + }) { + Text(stringResource("action.retry")) } } } + } - viewModel.pagedProjects?.isEmpty == true -> { - Box( - modifier = Modifier.fillMaxWidth().height(200.dp), - contentAlignment = Alignment.Center, + viewModel.pagedProjects?.isEmpty == true -> { + Box( + modifier = Modifier.fillMaxWidth().height(200.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(8.dp), - ) { - Text( - text = stringResource("projects.empty"), - style = MaterialTheme.typography.bodyLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - Text( - text = stringResource("projects.empty.hint"), - style = MaterialTheme.typography.bodyMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + Text( + text = stringResource("projects.empty"), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + Text( + text = stringResource("projects.empty.hint"), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) } } + } - else -> { - val pagedProjects = viewModel.pagedProjects!! - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - pagedProjects.items.forEach { project -> - projectCard( - project = project, - onSelectProject = onSelectProject, - onEditProject = onEditProject, - onDeleteProject = { viewModel.deleteProject(it) }, - ) - } + else -> { + val pagedProjects = viewModel.pagedProjects!! + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + pagedProjects.items.forEach { project -> + projectCard( + project = project, + onSelectProject = onSelectProject, + onEditProject = onEditProject, + onDeleteProject = { viewModel.deleteProject(it) }, + ) } } } @@ -255,7 +249,6 @@ fun projectsView( } // end width-constrained column } // end scrollable column - // ── Scrollbar ───────────────────────────────────────────────────────── VerticalScrollbar( adapter = rememberScrollbarAdapter(scrollState), modifier = Modifier @@ -282,9 +275,14 @@ private fun projectCard( ) { var showMenu by remember { mutableStateOf(false) } var isExpanded by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + val showActions = isHovered || showMenu Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .hoverable(interactionSource), colors = AppComponents.bannerCardColors(), ) { Column( @@ -312,6 +310,8 @@ private fun projectCard( style = MaterialTheme.typography.titleMedium, fontWeight = FontWeight.SemiBold, color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, ) project.description?.let { desc -> Spacer(modifier = Modifier.height(4.dp)) @@ -342,53 +342,58 @@ private fun projectCard( } } - // Menu button - Box { - IconButton( - onClick = { showMenu = true }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) { - Icon( - Icons.Default.MoreVert, - contentDescription = "More options", - tint = MaterialTheme.colorScheme.onSurface, - ) - } + // Menu button — only shown on hover, no space reserved when hidden + if (showActions) { + Box { + IconButton( + onClick = { showMenu = true }, + modifier = Modifier + .size(36.dp) + .pointerHoverIcon(PointerIcon.Hand), + ) { + Icon( + Icons.Default.MoreVert, + contentDescription = "More options", + tint = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.size(20.dp), + ) + } - AppComponents.dropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }, - ) { - DropdownMenuItem( - text = { Text(stringResource("action.edit")) }, - onClick = { - showMenu = false - onEditProject(project.id) - }, - leadingIcon = { - Icon( - Icons.Default.Edit, - contentDescription = null, - tint = MaterialTheme.colorScheme.onSurface, - ) - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) - DropdownMenuItem( - text = { Text(stringResource("action.delete")) }, - onClick = { - showMenu = false - onDeleteProject(project.id) - }, - leadingIcon = { - Icon( - Icons.Default.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - ) - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) + AppComponents.dropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource("action.edit")) }, + onClick = { + showMenu = false + onEditProject(project.id) + }, + leadingIcon = { + Icon( + Icons.Default.Edit, + contentDescription = null, + tint = MaterialTheme.colorScheme.onSurface, + ) + }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + DropdownMenuItem( + text = { Text(stringResource("action.delete")) }, + onClick = { + showMenu = false + onDeleteProject(project.id) + }, + leadingIcon = { + Icon( + Icons.Default.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + } } } } diff --git a/desktop-shared/src/main/kotlin/io/askimo/ui/session/SessionsView.kt b/desktop-shared/src/main/kotlin/io/askimo/ui/session/SessionsView.kt index 2467f433..7308d679 100644 --- a/desktop-shared/src/main/kotlin/io/askimo/ui/session/SessionsView.kt +++ b/desktop-shared/src/main/kotlin/io/askimo/ui/session/SessionsView.kt @@ -7,7 +7,9 @@ package io.askimo.ui.session import androidx.compose.foundation.ScrollbarStyle import androidx.compose.foundation.VerticalScrollbar import androidx.compose.foundation.clickable +import androidx.compose.foundation.hoverable import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsHoveredAsState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -18,6 +20,7 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.rememberScrollbarAdapter @@ -49,12 +52,14 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.input.pointer.PointerIcon import androidx.compose.ui.input.pointer.pointerHoverIcon import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import io.askimo.core.chat.domain.ChatSession import io.askimo.core.util.TimeUtil import io.askimo.ui.common.i18n.stringResource import io.askimo.ui.common.theme.AppComponents import io.askimo.ui.common.theme.ThemePreferences +import io.askimo.ui.common.ui.themedTooltip @Composable fun sessionsView( @@ -257,9 +262,14 @@ private fun sessionCard( onDeleteSession: (String) -> Unit, ) { var showMenu by remember { mutableStateOf(false) } + val interactionSource = remember { MutableInteractionSource() } + val isHovered by interactionSource.collectIsHoveredAsState() + val showActions = isHovered || showMenu Card( - modifier = Modifier.fillMaxWidth(), + modifier = Modifier + .fillMaxWidth() + .hoverable(interactionSource), colors = AppComponents.bannerCardColors(), ) { Row( @@ -269,7 +279,7 @@ private fun sessionCard( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.Top, ) { - // Clickable content area + // Clickable content area — takes full width when not hovered, shrinks to make room for menu Column( modifier = Modifier .weight(1f) @@ -279,12 +289,16 @@ private fun sessionCard( ) { onResumeSession(session.id) } .pointerHoverIcon(PointerIcon.Hand), ) { - Text( - text = session.title, - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - color = MaterialTheme.colorScheme.onSurface, - ) + themedTooltip(text = session.title) { + Text( + text = session.title, + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + color = MaterialTheme.colorScheme.onSurface, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } Spacer(modifier = Modifier.height(8.dp)) @@ -305,38 +319,43 @@ private fun sessionCard( } } - // Menu button - Box { - IconButton( - onClick = { showMenu = true }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) { - Icon( - Icons.Default.MoreVert, - contentDescription = "More options", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - ) - } + // Menu button — only shown on hover, no space reserved when hidden + if (showActions) { + Box { + IconButton( + onClick = { showMenu = true }, + modifier = Modifier + .size(36.dp) + .pointerHoverIcon(PointerIcon.Hand), + ) { + Icon( + Icons.Default.MoreVert, + contentDescription = "More options", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(20.dp), + ) + } - AppComponents.dropdownMenu( - expanded = showMenu, - onDismissRequest = { showMenu = false }, - ) { - DropdownMenuItem( - text = { Text(stringResource("action.delete")) }, - onClick = { - showMenu = false - onDeleteSession(session.id) - }, - leadingIcon = { - Icon( - Icons.Default.Delete, - contentDescription = null, - tint = MaterialTheme.colorScheme.error, - ) - }, - modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), - ) + AppComponents.dropdownMenu( + expanded = showMenu, + onDismissRequest = { showMenu = false }, + ) { + DropdownMenuItem( + text = { Text(stringResource("action.delete")) }, + onClick = { + showMenu = false + onDeleteSession(session.id) + }, + leadingIcon = { + Icon( + Icons.Default.Delete, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + }, + modifier = Modifier.pointerHoverIcon(PointerIcon.Hand), + ) + } } } } diff --git a/desktop/src/main/kotlin/io/askimo/desktop/Main.kt b/desktop/src/main/kotlin/io/askimo/desktop/Main.kt index 6d8ad465..1eec1028 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/Main.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/Main.kt @@ -40,6 +40,7 @@ import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color.Companion.Transparent import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.toComposeImageBitmap import androidx.compose.ui.input.key.onPreviewKeyEvent @@ -720,7 +721,7 @@ fun app(frameWindowScope: FrameWindowScope? = null, windowState: WindowState? = if (backgroundImage is BackgroundImage.None) { MaterialTheme.colorScheme.background } else { - androidx.compose.ui.graphics.Color.Transparent + Transparent }, ), ) { @@ -1695,7 +1696,7 @@ fun mainContent( .fillMaxSize() .background( if (LocalBackgroundActive.current) { - androidx.compose.ui.graphics.Color.Transparent + Transparent } else { MaterialTheme.colorScheme.background }, diff --git a/desktop/src/main/kotlin/io/askimo/desktop/shell/NavigationSidebar.kt b/desktop/src/main/kotlin/io/askimo/desktop/shell/NavigationSidebar.kt index d5e9decd..47f49453 100644 --- a/desktop/src/main/kotlin/io/askimo/desktop/shell/NavigationSidebar.kt +++ b/desktop/src/main/kotlin/io/askimo/desktop/shell/NavigationSidebar.kt @@ -651,29 +651,26 @@ private fun navigationItemLabelWithMenu( style = MaterialTheme.typography.labelLarge, maxLines = 1, overflow = TextOverflow.Ellipsis, - modifier = Modifier.weight(1f, fill = false), + modifier = Modifier.weight(1f), ) - Box( - modifier = Modifier.padding(start = 4.dp), - ) { - IconButton( - onClick = onMenuClick, - modifier = Modifier - .size(24.dp) - .pointerHoverIcon(PointerIcon.Hand), - enabled = isHovered, + if (isHovered) { + Box( + modifier = Modifier.padding(start = 4.dp), ) { - Icon( - Icons.Default.MoreVert, - contentDescription = "More options", - tint = if (isHovered) { - MaterialTheme.colorScheme.onSurfaceVariant - } else { - androidx.compose.ui.graphics.Color.Transparent - }, - modifier = Modifier.size(18.dp), - ) + IconButton( + onClick = onMenuClick, + modifier = Modifier + .size(24.dp) + .pointerHoverIcon(PointerIcon.Hand), + ) { + Icon( + Icons.Default.MoreVert, + contentDescription = "More options", + tint = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.size(18.dp), + ) + } } } } diff --git a/gradle.properties b/gradle.properties index 8804cf5f..64de82bd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ # Project metadata projectGroup=io.askimo -projectVersion=1.2.5 +projectVersion=1.2.25 # About information author=Hai Nguyen