feat(ui): implement gorgeous floating glassmorphic sidebar and rich 3D ambient shadows for suggestions, pills, and input
This commit is contained in:
parent
2c3d057641
commit
37e424df3e
3 changed files with 470 additions and 49 deletions
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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,53 +105,104 @@ fun ChatScreen(userName: String = "Bruno") {
|
|||
if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex)
|
||||
}
|
||||
|
||||
MeshBackground {
|
||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||
// 1. Scrollable Chat Content
|
||||
// Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating bottom dock.
|
||||
// Centered layout with responsive width capping on tablets.
|
||||
if (messages.isEmpty()) {
|
||||
EmptyHero(
|
||||
userName = userName,
|
||||
onPick = { haptics.tick(); send(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.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!
|
||||
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) }
|
||||
// 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
|
||||
// Stretches to the full screen, allowing bubbles to scroll cleanly behind the floating bottom dock.
|
||||
// Centered layout with responsive width capping on tablets.
|
||||
if (messages.isEmpty()) {
|
||||
EmptyHero(
|
||||
userName = userName,
|
||||
onPick = { haptics.tick(); send(it) },
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
)
|
||||
} else {
|
||||
LazyColumn(
|
||||
state = listState,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier),
|
||||
contentPadding = PaddingValues(
|
||||
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
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
items(messages, key = { it.id }) { message -> MessageRow(message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 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))
|
||||
ChatInput(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
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!
|
||||
// 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)
|
||||
.navigationBarsPadding()
|
||||
.then(if (isTablet) Modifier.widthIn(max = 720.dp) else Modifier)
|
||||
) {
|
||||
ModePillsRow(selected = selectedMode, onSelect = { selectedMode = it; haptics.tick() })
|
||||
Spacer(Modifier.height(10.dp))
|
||||
ChatInput(
|
||||
value = input,
|
||||
onValueChange = { input = it },
|
||||
onSend = { send(input) },
|
||||
enabled = !isStreaming,
|
||||
)
|
||||
Spacer(Modifier.height(16.dp)) // Completely clears keyboard edges on all screens
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
340
app/src/main/java/dev/kaizen/app/chat/Sidebar.kt
Normal file
340
app/src/main/java/dev/kaizen/app/chat/Sidebar.kt
Normal 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue