feat(ui): implement tablet optimizations and gemini-style dynamic keyboard suggestion-hiding

This commit is contained in:
Bruno Deanoz 2026-06-18 16:40:18 +02:00
parent e13f6798db
commit e3ea6d241f
2 changed files with 34 additions and 12 deletions

View file

@ -21,10 +21,14 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.ime
import androidx.compose.foundation.layout.isImeVisible
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
@ -129,6 +133,11 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
now.format(DateTimeFormatter.ofPattern("EEE, d. MMMM · HH:mm", Locale.GERMAN))
}
// Dynamic keyboard visibility check (Google Gemini style)
// When the soft keyboard is visible, we completely hide the Suggestion Pills
// to give the user maximum typing space and keep the layout extremely clean.
val isKeyboardOpen = WindowInsets.isImeVisible
Column(
modifier = modifier
.fillMaxSize()
@ -144,8 +153,12 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
Text("Was liegt an?", color = cs.onBackground, fontSize = 32.sp, fontWeight = FontWeight.Bold)
Spacer(Modifier.height(8.dp))
Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp)
Spacer(Modifier.height(32.dp))
SuggestionPills(onPick)
if (!isKeyboardOpen) {
Spacer(Modifier.height(32.dp))
SuggestionPills(onPick)
}
Spacer(Modifier.height(142.dp)) // Clear the floating bottom dock
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
@ -23,6 +24,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import dev.kaizen.app.haptics.rememberHaptics
import kotlinx.coroutines.delay
@ -39,6 +41,10 @@ fun ChatScreen(userName: String = "Bruno") {
var nextId by remember { mutableStateOf(0L) }
var selectedMode by remember { mutableStateOf("Standard") }
// Tablet & Landscape optimization: Detect large screens and cap the content width
val configuration = LocalConfiguration.current
val isTablet = configuration.screenWidthDp > 600
fun send(text: String) {
val trimmed = text.trim()
if (trimmed.isEmpty() || isStreaming) return
@ -78,21 +84,24 @@ fun ChatScreen(userName: String = "Bruno") {
}
MeshBackground {
Box(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// 1. Scrollable Chat Content
// Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating glass panels.
// Since the OS physically resizes the window when the keyboard opens, the Box container shrinks natively,
// placing the bottom dock exactly on top of the keyboard. We use constant Paddings to clear the docks.
// Responsive width capping and centering on tablets.
if (messages.isEmpty()) {
EmptyHero(
userName = userName,
onPick = { haptics.tick(); send(it) },
modifier = Modifier.fillMaxSize()
modifier = Modifier
.fillMaxSize()
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
)
} else {
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier),
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
@ -105,20 +114,20 @@ fun ChatScreen(userName: String = "Bruno") {
}
}
// 2. Floating Header (Translucent Frosted Glass)
// 2. Floating Header (Translucent Frosted Glass, responsive centering)
KaizenHeader(
modifier = Modifier
.align(Alignment.TopCenter)
.statusBarsPadding()
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
)
// 3. Floating Bottom Control Dock
// Positioned at the bottom of the window. When the keyboard is active, the OS resizes the window
// and this dock naturally sits exactly on top of the keyboard without any double-padding.
// 3. Floating Bottom Control Dock (responsive centering and clear bottom margins)
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
.navigationBarsPadding()
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
) {
ModePillsRow(selected = selectedMode, onSelect = { selectedMode = it; haptics.tick() })
Spacer(Modifier.height(10.dp))
@ -128,7 +137,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
Spacer(Modifier.height(16.dp)) // Increased from 12.dp to 16.dp to completely clear keyboard edges on all screens!
}
}
}