fix: wire theme switching — replace all hardcoded Amber colors with LocalKaizenAccent

All 7 files that imported Amber/AmberDeep/OnAmber/BlobX now read from
LocalKaizenAccent.current instead. Theme switching in settings actually
changes the UI now: message bubbles, send button, orb, blobs, sidebar
highlights, model sheet, stream blocks, login screen, and settings
toggles all respond to the selected theme.
This commit is contained in:
Bruno Deanoz 2026-06-21 18:29:42 +02:00
parent 918659422b
commit d468ec893f
7 changed files with 56 additions and 48 deletions

View file

@ -49,9 +49,7 @@ import dev.kaizen.app.chat.MeshBackground
import dev.kaizen.app.net.DEFAULT_BASE_URL
import dev.kaizen.app.net.LoginResult
import dev.kaizen.app.net.SessionViewModel
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.OnAmber
import dev.kaizen.app.ui.theme.LocalKaizenAccent
import androidx.compose.ui.res.stringResource
import dev.kaizen.app.R
import kotlinx.coroutines.launch
@ -65,6 +63,7 @@ import kotlinx.coroutines.launch
@Composable
fun LoginScreen(session: SessionViewModel, modifier: Modifier = Modifier) {
val cs = MaterialTheme.colorScheme
val accent = LocalKaizenAccent.current
val scope = rememberCoroutineScope()
val ctx = androidx.compose.ui.platform.LocalContext.current
@ -165,6 +164,7 @@ private fun GlassField(
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val glassBg = if (isDark) {
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.72f), Color(0xFF0F172A).copy(alpha = 0.85f)))
@ -196,7 +196,7 @@ private fun GlassField(
value = value,
onValueChange = onValueChange,
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp),
cursorBrush = SolidColor(Amber),
cursorBrush = SolidColor(accent.primary),
singleLine = true,
visualTransformation = if (password) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None,
keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
@ -209,8 +209,9 @@ private fun GlassField(
@Composable
private fun SignInButton(enabled: Boolean, loading: Boolean, onClick: () -> Unit) {
val cs = MaterialTheme.colorScheme
val accent = LocalKaizenAccent.current
val fill = if (enabled || loading) {
Modifier.background(Brush.linearGradient(listOf(Amber, AmberDeep)))
Modifier.background(Brush.linearGradient(listOf(accent.primary, accent.primaryDeep)))
} else {
Modifier.background(cs.surface)
}
@ -227,11 +228,11 @@ private fun SignInButton(enabled: Boolean, loading: Boolean, onClick: () -> Unit
contentAlignment = Alignment.Center,
) {
if (loading) {
CircularProgressIndicator(color = OnAmber, strokeWidth = 2.4.dp, modifier = Modifier.size(22.dp))
CircularProgressIndicator(color = accent.onPrimary, strokeWidth = 2.4.dp, modifier = Modifier.size(22.dp))
} else {
Text(
stringResource(R.string.login_submit),
color = if (enabled) OnAmber else cs.onSurfaceVariant,
color = if (enabled) accent.onPrimary else cs.onSurfaceVariant,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
)

View file

@ -35,11 +35,7 @@ import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import dev.kaizen.app.ui.motion.Durations
import dev.kaizen.app.ui.sensor.rememberTiltState
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.BlobAmber
import dev.kaizen.app.ui.theme.BlobCoral
import dev.kaizen.app.ui.theme.BlobTeal
import dev.kaizen.app.ui.theme.LocalKaizenAccent
import dev.kaizen.app.ui.theme.DITHER_ALPHA
import dev.kaizen.app.ui.theme.rememberDitherBrush
@ -47,6 +43,7 @@ import dev.kaizen.app.ui.theme.rememberDitherBrush
@Composable
fun MeshBackground(content: @Composable BoxScope.() -> Unit) {
val dark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val base = MaterialTheme.colorScheme.background
// Blobs are subtler in light mode.
val factor = if (dark) 1f else 0.55f
@ -68,9 +65,9 @@ fun MeshBackground(content: @Composable BoxScope.() -> Unit) {
drawRect(brush = ditherBrush, alpha = DITHER_ALPHA)
},
) {
Blob(BlobAmber, 360.dp, (-60f + drift * 30f).dp, (-40f + drift * 24f).dp, 0.30f * factor)
Blob(BlobCoral, 380.dp, (170f - drift * 44f).dp, (150f + drift * 40f).dp, 0.24f * factor)
Blob(BlobTeal, 300.dp, (30f + drift * 50f).dp, (560f - drift * 36f).dp, 0.22f * factor)
Blob(accent.blob1, 360.dp, (-60f + drift * 30f).dp, (-40f + drift * 24f).dp, 0.30f * factor)
Blob(accent.blob2, 380.dp, (170f - drift * 44f).dp, (150f + drift * 40f).dp, 0.24f * factor)
Blob(accent.blob4, 300.dp, (30f + drift * 50f).dp, (560f - drift * 36f).dp, 0.22f * factor)
// Vignette: darken the edges so the center comes forward (dark mode only).
if (dark) {
Box(
@ -113,6 +110,7 @@ fun KaizenOrb(
amplitude: Float? = null
) {
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val transition = rememberInfiniteTransition(label = "orb")
// Slow breathing loop (7s for idle, 2.4s fast breath for streaming/recording thinking)
@ -151,7 +149,7 @@ fun KaizenOrb(
colors = if (recording) {
listOf(Color(0xFFE0A23E).copy(alpha = 0.45f), Color.Transparent)
} else {
listOf(Amber.copy(alpha = if (streaming) 0.55f else 0.32f), Color.Transparent)
listOf(accent.primary.copy(alpha = if (streaming) 0.55f else 0.32f), Color.Transparent)
}
),
CircleShape,
@ -179,7 +177,7 @@ fun KaizenOrb(
colorStops = arrayOf(
0.0f to Color(0xFFFFF1D6), // Ultra bright glowing cream-white core
0.35f to Color(0xFFFFCC80), // Rich luminous warm amber
0.75f to AmberDeep, // Saturated golden amber
0.75f to accent.primaryDeep, // Saturated accent
1.0f to Color(0xFF8D4F00) // Deep bronze-amber shadow edge (creates high contrast!)
),
center = baseGradientCenter,
@ -210,10 +208,10 @@ fun KaizenOrb(
val sweepBrush1 = Brush.sweepGradient(
colorStops = arrayOf(
0.0f to Color.Transparent,
0.16f to Amber.copy(alpha = 0.65f),
0.16f to accent.primary.copy(alpha = 0.65f),
0.33f to Color.White.copy(alpha = 0.55f),
0.50f to Color.Transparent,
0.66f to AmberDeep.copy(alpha = 0.50f),
0.66f to accent.primaryDeep.copy(alpha = 0.50f),
1.0f to Color.Transparent
),
center = coreCenter
@ -230,7 +228,7 @@ fun KaizenOrb(
0.0f to Color.Transparent,
0.25f to Color.White.copy(alpha = 0.40f),
0.50f to Color.Transparent,
0.75f to Amber.copy(alpha = 0.50f),
0.75f to accent.primary.copy(alpha = 0.50f),
1.0f to Color.Transparent
),
center = coreCenter

View file

@ -64,9 +64,7 @@ import dev.kaizen.app.ui.effect.GlassTiers
import dev.kaizen.app.ui.effect.KaizenShadows
import dev.kaizen.app.ui.motion.Durations
import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.AmberDeep
import dev.kaizen.app.ui.theme.OnAmber
import dev.kaizen.app.ui.theme.LocalKaizenAccent
import dev.kaizen.app.R
import androidx.compose.ui.res.stringResource
import java.time.LocalDateTime
@ -189,13 +187,14 @@ fun ModePillsRow(selected: Set<Int>, onToggle: (Int) -> Unit, modifier: Modifier
fun MessageRow(message: Message) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val hasAttachments = message.attachments.isNotEmpty()
if (message.role == Role.User) {
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) {
val shape = KaizenShapes.lg
val borderBrush = remember { Brush.verticalGradient(listOf(Amber.copy(alpha = 0.45f), AmberDeep.copy(alpha = 0.15f))) }
val bgBrush = remember { Brush.verticalGradient(listOf(Amber.copy(alpha = 0.18f), AmberDeep.copy(alpha = 0.08f))) }
val borderBrush = remember(accent) { Brush.verticalGradient(listOf(accent.primary.copy(alpha = 0.45f), accent.primaryDeep.copy(alpha = 0.15f))) }
val bgBrush = remember(accent) { Brush.verticalGradient(listOf(accent.primary.copy(alpha = 0.18f), accent.primaryDeep.copy(alpha = 0.08f))) }
Box(
Modifier
@ -312,10 +311,11 @@ fun ChatInput(
}
Box(Modifier.weight(1f).heightIn(min = 36.dp).padding(horizontal = 4.dp), contentAlignment = Alignment.CenterStart) {
if (value.isEmpty()) Text(stringResource(R.string.chat_input_placeholder), color = cs.onSurfaceVariant, fontSize = 16.sp)
val accent = LocalKaizenAccent.current
BasicTextField(
value = value, onValueChange = onValueChange,
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp),
cursorBrush = SolidColor(Amber), maxLines = 6, modifier = Modifier.fillMaxWidth(),
cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(),
)
}
GhostIconButton(Icons.Rounded.Call, "Anruf")
@ -383,7 +383,8 @@ private fun SendButton(enabled: Boolean, onClick: () -> Unit) {
val interaction = remember { MutableInteractionSource() }
val pressed by interaction.collectIsPressedAsState()
val pressScale by animateFloatAsState(if (pressed) 0.86f else 1f, label = "send")
val fill = if (enabled) Modifier.background(Brush.linearGradient(listOf(Amber, AmberDeep)))
val accent = LocalKaizenAccent.current
val fill = if (enabled) Modifier.background(Brush.linearGradient(listOf(accent.primary, accent.primaryDeep)))
else Modifier.background(cs.surface)
Box(
@ -392,7 +393,7 @@ private fun SendButton(enabled: Boolean, onClick: () -> Unit) {
.clickable(interactionSource = interaction, indication = null, enabled = enabled, onClick = onClick),
contentAlignment = Alignment.Center,
) {
SendArrow(color = if (enabled) OnAmber else cs.onSurfaceVariant)
SendArrow(color = if (enabled) accent.onPrimary else cs.onSurfaceVariant)
}
}

View file

@ -21,7 +21,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import dev.kaizen.app.net.KaizenModel
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.LocalKaizenAccent
import dev.kaizen.app.haptics.rememberHaptics
/** Human label + composite-id resolver for the top pill — falls back to a de-prefixed id. */
@ -32,6 +32,7 @@ fun modelDisplayName(modelId: String, models: List<KaizenModel>): String =
@Composable
fun ModelPill(label: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
val cs = MaterialTheme.colorScheme
val accent = LocalKaizenAccent.current
Row(
modifier = modifier
.clip(KaizenShapes.xl)
@ -108,6 +109,7 @@ fun ModelSheet(
onDismiss: () -> Unit,
) {
val cs = MaterialTheme.colorScheme
val accent = LocalKaizenAccent.current
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
var query by remember { mutableStateOf("") }
val groups = remember(models, favorites, query) { buildGroups(models, favorites, query) }
@ -152,7 +154,7 @@ fun ModelSheet(
value = query,
onValueChange = { query = it },
singleLine = true,
cursorBrush = SolidColor(Amber),
cursorBrush = SolidColor(accent.primary),
textStyle = androidx.compose.ui.text.TextStyle(
color = cs.onBackground,
fontSize = 14.sp
@ -221,6 +223,7 @@ private fun ModelRow(
onToggleFavorite: () -> Unit,
) {
val cs = MaterialTheme.colorScheme
val accent = LocalKaizenAccent.current
val haptics = rememberHaptics()
val providerInfo = remember(model.id, model.provider) { getProviderInfo(model.id, model.provider) }
@ -231,14 +234,14 @@ private fun ModelRow(
.clip(KaizenShapes.md)
.background(
if (selected) {
Amber.copy(alpha = 0.08f)
accent.primary.copy(alpha = 0.08f)
} else {
cs.surfaceVariant.copy(alpha = 0.15f)
}
)
.border(
width = 1.dp,
color = if (selected) Amber.copy(alpha = 0.25f) else Color.Transparent,
color = if (selected) accent.primary.copy(alpha = 0.25f) else Color.Transparent,
shape = KaizenShapes.md
)
.clickable { haptics.tick(); onClick() }
@ -290,7 +293,7 @@ private fun ModelRow(
Icon(
imageVector = if (favorite) Icons.Rounded.Star else Icons.Rounded.StarBorder,
contentDescription = "Favorit",
tint = if (favorite) Amber else cs.onSurfaceVariant.copy(alpha = 0.35f),
tint = if (favorite) accent.primary else cs.onSurfaceVariant.copy(alpha = 0.35f),
modifier = Modifier.size(20.dp),
)
}
@ -300,7 +303,7 @@ private fun ModelRow(
Icon(
Icons.Rounded.Check,
contentDescription = "Ausgewählt",
tint = Amber,
tint = accent.primary,
modifier = Modifier.size(20.dp)
)
}

View file

@ -46,7 +46,7 @@ import dev.kaizen.app.ui.effect.GlassTiers
import dev.kaizen.app.ui.effect.KaizenShadows
import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.R
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.LocalKaizenAccent
import androidx.compose.ui.res.stringResource
import java.time.Instant
import java.time.LocalDate
@ -243,14 +243,15 @@ fun KaizenSidebar(
private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick: () -> Unit) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val itemBg = when {
selected -> Amber.copy(alpha = 0.14f)
selected -> accent.primary.copy(alpha = 0.14f)
isDark -> Color(0x0AFFFFFF)
else -> Color(0x05000000)
}
val itemBorder = when {
selected -> Amber.copy(alpha = 0.35f)
selected -> accent.primary.copy(alpha = 0.35f)
isDark -> Color.White.copy(alpha = 0.04f)
else -> Color.Black.copy(alpha = 0.03f)
}
@ -266,7 +267,7 @@ private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick
verticalAlignment = Alignment.CenterVertically
) {
if (item.pinned) {
Icon(Icons.Rounded.Star, null, tint = Amber, modifier = Modifier.size(14.dp))
Icon(Icons.Rounded.Star, null, tint = accent.primary, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(8.dp))
}
Text(
@ -279,7 +280,7 @@ private fun HistoryItemRow(item: ConversationSummary, selected: Boolean, onClick
)
if (item.locked) {
Spacer(Modifier.width(8.dp))
Icon(Icons.Rounded.Lock, stringResource(R.string.chat_locked_badge), tint = Amber, modifier = Modifier.size(15.dp))
Icon(Icons.Rounded.Lock, stringResource(R.string.chat_locked_badge), tint = accent.primary, modifier = Modifier.size(15.dp))
}
}
}

View file

@ -46,12 +46,13 @@ import androidx.compose.ui.unit.sp
import dev.kaizen.app.net.SearchSource
import dev.kaizen.app.net.ToolEvent
import dev.kaizen.app.ui.shape.KaizenShapes
import dev.kaizen.app.ui.theme.Amber
import dev.kaizen.app.ui.theme.LocalKaizenAccent
@Composable
fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
var expanded by remember { mutableStateOf(streaming) }
val bg = if (isDark) Color.White.copy(alpha = 0.05f) else Color.Black.copy(alpha = 0.03f)
@ -71,7 +72,7 @@ fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
.padding(horizontal = 12.dp, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(Icons.Rounded.Psychology, null, tint = Amber, modifier = Modifier.size(16.dp))
Icon(Icons.Rounded.Psychology, null, tint = accent.primary, modifier = Modifier.size(16.dp))
Spacer(Modifier.width(8.dp))
Text(
"Gedankengang",
@ -101,6 +102,7 @@ fun ReasoningBlock(reasoning: String, streaming: Boolean = false) {
fun ToolEventsBlock(tools: List<ToolEvent>, streaming: Boolean = false) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val bg = if (isDark) Color.White.copy(alpha = 0.04f) else Color.Black.copy(alpha = 0.02f)
val borderColor = if (isDark) Color.White.copy(alpha = 0.06f) else Color.Black.copy(alpha = 0.04f)
@ -123,7 +125,7 @@ fun ToolEventsBlock(tools: List<ToolEvent>, streaming: Boolean = false) {
val tint = when (tool.type) {
"end" -> Color(0xFF10B981)
"error" -> Color(0xFFEF4444)
else -> Amber
else -> accent.primary
}
Icon(icon, null, tint = tint, modifier = Modifier.size(14.dp))
Spacer(Modifier.width(8.dp))

View file

@ -54,8 +54,7 @@ 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.LocalKaizenAccent
import dev.kaizen.app.ui.theme.Oklab
import dev.kaizen.app.ui.theme.themes.KaizenThemeId
@ -159,6 +158,7 @@ fun SettingsInputCapsule(
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
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)
@ -180,7 +180,7 @@ fun SettingsInputCapsule(
fontSize = 14.sp,
fontWeight = FontWeight.Medium
),
cursorBrush = SolidColor(Amber),
cursorBrush = SolidColor(accent.primary),
singleLine = true,
modifier = Modifier.fillMaxWidth()
)
@ -196,6 +196,7 @@ fun ThemeOptionPill(
modifier: Modifier = Modifier
) {
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
// Exact OKLCH brand stops sourced directly from amber.css, blue.css, aurora.css, and mono.css
val (colorStart, colorEnd) = remember(themeId, isDark) {
@ -231,7 +232,7 @@ fun ThemeOptionPill(
.border(
width = if (selected) 2.5.dp else 1.2.dp,
brush = if (selected) {
Brush.verticalGradient(listOf(Amber, AmberDeep))
Brush.verticalGradient(listOf(accent.primary, accent.primaryDeep))
} else {
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.35f), Color.White.copy(alpha = 0.05f)))
},
@ -269,6 +270,7 @@ fun AppearanceTogglePill(
) {
val cs = MaterialTheme.colorScheme
val isDark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current
val glassBorder = remember(isDark) {
if (isDark) {
@ -284,7 +286,7 @@ fun AppearanceTogglePill(
.clip(KaizenShapes.circle)
.background(
if (selected) {
Amber.copy(alpha = 0.16f)
accent.primary.copy(alpha = 0.16f)
} else {
if (isDark) Color(0x0AFFFFFF) else Color(0x82FFFFFF)
}
@ -292,7 +294,7 @@ fun AppearanceTogglePill(
.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)))
Brush.verticalGradient(listOf(accent.primary.copy(alpha = 0.50f), accent.primary.copy(alpha = 0.15f)))
} else {
glassBorder
},