- 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
312 lines
11 KiB
Kotlin
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
|
|
)
|
|
}
|
|
}
|