feat: visual refresh — lighter typography, message animations, richer themes

Typography: body text W400→W300 for airier feel, headings scaled up
(H1 26sp, H2 22sp, H3 18sp), W600/W500 weight hierarchy for contrast.

Animations: animateItem() on LazyColumn messages — 350ms fade-in,
spring placement, 200ms fade-out.

Themes: blobs 20% larger + better full-screen distribution, higher
alpha on OLED (0.48 peak), light mode factor 0.65→0.78, reduced
vignette, distinct primaryDeep per theme for richer gradients.
This commit is contained in:
Bruno Deanoz 2026-06-23 12:22:38 +02:00
parent fbead82ada
commit 75ca8c0e7b
9 changed files with 33 additions and 23 deletions

View file

@ -45,7 +45,7 @@ fun MeshBackground(animateBlobs: Boolean = true, content: @Composable BoxScope.(
val dark = isSystemInDarkTheme() val dark = isSystemInDarkTheme()
val accent = LocalKaizenAccent.current val accent = LocalKaizenAccent.current
val base = MaterialTheme.colorScheme.background val base = MaterialTheme.colorScheme.background
val factor = if (dark) 1f else 0.65f val factor = if (dark) 1f else 0.78f
val drift = if (animateBlobs) { val drift = if (animateBlobs) {
val transition = rememberInfiniteTransition(label = "blobs") val transition = rememberInfiniteTransition(label = "blobs")
@ -70,14 +70,14 @@ fun MeshBackground(animateBlobs: Boolean = true, content: @Composable BoxScope.(
drawRect(brush = ditherBrush, alpha = DITHER_ALPHA) drawRect(brush = ditherBrush, alpha = DITHER_ALPHA)
}, },
) { ) {
Blob(accent.blob1, 360.dp, (-60f + drift * 30f).dp, (-40f + drift * 24f).dp, 0.42f * factor) Blob(accent.blob1, 440.dp, (-80f + drift * 40f).dp, (-60f + drift * 30f).dp, 0.48f * factor)
Blob(accent.blob2, 380.dp, (170f - drift * 44f).dp, (150f + drift * 40f).dp, 0.36f * factor) Blob(accent.blob2, 460.dp, (140f - drift * 50f).dp, (200f + drift * 50f).dp, 0.40f * factor)
Blob(accent.blob3, 320.dp, (220f + drift * 36f).dp, (80f - drift * 28f).dp, 0.30f * factor) Blob(accent.blob3, 400.dp, (-30f + drift * 60f).dp, (450f - drift * 40f).dp, 0.36f * factor)
Blob(accent.blob4, 300.dp, (30f + drift * 50f).dp, (560f - drift * 36f).dp, 0.34f * factor) Blob(accent.blob4, 420.dp, (180f + drift * 40f).dp, (620f - drift * 44f).dp, 0.38f * factor)
if (dark) { if (dark) {
Box( Box(
Modifier.fillMaxSize().background( Modifier.fillMaxSize().background(
Brush.radialGradient(listOf(Color.Transparent, base.copy(alpha = 0.40f))), Brush.radialGradient(listOf(Color.Transparent, base.copy(alpha = 0.35f))),
), ),
) )
} }

View file

@ -135,11 +135,11 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = M
) { ) {
KaizenOrb(104.dp) KaizenOrb(104.dp)
Spacer(Modifier.height(24.dp)) Spacer(Modifier.height(24.dp))
Text("${greeting(now.hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp) Text("${greeting(now.hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp, fontWeight = FontWeight.W300)
Spacer(Modifier.height(6.dp)) Spacer(Modifier.height(6.dp))
Text(stringResource(R.string.hero_headline), color = cs.onBackground, fontSize = 32.sp, fontWeight = FontWeight.Bold) Text(stringResource(R.string.hero_headline), color = cs.onBackground, fontSize = 32.sp, fontWeight = FontWeight.W600)
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp) Text(dateLine, color = cs.onSurfaceVariant.copy(alpha = 0.7f), fontSize = 13.sp, fontWeight = FontWeight.W300)
} }
} }
@ -520,7 +520,7 @@ fun MessageRow(
if (hasAttachments) AttachmentGrid(message.attachments, Modifier.padding(6.dp)) if (hasAttachments) AttachmentGrid(message.attachments, Modifier.padding(6.dp))
if (message.content.isNotBlank()) { if (message.content.isNotBlank()) {
Text( Text(
message.content, color = cs.onBackground, fontSize = 16.sp, lineHeight = 26.sp, message.content, color = cs.onBackground, fontSize = 16.sp, lineHeight = 26.sp, fontWeight = FontWeight.W300,
modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = if (hasAttachments) 4.dp else 13.dp, bottom = 13.dp), modifier = Modifier.padding(start = 16.dp, end = 16.dp, top = if (hasAttachments) 4.dp else 13.dp, bottom = 13.dp),
) )
} }
@ -784,10 +784,11 @@ fun ChatInput(
stringResource(R.string.chat_input_placeholder), stringResource(R.string.chat_input_placeholder),
color = cs.onSurfaceVariant.copy(alpha = 0.5f), color = cs.onSurfaceVariant.copy(alpha = 0.5f),
fontSize = 16.sp, fontSize = 16.sp,
fontWeight = FontWeight.W300,
) )
BasicTextField( BasicTextField(
value = value, onValueChange = onValueChange, value = value, onValueChange = onValueChange,
textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp), textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 24.sp, fontWeight = FontWeight.W300),
cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(), cursorBrush = SolidColor(accent.primary), maxLines = 6, modifier = Modifier.fillMaxWidth(),
) )
} }

View file

@ -88,6 +88,9 @@ import android.media.MediaPlayer
import android.media.MediaRecorder import android.media.MediaRecorder
import android.widget.Toast import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -866,6 +869,11 @@ fun ChatScreen(
verticalArrangement = Arrangement.spacedBy(24.dp), verticalArrangement = Arrangement.spacedBy(24.dp),
) { ) {
items(messages, key = { it.id }) { message -> items(messages, key = { it.id }) { message ->
Box(Modifier.animateItem(
fadeInSpec = tween(350),
placementSpec = spring(stiffness = Spring.StiffnessMediumLow),
fadeOutSpec = tween(200),
)) {
MessageRow( MessageRow(
message = message, message = message,
onDelete = if (conversationId != null && !isStreaming) { wireId -> onDelete = if (conversationId != null && !isStreaming) { wireId ->
@ -904,6 +912,7 @@ fun ChatScreen(
} else null, } else null,
isPlayingTts = playingTtsForId == message.id, isPlayingTts = playingTtsForId == message.id,
) )
}
} }
} }
@ -1014,7 +1023,7 @@ fun ChatScreen(
modelDisplayName(displayModel, models), modelDisplayName(displayModel, models),
color = MaterialTheme.colorScheme.onBackground, color = MaterialTheme.colorScheme.onBackground,
fontSize = 13.sp, fontSize = 13.sp,
fontWeight = FontWeight.Medium, fontWeight = FontWeight.W300,
maxLines = 1, maxLines = 1,
modifier = Modifier.widthIn(max = 180.dp), modifier = Modifier.widthIn(max = 180.dp),
overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis, overflow = androidx.compose.ui.text.style.TextOverflow.Ellipsis,

View file

@ -461,9 +461,9 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier =
if (idx > 0) Spacer(Modifier.height(18.dp)) if (idx > 0) Spacer(Modifier.height(18.dp))
val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor) val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor)
ClickableMarkdownText(rendered, context, cs.onBackground, ClickableMarkdownText(rendered, context, cs.onBackground,
fontSize = when (block.level) { 1 -> 22.sp; 2 -> 19.sp; else -> 17.sp }, fontSize = when (block.level) { 1 -> 26.sp; 2 -> 22.sp; else -> 18.sp },
fontWeight = FontWeight.Bold, fontWeight = when (block.level) { 1 -> FontWeight.W600; 2 -> FontWeight.W600; else -> FontWeight.W500 },
lineHeight = when (block.level) { 1 -> 30.sp; 2 -> 27.sp; else -> 24.sp }, lineHeight = when (block.level) { 1 -> 34.sp; 2 -> 30.sp; else -> 26.sp },
) )
Spacer(Modifier.height(8.dp)) Spacer(Modifier.height(8.dp))
} }
@ -592,7 +592,7 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier =
val appendCursor = isLast && itemIdx == block.items.lastIndex val appendCursor = isLast && itemIdx == block.items.lastIndex
val rendered = inlineMarkdown(item.text + if (appendCursor) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor) val rendered = inlineMarkdown(item.text + if (appendCursor) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor)
ClickableMarkdownText(rendered, context, cs.onBackground, ClickableMarkdownText(rendered, context, cs.onBackground,
fontSize = 16.sp, lineHeight = 26.sp, fontSize = 16.sp, lineHeight = 26.sp, fontWeight = FontWeight.W300,
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
) )
} }
@ -641,7 +641,7 @@ fun MarkdownText(text: String, streaming: Boolean = false, modifier: Modifier =
if (idx > 0) Spacer(Modifier.height(12.dp)) if (idx > 0) Spacer(Modifier.height(12.dp))
val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor) val rendered = inlineMarkdown(block.text + if (isLast) cursor else "", cs.onBackground, inlineCodeColor, inlineCodeBg, linkColor)
ClickableMarkdownText(rendered, context, cs.onBackground, ClickableMarkdownText(rendered, context, cs.onBackground,
fontSize = 16.sp, lineHeight = 26.sp, fontSize = 16.sp, lineHeight = 26.sp, fontWeight = FontWeight.W300,
) )
} }

View file

@ -88,14 +88,14 @@ val Typography = Typography(
), ),
bodyLarge = TextStyle( bodyLarge = TextStyle(
fontFamily = InterVariable, fontFamily = InterVariable,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W300,
fontSize = 16.sp, fontSize = 16.sp,
lineHeight = 24.sp, lineHeight = 24.sp,
letterSpacing = 0.08.sp, letterSpacing = 0.08.sp,
), ),
bodyMedium = TextStyle( bodyMedium = TextStyle(
fontFamily = InterVariable, fontFamily = InterVariable,
fontWeight = FontWeight.W400, fontWeight = FontWeight.W300,
fontSize = 14.sp, fontSize = 14.sp,
lineHeight = 20.sp, lineHeight = 20.sp,
letterSpacing = 0.11.sp, letterSpacing = 0.11.sp,

View file

@ -12,7 +12,7 @@ val AmberTheme = KaizenThemeSpec(
label = "Amber", label = "Amber",
light = KaizenAccentScheme( light = KaizenAccentScheme(
primary = p3(0.811f, 0.608f, 0.126f), // oklch(0.72 0.14 83) primary = p3(0.811f, 0.608f, 0.126f), // oklch(0.72 0.14 83)
primaryDeep = p3(0.811f, 0.608f, 0.126f), primaryDeep = p3(0.680f, 0.480f, 0.050f), // oklch(0.62 0.15 83) richer gold
onPrimary = p3(0.015f, 0.013f, 0.009f), // oklch(0.10 0.005 83) onPrimary = p3(0.015f, 0.013f, 0.009f), // oklch(0.10 0.005 83)
ring = p3(0.811f, 0.608f, 0.126f), ring = p3(0.811f, 0.608f, 0.126f),
blob1 = p3(0.992f, 0.767f, 0.282f), // oklch(0.85 0.15 83) gold blob1 = p3(0.992f, 0.767f, 0.282f), // oklch(0.85 0.15 83) gold

View file

@ -14,7 +14,7 @@ val AuroraTheme = KaizenThemeSpec(
label = "Aurora", label = "Aurora",
light = KaizenAccentScheme( light = KaizenAccentScheme(
primary = p3(0.724f, 0.328f, 0.812f), // oklch(0.62 0.20 320) purple primary = p3(0.724f, 0.328f, 0.812f), // oklch(0.62 0.20 320) purple
primaryDeep = p3(0.724f, 0.328f, 0.812f), primaryDeep = p3(0.580f, 0.200f, 0.700f), // oklch(0.50 0.22 320) deeper purple
onPrimary = p3(1.000f, 0.979f, 1.000f), // oklch(0.99 0.01 320) onPrimary = p3(1.000f, 0.979f, 1.000f), // oklch(0.99 0.01 320)
ring = p3(0.724f, 0.328f, 0.812f), ring = p3(0.724f, 0.328f, 0.812f),
blob1 = p3(0.855f, 0.540f, 0.926f), // oklch(0.75 0.16 320) purple blob1 = p3(0.855f, 0.540f, 0.926f), // oklch(0.75 0.16 320) purple

View file

@ -12,7 +12,7 @@ val BlauTheme = KaizenThemeSpec(
label = "Blau", label = "Blau",
light = KaizenAccentScheme( light = KaizenAccentScheme(
primary = p3(0.168f, 0.513f, 0.962f), // oklch(0.62 0.19 257) primary = p3(0.168f, 0.513f, 0.962f), // oklch(0.62 0.19 257)
primaryDeep = p3(0.168f, 0.513f, 0.962f), primaryDeep = p3(0.090f, 0.380f, 0.820f), // oklch(0.52 0.20 257) deeper blue
onPrimary = p3(0.971f, 0.989f, 1.000f), // oklch(0.99 0.01 257) onPrimary = p3(0.971f, 0.989f, 1.000f), // oklch(0.99 0.01 257)
ring = p3(0.168f, 0.513f, 0.962f), ring = p3(0.168f, 0.513f, 0.962f),
blob1 = p3(0.464f, 0.725f, 1.000f), // oklch(0.78 0.15 257) sky blob1 = p3(0.464f, 0.725f, 1.000f), // oklch(0.78 0.15 257) sky

View file

@ -12,7 +12,7 @@ val MonoTheme = KaizenThemeSpec(
label = "Mono", label = "Mono",
light = KaizenAccentScheme( light = KaizenAccentScheme(
primary = p3(0.104f, 0.104f, 0.104f), // oklch(0.22 0 255) near-black primary = p3(0.104f, 0.104f, 0.104f), // oklch(0.22 0 255) near-black
primaryDeep = p3(0.104f, 0.104f, 0.104f), primaryDeep = p3(0.050f, 0.050f, 0.050f), // oklch(0.14 0 255) deeper black
onPrimary = p3(0.987f, 0.987f, 0.987f), // oklch(0.99 0 255) near-white onPrimary = p3(0.987f, 0.987f, 0.987f), // oklch(0.99 0 255) near-white
ring = p3(0.104f, 0.104f, 0.104f), ring = p3(0.104f, 0.104f, 0.104f),
blob1 = p3(0.806f, 0.806f, 0.806f), // oklch(0.85 0 255) light gray blob1 = p3(0.806f, 0.806f, 0.806f), // oklch(0.85 0 255) light gray