From 1c9491e3dc9592013f2b014b700c99c06f0bf36e Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Tue, 23 Jun 2026 13:45:23 +0200 Subject: [PATCH] feat: text selection, modern sidebar profile, markdown spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Text selection: SelectionContainer wraps both user and assistant messages — long-press to select, copy, share. Sidebar profile: avatar 30→38dp with radial gradient, rounded lg shape, icon buttons with subtle backgrounds, bigger text (15sp W400). Markdown: heading spacing 18→24/20dp, list items better aligned (8dp gap, bullet muted), blockquote bar 4dp inset + 12dp gap. --- .../dev/kaizen/app/chat/ChatComponents.kt | 4 ++ .../main/java/dev/kaizen/app/chat/Markdown.kt | 27 +++++----- .../main/java/dev/kaizen/app/chat/Sidebar.kt | 50 +++++++++++-------- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt index c91c201..a7052bb 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -502,6 +502,7 @@ fun MessageRow( .background(innerHighlight) .border(1.2.dp, borderBrush, shape), ) { + androidx.compose.foundation.text.selection.SelectionContainer { Column { if (isVoiceOnly) { Row( @@ -527,6 +528,7 @@ fun MessageRow( } } } + } } } if (!message.streaming) { @@ -567,6 +569,7 @@ fun MessageRow( KaizenOrb(28.dp, streaming = message.streaming) Spacer(Modifier.width(10.dp)) Box(Modifier.weight(1f).padding(top = 3.dp)) { + androidx.compose.foundation.text.selection.SelectionContainer { Column { if (message.reasoning.isNotBlank()) { ReasoningBlock(message.reasoning, streaming = message.streaming) @@ -587,6 +590,7 @@ fun MessageRow( SourcesBlock(message.sources, query = message.query) } } + } } } if (!message.streaming && !message.thinking && message.content.isNotBlank()) { diff --git a/app/src/main/java/dev/kaizen/app/chat/Markdown.kt b/app/src/main/java/dev/kaizen/app/chat/Markdown.kt index 90803cd..201fad4 100644 --- a/app/src/main/java/dev/kaizen/app/chat/Markdown.kt +++ b/app/src/main/java/dev/kaizen/app/chat/Markdown.kt @@ -459,14 +459,14 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = val isLast = idx == blocks.lastIndex when (block) { is MdBlock.Heading -> { - if (idx > 0) Spacer(Modifier.height(18.dp)) + if (idx > 0) Spacer(Modifier.height(if (block.level == 1) 24.dp else 20.dp)) val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor) ClickableMarkdownText(rendered, context, cs.onBackground, fontSize = when (block.level) { 1 -> 26.sp; 2 -> 22.sp; else -> 18.sp }, fontWeight = when (block.level) { 1 -> FontWeight.W600; 2 -> FontWeight.W600; else -> FontWeight.W500 }, lineHeight = when (block.level) { 1 -> 34.sp; 2 -> 30.sp; else -> 26.sp }, ) - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(10.dp)) } is MdBlock.CodeFence -> { @@ -573,21 +573,22 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = } is MdBlock.ListBlock -> { - if (idx > 0) Spacer(Modifier.height(8.dp)) + if (idx > 0) Spacer(Modifier.height(10.dp)) block.items.forEachIndexed { itemIdx, item -> - Row(Modifier.fillMaxWidth().padding(start = 6.dp, bottom = 6.dp)) { + Row(Modifier.fillMaxWidth().padding(start = 4.dp, bottom = 8.dp)) { if (item.checked != null) { val checkColor = if (item.checked) linkColor else cs.onSurfaceVariant.copy(alpha = 0.4f) val checkIcon = if (item.checked) KaizenIcons.Check else KaizenIcons.Square - Icon(checkIcon, null, tint = checkColor, modifier = Modifier.size(18.dp).padding(top = 3.dp)) - Spacer(Modifier.width(6.dp)) + Icon(checkIcon, null, tint = checkColor, modifier = Modifier.size(18.dp).padding(top = 4.dp)) + Spacer(Modifier.width(8.dp)) } else { val bullet = if (item.ordered) "${item.index}." else "•" Text( bullet, - color = cs.onSurfaceVariant, + color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 16.sp, - modifier = Modifier.width(if (item.ordered) 24.dp else 16.dp), + fontWeight = FontWeight.W300, + modifier = Modifier.width(if (item.ordered) 24.dp else 18.dp).padding(top = 1.dp), ) } val appendCursor = isLast && itemIdx == block.items.lastIndex @@ -598,7 +599,7 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = ) } } - Spacer(Modifier.height(8.dp)) + Spacer(Modifier.height(10.dp)) } is MdBlock.HorizontalRule -> { @@ -613,21 +614,21 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier = } is MdBlock.Blockquote -> { - if (idx > 0) Spacer(Modifier.height(12.dp)) + if (idx > 0) Spacer(Modifier.height(14.dp)) Row( Modifier .fillMaxWidth() .height(IntrinsicSize.Min) - .padding(start = 2.dp) + .padding(start = 4.dp) ) { Box( Modifier .width(3.dp) .fillMaxHeight() .clip(RoundedCornerShape(1.5.dp)) - .background(cs.primary.copy(alpha = 0.5f)) + .background(cs.primary.copy(alpha = 0.45f)) ) - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(12.dp)) val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onSurfaceVariant, inlineCodeColor, inlineCodeBg, linkColor) ClickableMarkdownText(rendered, context, cs.onSurfaceVariant, fontSize = 15.sp, lineHeight = 24.sp, diff --git a/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt b/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt index e47a1f6..180e930 100644 --- a/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt +++ b/app/src/main/java/dev/kaizen/app/chat/Sidebar.kt @@ -286,58 +286,68 @@ fun KaizenSidebar( Row( modifier = Modifier .fillMaxWidth() - .clip(KaizenShapes.md) - .kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 12.dp) + .clip(KaizenShapes.lg) + .kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 20.dp) .background(if (isDark) Color(0x14FFFFFF) else Color(0x0A000000)) - .border(0.8.dp, cardBorder, KaizenShapes.md) + .border(0.8.dp, cardBorder, KaizenShapes.lg) .innerTopHighlight() .clickable { onOpenSettings() } - .padding(horizontal = 12.dp, vertical = 10.dp), + .padding(horizontal = 14.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically ) { Box( modifier = Modifier - .size(30.dp) + .size(38.dp) .clip(KaizenShapes.circle) .background( - Brush.verticalGradient( - listOf(accent.primary.copy(alpha = 0.8f), accent.primary.copy(alpha = 0.6f)) + Brush.radialGradient( + listOf(accent.primary.copy(alpha = 0.9f), accent.primaryDeep.copy(alpha = 0.75f)) ) ), contentAlignment = Alignment.Center ) { Text( userName.firstOrNull()?.toString()?.uppercase() ?: "A", - color = Color.White, fontSize = 13.sp, fontWeight = FontWeight.Bold + color = Color.White, fontSize = 15.sp, fontWeight = FontWeight.W600 ) } - Spacer(Modifier.width(10.dp)) + Spacer(Modifier.width(12.dp)) Text( userName, color = cs.onBackground, - fontSize = 14.sp, - fontWeight = FontWeight.Medium, + fontSize = 15.sp, + fontWeight = FontWeight.W400, modifier = Modifier.weight(1f), ) - Icon( - KaizenIcons.Settings, - stringResource(R.string.sidebar_settings), - tint = cs.onSurfaceVariant.copy(alpha = 0.45f), - modifier = Modifier.size(17.dp), - ) + Box( + modifier = Modifier + .size(32.dp) + .clip(KaizenShapes.circle) + .background(if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)) + .clickable { onOpenSettings() }, + contentAlignment = Alignment.Center, + ) { + Icon( + KaizenIcons.Settings, + stringResource(R.string.sidebar_settings), + tint = cs.onSurfaceVariant.copy(alpha = 0.55f), + modifier = Modifier.size(16.dp), + ) + } Spacer(Modifier.width(6.dp)) Box( modifier = Modifier - .size(28.dp) + .size(32.dp) .clip(KaizenShapes.circle) + .background(if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)) .clickable { onLogout() }, contentAlignment = Alignment.Center, ) { Icon( KaizenIcons.LogOut, stringResource(R.string.sidebar_logout), - tint = cs.onSurfaceVariant.copy(alpha = 0.45f), - modifier = Modifier.size(15.dp), + tint = cs.onSurfaceVariant.copy(alpha = 0.55f), + modifier = Modifier.size(16.dp), ) } }