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 35c4bc6..cf198b3 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -14,6 +14,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.horizontalScroll import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -64,31 +65,77 @@ import java.time.format.DateTimeFormatter import java.util.Locale @Composable -fun KaizenHeader() { +fun KaizenHeader(modifier: Modifier = Modifier) { val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + + val glassBg = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf( + Color(0xFF1E293B).copy(alpha = 0.65f), // Slate 800 with 65% opacity + Color(0xFF0F172A).copy(alpha = 0.75f) // Slate 900 with 75% opacity + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.85f), + Color(0xFFF1F5F9).copy(alpha = 0.75f) // Slate 100 with 75% opacity + ) + ) + } + } + + val glassBorder = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.16f), // Specular light highlight on top + Color.White.copy(alpha = 0.03f) // Subtle dark fade on bottom + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.55f), + Color.Black.copy(alpha = 0.05f) + ) + ) + } + } + Row( - Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 12.dp), + modifier + .fillMaxWidth() + .padding(horizontal = 16.dp, vertical = 10.dp) + .clip(RoundedCornerShape(24.dp)) + .background(glassBg) + .border(1.2.dp, glassBorder, RoundedCornerShape(24.dp)) + .padding(horizontal = 16.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { KaizenOrb(24.dp) - Spacer(Modifier.width(9.dp)) + Spacer(Modifier.width(10.dp)) Text("Kaizen", color = cs.onBackground, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) } } @Composable -fun EmptyHero(userName: String, onPick: (String) -> Unit) { +fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = Modifier) { val cs = MaterialTheme.colorScheme val now = remember { LocalDateTime.now() } val dateLine = remember { now.format(DateTimeFormatter.ofPattern("EEE, d. MMMM · HH:mm", Locale.GERMAN)) } - // One grouped, scrollable block so the keyboard never squeezes or clips it. Column( - Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 24.dp), + modifier = modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(horizontal = 24.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(Modifier.height(64.dp)) + Spacer(Modifier.height(88.dp)) // Clear the floating glass header KaizenOrb(104.dp) Spacer(Modifier.height(24.dp)) Text("${greeting(now.hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp) @@ -98,13 +145,22 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit) { Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp) Spacer(Modifier.height(32.dp)) SuggestionPills(onPick) - Spacer(Modifier.height(24.dp)) + Spacer(Modifier.height(142.dp)) // Clear the floating bottom dock } } @Composable private fun SuggestionPills(onPick: (String) -> Unit) { val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + + val glassBg = if (isDark) cs.surface.copy(alpha = 0.40f) else cs.surface.copy(alpha = 0.85f) + val glassBorder = if (isDark) { + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.10f), Color.White.copy(alpha = 0.02f))) + } else { + Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f))) + } + FlowRow( Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.CenterHorizontally), @@ -114,8 +170,8 @@ private fun SuggestionPills(onPick: (String) -> Unit) { Row( Modifier .clip(CircleShape) - .background(cs.surface.copy(alpha = 0.82f)) - .border(1.dp, cs.outline, CircleShape) + .background(glassBg) + .border(1.dp, glassBorder, CircleShape) .clickable { onPick(suggestion.prompt) } .padding(horizontal = 16.dp, vertical = 11.dp), verticalAlignment = Alignment.CenterVertically, @@ -129,19 +185,49 @@ private fun SuggestionPills(onPick: (String) -> Unit) { } @Composable -fun ModePillsRow(selected: String, onSelect: (String) -> Unit) { +fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifier = Modifier) { val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + + val glassBorderInactive = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf(Color.White.copy(alpha = 0.12f), Color.White.copy(alpha = 0.02f)) + ) + } else { + Brush.verticalGradient( + listOf(Color.White.copy(alpha = 0.45f), Color.Black.copy(alpha = 0.04f)) + ) + } + } + + val glassBorderActive = remember(isDark) { + Brush.verticalGradient( + listOf(Amber.copy(alpha = 0.50f), Amber.copy(alpha = 0.15f)) + ) + } + Row( - Modifier.fillMaxWidth().horizontalScroll(rememberScrollState()).padding(horizontal = 14.dp), + modifier + .fillMaxWidth() + .horizontalScroll(rememberScrollState()) + .padding(horizontal = 14.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), ) { chatModes.forEach { mode -> val active = mode.label == selected + val backgroundFill = if (active) { + Amber.copy(alpha = 0.16f) + } else { + if (isDark) Color(0x12FFFFFF) else Color(0x82FFFFFF) + } + val borderBrush = if (active) glassBorderActive else glassBorderInactive + Row( Modifier .clip(CircleShape) - .background(if (active) cs.primary.copy(alpha = 0.16f) else cs.surface.copy(alpha = 0.7f)) - .border(1.dp, if (active) cs.primary.copy(alpha = 0.40f) else cs.outline, CircleShape) + .background(backgroundFill) + .border(1.2.dp, borderBrush, CircleShape) .clickable { onSelect(mode.label) } .padding(horizontal = 13.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, @@ -149,7 +235,7 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit) { Icon( mode.icon, null, - tint = if (active) cs.primary else cs.onSurfaceVariant, + tint = if (active) Amber else cs.onSurfaceVariant, modifier = Modifier.size(16.dp), ) Spacer(Modifier.width(6.dp)) @@ -167,15 +253,36 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit) { @Composable fun MessageRow(message: Message) { val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + if (message.role == Role.User) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { val shape = RoundedCornerShape(20.dp, 20.dp, 6.dp, 20.dp) + + // Specular highlighting and gradient matching the oklch brand colors + val borderBrush = remember { + Brush.verticalGradient( + listOf( + Amber.copy(alpha = 0.45f), + AmberDeep.copy(alpha = 0.15f) + ) + ) + } + val bgBrush = remember { + Brush.verticalGradient( + listOf( + Amber.copy(alpha = 0.18f), + AmberDeep.copy(alpha = 0.08f) + ) + ) + } + Box( Modifier .widthIn(max = 300.dp) .clip(shape) - .background(Amber.copy(alpha = 0.16f)) - .border(1.dp, Amber.copy(alpha = 0.30f), shape) + .background(bgBrush) + .border(1.2.dp, borderBrush, shape) .padding(horizontal = 16.dp, vertical = 11.dp), ) { Text(message.content, color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp) @@ -232,18 +339,62 @@ fun ChatInput( modifier: Modifier = Modifier, ) { val cs = MaterialTheme.colorScheme + val isDark = isSystemInDarkTheme() + + val glassBg = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf( + Color(0xFF1E293B).copy(alpha = 0.72f), // Slate 800 translucent + Color(0xFF0F172A).copy(alpha = 0.85f) // Slate 900 translucent + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.90f), + Color(0xFFEEF2F6).copy(alpha = 0.80f) + ) + ) + } + } + + val glassBorder = remember(isDark) { + if (isDark) { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.18f), + Color.White.copy(alpha = 0.04f) + ) + ) + } else { + Brush.verticalGradient( + listOf( + Color.White.copy(alpha = 0.60f), + Color.Black.copy(alpha = 0.06f) + ) + ) + } + } + Row( modifier .fillMaxWidth() .padding(horizontal = 12.dp) .clip(RoundedCornerShape(28.dp)) - .background(cs.surfaceVariant.copy(alpha = 0.94f)) - .border(1.dp, cs.outline, RoundedCornerShape(28.dp)) + .background(glassBg) + .border(1.2.dp, glassBorder, RoundedCornerShape(28.dp)) .padding(horizontal = 8.dp, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { GhostIconButton(Icons.Rounded.Add, "Anhang") - Box(Modifier.weight(1f).heightIn(min = 36.dp).padding(horizontal = 4.dp)) { + Box( + Modifier + .weight(1f) + .heightIn(min = 36.dp) + .padding(horizontal = 4.dp), + contentAlignment = Alignment.CenterStart + ) { if (value.isEmpty()) { Text("Nachricht eingeben …", color = cs.onSurfaceVariant, fontSize = 16.sp) } diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index 3608dd2..33d28ca 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -22,6 +22,7 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import dev.kaizen.app.haptics.rememberHaptics @@ -78,23 +79,47 @@ fun ChatScreen(userName: String = "Bruno") { } MeshBackground { - Column(Modifier.fillMaxSize().statusBarsPadding()) { - KaizenHeader() - Box(Modifier.weight(1f).fillMaxWidth()) { - if (messages.isEmpty()) { - EmptyHero(userName = userName, onPick = { haptics.tick(); send(it) }) - } else { - LazyColumn( - state = listState, - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 16.dp, vertical = 16.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - items(messages, key = { it.id }) { message -> MessageRow(message) } - } + Box(Modifier.fillMaxSize()) { + // 1. Scrollable Chat Content + // Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating glass panels. + if (messages.isEmpty()) { + EmptyHero( + userName = userName, + onPick = { haptics.tick(); send(it) }, + modifier = Modifier.imePadding() // Viewport shrinks so the scrollable Column adapts without warping + ) + } else { + LazyColumn( + state = listState, + modifier = Modifier + .fillMaxSize() + .imePadding(), // Adjusts viewport height so scrollable content stops exactly above the keyboard + contentPadding = PaddingValues( + top = 92.dp, // Leaves luxurious room for the floating KaizenHeader + status bars + bottom = 152.dp, // Leaves luxurious room for the floating ModePills + ChatInput + bottom margins + start = 16.dp, + end = 16.dp + ), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + items(messages, key = { it.id }) { message -> MessageRow(message) } } } - Column(Modifier.navigationBarsPadding().imePadding()) { + + // 2. Floating Header (Translucent Frosted Glass) + KaizenHeader( + modifier = Modifier + .align(Alignment.TopCenter) + .statusBarsPadding() + ) + + // 3. Floating Bottom Control Dock + Column( + modifier = Modifier + .align(Alignment.BottomCenter) + .navigationBarsPadding() + .imePadding() + ) { ModePillsRow(selected = selectedMode, onSelect = { selectedMode = it; haptics.tick() }) Spacer(Modifier.height(10.dp)) ChatInput( @@ -103,6 +128,7 @@ fun ChatScreen(userName: String = "Bruno") { onSend = { send(input) }, enabled = !isStreaming, ) + Spacer(Modifier.height(12.dp)) // Extra breathing margin at the bottom of the floating card } } }