diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5291775..f7e8391 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,6 +2,9 @@ + + + Unit) { + val dark = isSystemInDarkTheme() + val base = MaterialTheme.colorScheme.background + // Blobs are subtler in light mode. + val factor = if (dark) 1f else 0.55f val transition = rememberInfiniteTransition(label = "blobs") val drift by transition.animateFloat( initialValue = 0f, @@ -44,16 +49,18 @@ fun MeshBackground(content: @Composable BoxScope.() -> Unit) { animationSpec = infiniteRepeatable(tween(22000, easing = LinearEasing), RepeatMode.Reverse), label = "drift", ) - Box(Modifier.fillMaxSize().background(Obsidian)) { - Blob(BlobAmber, 360.dp, (-60f + drift * 30f).dp, (-40f + drift * 24f).dp, 0.30f) - Blob(BlobBlue, 380.dp, (170f - drift * 44f).dp, (150f + drift * 40f).dp, 0.24f) - Blob(BlobTeal, 300.dp, (30f + drift * 50f).dp, (560f - drift * 36f).dp, 0.22f) - // Vignette: Ränder abdunkeln, Mitte tritt hervor - Box( - Modifier.fillMaxSize().background( - Brush.radialGradient(listOf(Color.Transparent, Obsidian.copy(alpha = 0.55f))), - ), - ) + Box(Modifier.fillMaxSize().background(base)) { + Blob(BlobAmber, 360.dp, (-60f + drift * 30f).dp, (-40f + drift * 24f).dp, 0.30f * factor) + Blob(BlobBlue, 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) + // Vignette: darken the edges so the center comes forward (dark mode only). + if (dark) { + Box( + Modifier.fillMaxSize().background( + Brush.radialGradient(listOf(Color.Transparent, base.copy(alpha = 0.55f))), + ), + ) + } content() } } @@ -72,7 +79,7 @@ private fun BoxScope.Blob(color: Color, size: Dp, x: Dp, y: Dp, alpha: Float) { ) } -/** Wiederverwendbarer Liquid-Glass-Orb (Hero + Assistant-Avatar). */ +/** Reusable liquid-glass orb (hero + assistant avatar). */ @Composable fun KaizenOrb(size: Dp, modifier: Modifier = Modifier, streaming: Boolean = false) { val transition = rememberInfiniteTransition(label = "orb") @@ -99,7 +106,7 @@ fun KaizenOrb(size: Dp, modifier: Modifier = Modifier, streaming: Boolean = fals CircleShape, ), ) - // Glaskugel + // Glass sphere Box( Modifier .size(size) @@ -108,7 +115,7 @@ fun KaizenOrb(size: Dp, modifier: Modifier = Modifier, streaming: Boolean = fals .background(Brush.radialGradient(listOf(Color(0xFFF2C879), AmberDeep, Color(0xFF6E4A16)))) .border(1.dp, Brush.radialGradient(listOf(Color.White.copy(alpha = 0.5f), Color.Transparent)), CircleShape), ) - // Specular-Highlight oben links ("wet"-Reflexion) + // Specular highlight, top-left ("wet" reflection) Box( Modifier .size(size * 0.42f) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt index 5d2c77b..33d4539 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -7,6 +7,7 @@ import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.infiniteRepeatable import androidx.compose.animation.core.rememberInfiniteTransition import androidx.compose.animation.core.tween +import androidx.compose.foundation.Canvas import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.clickable @@ -25,9 +26,12 @@ 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 androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -36,48 +40,50 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.scale +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.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.GlassStroke -import dev.kaizen.app.ui.theme.ObsidianElevated -import dev.kaizen.app.ui.theme.ObsidianInput import dev.kaizen.app.ui.theme.OnAmber -import dev.kaizen.app.ui.theme.TextMuted -import dev.kaizen.app.ui.theme.TextPrimary import java.time.LocalTime @Composable fun KaizenHeader() { + val cs = MaterialTheme.colorScheme Row( Modifier.fillMaxWidth().padding(horizontal = 18.dp, vertical = 12.dp), verticalAlignment = Alignment.CenterVertically, ) { KaizenOrb(24.dp) Spacer(Modifier.width(9.dp)) - Text("Kaizen", color = TextPrimary, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) + Text("Kaizen", color = cs.onBackground, fontSize = 18.sp, fontWeight = FontWeight.SemiBold) } } @Composable fun EmptyHero(userName: String, onPick: (String) -> Unit) { + val cs = MaterialTheme.colorScheme val hour = remember { LocalTime.now().hour } + // One grouped block (orb + greeting + tiles), scrollable so the keyboard never + // squeezes or clips it — only the input rises above the keyboard. Column( - Modifier.fillMaxSize().padding(horizontal = 22.dp), + Modifier.fillMaxSize().verticalScroll(rememberScrollState()).padding(horizontal = 22.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - Spacer(Modifier.height(56.dp)) + Spacer(Modifier.height(64.dp)) KaizenOrb(96.dp) Spacer(Modifier.height(22.dp)) - Text("${greeting(hour)}, $userName", color = TextMuted, fontSize = 17.sp) + Text("${greeting(hour)}, $userName", color = cs.onSurfaceVariant, fontSize = 17.sp) Spacer(Modifier.height(6.dp)) - Text("Was liegt an?", color = TextPrimary, fontSize = 30.sp, fontWeight = FontWeight.Bold) - Spacer(Modifier.weight(1f)) + Text("Was liegt an?", color = cs.onBackground, fontSize = 30.sp, fontWeight = FontWeight.Bold) + Spacer(Modifier.height(36.dp)) Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp)) { SuggestionTile(suggestions[0], Modifier.weight(1f)) { onPick(suggestions[0].prompt) } SuggestionTile(suggestions[1], Modifier.weight(1f)) { onPick(suggestions[1].prompt) } @@ -87,20 +93,21 @@ fun EmptyHero(userName: String, onPick: (String) -> Unit) { SuggestionTile(suggestions[2], Modifier.weight(1f)) { onPick(suggestions[2].prompt) } SuggestionTile(suggestions[3], Modifier.weight(1f)) { onPick(suggestions[3].prompt) } } - Spacer(Modifier.height(16.dp)) + Spacer(Modifier.height(24.dp)) } } @Composable private fun SuggestionTile(suggestion: Suggestion, modifier: Modifier = Modifier, onClick: () -> Unit) { + val cs = MaterialTheme.colorScheme Column( modifier - .height(96.dp) + .height(112.dp) .clip(RoundedCornerShape(20.dp)) - .background(ObsidianElevated.copy(alpha = 0.72f)) - .border(1.dp, GlassStroke, RoundedCornerShape(20.dp)) + .background(cs.surface.copy(alpha = 0.72f)) + .border(1.dp, cs.outline, RoundedCornerShape(20.dp)) .clickable { onClick() } - .padding(16.dp), + .padding(horizontal = 16.dp, vertical = 15.dp), verticalArrangement = Arrangement.SpaceBetween, ) { Box( @@ -110,14 +117,15 @@ private fun SuggestionTile(suggestion: Suggestion, modifier: Modifier = Modifier .background(Brush.linearGradient(listOf(Amber, AmberDeep))), ) Column { - Text(suggestion.title, color = TextPrimary, fontSize = 15.sp, fontWeight = FontWeight.Medium) - Text(suggestion.subtitle, color = TextMuted, fontSize = 12.sp) + Text(suggestion.title, color = cs.onBackground, fontSize = 15.sp, fontWeight = FontWeight.Medium) + Text(suggestion.subtitle, color = cs.onSurfaceVariant, fontSize = 12.sp) } } } @Composable fun MessageRow(message: Message) { + val cs = MaterialTheme.colorScheme if (message.role == Role.User) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.End) { val shape = RoundedCornerShape(20.dp, 20.dp, 6.dp, 20.dp) @@ -126,10 +134,10 @@ fun MessageRow(message: Message) { .widthIn(max = 300.dp) .clip(shape) .background(Amber.copy(alpha = 0.16f)) - .border(1.dp, Amber.copy(alpha = 0.28f), shape) + .border(1.dp, Amber.copy(alpha = 0.30f), shape) .padding(horizontal = 16.dp, vertical = 11.dp), ) { - Text(message.content, color = TextPrimary, fontSize = 16.sp, lineHeight = 22.sp) + Text(message.content, color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp) } } } else { @@ -141,7 +149,7 @@ fun MessageRow(message: Message) { TypingDots() } else { val display = if (message.streaming) message.content + " ▌" else message.content - Text(display, color = TextPrimary, fontSize = 16.sp, lineHeight = 23.sp) + Text(display, color = cs.onBackground, fontSize = 16.sp, lineHeight = 23.sp) } } } @@ -150,6 +158,7 @@ fun MessageRow(message: Message) { @Composable private fun TypingDots() { + val cs = MaterialTheme.colorScheme val transition = rememberInfiniteTransition(label = "typing") Row(verticalAlignment = Alignment.CenterVertically) { repeat(3) { i -> @@ -167,7 +176,7 @@ private fun TypingDots() { .padding(end = 5.dp) .size(7.dp) .clip(CircleShape) - .background(TextMuted.copy(alpha = alpha)), + .background(cs.onSurfaceVariant.copy(alpha = alpha)), ) } } @@ -181,6 +190,7 @@ fun ChatInput( enabled: Boolean, modifier: Modifier = Modifier, ) { + val cs = MaterialTheme.colorScheme Row( modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.Bottom, @@ -190,19 +200,19 @@ fun ChatInput( .weight(1f) .heightIn(min = 52.dp) .clip(RoundedCornerShape(26.dp)) - .background(ObsidianInput.copy(alpha = 0.94f)) - .border(1.dp, GlassStroke, RoundedCornerShape(26.dp)) + .background(cs.surfaceVariant.copy(alpha = 0.94f)) + .border(1.dp, cs.outline, RoundedCornerShape(26.dp)) .padding(horizontal = 18.dp, vertical = 15.dp), verticalAlignment = Alignment.CenterVertically, ) { Box(Modifier.weight(1f)) { if (value.isEmpty()) { - Text("Frag Kaizen …", color = TextMuted, fontSize = 16.sp) + Text("Frag Kaizen …", color = cs.onSurfaceVariant, fontSize = 16.sp) } BasicTextField( value = value, onValueChange = onValueChange, - textStyle = TextStyle(color = TextPrimary, fontSize = 16.sp, lineHeight = 22.sp), + textStyle = TextStyle(color = cs.onBackground, fontSize = 16.sp, lineHeight = 22.sp), cursorBrush = SolidColor(Amber), maxLines = 6, modifier = Modifier.fillMaxWidth(), @@ -216,28 +226,39 @@ fun ChatInput( @Composable private fun SendButton(enabled: Boolean, onClick: () -> Unit) { + val cs = MaterialTheme.colorScheme val interaction = remember { MutableInteractionSource() } val pressed by interaction.collectIsPressedAsState() val pressScale by animateFloatAsState(if (pressed) 0.86f else 1f, label = "send") - val background = if (enabled) { + val fill = if (enabled) { Modifier.background(Brush.linearGradient(listOf(Amber, AmberDeep))) } else { - Modifier.background(ObsidianElevated) + Modifier.background(cs.surfaceVariant) } Box( Modifier .size(52.dp) .scale(pressScale) .clip(CircleShape) - .then(background) + .then(fill) + .border(1.dp, cs.outline, CircleShape) .clickable(interactionSource = interaction, indication = null, enabled = enabled, onClick = onClick), contentAlignment = Alignment.Center, ) { - Text( - "↑", - color = if (enabled) OnAmber else TextMuted, - fontSize = 22.sp, - fontWeight = FontWeight.Bold, - ) + SendArrow(color = if (enabled) OnAmber else cs.onSurfaceVariant) + } +} + +/** Crisp upward arrow drawn with strokes (sharper than a glyph). */ +@Composable +private fun SendArrow(color: Color) { + Canvas(Modifier.size(20.dp)) { + val w = size.width + val h = size.height + val cx = w / 2f + val stroke = w * 0.135f + drawLine(color, Offset(cx, h * 0.82f), Offset(cx, h * 0.20f), stroke, StrokeCap.Round) + drawLine(color, Offset(cx, h * 0.18f), Offset(w * 0.28f, h * 0.46f), stroke, StrokeCap.Round) + drawLine(color, Offset(cx, h * 0.18f), Offset(w * 0.72f, h * 0.46f), stroke, StrokeCap.Round) } } diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatModels.kt b/app/src/main/java/dev/kaizen/app/chat/ChatModels.kt index d6ee811..5fcd686 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatModels.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatModels.kt @@ -30,7 +30,7 @@ fun greeting(hour: Int): String = when (hour) { else -> "Hallo" } -// --- Phase 0: gefakter Stream, damit das Feel ohne Backend testbar ist --- +// --- Phase 0: faked stream so the feel is testable without a backend --- private val cannedReplies = listOf( "Klar, das kriegen wir hin. Lass uns das Schritt für Schritt durchgehen: " + @@ -50,7 +50,7 @@ fun fakeReply(prompt: String): Flow = flow { for (token in tokens) { emit(token) val base = Random.nextLong(26L, 64L) - // kleine Pause nach Satzzeichen → natürlicherer Rhythmus + // small pause after punctuation -> more natural rhythm val pause = if (token.endsWith(". ") || token.endsWith(": ")) base + 130L else base delay(pause) } diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index abf9dfa..fd9ff0d 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -48,7 +48,7 @@ fun ChatScreen(userName: String = "Bruno") { var first = true fakeReply(trimmed).collect { chunk -> if (first) { - haptics.responseStart() // "die Antwort kommt" + haptics.responseStart() // "the answer is arriving" first = false } val idx = messages.indexOfLast { it.id == assistantId } @@ -61,7 +61,7 @@ fun ChatScreen(userName: String = "Bruno") { } } - // Beim Wachsen des Streams ans Ende scrollen + // Keep pinned to the bottom as the stream grows val lastLength = messages.lastOrNull()?.content?.length ?: 0 LaunchedEffect(messages.size, lastLength) { if (messages.isNotEmpty()) listState.scrollToItem(messages.lastIndex) diff --git a/app/src/main/java/dev/kaizen/app/haptics/Haptics.kt b/app/src/main/java/dev/kaizen/app/haptics/Haptics.kt index 13735e7..4d6c893 100644 --- a/app/src/main/java/dev/kaizen/app/haptics/Haptics.kt +++ b/app/src/main/java/dev/kaizen/app/haptics/Haptics.kt @@ -11,21 +11,22 @@ import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext /** - * Feines haptisches Feedback — das, was sich auf dem S25 Ultra "premium" anfühlt. + * Fine-grained haptic feedback — the "premium" feel on devices with a good LRA. * - * Drei Stufen, je nach Gerät: - * - API 30+ mit gutem Aktuator (LRA): VibrationEffect.Composition mit Primitives (TICK/CLICK) - * → kurze, präzise Ticks statt dumpfes "brrr". - * - API 29: vordefinierte Effekte (EFFECT_TICK/EFFECT_CLICK). - * - API 26–28: createOneShot-Fallback. + * Three tiers depending on the device: + * - API 30+ with a capable actuator: VibrationEffect.Composition with primitives + * (TICK/CLICK) -> short, precise taps instead of a dull buzz. + * - API 29: predefined effects (EFFECT_TICK/EFFECT_CLICK). + * - API 26-28: createOneShot fallback. * - * WICHTIG: Im Emulator gibt es keine echte Haptik — immer am echten Gerät testen. + * NOTE: the emulator has no real haptics — always evaluate on a physical device. + * All vibrate() calls are wrapped so a missing permission / quirk never crashes. */ class Haptics(private val vibrator: Vibrator?) { private fun active(): Vibrator? = vibrator?.takeIf { it.hasVibrator() } - /** Leichter Tap — Kachel-/Mode-Auswahl. */ + /** Light tap — tile / mode selection. */ fun tick() { val vib = active() ?: return if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_TICK)) { @@ -35,7 +36,7 @@ class Haptics(private val vibrator: Vibrator?) { } } - /** Festerer Klick — Senden-Button. */ + /** Firmer click — send button. */ fun click() { val vib = active() ?: return if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_CLICK)) { @@ -45,7 +46,7 @@ class Haptics(private val vibrator: Vibrator?) { } } - /** "Die Antwort kommt" — ein leiser Tick, der zu einem festeren anschwillt. */ + /** "The answer is arriving" — a soft tick swelling into a firmer one. */ fun responseStart() { val vib = active() ?: return if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_TICK)) { @@ -58,7 +59,7 @@ class Haptics(private val vibrator: Vibrator?) { } } - /** Sanftes Absetzen — Stream fertig. */ + /** Gentle settle — stream finished. */ fun responseEnd() { val vib = active() ?: return if (supportsPrimitive(vib, VibrationEffect.Composition.PRIMITIVE_TICK)) { @@ -68,27 +69,27 @@ class Haptics(private val vibrator: Vibrator?) { } } - // --- intern --- + // --- internal --- private fun supportsPrimitive(vib: Vibrator, primitive: Int): Boolean = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && vib.arePrimitivesSupported(primitive)[0] @RequiresApi(Build.VERSION_CODES.R) - private fun Vibrator.composePrimitives(vararg steps: Pair) { + private fun Vibrator.composePrimitives(vararg steps: Pair) = runCatching { val comp = VibrationEffect.startComposition() for ((primitive, scale) in steps) comp.addPrimitive(primitive, scale) vibrate(comp.compose()) } @RequiresApi(Build.VERSION_CODES.R) - private fun Vibrator.composePrimitives(vararg steps: Triple) { + private fun Vibrator.composePrimitives(vararg steps: Triple) = runCatching { val comp = VibrationEffect.startComposition() for ((primitive, scale, delayMs) in steps) comp.addPrimitive(primitive, scale, delayMs) vibrate(comp.compose()) } - private fun Vibrator.predefinedOrOneShot(soft: Boolean) { + private fun Vibrator.predefinedOrOneShot(soft: Boolean) = runCatching { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val effect = if (soft) VibrationEffect.EFFECT_TICK else VibrationEffect.EFFECT_CLICK vibrate(VibrationEffect.createPredefined(effect)) @@ -98,7 +99,7 @@ class Haptics(private val vibrator: Vibrator?) { } } -/** Baut den passenden Vibrator für die API-Stufe und merkt ihn sich über Recompositions. */ +/** Builds the right Vibrator for the API level and keeps it across recompositions. */ @Composable fun rememberHaptics(): Haptics { val context = LocalContext.current diff --git a/app/src/main/java/dev/kaizen/app/ui/theme/Color.kt b/app/src/main/java/dev/kaizen/app/ui/theme/Color.kt index f1abf22..0fb7d98 100644 --- a/app/src/main/java/dev/kaizen/app/ui/theme/Color.kt +++ b/app/src/main/java/dev/kaizen/app/ui/theme/Color.kt @@ -2,19 +2,31 @@ package dev.kaizen.app.ui.theme import androidx.compose.ui.graphics.Color -// Kaizen-Palette — Obsidian Stahl-Blau + Amber-Akzent (dark-first). -// OKLCH-Tokens aus dem Web (globals.css) nach sRGB angenähert; wird am Gerät feinjustiert. -val Obsidian = Color(0xFF0A0E16) // background (~oklch(0.10 0.025 245)) -val ObsidianElevated = Color(0xFF141B27) // cards / surfaces -val ObsidianInput = Color(0xFF111824) // input surface -val Amber = Color(0xFFE0A23E) // primary (~oklch(0.72 0.14 83)) +// Kaizen palette — Obsidian steel-blue + amber accent. +// OKLCH tokens from the web (globals.css) approximated to sRGB; fine-tuned on device. + +// Brand amber — kept constant across light/dark (orb, accents, send button). +val Amber = Color(0xFFE0A23E) // ~oklch(0.72 0.14 83) val AmberDeep = Color(0xFFC8862A) val OnAmber = Color(0xFF1B1206) -val TextPrimary = Color(0xFFE8EBF1) -val TextMuted = Color(0xFF8B94A4) -val GlassStroke = Color(0x16FFFFFF) // dezenter heller Rand (Top-Highlight-Look) -// Hintergrund-Blobs (.bg-blobs) +// Dark scheme (Obsidian) +val Obsidian = Color(0xFF0A0E16) // background ~oklch(0.10 0.025 245) +val ObsidianElevated = Color(0xFF141B27) // surface +val ObsidianInput = Color(0xFF111824) // input surface +val DarkText = Color(0xFFE8EBF1) +val DarkMuted = Color(0xFF8B94A4) +val DarkStroke = Color(0x16FFFFFF) // subtle light hairline + +// Light scheme +val LightBackground = Color(0xFFF6F7F9) +val LightSurface = Color(0xFFFFFFFF) +val LightSurfaceVariant = Color(0xFFEEF0F3) +val LightText = Color(0xFF1A1F29) +val LightMuted = Color(0xFF5B6470) +val LightStroke = Color(0x14000000) // subtle dark hairline + +// Background blobs (.bg-blobs) val BlobAmber = Color(0xFFE0A23E) val BlobBlue = Color(0xFF3C5FD6) val BlobTeal = Color(0xFF2BB6A6) diff --git a/app/src/main/java/dev/kaizen/app/ui/theme/Theme.kt b/app/src/main/java/dev/kaizen/app/ui/theme/Theme.kt index f678460..54fa610 100644 --- a/app/src/main/java/dev/kaizen/app/ui/theme/Theme.kt +++ b/app/src/main/java/dev/kaizen/app/ui/theme/Theme.kt @@ -1,29 +1,48 @@ package dev.kaizen.app.ui.theme +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color -// Phase 0: bewusst dark-first und fest (kein System-/Dynamic-Color), damit der -// Premium-/Glass-Look konsistent ist. Light-Variante kommt später. -private val KaizenColors = darkColorScheme( +// Fixed Kaizen schemes (no dynamic color) so the glass look stays consistent. +private val KaizenDark = darkColorScheme( primary = Amber, onPrimary = OnAmber, secondary = Amber, onSecondary = OnAmber, background = Obsidian, - onBackground = TextPrimary, + onBackground = DarkText, surface = ObsidianElevated, - onSurface = TextPrimary, + onSurface = DarkText, surfaceVariant = ObsidianInput, - onSurfaceVariant = TextMuted, - outline = GlassStroke, + onSurfaceVariant = DarkMuted, + outline = DarkStroke, +) + +private val KaizenLight = lightColorScheme( + primary = AmberDeep, + onPrimary = Color.White, + secondary = AmberDeep, + onSecondary = Color.White, + background = LightBackground, + onBackground = LightText, + surface = LightSurface, + onSurface = LightText, + surfaceVariant = LightSurfaceVariant, + onSurfaceVariant = LightMuted, + outline = LightStroke, ) @Composable -fun KaizenTheme(content: @Composable () -> Unit) { +fun KaizenTheme( + darkTheme: Boolean = isSystemInDarkTheme(), + content: @Composable () -> Unit, +) { MaterialTheme( - colorScheme = KaizenColors, + colorScheme = if (darkTheme) KaizenDark else KaizenLight, typography = Typography, content = content, ) diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..a91d634 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,5 @@ + + + + #0A0E16 + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index f8c6127..4226349 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,10 +1,5 @@ - #FFBB86FC - #FF6200EE - #FF3700B3 - #FF03DAC5 - #FF018786 - #FF000000 - #FFFFFFFF - \ No newline at end of file + + #F6F7F9 + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index d5c6bb5..8881f53 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,8 +2,8 @@