kaizen-app/app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt
Bruno Deanoz 94c0f85fb7 feat: migrate all UI components to design system v2
- Sidebar: GlassSurface + KaizenShapes (lg, sm, md, pill, circle)
- ChatInput: GlassSurface with input tier opacity
- ChatScreen: menu button uses GlassSurface, error banner uses KaizenShapes
- EmptyHero: suggestion chips removed on mobile (DESIGN.md §11.1)
- LoginScreen: GlassFields use KaizenShapes.lg
- ModelSheet: KaizenShapes (xl, md, xs, circle)
- SettingsCard: GlassSurface with card tier opacity
- SettingsScreen: back button uses GlassSurface
- SettingsComponents: all CircleShape/RoundedCornerShape → KaizenShapes
- Markdown: copy button uses KaizenShapes.circle
- GlassSurface: removed misleading Modifier.blur (Android has no backdrop-filter)
- TypingDots: uses motion duration tokens
- Fixed deprecated ArrowBack icon → AutoMirrored variant
2026-06-21 14:50:11 +02:00

312 lines
11 KiB
Kotlin

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.ColumnScope
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
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.rememberScrollState
import dev.kaizen.app.ui.effect.GlassSurface
import dev.kaizen.app.ui.effect.GlassTiers
import dev.kaizen.app.ui.effect.KaizenShadows
import dev.kaizen.app.ui.shape.KaizenShapes
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll
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.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.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.StrokeCap
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) {
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()
GlassSurface(
modifier = modifier.fillMaxWidth(),
shape = KaizenShapes.lg,
tintAlpha = GlassTiers.card(isDark),
shadowStack = KaizenShadows.level2,
cornerRadius = 24.dp,
) {
Column(modifier = Modifier.fillMaxWidth().padding(18.dp)) {
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(KaizenShapes.circle)
.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(KaizenShapes.sm)
.background(inputGlassBg)
.border(1.dp, inputGlassBorder, KaizenShapes.sm)
.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 = KaizenShapes.circle, clip = false)
.clip(KaizenShapes.circle)
.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 = KaizenShapes.circle
)
.padding(if (selected) 3.dp else 0.dp) // creates a beautiful inner gap for the selected ring
.clip(KaizenShapes.circle)
) {
// 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 = KaizenShapes.circle, clip = false)
.clip(KaizenShapes.circle)
.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 = KaizenShapes.circle
)
.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
)
}
}