design polish: bug fixes, token migration, a11y touch targets
- GlassSurface: derive shadow geometry from shape (pill/circle now curve correctly)
new shape-aware kaizenShadow(stack, shape) overload in Shadow.kt
drop cornerRadius parameter from GlassSurface and all 8 call sites
- EmptyHero: replace remember { LocalDateTime.now() } with produceState that
re-emits on minute boundaries; clock no longer freezes on first composition
- ChatScreen + MeshBackground: skip blob layer + dither while LiveCallOverlay
is up (renderBlobs flag); avoids two animated MeshBackgrounds compositing
in parallel
- Color tokens: add KaizenSemanticColors.warning/warningDeep,
KaizenNeutralWashes (idle/hover/selected), KaizenGlassTint
(darkTop/darkBottom/lightTop/lightBottom)
- Migrate inline Color(0xFF...) literals to tokens in LiveCallOverlay,
ChatScreen (version-skew banner), Sidebar (4x neutral washes),
LoginScreen / PasswordUnlockDialog / SecuritySection (glass tints),
GlassSurface (use KaizenGlassTint)
- New ui/theme/Spacing.kt: KaizenSpacing scale (xxs-xxl),
KaizenTouch (minTouch/comfortable/hero), KaizenIconSize (xs-xl)
- Accessibility touch targets:
Sidebar Settings/Logout 30 -> 44dp
Sidebar New-Chat 34 -> 44dp
Sidebar history kebab 28 -> 44dp
ChatInput Plus/Tune/Mic/Send/Call/Stop 40 -> 48dp
- Remove no-op pointerInput(Unit) {} on KaizenOrb
- Expose SquircleShape.radius (val instead of private) so shadow can read it
This commit is contained in:
parent
3b32b208b6
commit
1b4cc475f9
15 changed files with 245 additions and 69 deletions
|
|
@ -50,6 +50,7 @@ import dev.kaizen.app.net.LoginResult
|
||||||
import dev.kaizen.app.net.SessionViewModel
|
import dev.kaizen.app.net.SessionViewModel
|
||||||
import dev.kaizen.app.ui.effect.KaizenShadows
|
import dev.kaizen.app.ui.effect.KaizenShadows
|
||||||
import dev.kaizen.app.ui.effect.kaizenShadow
|
import dev.kaizen.app.ui.effect.kaizenShadow
|
||||||
|
import dev.kaizen.app.ui.theme.KaizenGlassTint
|
||||||
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
||||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -169,9 +170,9 @@ private fun GlassField(
|
||||||
val accent = LocalKaizenAccent.current
|
val accent = LocalKaizenAccent.current
|
||||||
|
|
||||||
val glassBg = if (isDark) {
|
val glassBg = if (isDark) {
|
||||||
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.72f), Color(0xFF0F172A).copy(alpha = 0.85f)))
|
Brush.verticalGradient(listOf(KaizenGlassTint.darkTop.copy(alpha = 0.72f), KaizenGlassTint.darkBottom.copy(alpha = 0.85f)))
|
||||||
} else {
|
} else {
|
||||||
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.90f), Color(0xFFEEF2F6).copy(alpha = 0.80f)))
|
Brush.verticalGradient(listOf(KaizenGlassTint.lightTop.copy(alpha = 0.90f), KaizenGlassTint.lightBottom.copy(alpha = 0.80f)))
|
||||||
}
|
}
|
||||||
val glassBorder = if (isDark) {
|
val glassBorder = if (isDark) {
|
||||||
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.18f), Color.White.copy(alpha = 0.04f)))
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.18f), Color.White.copy(alpha = 0.04f)))
|
||||||
|
|
@ -183,7 +184,7 @@ private fun GlassField(
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.widthIn(max = 420.dp)
|
.widthIn(max = 420.dp)
|
||||||
.kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 20.dp)
|
.kaizenShadow(stack = KaizenShadows.level1, shape = KaizenShapes.lg)
|
||||||
.clip(KaizenShapes.lg)
|
.clip(KaizenShapes.lg)
|
||||||
.background(glassBg)
|
.background(glassBg)
|
||||||
.border(1.2.dp, glassBorder, KaizenShapes.lg)
|
.border(1.2.dp, glassBorder, KaizenShapes.lg)
|
||||||
|
|
@ -222,7 +223,7 @@ private fun SignInButton(enabled: Boolean, loading: Boolean, onClick: () -> Unit
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.widthIn(max = 420.dp)
|
.widthIn(max = 420.dp)
|
||||||
.heightIn(min = 54.dp)
|
.heightIn(min = 54.dp)
|
||||||
.kaizenShadow(stack = KaizenShadows.level2, cornerRadius = 20.dp)
|
.kaizenShadow(stack = KaizenShadows.level2, shape = KaizenShapes.lg)
|
||||||
.clip(KaizenShapes.lg)
|
.clip(KaizenShapes.lg)
|
||||||
.then(fill)
|
.then(fill)
|
||||||
.clickable(enabled = enabled, onClick = onClick)
|
.clickable(enabled = enabled, onClick = onClick)
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,7 @@ import dev.kaizen.app.R
|
||||||
import dev.kaizen.app.ui.effect.GlassSurface
|
import dev.kaizen.app.ui.effect.GlassSurface
|
||||||
import dev.kaizen.app.ui.effect.GlassTiers
|
import dev.kaizen.app.ui.effect.GlassTiers
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
|
import dev.kaizen.app.ui.theme.KaizenGlassTint
|
||||||
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
||||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||||
|
|
||||||
|
|
@ -130,9 +131,9 @@ private fun PasswordField(
|
||||||
val accent = LocalKaizenAccent.current
|
val accent = LocalKaizenAccent.current
|
||||||
|
|
||||||
val glassBg = if (isDark) {
|
val glassBg = if (isDark) {
|
||||||
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.72f), Color(0xFF0F172A).copy(alpha = 0.85f)))
|
Brush.verticalGradient(listOf(KaizenGlassTint.darkTop.copy(alpha = 0.72f), KaizenGlassTint.darkBottom.copy(alpha = 0.85f)))
|
||||||
} else {
|
} else {
|
||||||
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.90f), Color(0xFFEEF2F6).copy(alpha = 0.80f)))
|
Brush.verticalGradient(listOf(KaizenGlassTint.lightTop.copy(alpha = 0.90f), KaizenGlassTint.lightBottom.copy(alpha = 0.80f)))
|
||||||
}
|
}
|
||||||
val glassBorder = if (isDark) {
|
val glassBorder = if (isDark) {
|
||||||
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.18f), Color.White.copy(alpha = 0.04f)))
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.18f), Color.White.copy(alpha = 0.04f)))
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,6 @@ import androidx.compose.ui.graphics.BlendMode
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.input.pointer.pointerInput
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.kaizen.app.ui.motion.Durations
|
import dev.kaizen.app.ui.motion.Durations
|
||||||
|
|
@ -39,15 +38,25 @@ import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||||
import dev.kaizen.app.ui.theme.DITHER_ALPHA
|
import dev.kaizen.app.ui.theme.DITHER_ALPHA
|
||||||
import dev.kaizen.app.ui.theme.rememberDitherBrush
|
import dev.kaizen.app.ui.theme.rememberDitherBrush
|
||||||
|
|
||||||
/** Mesh/blob background: large, heavily blurred color circles that drift slowly. */
|
/** Mesh/blob background: large, heavily blurred color circles that drift slowly.
|
||||||
|
*
|
||||||
|
* @param animateBlobs whether the blobs should drift; set false to freeze them.
|
||||||
|
* @param renderBlobs whether to actually paint the blob layer. Set false when a
|
||||||
|
* fullscreen sibling (e.g. [dev.kaizen.app.live.LiveCallOverlay]) already paints
|
||||||
|
* its own MeshBackground — avoids two animated blob stacks compositing at once.
|
||||||
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
fun MeshBackground(animateBlobs: Boolean = true, content: @Composable BoxScope.() -> Unit) {
|
fun MeshBackground(
|
||||||
|
animateBlobs: Boolean = true,
|
||||||
|
renderBlobs: Boolean = true,
|
||||||
|
content: @Composable BoxScope.() -> Unit,
|
||||||
|
) {
|
||||||
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.78f
|
val factor = if (dark) 1f else 0.78f
|
||||||
|
|
||||||
val drift = if (animateBlobs) {
|
val drift = if (animateBlobs && renderBlobs) {
|
||||||
val transition = rememberInfiniteTransition(label = "blobs")
|
val transition = rememberInfiniteTransition(label = "blobs")
|
||||||
val d by transition.animateFloat(
|
val d by transition.animateFloat(
|
||||||
initialValue = 0f,
|
initialValue = 0f,
|
||||||
|
|
@ -70,6 +79,7 @@ fun MeshBackground(animateBlobs: Boolean = true, content: @Composable BoxScope.(
|
||||||
drawRect(brush = ditherBrush, alpha = DITHER_ALPHA)
|
drawRect(brush = ditherBrush, alpha = DITHER_ALPHA)
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
|
if (renderBlobs) {
|
||||||
Blob(accent.blob1, 440.dp, (-80f + drift * 40f).dp, (-60f + drift * 30f).dp, 0.48f * factor)
|
Blob(accent.blob1, 440.dp, (-80f + drift * 40f).dp, (-60f + drift * 30f).dp, 0.48f * factor)
|
||||||
Blob(accent.blob2, 460.dp, (140f - drift * 50f).dp, (200f + drift * 50f).dp, 0.40f * factor)
|
Blob(accent.blob2, 460.dp, (140f - drift * 50f).dp, (200f + drift * 50f).dp, 0.40f * factor)
|
||||||
Blob(accent.blob3, 400.dp, (-30f + drift * 60f).dp, (450f - drift * 40f).dp, 0.36f * factor)
|
Blob(accent.blob3, 400.dp, (-30f + drift * 60f).dp, (450f - drift * 40f).dp, 0.36f * factor)
|
||||||
|
|
@ -81,6 +91,7 @@ fun MeshBackground(animateBlobs: Boolean = true, content: @Composable BoxScope.(
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
content()
|
content()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -138,8 +149,7 @@ fun KaizenOrb(
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.size(size)
|
.size(size)
|
||||||
.scale(scale * reactiveScale)
|
.scale(scale * reactiveScale),
|
||||||
.pointerInput(Unit) {},
|
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
// --- LAYER 1: Ambient Outer Halo ---
|
// --- LAYER 1: Ambient Outer Halo ---
|
||||||
|
|
|
||||||
|
|
@ -79,13 +79,14 @@ import java.util.Locale
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.platform.LocalClipboard
|
import androidx.compose.ui.platform.LocalClipboard
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import dev.kaizen.app.haptics.rememberHaptics
|
import dev.kaizen.app.haptics.rememberHaptics
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.graphics.ImageBitmap
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
import androidx.compose.ui.graphics.asImageBitmap
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
|
@ -104,7 +105,6 @@ fun KaizenHeader(modifier: Modifier = Modifier) {
|
||||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||||
shape = KaizenShapes.lg,
|
shape = KaizenShapes.lg,
|
||||||
shadowStack = KaizenShadows.level1,
|
shadowStack = KaizenShadows.level1,
|
||||||
cornerRadius = 24.dp,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
|
@ -121,8 +121,19 @@ fun KaizenHeader(modifier: Modifier = Modifier) {
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = Modifier) {
|
fun EmptyHero(userName: String, onPick: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||||
val cs = MaterialTheme.colorScheme
|
val cs = MaterialTheme.colorScheme
|
||||||
val now = remember { LocalDateTime.now() }
|
|
||||||
val dateLine = remember {
|
// Live clock: re-emits LocalDateTime aligned to the next minute boundary,
|
||||||
|
// so the greeting + "HH:mm" date line never go stale while the user
|
||||||
|
// keeps the empty state open.
|
||||||
|
val now by produceState(initialValue = LocalDateTime.now()) {
|
||||||
|
while (true) {
|
||||||
|
val current = LocalDateTime.now()
|
||||||
|
value = current
|
||||||
|
val msToNextMinute = 60_000L - (System.currentTimeMillis() % 60_000L)
|
||||||
|
delay(msToNextMinute)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val dateLine = remember(now.minute, now.hour, now.dayOfYear) {
|
||||||
now.format(DateTimeFormatter.ofPattern("EEE, d. MMMM · HH:mm", Locale.getDefault()))
|
now.format(DateTimeFormatter.ofPattern("EEE, d. MMMM · HH:mm", Locale.getDefault()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -424,7 +435,6 @@ fun TokenBadge(used: Int, contextLength: Int, modifier: Modifier = Modifier) {
|
||||||
shape = KaizenShapes.pill,
|
shape = KaizenShapes.pill,
|
||||||
tintAlpha = GlassTiers.pill(isDark),
|
tintAlpha = GlassTiers.pill(isDark),
|
||||||
shadowStack = KaizenShadows.level1,
|
shadowStack = KaizenShadows.level1,
|
||||||
cornerRadius = 16.dp,
|
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
|
Modifier.padding(horizontal = 10.dp, vertical = 7.dp),
|
||||||
|
|
@ -543,7 +553,7 @@ fun MessageRow(
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.widthIn(max = 300.dp)
|
.widthIn(max = 300.dp)
|
||||||
.kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 20.dp)
|
.kaizenShadow(stack = KaizenShadows.level1, shape = shape)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(bgBrush)
|
.background(bgBrush)
|
||||||
.background(innerHighlight)
|
.background(innerHighlight)
|
||||||
|
|
@ -814,7 +824,6 @@ fun ChatInput(
|
||||||
shape = KaizenShapes.pill,
|
shape = KaizenShapes.pill,
|
||||||
tintAlpha = if (isDark) 0.92f else 0.97f,
|
tintAlpha = if (isDark) 0.92f else 0.97f,
|
||||||
shadowStack = KaizenShadows.level4,
|
shadowStack = KaizenShadows.level4,
|
||||||
cornerRadius = 34.dp,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier.heightIn(min = 64.dp),
|
Modifier.heightIn(min = 64.dp),
|
||||||
|
|
@ -874,7 +883,7 @@ fun ChatInput(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.size(40.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick),
|
Modifier.size(48.dp).clip(KaizenShapes.circle).clickable(onClick = onAddClick),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
Icon(KaizenIcons.Plus, stringResource(R.string.chat_attachment), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||||
|
|
@ -900,7 +909,7 @@ fun ChatInput(
|
||||||
|
|
||||||
if (pillsContent != null) {
|
if (pillsContent != null) {
|
||||||
Box(
|
Box(
|
||||||
Modifier.size(40.dp).clip(KaizenShapes.circle).clickable { showPills = !showPills },
|
Modifier.size(48.dp).clip(KaizenShapes.circle).clickable { showPills = !showPills },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
|
|
@ -912,7 +921,7 @@ fun ChatInput(
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier.size(40.dp).clip(KaizenShapes.circle).clickable(onClick = onMicClick),
|
Modifier.size(48.dp).clip(KaizenShapes.circle).clickable(onClick = onMicClick),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Icon(KaizenIcons.Mic, stringResource(R.string.chat_mic_permission), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
Icon(KaizenIcons.Mic, stringResource(R.string.chat_mic_permission), tint = cs.onSurfaceVariant, modifier = Modifier.size(22.dp))
|
||||||
|
|
@ -996,7 +1005,7 @@ private fun SendButton(enabled: Boolean, onClick: () -> Unit) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier.size(40.dp).scale(pressScale).clip(KaizenShapes.circle).then(fill)
|
Modifier.size(48.dp).scale(pressScale).clip(KaizenShapes.circle).then(fill)
|
||||||
.clickable(interactionSource = interaction, indication = null, enabled = enabled, onClick = onClick),
|
.clickable(interactionSource = interaction, indication = null, enabled = enabled, onClick = onClick),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
|
|
@ -1015,7 +1024,7 @@ private fun CallButton(onClick: () -> Unit) {
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.scale(pressScale)
|
.scale(pressScale)
|
||||||
.clip(KaizenShapes.circle)
|
.clip(KaizenShapes.circle)
|
||||||
.background(
|
.background(
|
||||||
|
|
@ -1042,7 +1051,7 @@ private fun StopButton(onClick: () -> Unit) {
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
Modifier
|
Modifier
|
||||||
.size(40.dp)
|
.size(48.dp)
|
||||||
.scale(pressScale)
|
.scale(pressScale)
|
||||||
.clip(KaizenShapes.circle)
|
.clip(KaizenShapes.circle)
|
||||||
.background(KaizenSemanticColors.error.copy(alpha = if (isDark) 0.85f else 0.80f))
|
.background(KaizenSemanticColors.error.copy(alpha = if (isDark) 0.85f else 0.80f))
|
||||||
|
|
@ -1181,7 +1190,6 @@ private fun AttachPill(icon: ImageVector, label: String, tint: Color, isDark: Bo
|
||||||
shape = KaizenShapes.pill,
|
shape = KaizenShapes.pill,
|
||||||
tintAlpha = GlassTiers.pill(isDark),
|
tintAlpha = GlassTiers.pill(isDark),
|
||||||
shadowStack = KaizenShadows.level2,
|
shadowStack = KaizenShadows.level2,
|
||||||
cornerRadius = 24.dp,
|
|
||||||
modifier = Modifier.clickable(onClick = onClick),
|
modifier = Modifier.clickable(onClick = onClick),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
|
|
|
||||||
|
|
@ -225,7 +225,13 @@ fun ChatScreen(
|
||||||
},
|
},
|
||||||
gesturesEnabled = true,
|
gesturesEnabled = true,
|
||||||
) {
|
) {
|
||||||
MeshBackground(animateBlobs = vm.messages.isEmpty()) {
|
// Hide the underlying MeshBackground while the LiveCallOverlay is up —
|
||||||
|
// the overlay renders its own fullscreen MeshBackground, so painting two
|
||||||
|
// stacks of animated blobs simultaneously is pure GPU/CPU waste.
|
||||||
|
MeshBackground(
|
||||||
|
animateBlobs = vm.messages.isEmpty() && !vm.liveCallActive,
|
||||||
|
renderBlobs = !vm.liveCallActive,
|
||||||
|
) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
if (vm.messages.isEmpty()) {
|
if (vm.messages.isEmpty()) {
|
||||||
EmptyHero(
|
EmptyHero(
|
||||||
|
|
@ -302,7 +308,6 @@ fun ChatScreen(
|
||||||
shape = KaizenShapes.circle,
|
shape = KaizenShapes.circle,
|
||||||
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
|
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
|
||||||
shadowStack = KaizenShadows.level1,
|
shadowStack = KaizenShadows.level1,
|
||||||
cornerRadius = 18.dp,
|
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.CenterEnd)
|
.align(Alignment.CenterEnd)
|
||||||
.padding(end = 8.dp)
|
.padding(end = 8.dp)
|
||||||
|
|
@ -353,7 +358,6 @@ fun ChatScreen(
|
||||||
shape = KaizenShapes.pill,
|
shape = KaizenShapes.pill,
|
||||||
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
|
tintAlpha = GlassTiers.pill(isSystemInDarkTheme()),
|
||||||
shadowStack = KaizenShadows.level1,
|
shadowStack = KaizenShadows.level1,
|
||||||
cornerRadius = 20.dp,
|
|
||||||
) {
|
) {
|
||||||
Row(Modifier.heightIn(min = 38.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(Modifier.heightIn(min = 38.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -414,7 +418,7 @@ fun ChatScreen(
|
||||||
// Version skew banner
|
// Version skew banner
|
||||||
val skew = vm.session.versionSkew
|
val skew = vm.session.versionSkew
|
||||||
if (skew != null) {
|
if (skew != null) {
|
||||||
val warningColor = Color(0xFFD97706)
|
val warningColor = KaizenSemanticColors.warning
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.align(Alignment.TopCenter)
|
.align(Alignment.TopCenter)
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ import dev.kaizen.app.ui.effect.kaizenShadow
|
||||||
import dev.kaizen.app.ui.effect.innerTopHighlight
|
import dev.kaizen.app.ui.effect.innerTopHighlight
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
import dev.kaizen.app.R
|
import dev.kaizen.app.R
|
||||||
|
import dev.kaizen.app.ui.theme.KaizenNeutralWashes
|
||||||
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
||||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -126,7 +127,6 @@ fun KaizenSidebar(
|
||||||
shape = KaizenShapes.lg,
|
shape = KaizenShapes.lg,
|
||||||
tintAlpha = if (isDark) 0.80f else 0.86f,
|
tintAlpha = if (isDark) 0.80f else 0.86f,
|
||||||
shadowStack = KaizenShadows.level3,
|
shadowStack = KaizenShadows.level3,
|
||||||
cornerRadius = 24.dp,
|
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
Modifier
|
Modifier
|
||||||
|
|
@ -147,9 +147,9 @@ fun KaizenSidebar(
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(34.dp)
|
.size(44.dp)
|
||||||
.clip(KaizenShapes.circle)
|
.clip(KaizenShapes.circle)
|
||||||
.background(if (isDark) Color(0x1AFFFFFF) else Color(0x0A000000))
|
.background(KaizenNeutralWashes.hover(isDark))
|
||||||
.clickable { onNewChat() },
|
.clickable { onNewChat() },
|
||||||
contentAlignment = Alignment.Center
|
contentAlignment = Alignment.Center
|
||||||
) {
|
) {
|
||||||
|
|
@ -172,7 +172,7 @@ fun KaizenSidebar(
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(KaizenShapes.pill)
|
.clip(KaizenShapes.pill)
|
||||||
.border(0.8.dp, searchBorder, KaizenShapes.pill)
|
.border(0.8.dp, searchBorder, KaizenShapes.pill)
|
||||||
.background(if (isDark) Color(0x12FFFFFF) else Color(0x08000000))
|
.background(KaizenNeutralWashes.hover(isDark))
|
||||||
.padding(horizontal = 14.dp, vertical = 9.dp),
|
.padding(horizontal = 14.dp, vertical = 9.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
|
|
@ -314,7 +314,7 @@ fun KaizenSidebar(
|
||||||
)
|
)
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(44.dp)
|
||||||
.clip(KaizenShapes.circle)
|
.clip(KaizenShapes.circle)
|
||||||
.clickable { onOpenSettings() },
|
.clickable { onOpenSettings() },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
@ -329,7 +329,7 @@ fun KaizenSidebar(
|
||||||
Spacer(Modifier.width(4.dp))
|
Spacer(Modifier.width(4.dp))
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(30.dp)
|
.size(44.dp)
|
||||||
.clip(KaizenShapes.circle)
|
.clip(KaizenShapes.circle)
|
||||||
.clickable { onLogout() },
|
.clickable { onLogout() },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
@ -394,7 +394,7 @@ fun KaizenSidebar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(KaizenShapes.sm)
|
.clip(KaizenShapes.sm)
|
||||||
.background(if (isDark) Color(0x12FFFFFF) else Color(0x08000000))
|
.background(KaizenNeutralWashes.hover(isDark))
|
||||||
.padding(horizontal = 14.dp, vertical = 12.dp),
|
.padding(horizontal = 14.dp, vertical = 12.dp),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -458,12 +458,12 @@ private fun HistoryItemRow(
|
||||||
.then(
|
.then(
|
||||||
if (selected) {
|
if (selected) {
|
||||||
Modifier
|
Modifier
|
||||||
.kaizenShadow(stack = KaizenShadows.level1, cornerRadius = 16.dp)
|
.kaizenShadow(stack = KaizenShadows.level1, shape = KaizenShapes.md)
|
||||||
.background(accent.primary.copy(alpha = if (isDark) 0.12f else 0.08f))
|
.background(accent.primary.copy(alpha = if (isDark) 0.12f else 0.08f))
|
||||||
.border(0.8.dp, activeBorder, KaizenShapes.md)
|
.border(0.8.dp, activeBorder, KaizenShapes.md)
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
.background(if (isDark) Color(0x08FFFFFF) else Color(0x05000000))
|
.background(KaizenNeutralWashes.idle(isDark))
|
||||||
.border(0.5.dp, inactiveBorder, KaizenShapes.md)
|
.border(0.5.dp, inactiveBorder, KaizenShapes.md)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -491,7 +491,7 @@ private fun HistoryItemRow(
|
||||||
}
|
}
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.size(28.dp)
|
.size(44.dp) // accessibility tap target
|
||||||
.clip(KaizenShapes.circle)
|
.clip(KaizenShapes.circle)
|
||||||
.clickable { showMenu = true },
|
.clickable { showMenu = true },
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@ import dev.kaizen.app.ui.effect.GlassSurface
|
||||||
import dev.kaizen.app.ui.effect.KaizenShadows
|
import dev.kaizen.app.ui.effect.KaizenShadows
|
||||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
|
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
||||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -162,8 +163,8 @@ private fun EndCallButton(onClick: () -> Unit) {
|
||||||
.background(
|
.background(
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
Color(0xFFEF4444),
|
KaizenSemanticColors.error,
|
||||||
Color(0xFFDC2626),
|
KaizenSemanticColors.errorDeep,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,7 @@ import dev.kaizen.app.R
|
||||||
import dev.kaizen.app.net.AppDevice
|
import dev.kaizen.app.net.AppDevice
|
||||||
import dev.kaizen.app.net.ServerConfig
|
import dev.kaizen.app.net.ServerConfig
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
|
import dev.kaizen.app.ui.theme.KaizenGlassTint
|
||||||
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
import dev.kaizen.app.ui.theme.KaizenSemanticColors
|
||||||
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
import dev.kaizen.app.ui.theme.LocalKaizenAccent
|
||||||
import dev.kaizen.app.ui.icon.KaizenIcons
|
import dev.kaizen.app.ui.icon.KaizenIcons
|
||||||
|
|
@ -319,9 +320,9 @@ private fun PasswordField(
|
||||||
val accent = LocalKaizenAccent.current
|
val accent = LocalKaizenAccent.current
|
||||||
|
|
||||||
val bg = if (isDark) {
|
val bg = if (isDark) {
|
||||||
Brush.verticalGradient(listOf(Color(0xFF1E293B).copy(alpha = 0.60f), Color(0xFF0F172A).copy(alpha = 0.72f)))
|
Brush.verticalGradient(listOf(KaizenGlassTint.darkTop.copy(alpha = 0.60f), KaizenGlassTint.darkBottom.copy(alpha = 0.72f)))
|
||||||
} else {
|
} else {
|
||||||
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.85f), Color(0xFFEEF2F6).copy(alpha = 0.75f)))
|
Brush.verticalGradient(listOf(KaizenGlassTint.lightTop.copy(alpha = 0.85f), KaizenGlassTint.lightBottom.copy(alpha = 0.75f)))
|
||||||
}
|
}
|
||||||
val border = if (isDark) {
|
val border = if (isDark) {
|
||||||
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.14f), Color.White.copy(alpha = 0.03f)))
|
Brush.verticalGradient(listOf(Color.White.copy(alpha = 0.14f), Color.White.copy(alpha = 0.03f)))
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,6 @@ fun SettingsCard(
|
||||||
shape = KaizenShapes.lg,
|
shape = KaizenShapes.lg,
|
||||||
tintAlpha = GlassTiers.card(isDark),
|
tintAlpha = GlassTiers.card(isDark),
|
||||||
shadowStack = KaizenShadows.level2,
|
shadowStack = KaizenShadows.level2,
|
||||||
cornerRadius = 24.dp,
|
|
||||||
) {
|
) {
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(18.dp)) {
|
Column(modifier = Modifier.fillMaxWidth().padding(18.dp)) {
|
||||||
content()
|
content()
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,6 @@ fun SettingsScreen(
|
||||||
shape = KaizenShapes.circle,
|
shape = KaizenShapes.circle,
|
||||||
tintAlpha = GlassTiers.pill(isDark),
|
tintAlpha = GlassTiers.pill(isDark),
|
||||||
shadowStack = KaizenShadows.level2,
|
shadowStack = KaizenShadows.level2,
|
||||||
cornerRadius = 22.dp,
|
|
||||||
modifier = Modifier.size(44.dp).clickable { onBack() },
|
modifier = Modifier.size(44.dp).clickable { onBack() },
|
||||||
) {
|
) {
|
||||||
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
Box(Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
|
|
||||||
|
|
@ -13,9 +13,9 @@ import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Shape
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.unit.Dp
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.kaizen.app.ui.shape.KaizenShapes
|
import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
|
import dev.kaizen.app.ui.theme.KaizenGlassTint
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A frosted-glass surface — the core visual primitive of the Kaizen design system.
|
* A frosted-glass surface — the core visual primitive of the Kaizen design system.
|
||||||
|
|
@ -32,6 +32,9 @@ import dev.kaizen.app.ui.shape.KaizenShapes
|
||||||
* The blob layer behind (MeshBackground) is what "shows through" the
|
* The blob layer behind (MeshBackground) is what "shows through" the
|
||||||
* transparency, creating the frosted-glass illusion.
|
* transparency, creating the frosted-glass illusion.
|
||||||
*
|
*
|
||||||
|
* The shadow geometry is derived from [shape], so pill/circle surfaces get
|
||||||
|
* correctly curved shadows instead of a rectangular fallback.
|
||||||
|
*
|
||||||
* See DESIGN.md §4 for the full anatomy and opacity tiers.
|
* See DESIGN.md §4 for the full anatomy and opacity tiers.
|
||||||
*/
|
*/
|
||||||
@Composable
|
@Composable
|
||||||
|
|
@ -40,7 +43,6 @@ fun GlassSurface(
|
||||||
shape: Shape = KaizenShapes.md,
|
shape: Shape = KaizenShapes.md,
|
||||||
tintAlpha: Float = 0.72f,
|
tintAlpha: Float = 0.72f,
|
||||||
shadowStack: ShadowStack = KaizenShadows.level2,
|
shadowStack: ShadowStack = KaizenShadows.level2,
|
||||||
cornerRadius: Dp = 16.dp,
|
|
||||||
content: @Composable BoxScope.() -> Unit,
|
content: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val isDark = isSystemInDarkTheme()
|
val isDark = isSystemInDarkTheme()
|
||||||
|
|
@ -50,15 +52,15 @@ fun GlassSurface(
|
||||||
if (isDark) {
|
if (isDark) {
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
Color(0xFF1E293B).copy(alpha = tintAlpha),
|
KaizenGlassTint.darkTop.copy(alpha = tintAlpha),
|
||||||
Color(0xFF0F172A).copy(alpha = (tintAlpha + 0.10f).coerceAtMost(1f)),
|
KaizenGlassTint.darkBottom.copy(alpha = (tintAlpha + 0.10f).coerceAtMost(1f)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
Brush.verticalGradient(
|
Brush.verticalGradient(
|
||||||
listOf(
|
listOf(
|
||||||
Color.White.copy(alpha = tintAlpha),
|
KaizenGlassTint.lightTop.copy(alpha = tintAlpha),
|
||||||
Color(0xFFF1F5F9).copy(alpha = (tintAlpha - 0.05f).coerceAtLeast(0f)),
|
KaizenGlassTint.lightBottom.copy(alpha = (tintAlpha - 0.05f).coerceAtLeast(0f)),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -78,7 +80,7 @@ fun GlassSurface(
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.kaizenShadow(stack = shadowStack, cornerRadius = cornerRadius)
|
.kaizenShadow(stack = shadowStack, shape = shape)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(glassBg)
|
.background(glassBg)
|
||||||
.innerTopHighlight()
|
.innerTopHighlight()
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,24 @@
|
||||||
package dev.kaizen.app.ui.effect
|
package dev.kaizen.app.ui.effect
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.drawBehind
|
import androidx.compose.ui.draw.drawBehind
|
||||||
import androidx.compose.ui.geometry.CornerRadius
|
|
||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.geometry.Size
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.Paint
|
import androidx.compose.ui.graphics.Paint
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
|
||||||
import androidx.compose.ui.graphics.toArgb
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import dev.kaizen.app.ui.shape.SquircleShape
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A single layer in a multi-layer shadow stack.
|
* A single layer in a multi-layer shadow stack.
|
||||||
|
|
@ -80,6 +84,10 @@ object KaizenShadows {
|
||||||
/**
|
/**
|
||||||
* Draws a multi-layer shadow stack behind the content.
|
* Draws a multi-layer shadow stack behind the content.
|
||||||
*
|
*
|
||||||
|
* Prefer the [Shape]-based overload below — it derives the shadow's corner
|
||||||
|
* radius from the surface shape automatically, preventing the geometric
|
||||||
|
* mismatch where a pill/circle surface gets a rectangular shadow.
|
||||||
|
*
|
||||||
* [darkMultiplier] scales shadow alpha in dark mode (shadows need to be
|
* [darkMultiplier] scales shadow alpha in dark mode (shadows need to be
|
||||||
* stronger on dark surfaces to read). Default 1.8× matches the DESIGN.md spec.
|
* stronger on dark surfaces to read). Default 1.8× matches the DESIGN.md spec.
|
||||||
*/
|
*/
|
||||||
|
|
@ -97,12 +105,63 @@ fun Modifier.kaizenShadow(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return this.drawBehind {
|
return this.drawBehind {
|
||||||
drawShadowLayers(layers, cornerRadius)
|
drawShadowLayers(layers) { _ -> cornerRadius.toPx() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun DrawScope.drawShadowLayers(layers: List<ShadowLayer>, cornerRadius: Dp) {
|
/**
|
||||||
val cr = cornerRadius.toPx()
|
* Shape-aware shadow stack — derives the corner radius from [shape] so the
|
||||||
|
* shadow geometry always matches the surface (especially important for
|
||||||
|
* [SquircleShape] pill/circle variants where the radius is `min(w,h)/2`).
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Modifier.kaizenShadow(
|
||||||
|
stack: ShadowStack,
|
||||||
|
shape: Shape,
|
||||||
|
darkMultiplier: Float = 1.8f,
|
||||||
|
): Modifier {
|
||||||
|
val isDark = isSystemInDarkTheme()
|
||||||
|
val effectiveMultiplier = if (isDark) darkMultiplier else 1f
|
||||||
|
val layers = remember(stack, effectiveMultiplier) {
|
||||||
|
stack.layers.map { layer ->
|
||||||
|
layer.copy(alpha = (layer.alpha * effectiveMultiplier).coerceAtMost(1f))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this.drawBehind {
|
||||||
|
drawShadowLayers(layers) { sz -> resolveCornerRadiusPx(shape, sz) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the effective corner radius (in pixels) for a known [Shape] at a
|
||||||
|
* given [size]. Falls back to 16 dp for unknown shapes.
|
||||||
|
*/
|
||||||
|
private fun DrawScope.resolveCornerRadiusPx(shape: Shape, size: Size): Float {
|
||||||
|
return when (shape) {
|
||||||
|
is SquircleShape -> {
|
||||||
|
// Squircle clamps radius to min(w,h)/2 — mirror that here so pill/circle
|
||||||
|
// shadows curve correctly instead of drawing a rounded rect with r=999.
|
||||||
|
val r = shape.radius.toPx()
|
||||||
|
r.coerceAtMost(min(size.width, size.height) / 2f)
|
||||||
|
}
|
||||||
|
CircleShape -> min(size.width, size.height) / 2f
|
||||||
|
is RoundedCornerShape -> {
|
||||||
|
val outline = shape.createOutline(size, androidx.compose.ui.unit.LayoutDirection.Ltr, this)
|
||||||
|
when (outline) {
|
||||||
|
is androidx.compose.ui.graphics.Outline.Rounded ->
|
||||||
|
outline.roundRect.topLeftCornerRadius.x
|
||||||
|
else -> 16.dp.toPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> 16.dp.toPx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun DrawScope.drawShadowLayers(
|
||||||
|
layers: List<ShadowLayer>,
|
||||||
|
radiusForSize: (Size) -> Float,
|
||||||
|
) {
|
||||||
|
val cr = radiusForSize(size)
|
||||||
drawIntoCanvas { canvas ->
|
drawIntoCanvas { canvas ->
|
||||||
layers.forEach { layer ->
|
layers.forEach { layer ->
|
||||||
val paint = Paint().apply {
|
val paint = Paint().apply {
|
||||||
|
|
|
||||||
|
|
@ -32,8 +32,8 @@ import kotlin.math.sin
|
||||||
* screen density; reduce to 16 for small shapes (< 32 dp).
|
* screen density; reduce to 16 for small shapes (< 32 dp).
|
||||||
*/
|
*/
|
||||||
class SquircleShape(
|
class SquircleShape(
|
||||||
private val radius: Dp,
|
val radius: Dp,
|
||||||
private val smoothness: Float = 4.5f,
|
val smoothness: Float = 4.5f,
|
||||||
private val samplesPerCorner: Int = 32,
|
private val samplesPerCorner: Int = 32,
|
||||||
) : androidx.compose.ui.graphics.Shape {
|
) : androidx.compose.ui.graphics.Shape {
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,8 @@ val BlobTeal = p3(0.461f, 0.885f, 0.908f) // oklch(0.85 0.10 200) teal accent
|
||||||
object KaizenSemanticColors {
|
object KaizenSemanticColors {
|
||||||
val error = Color(0xFFEF4444) // destructive actions, stop, delete
|
val error = Color(0xFFEF4444) // destructive actions, stop, delete
|
||||||
val errorDeep = Color(0xFFDC2626) // error banners, stronger emphasis
|
val errorDeep = Color(0xFFDC2626) // error banners, stronger emphasis
|
||||||
|
val warning = Color(0xFFD97706) // outdated client / non-fatal warnings
|
||||||
|
val warningDeep = Color(0xFFB45309) // warning banners, stronger emphasis
|
||||||
val success = Color(0xFF10B981) // success confirmation, check marks
|
val success = Color(0xFF10B981) // success confirmation, check marks
|
||||||
val successBright = Color(0xFF22C55E) // copy-confirmed flash
|
val successBright = Color(0xFF22C55E) // copy-confirmed flash
|
||||||
val indigo = Color(0xFF6366F1) // Vertex AI badge, gallery accent
|
val indigo = Color(0xFF6366F1) // Vertex AI badge, gallery accent
|
||||||
|
|
@ -61,3 +63,33 @@ object KaizenSemanticColors {
|
||||||
val dropdownDark = Color(0xFF1A2030) // dropdown/menu bg (dark)
|
val dropdownDark = Color(0xFF1A2030) // dropdown/menu bg (dark)
|
||||||
val dropdownLight = Color(0xFFF8F9FB) // dropdown/menu bg (light)
|
val dropdownLight = Color(0xFFF8F9FB) // dropdown/menu bg (light)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Neutral surface washes ────────────────────────────────────────────────
|
||||||
|
// Tinted-white / tinted-black overlays used for hover/idle/pressed states
|
||||||
|
// on neutral surfaces (sidebar rows, menu items, sub-buttons). Encapsulates
|
||||||
|
// the alpha values that were previously littered inline as `Color(0x1AFFFFFF)`.
|
||||||
|
object KaizenNeutralWashes {
|
||||||
|
/** Lightest idle / resting tint. Dark vs light themed. */
|
||||||
|
fun idle(isDark: Boolean): Color =
|
||||||
|
if (isDark) Color(0x08FFFFFF) else Color(0x05000000)
|
||||||
|
|
||||||
|
/** Subtle hover/pressed tint, brighter than idle. */
|
||||||
|
fun hover(isDark: Boolean): Color =
|
||||||
|
if (isDark) Color(0x12FFFFFF) else Color(0x0A000000)
|
||||||
|
|
||||||
|
/** Most prominent neutral wash — selected rows, focus rings. */
|
||||||
|
fun selected(isDark: Boolean): Color =
|
||||||
|
if (isDark) Color(0x1AFFFFFF) else Color(0x14000000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Glass-surface gradient tints ──────────────────────────────────────────
|
||||||
|
// Two-stop vertical gradient stops for the frosted-glass body, owned here
|
||||||
|
// so the GlassSurface implementation does not embed brand hex literals.
|
||||||
|
object KaizenGlassTint {
|
||||||
|
/** Dark-mode glass body: deep slate top → near-black bottom. */
|
||||||
|
val darkTop = Color(0xFF1E293B)
|
||||||
|
val darkBottom = Color(0xFF0F172A)
|
||||||
|
/** Light-mode glass body: pure white top → cool grey bottom. */
|
||||||
|
val lightTop = Color(0xFFFFFFFF)
|
||||||
|
val lightBottom = Color(0xFFF1F5F9)
|
||||||
|
}
|
||||||
|
|
|
||||||
59
app/src/main/java/dev/kaizen/app/ui/theme/Spacing.kt
Normal file
59
app/src/main/java/dev/kaizen/app/ui/theme/Spacing.kt
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
package dev.kaizen.app.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kaizen spacing scale — 4-dp baseline grid.
|
||||||
|
*
|
||||||
|
* Every padding, gap, and inset in the app should reference one of these
|
||||||
|
* tokens. If a value does not fit the scale, that is a signal to either
|
||||||
|
* round to the nearest step or add a new named token (e.g. `topBarHeight`).
|
||||||
|
*
|
||||||
|
* Scale rationale:
|
||||||
|
* xxs 2 dp hairline gaps, icon-to-label tightening
|
||||||
|
* xs 4 dp intra-component spacing
|
||||||
|
* sm 8 dp default gap between siblings
|
||||||
|
* md 12 dp card inner padding, pill padding
|
||||||
|
* lg 16 dp screen edge gutters, section gaps
|
||||||
|
* xl 24 dp hero spacing, dialog padding
|
||||||
|
* xxl 32 dp large hero / fullscreen padding
|
||||||
|
*/
|
||||||
|
object KaizenSpacing {
|
||||||
|
val xxs = 2.dp
|
||||||
|
val xs = 4.dp
|
||||||
|
val sm = 8.dp
|
||||||
|
val md = 12.dp
|
||||||
|
val lg = 16.dp
|
||||||
|
val xl = 24.dp
|
||||||
|
val xxl = 32.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Touch-target size tokens.
|
||||||
|
*
|
||||||
|
* Material Design and WCAG both require a minimum 48 dp touch target.
|
||||||
|
* Use [minTouch] as the floor for any `Modifier.size(...)` that is also
|
||||||
|
* `.clickable`. The visual icon inside may be smaller — wrap it in a Box
|
||||||
|
* of `minTouch` size centered.
|
||||||
|
*/
|
||||||
|
object KaizenTouch {
|
||||||
|
/** Minimum tappable surface (Material / WCAG). */
|
||||||
|
val minTouch = 48.dp
|
||||||
|
/** Comfortable tap target (slightly above minimum). */
|
||||||
|
val comfortable = 52.dp
|
||||||
|
/** Hero-level button — bottom dock, primary CTA, end-call. */
|
||||||
|
val hero = 64.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon size scale — every Icon size should be one of these tokens.
|
||||||
|
*
|
||||||
|
* Aligns visually with the Lucide stroke-width 1.0 px design language.
|
||||||
|
*/
|
||||||
|
object KaizenIconSize {
|
||||||
|
val xs = 12.dp // inline indicator, badge dot
|
||||||
|
val sm = 14.dp // chip / pill icon
|
||||||
|
val md = 16.dp // standard control icon
|
||||||
|
val lg = 20.dp // primary action icon (send, mic)
|
||||||
|
val xl = 24.dp // header icon, hero icon
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue