feat(ui): implement gorgeous floating glassmorphic sidebar and rich 3D ambient shadows for suggestions, pills, and input

This commit is contained in:
Bruno Deanoz 2026-06-18 17:13:13 +02:00
parent 2c3d057641
commit 37e424df3e
3 changed files with 470 additions and 49 deletions

View file

@ -52,6 +52,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.scale
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
@ -59,6 +60,7 @@ import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@ -177,11 +179,12 @@ 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)
// Highly responsive 3D floating glass capsules (iOS 26 / Apple Liquid Glass style)
val glassBg = if (isDark) cs.surface.copy(alpha = 0.45f) else cs.surface.copy(alpha = 0.88f)
val glassBorder = if (isDark) {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.10f), Color.White.copy(alpha = 0.02f)))
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.16f), Color.White.copy(alpha = 0.02f)))
} else {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.50f), Color.Black.copy(alpha = 0.04f)))
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.65f), Color.Black.copy(alpha = 0.05f)))
}
FlowRow(
@ -192,9 +195,10 @@ private fun SuggestionPills(onPick: (String) -> Unit) {
suggestions.forEach { suggestion ->
Row(
Modifier
.shadow(elevation = 3.dp, shape = CircleShape, clip = false) // Ambient 3D shadow for floating look
.clip(CircleShape)
.background(glassBg)
.border(1.dp, glassBorder, CircleShape)
.border(1.2.dp, glassBorder, CircleShape)
.clickable { onPick(suggestion.prompt) }
.padding(horizontal = 16.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically,
@ -248,6 +252,7 @@ fun ModePillsRow(selected: String, onSelect: (String) -> Unit, modifier: Modifie
Row(
Modifier
.shadow(elevation = 2.dp, shape = CircleShape, clip = false) // Floating tabs
.clip(CircleShape)
.background(backgroundFill)
.border(1.2.dp, borderBrush, CircleShape)
@ -303,6 +308,7 @@ fun MessageRow(message: Message) {
Box(
Modifier
.widthIn(max = 300.dp)
.shadow(elevation = 3.dp, shape = shape, clip = false) // Raised amber glass bubble!
.clip(shape)
.background(bgBrush)
.border(1.2.dp, borderBrush, shape)
@ -404,6 +410,7 @@ fun ChatInput(
modifier
.fillMaxWidth()
.padding(horizontal = 12.dp)
.shadow(elevation = 8.dp, shape = RoundedCornerShape(28.dp), clip = false) // Luxurious floating ambient shadow!
.clip(RoundedCornerShape(28.dp))
.background(glassBg)
.border(1.2.dp, glassBorder, RoundedCornerShape(28.dp))

View file

@ -1,5 +1,8 @@
package dev.kaizen.app.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@ -9,10 +12,23 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Menu
import androidx.compose.material3.DrawerValue
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
@ -23,6 +39,10 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import dev.kaizen.app.haptics.rememberHaptics
@ -44,6 +64,9 @@ fun ChatScreen(userName: String = "Bruno") {
val configuration = LocalConfiguration.current
val isTablet = configuration.screenWidthDp > 600
// Native sidebar drawer state
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
fun send(text: String) {
val trimmed = text.trim()
if (trimmed.isEmpty() || isStreaming) return
@ -82,6 +105,22 @@ fun ChatScreen(userName: String = "Bruno") {
if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex)
}
// ModalNavigationDrawer provides a native drawer that slides from left-to-right
ModalNavigationDrawer(
drawerState = drawerState,
drawerContent = {
KaizenSidebar(
userName = userName,
onClose = { scope.launch { drawerState.close() } },
onNewChat = {
haptics.tick()
messages.clear()
scope.launch { drawerState.close() }
}
)
},
gesturesEnabled = true // Allows swiping from the screen's left edge to open the drawer
) {
MeshBackground {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
// 1. Scrollable Chat Content
@ -102,7 +141,7 @@ fun ChatScreen(userName: String = "Bruno") {
.fillMaxSize()
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier),
contentPadding = PaddingValues(
top = 16.dp, // Clean, native layout allows messages to scroll all the way to the top status bar!
top = 76.dp, // Leaves breathing room for the floating top menu button
bottom = 152.dp, // Leaves luxurious room for the floating ModePills + ChatInput + bottom margins
start = 16.dp,
end = 16.dp
@ -113,7 +152,41 @@ fun ChatScreen(userName: String = "Bruno") {
}
}
// 2. Floating Bottom Control Dock (responsive centering and clear bottom margins)
// 2. Floating Top Menu Button (Apple Liquid Glass - completely independent of a header bar)
val isDark = remember { isStreaming || messages.isNotEmpty() } // slight adaptive context tint
val menuGlassBg = if (isDark.not() && isSystemInDarkTheme().not()) {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.85f), Color(0xFFF1F5F9).copy(alpha = 0.75f)))
} else {
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.65f), Color(0xFF0F172A).copy(alpha = 0.75f)))
}
val menuGlassBorder = if (isDark.not() && isSystemInDarkTheme().not()) {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.55f), Color.Black.copy(alpha = 0.05f)))
} else {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.16f), Color.White.copy(alpha = 0.03f)))
}
Box(
modifier = Modifier
.align(Alignment.TopStart)
.statusBarsPadding()
.padding(start = 16.dp, top = 10.dp)
.shadow(elevation = 6.dp, shape = CircleShape, clip = false) // Floats above backgrounds
.clip(CircleShape)
.background(menuGlassBg)
.border(1.2.dp, menuGlassBorder, CircleShape)
.clickable { haptics.tick(); scope.launch { drawerState.open() } }
.size(44.dp),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.Menu,
contentDescription = "Menü öffnen",
tint = MaterialTheme.colorScheme.onBackground,
modifier = Modifier.size(20.dp)
)
}
// 3. Floating Bottom Control Dock (responsive centering and clear bottom margins)
Column(
modifier = Modifier
.align(Alignment.BottomCenter)
@ -128,7 +201,8 @@ fun ChatScreen(userName: String = "Bruno") {
onSend = { send(input) },
enabled = !isStreaming,
)
Spacer(Modifier.height(16.dp)) // Increased from 12.dp to 16.dp to completely clear keyboard edges on all screens!
Spacer(Modifier.height(16.dp)) // Completely clears keyboard edges on all screens
}
}
}
}

View file

@ -0,0 +1,340 @@
package dev.kaizen.app.chat
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
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.statusBarsPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Edit
import androidx.compose.material.icons.rounded.Lock
import androidx.compose.material.icons.rounded.Search
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material.icons.rounded.Star
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.kaizen.app.ui.theme.Amber
// Mock sidebar history item representing a past conversation
data class ConvoHistoryItem(
val id: String,
val title: String,
val dateGroup: String,
val locked: Boolean = false,
val pinned: Boolean = false
)
// Authentically mirrored mock history from sidebar.png
val mockConvoHistory = listOf(
ConvoHistoryItem("1", "Planetenübersicht P...", "16. JUNI 2026"),
ConvoHistoryItem("2", "Orbitaltheorie in der ...", "16. JUNI 2026", locked = true),
ConvoHistoryItem("3", "Wetter in Magdeburg", "16. JUNI 2026"),
ConvoHistoryItem("4", "Aktuelle Uhrzeit in To...", "16. JUNI 2026"),
ConvoHistoryItem("5", "📷 Aussehen und Styl...", "15. JUNI 2026"),
ConvoHistoryItem("6", "👤 Objektive Bewertu...", "15. JUNI 2026"),
ConvoHistoryItem("7", "Beziehungen und ihre...", "14. JUNI 2026"),
ConvoHistoryItem("8", "Online-Test erfolgreic...", "12. JUNI 2026"),
ConvoHistoryItem("9", "Hallo Gemini", "11. JUNI 2026"),
ConvoHistoryItem("10", "Gesperrter Chat", "10. JUNI 2026", locked = true)
)
/**
* Floating glassmorphic sidebar drawer content.
*
* Styled as a suspended glass panel with a thick "milky glass" background,
* dual-border specular light reflections, deep ambient shadows, and authentic Kaizen layouts.
*/
@Composable
fun KaizenSidebar(
userName: String,
onClose: () -> Unit,
onNewChat: () -> Unit,
modifier: Modifier = Modifier
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
// 1. Thick, premium milky-glass background gradient
val glassBg = remember(isDark) {
if (isDark) {
Brush.verticalGradient(
listOf(
Color(0xCC141B27), // Deep Obsidian-blue with 80% opacity
Color(0xE60A0E16) // Even darker at the bottom (90% opacity)
)
)
} else {
Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.94f), // Creamy white with 94% opacity
Color(0xFFF1F5F9).copy(alpha = 0.85f) // Slightly slate tint at bottom
)
)
}
}
// 2. Specular border light reflections (crisp glass edge highlight)
val glassBorder = remember(isDark) {
if (isDark) {
Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.16f), // Bright highlight on top-left
Color.White.copy(alpha = 0.02f) // Faint dark shade on bottom
)
)
} else {
Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.65f),
Color.Black.copy(alpha = 0.05f)
)
)
}
}
// Floating design: 12dp margins all around so it physically hovers above the background!
Box(
modifier = modifier
.fillMaxHeight()
.width(300.dp)
.padding(vertical = 12.dp, start = 12.dp, end = 4.dp)
.shadow(elevation = 16.dp, shape = RoundedCornerShape(24.dp), clip = false)
.clip(RoundedCornerShape(24.dp))
.background(glassBg)
.border(1.2.dp, glassBorder, RoundedCornerShape(24.dp))
.statusBarsPadding()
.padding(16.dp)
) {
Column(Modifier.fillMaxSize()) {
// --- HEADER Sektion: Brand title and New Chat button ---
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "kaizen",
color = cs.onBackground,
fontSize = 20.sp,
fontWeight = FontWeight.Bold
)
// Translucent "New Chat" pencil button
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(if (isDark) Color(0x1AFFFFFF) else Color(0x0A000000))
.clickable { onNewChat() },
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.Edit,
contentDescription = "Neuer Chat",
tint = cs.onBackground,
modifier = Modifier.size(18.dp)
)
}
}
Spacer(Modifier.height(16.dp))
// --- SEARCH Sektion: Glassmorphic search capsule ---
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(12.dp))
.background(if (isDark) Color(0x12FFFFFF) else Color(0x06000000))
.border(
1.dp,
if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f),
RoundedCornerShape(12.dp)
)
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = Icons.Rounded.Search,
contentDescription = null,
tint = cs.onSurfaceVariant.copy(alpha = 0.6f),
modifier = Modifier.size(18.dp)
)
Spacer(Modifier.width(8.dp))
Text(
text = "Suchen...",
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
fontSize = 14.sp
)
}
Spacer(Modifier.height(24.dp))
// --- BODY Sektion: Scrollable Chats list grouped by Date ---
Box(Modifier.weight(1f).fillMaxWidth()) {
val groupedHistory = remember { mockConvoHistory.groupBy { it.dateGroup } }
LazyColumn(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
groupedHistory.forEach { (dateGroup, items) ->
// Date header (e.g., 16. JUNI 2026)
item(key = "header_$dateGroup") {
Text(
text = dateGroup,
color = cs.onSurfaceVariant.copy(alpha = 0.45f),
fontSize = 11.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 12.dp, top = 8.dp, bottom = 4.dp)
)
}
// History items
items(items, key = { it.id }) { item ->
HistoryItemRow(item, onClick = { onClose() })
}
}
}
}
Spacer(Modifier.height(16.dp))
// --- FOOTER Sektion: User Profile Card ---
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(18.dp))
.background(if (isDark) Color(0x12FFFFFF) else Color(0x06000000))
.border(
1.dp,
if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f),
RoundedCornerShape(18.dp)
)
.clickable { }
.padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
// User Avatar bubble: Obsidian "N" bubble
Box(
modifier = Modifier
.size(40.dp)
.clip(CircleShape)
.background(if (isDark) Color(0xFF1E293B) else Color(0xFF0F172A)),
contentAlignment = Alignment.Center
) {
Text(
text = "N",
color = Color.White,
fontSize = 17.sp,
fontWeight = FontWeight.Bold
)
}
Spacer(Modifier.width(10.dp))
Column(Modifier.weight(1f)) {
Text(
text = "Admin",
color = cs.onBackground,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
)
}
Icon(
imageVector = Icons.Rounded.Settings,
contentDescription = "Einstellungen",
tint = cs.onSurfaceVariant.copy(alpha = 0.7f),
modifier = Modifier.size(20.dp)
)
}
}
}
}
@Composable
private fun HistoryItemRow(item: ConvoHistoryItem, onClick: () -> Unit) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val itemBg = if (isDark) Color(0x0AFFFFFF) else Color(0x05000000)
val itemBorder = if (isDark) Color.White.copy(alpha = 0.04f) else Color.Black.copy(alpha = 0.03f)
Row(
modifier = Modifier
.fillMaxWidth()
.clip(CircleShape)
.background(itemBg)
.border(1.dp, itemBorder, CircleShape)
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically
) {
if (item.pinned) {
Icon(
imageVector = Icons.Rounded.Star,
contentDescription = null,
tint = Amber,
modifier = Modifier.size(14.dp)
)
Spacer(Modifier.width(8.dp))
}
Text(
text = if (item.locked) "Gesperrter Chat" else item.title,
color = if (item.locked) cs.onSurfaceVariant.copy(alpha = 0.5f) else cs.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
fontStyle = if (item.locked) FontStyle.Italic else FontStyle.Normal,
modifier = Modifier.weight(1f)
)
if (item.locked) {
Spacer(Modifier.width(8.dp))
Icon(
imageVector = Icons.Rounded.Lock,
contentDescription = "Gesperrt",
tint = Amber,
modifier = Modifier.size(15.dp)
)
}
}
}