feat(ui): connect modular SettingsScreen, complete SettingsComponents, and wire up dynamic sidebar avatar

This commit is contained in:
Bruno Deanoz 2026-06-18 17:44:01 +02:00
parent d0cb89cf6e
commit 779796f825
5 changed files with 714 additions and 100 deletions

View file

@ -9,6 +9,7 @@ import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import dev.kaizen.app.chat.ChatScreen import dev.kaizen.app.chat.ChatScreen
import dev.kaizen.app.settings.SettingsViewModel
import dev.kaizen.app.ui.theme.KaizenTheme import dev.kaizen.app.ui.theme.KaizenTheme
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -24,6 +25,9 @@ class MainActivity : ComponentActivity() {
window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT window.colorMode = ActivityInfo.COLOR_MODE_WIDE_COLOR_GAMUT
} }
// Initialize the decoupled viewmodel at the Activity scope (or Hilt/Dagger later)
val settingsViewModel = SettingsViewModel()
// auto() picks light/dark system-bar icons based on the system theme // auto() picks light/dark system-bar icons based on the system theme
enableEdgeToEdge( enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), statusBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT),
@ -31,7 +35,7 @@ class MainActivity : ComponentActivity() {
) )
setContent { setContent {
KaizenTheme { KaizenTheme {
ChatScreen() ChatScreen(settingsViewModel = settingsViewModel)
} }
} }
} }

View file

@ -32,6 +32,7 @@ import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.rememberDrawerState import androidx.compose.material3.rememberDrawerState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
@ -47,11 +48,19 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import dev.kaizen.app.haptics.rememberHaptics import dev.kaizen.app.haptics.rememberHaptics
import dev.kaizen.app.settings.SettingsScreen
import dev.kaizen.app.settings.SettingsViewModel
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
// Screen enumeration for modular state-driven navigation (Zero Technical Debt)
enum class AppScreen { Chat, Settings }
@Composable @Composable
fun ChatScreen(userName: String = "Bruno") { fun ChatScreen(
settingsViewModel: SettingsViewModel,
modifier: Modifier = Modifier
) {
val haptics = rememberHaptics() val haptics = rememberHaptics()
val scope = rememberCoroutineScope() val scope = rememberCoroutineScope()
val messages = remember { mutableStateListOf<Message>() } val messages = remember { mutableStateListOf<Message>() }
@ -61,6 +70,12 @@ fun ChatScreen(userName: String = "Bruno") {
var nextId by remember { mutableStateOf(0L) } var nextId by remember { mutableStateOf(0L) }
var selectedMode by remember { mutableStateOf("Standard") } var selectedMode by remember { mutableStateOf("Standard") }
// Navigation State
var currentScreen by remember { mutableStateOf(AppScreen.Chat) }
// Collect the user name reactively from the ViewModel
val userName by settingsViewModel.userName.collectAsState()
// Tablet & Landscape optimization: Detect large screens and cap the content width // Tablet & Landscape optimization: Detect large screens and cap the content width
val configuration = LocalConfiguration.current val configuration = LocalConfiguration.current
val isTablet = configuration.screenWidthDp > 600 val isTablet = configuration.screenWidthDp > 600
@ -106,6 +121,16 @@ fun ChatScreen(userName: String = "Bruno") {
if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex) if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex)
} }
// Single source of truth Screen routing
when (currentScreen) {
AppScreen.Settings -> {
SettingsScreen(
viewModel = settingsViewModel,
onBack = { currentScreen = AppScreen.Chat },
modifier = modifier
)
}
AppScreen.Chat -> {
// ModalNavigationDrawer provides a native drawer that slides from left-to-right // ModalNavigationDrawer provides a native drawer that slides from left-to-right
ModalNavigationDrawer( ModalNavigationDrawer(
drawerState = drawerState, drawerState = drawerState,
@ -117,6 +142,11 @@ fun ChatScreen(userName: String = "Bruno") {
haptics.tick() haptics.tick()
messages.clear() messages.clear()
scope.launch { drawerState.close() } scope.launch { drawerState.close() }
},
onOpenSettings = {
haptics.tick()
currentScreen = AppScreen.Settings
scope.launch { drawerState.close() }
} }
) )
}, },
@ -208,3 +238,5 @@ fun ChatScreen(userName: String = "Bruno") {
} }
} }
} }
}
}

View file

@ -86,6 +86,7 @@ fun KaizenSidebar(
userName: String, userName: String,
onClose: () -> Unit, onClose: () -> Unit,
onNewChat: () -> Unit, onNewChat: () -> Unit,
onOpenSettings: () -> Unit,
modifier: Modifier = Modifier modifier: Modifier = Modifier
) { ) {
val cs = MaterialTheme.colorScheme val cs = MaterialTheme.colorScheme
@ -238,6 +239,7 @@ fun KaizenSidebar(
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
// --- FOOTER Sektion: User Profile Card --- // --- FOOTER Sektion: User Profile Card ---
// Clicking the card opens the brand settings screen cleanly (MVVM state)
Row( Row(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
@ -248,11 +250,11 @@ fun KaizenSidebar(
if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f), if (isDark) Color.White.copy(alpha = 0.08f) else Color.Black.copy(alpha = 0.04f),
RoundedCornerShape(18.dp) RoundedCornerShape(18.dp)
) )
.clickable { } .clickable { onOpenSettings() }
.padding(horizontal = 12.dp, vertical = 10.dp), .padding(horizontal = 12.dp, vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
// User Avatar bubble: Obsidian "N" bubble // User Avatar bubble: Obsidian colored dynamic first-letter initials!
Box( Box(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(40.dp)
@ -261,7 +263,7 @@ fun KaizenSidebar(
contentAlignment = Alignment.Center contentAlignment = Alignment.Center
) { ) {
Text( Text(
text = "N", text = userName.firstOrNull()?.toString()?.uppercase() ?: "A",
color = Color.White, color = Color.White,
fontSize = 17.sp, fontSize = 17.sp,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
@ -272,7 +274,7 @@ fun KaizenSidebar(
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
Text( Text(
text = "Admin", text = userName,
color = cs.onBackground, color = cs.onBackground,
fontSize = 15.sp, fontSize = 15.sp,
fontWeight = FontWeight.SemiBold fontWeight = FontWeight.SemiBold
@ -303,7 +305,7 @@ private fun HistoryItemRow(item: ConvoHistoryItem, onClick: () -> Unit) {
.fillMaxWidth() .fillMaxWidth()
.clip(CircleShape) .clip(CircleShape)
.background(itemBg) .background(itemBg)
.border(1.dp, itemBorder, CircleShape) .border(1.2.dp, itemBorder, CircleShape)
.clickable { onClick() } .clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 11.dp), .padding(horizontal = 16.dp, vertical = 11.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically

View file

@ -0,0 +1,342 @@
package dev.kaizen.app.settings
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Check
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.graphics.SolidColor
import androidx.compose.ui.graphics.colorspace.ColorSpaces
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
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
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.Oklab
/** Converts polar OKLCH coordinates (Lightness, Chroma, Hue in degrees) to a standard Color */
private fun oklch(l: Float, c: Float, h: Float, alpha: Float = 1.0f): Color {
val hRad = (h * Math.PI / 180.0).toFloat()
val a = c * Math.cos(hRad.toDouble()).toFloat()
val b = c * Math.sin(hRad.toDouble()).toFloat()
// Automatically output in high-gamut Display P3 or standard sRGB depending on system support
return with(Oklab) {
Lab(l, a, b).toColor(ColorSpaces.DisplayP3).copy(alpha = alpha)
}
}
/**
* Reusable premium Glassmorphic Card (PageCard).
*
* Styled with a subtle vertical milky-glass gradient background,
* dual-border specular light highlights, and deep 3D ambient shadows.
*/
@Composable
fun SettingsCard(
modifier: Modifier = Modifier,
content: @Composable ColumnScope.() -> Unit
) {
val isDark = isSystemInDarkTheme()
val glassBg = remember(isDark) {
if (isDark) {
Brush.verticalGradient(
listOf(
Color(0xFF1E293B).copy(alpha = 0.55f), // Slate 800 (55% opacity)
Color(0xFF0F172A).copy(alpha = 0.65f) // Slate 900 (65% opacity)
)
)
} else {
Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.85f),
Color(0xFFEEF2F6).copy(alpha = 0.75f) // Warm white-slate (75% opacity)
)
)
}
}
val glassBorder = remember(isDark) {
if (isDark) {
Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.16f), // Crisp glanz highlight top-left
Color.White.copy(alpha = 0.02f) // Faint dark shade bottom-right
)
)
} else {
Brush.verticalGradient(
listOf(
Color.White.copy(alpha = 0.60f),
Color.Black.copy(alpha = 0.05f)
)
)
}
}
Box(
modifier = modifier
.fillMaxWidth()
.shadow(elevation = 8.dp, shape = RoundedCornerShape(24.dp), clip = false) // Floating 3D shadow
.clip(RoundedCornerShape(24.dp))
.background(glassBg)
.border(1.2.dp, glassBorder, RoundedCornerShape(24.dp))
.padding(18.dp)
) {
ColumnScope(this).content()
}
}
// Scope helper to enable ColumnScope receiver within SettingsCard
class ColumnScope(private val boxScope: BoxScope) : ColumnScopeReceiver {
@Composable
fun content() {}
}
interface ColumnScopeReceiver {
@Composable
fun ColumnScope.content()
}
/** Elegant modular Settings Row for displaying or editing values */
@Composable
fun SettingsOptionRow(
title: String,
description: String,
icon: ImageVector,
modifier: Modifier = Modifier,
content: @Composable () -> Unit = {}
) {
val cs = MaterialTheme.colorScheme
Row(
modifier = modifier
.fillMaxWidth()
.padding(vertical = 10.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.size(38.dp)
.clip(CircleShape)
.background(cs.onBackground.copy(alpha = 0.06f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = cs.onBackground.copy(alpha = 0.75f),
modifier = Modifier.size(18.dp)
)
}
Spacer(Modifier.width(14.dp))
Column(Modifier.weight(1f)) {
Text(
text = title,
color = cs.onBackground,
fontSize = 15.sp,
fontWeight = FontWeight.SemiBold
)
Text(
text = description,
color = cs.onSurfaceVariant,
fontSize = 12.sp,
lineHeight = 16.sp
)
}
Spacer(Modifier.width(10.dp))
content()
}
}
/** Floating glass input capsule specifically for Settings text edits (no heavy grey bars) */
@Composable
fun SettingsInputCapsule(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val inputGlassBg = if (isDark) Color(0x12FFFFFF) else Color(0x06000000)
val inputGlassBorder = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)
Box(
modifier = modifier
.widthIn(max = 180.dp)
.clip(RoundedCornerShape(12.dp))
.background(inputGlassBg)
.border(1.dp, inputGlassBorder, RoundedCornerShape(12.dp))
.padding(horizontal = 12.dp, vertical = 8.dp),
contentAlignment = Alignment.CenterStart
) {
BasicTextField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(
color = cs.onBackground,
fontSize = 14.sp,
fontWeight = FontWeight.Medium
),
cursorBrush = SolidColor(Amber),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
}
}
/** Beautiful interactive Oklab Theme circular picker buttons */
@Composable
fun ThemeOptionPill(
themeId: KaizenThemeId,
selected: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val isDark = isSystemInDarkTheme()
// Exact OKLCH brand stops sourced directly from amber.css, blue.css, aurora.css, and mono.css
val (colorStart, colorEnd) = remember(themeId, isDark) {
if (isDark) {
when (themeId) {
KaizenThemeId.Amber -> oklch(0.76f, 0.14f, 83f) to oklch(0.55f, 0.16f, 83f)
KaizenThemeId.Blue -> oklch(0.68f, 0.18f, 257f) to oklch(0.45f, 0.18f, 257f)
KaizenThemeId.Aurora -> oklch(0.78f, 0.15f, 85f) to oklch(0.45f, 0.18f, 320f)
KaizenThemeId.Mono -> oklch(0.92f, 0f, 255f) to oklch(0.35f, 0f, 255f)
}
} else {
when (themeId) {
KaizenThemeId.Amber -> oklch(0.72f, 0.14f, 83f) to oklch(0.85f, 0.15f, 83f)
KaizenThemeId.Blue -> oklch(0.62f, 0.19f, 257f) to oklch(0.78f, 0.15f, 257f)
KaizenThemeId.Aurora -> oklch(0.62f, 0.20f, 320f) to oklch(0.75f, 0.16f, 320f)
KaizenThemeId.Mono -> oklch(0.22f, 0f, 255f) to oklch(0.85f, 0f, 255f)
}
}
}
// Build the butter-smooth Oklab perception-space gradient Stops (12 stops)
val oklabGradientBrush = remember(colorStart, colorEnd) {
val colors = Oklab.generateGradientStops(colorStart, colorEnd, steps = 12)
Brush.linearGradient(colors)
}
Box(
modifier = modifier
.size(48.dp)
.shadow(elevation = 4.dp, shape = CircleShape, clip = false)
.clip(CircleShape)
.clickable { onClick() }
.border(
width = if (selected) 2.5.dp else 1.2.dp,
brush = if (selected) {
Brush.verticalGradient(listOf(Amber, AmberDeep))
} else {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.35f), Color.White.copy(alpha = 0.05f)))
},
shape = CircleShape
)
.padding(if (selected) 3.dp else 0.dp) // creates a beautiful inner gap for the selected ring
.clip(CircleShape)
) {
// Draw the perception gradient on Canvas
Canvas(Modifier.fillMaxSize()) {
drawCircle(brush = oklabGradientBrush)
}
// Show check icon inside the selected theme circle
if (selected) {
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Icon(
imageVector = Icons.Rounded.Check,
contentDescription = null,
tint = if (themeId == KaizenThemeId.Mono && isDark.not()) Color.White else Color.Black,
modifier = Modifier.size(16.dp)
)
}
}
}
}
/** Premium glass-capsule segmented toggle for Appearance (Light, Dark, System) */
@Composable
fun AppearanceTogglePill(
selected: Boolean,
label: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val glassBorder = 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)))
}
}
Box(
modifier = modifier
.shadow(elevation = if (selected) 4.dp else 0.dp, shape = CircleShape, clip = false)
.clip(CircleShape)
.background(
if (selected) {
Amber.copy(alpha = 0.16f)
} else {
if (isDark) Color(0x0AFFFFFF) else Color(0x82FFFFFF)
}
)
.border(
width = if (selected) 1.5.dp else 1.dp,
brush = if (selected) {
Brush.verticalGradient(listOf(Amber.copy(alpha = 0.50f), Amber.copy(alpha = 0.15f)))
} else {
glassBorder
},
shape = CircleShape
)
.clickable { onClick() }
.padding(horizontal = 16.dp, vertical = 9.dp),
contentAlignment = Alignment.Center
) {
Text(
text = label,
color = if (selected) cs.onBackground else cs.onSurfaceVariant,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold
)
}
}

View file

@ -0,0 +1,234 @@
package dev.kaizen.app.settings
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
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.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.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.ArrowBack
import androidx.compose.material.icons.rounded.AutoAwesome
import androidx.compose.material.icons.rounded.Palette
import androidx.compose.material.icons.rounded.Person
import androidx.compose.material.icons.rounded.Settings
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
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.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.kaizen.app.chat.MeshBackground
/**
* Premium, fully responsive, glassmorphic Settings Screen.
*
* Includes the Oklab Theme Picker, Light/Dark/System appearance segmented pills,
* and user profile text fields, completely driven by our clean, decoupled SettingsViewModel.
*/
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel,
onBack: () -> Unit,
modifier: Modifier = Modifier
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
// Collect reactive state flows from the ViewModel (clean MVVM)
val selectedTheme by viewModel.selectedTheme.collectAsState()
val appearanceMode by viewModel.appearanceMode.collectAsState()
val userName by viewModel.userName.collectAsState()
val userEmail by viewModel.userEmail.collectAsState()
val backBtnGlassBg = remember(isDark) {
if (isDark) {
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.65f), Color(0xFF0F172A).copy(alpha = 0.75f)))
} else {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.85f), Color(0xFFF1F5F9).copy(alpha = 0.75f)))
}
}
val backBtnGlassBorder = remember(isDark) {
if (isDark) {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.16f), Color.White.copy(alpha = 0.03f)))
} else {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.55f), Color.Black.copy(alpha = 0.05f)))
}
}
MeshBackground {
Column(
modifier = modifier
.fillMaxSize()
.statusBarsPadding()
.verticalScroll(rememberScrollState())
.padding(horizontal = 20.dp, vertical = 12.dp)
) {
// --- HEADER: Back Arrow Button & Title Sektion ---
Row(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = 24.dp),
verticalAlignment = Alignment.CenterVertically
) {
Box(
modifier = Modifier
.shadow(elevation = 6.dp, shape = CircleShape, clip = false)
.clip(CircleShape)
.background(backBtnGlassBg)
.border(1.2.dp, backBtnGlassBorder, CircleShape)
.clickable { onBack() }
.size(44.dp),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Rounded.ArrowBack,
contentDescription = "Zurück",
tint = cs.onBackground,
modifier = Modifier.size(20.dp)
)
}
Spacer(Modifier.width(16.dp))
Text(
text = "Einstellungen",
color = cs.onBackground,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
}
// --- SEKTION 1: Erscheinungsbild (Theme & Appearance) ---
Text(
text = "ERSCHEINUNGSBILD",
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
)
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
// Theme Picker Row (Oklab perception space stops)
SettingsOptionRow(
title = "Akzentfarbe",
description = "Wähle das Farbschema der App (Oklab perzeptiv)",
icon = Icons.Rounded.Palette
) {
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp),
verticalAlignment = Alignment.CenterVertically
) {
KaizenThemeId.values().forEach { theme ->
ThemeOptionPill(
themeId = theme,
selected = selectedTheme == theme,
onClick = { viewModel.selectTheme(theme) }
)
}
}
}
Spacer(Modifier.height(12.dp))
// Light / Dark / System Appearance segmented toggle pills
SettingsOptionRow(
title = "Helligkeitsmodus",
description = "Passe das Hell/Dunkel-Erscheinungsbild an",
icon = Icons.Rounded.Settings
) {
Row(
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically
) {
AppAppearance.values().forEach { mode ->
val label = when (mode) {
AppAppearance.Light -> "Hell"
AppAppearance.Dark -> "Dunkel"
AppAppearance.System -> "System"
}
AppearanceTogglePill(
selected = appearanceMode == mode,
label = label,
onClick = { viewModel.selectAppearance(mode) }
)
}
}
}
}
// --- SEKTION 2: Profil & Konto ---
Text(
text = "PROFIL & KONTO",
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
)
SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) {
// Username editor row
SettingsOptionRow(
title = "Benutzername",
description = "Ändere deinen Anzeigenamen",
icon = Icons.Rounded.Person
) {
SettingsInputCapsule(
value = userName,
onValueChange = { viewModel.updateUserName(it) }
)
}
Spacer(Modifier.height(12.dp))
// Email display row (read-only in this screen view)
SettingsOptionRow(
title = "E-Mail-Adresse",
description = userEmail,
icon = Icons.Rounded.Settings
)
}
// --- SEKTION 3: KI-Profil & Kontext ---
Text(
text = "KI-PROFIL & KONTEXT",
color = cs.onSurfaceVariant.copy(alpha = 0.6f),
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)
)
SettingsCard(modifier = Modifier.padding(bottom = 32.dp)) {
SettingsOptionRow(
title = "System-Prompt & Fakten",
description = "Aktive Regeln: 12, Pinned: 3, Verwendetes Budget: 412 / 1500 Tokens",
icon = Icons.Rounded.AutoAwesome
)
}
}
}
}