diff --git a/CLAUDE.md b/CLAUDE.md index c274c41..61e5181 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -71,7 +71,7 @@ Full spec: `DESIGN.md`. Branch: `feature/design-system-v2`. 2. **Squircle shapes** — iOS-style superellipse with G2 continuity (`ui/shape/Squircle.kt`). Token system `KaizenShapes.{xs,sm,md,lg,xl,pill,circle}`. Replaces all `RoundedCornerShape`. 3. **GlassSurface** — frosted-glass composable (gradient tint + specular border + inner highlight + multi-layer shadow). Android has no backdrop-filter; glass effect works via semi-transparent tint over the blob layer. Opacity tiers per component in `GlassTiers`. 4. **Multi-layer shadows** — `ShadowStack` with 2–4 overlapping layers (`KaizenShadows.level0..4`). Dark mode multiplies alphas ×1.8. Replaces all `Modifier.shadow()`. -5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Server sync planned via `GET /api/v1/me` → `uiTheme`. +5. **4-theme system** — Amber/Blau/Mono/Aurora. Each theme = `KaizenAccentScheme` (primary + 4 blob colors). Neutrals are theme-independent. `LocalKaizenAccent` CompositionLocal. Theme selection wired end-to-end: `SettingsViewModel.selectedTheme` → `MainActivity` → `KaizenTheme(themeId=...)`. No server sync (web stores in localStorage, app stores locally). Appearance (Light/Dark/System) also wired end-to-end. 6. **Sensor-reactive motion** — `rememberTiltState()` provides smoothed tilt `Offset(-1..1)`. Uses `TYPE_GAME_ROTATION_VECTOR` (fused, no drift). Auto-disables on battery saver, reduced motion, background, extreme tilt. **Motion tokens** (`ui/motion/Motion.kt`): `EaseSpring` (overshoot), `EaseSmooth` (expo-out), `EaseEmphasized` (M3), `SpringSnappy/Default/Gentle`. All durations centralised in `Durations` object. @@ -80,6 +80,10 @@ Full spec: `DESIGN.md`. Branch: `feature/design-system-v2`. **KaizenOrb** (`chat/Background.kt`) uses `rememberTiltState()` from `ui/sensor/` instead of inline sensor code. Breath animation uses `Durations.ORB_BREATH` / `ORB_BREATH_STREAMING` from `ui/motion/`. +**Typography** — Inter Variable (`res/font/inter_variable.ttf`, 859 KB, SIL OFL). Full type scale from DESIGN.md §8: `displayLarge` (32sp/W600) through `labelSmall` (11sp/W500). All 9 weight axes registered (W100–W900). Negative tracking at display sizes, positive at label sizes. `@OptIn(ExperimentalTextApi::class)` for `FontVariation.Settings`. + +**Oklab** (`ui/theme/Oklab.kt`) — Double-precision matrix math for accurate sRGB↔Oklab round-trips. Used by theme picker's Oklab gradient swatches and `generateGradientStops()`. Forward LMS matrix corrected to Björn Ottosson reference. + ### Internationalization (i18n) - **de** (default), **en** — same as web (`messages/de.json`, `messages/en.json`). diff --git a/app/src/main/java/dev/kaizen/app/MainActivity.kt b/app/src/main/java/dev/kaizen/app/MainActivity.kt index 1d66d62..72705c7 100644 --- a/app/src/main/java/dev/kaizen/app/MainActivity.kt +++ b/app/src/main/java/dev/kaizen/app/MainActivity.kt @@ -7,6 +7,7 @@ import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent +import androidx.compose.runtime.collectAsState import androidx.activity.enableEdgeToEdge import dev.kaizen.app.auth.LoginScreen import dev.kaizen.app.chat.ChatScreen @@ -14,6 +15,7 @@ import dev.kaizen.app.chat.ChatViewModel import dev.kaizen.app.net.SecureStore import dev.kaizen.app.net.SessionViewModel import dev.kaizen.app.settings.SettingsViewModel +import dev.kaizen.app.settings.AppAppearance import dev.kaizen.app.ui.theme.KaizenTheme class MainActivity : ComponentActivity() { @@ -39,7 +41,14 @@ class MainActivity : ComponentActivity() { navigationBarStyle = SystemBarStyle.auto(Color.TRANSPARENT, Color.TRANSPARENT), ) setContent { - KaizenTheme { + val themeId = settingsViewModel.selectedTheme.collectAsState() + val appearance = settingsViewModel.appearanceMode.collectAsState() + val darkOverride = when (appearance.value) { + AppAppearance.Light -> false + AppAppearance.Dark -> true + AppAppearance.System -> androidx.compose.foundation.isSystemInDarkTheme() + } + KaizenTheme(themeId = themeId.value, darkTheme = darkOverride) { // Snapshot-state routing: logging in/out flips session.config and // recomposes this branch — no NavHost needed for two top-level states. if (sessionViewModel.isLoggedIn) { diff --git a/app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt b/app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt index 1bc3b11..2828ec2 100644 --- a/app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt +++ b/app/src/main/java/dev/kaizen/app/settings/SettingsComponents.kt @@ -57,6 +57,7 @@ import androidx.compose.ui.unit.sp import dev.kaizen.app.ui.theme.Amber import dev.kaizen.app.ui.theme.AmberDeep import dev.kaizen.app.ui.theme.Oklab +import dev.kaizen.app.ui.theme.themes.KaizenThemeId /** Converts polar OKLCH coordinates (Lightness, Chroma, Hue in degrees) to a standard Color */ private fun oklch(l: Float, c: Float, h: Float, alpha: Float = 1.0f): Color { @@ -201,17 +202,17 @@ fun ThemeOptionPill( val (colorStart, colorEnd) = remember(themeId, isDark) { if (isDark) { when (themeId) { - KaizenThemeId.Amber -> oklch(0.76f, 0.14f, 83f) to oklch(0.55f, 0.16f, 83f) - KaizenThemeId.Blue -> oklch(0.68f, 0.18f, 257f) to oklch(0.45f, 0.18f, 257f) - KaizenThemeId.Aurora -> oklch(0.78f, 0.15f, 85f) to oklch(0.45f, 0.18f, 320f) - KaizenThemeId.Mono -> oklch(0.92f, 0f, 255f) to oklch(0.35f, 0f, 255f) + KaizenThemeId.AMBER -> oklch(0.76f, 0.14f, 83f) to oklch(0.55f, 0.16f, 83f) + KaizenThemeId.BLAU -> oklch(0.68f, 0.18f, 257f) to oklch(0.45f, 0.18f, 257f) + KaizenThemeId.AURORA -> oklch(0.78f, 0.15f, 85f) to oklch(0.45f, 0.18f, 320f) + KaizenThemeId.MONO -> oklch(0.92f, 0f, 255f) to oklch(0.35f, 0f, 255f) } } else { when (themeId) { - KaizenThemeId.Amber -> oklch(0.72f, 0.14f, 83f) to oklch(0.85f, 0.15f, 83f) - KaizenThemeId.Blue -> oklch(0.62f, 0.19f, 257f) to oklch(0.78f, 0.15f, 257f) - KaizenThemeId.Aurora -> oklch(0.62f, 0.20f, 320f) to oklch(0.75f, 0.16f, 320f) - KaizenThemeId.Mono -> oklch(0.22f, 0f, 255f) to oklch(0.85f, 0f, 255f) + KaizenThemeId.AMBER -> oklch(0.72f, 0.14f, 83f) to oklch(0.85f, 0.15f, 83f) + KaizenThemeId.BLAU -> oklch(0.62f, 0.19f, 257f) to oklch(0.78f, 0.15f, 257f) + KaizenThemeId.AURORA -> oklch(0.62f, 0.20f, 320f) to oklch(0.75f, 0.16f, 320f) + KaizenThemeId.MONO -> oklch(0.22f, 0f, 255f) to oklch(0.85f, 0f, 255f) } } } @@ -251,7 +252,7 @@ fun ThemeOptionPill( Icon( imageVector = Icons.Rounded.Check, contentDescription = null, - tint = if (themeId == KaizenThemeId.Mono && isDark.not()) Color.White else Color.Black, + tint = if (themeId == KaizenThemeId.MONO && isDark.not()) Color.White else Color.Black, modifier = Modifier.size(16.dp) ) } diff --git a/app/src/main/java/dev/kaizen/app/settings/SettingsScreen.kt b/app/src/main/java/dev/kaizen/app/settings/SettingsScreen.kt index b067324..13942a5 100644 --- a/app/src/main/java/dev/kaizen/app/settings/SettingsScreen.kt +++ b/app/src/main/java/dev/kaizen/app/settings/SettingsScreen.kt @@ -38,6 +38,9 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import dev.kaizen.app.R +import dev.kaizen.app.ui.theme.themes.KaizenThemeId import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp @@ -94,119 +97,48 @@ fun SettingsScreen( Spacer(Modifier.width(16.dp)) - Text( - text = "Einstellungen", - color = cs.onBackground, - fontSize = 24.sp, - fontWeight = FontWeight.Bold - ) + Text(stringResource(R.string.settings_title), color = cs.onBackground, fontSize = 24.sp, fontWeight = FontWeight.Bold) } - // --- SEKTION 1: Erscheinungsbild (Theme & Appearance) --- - Text( - text = "ERSCHEINUNGSBILD", - color = cs.onSurfaceVariant.copy(alpha = 0.6f), - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) - ) - + Text(stringResource(R.string.settings_section_appearance), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) + SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) { - // Theme Picker Row (Oklab perception space stops) - SettingsOptionRow( - title = "Akzentfarbe", - description = "Wähle das Farbschema der App (Oklab perzeptiv)", - icon = Icons.Rounded.Palette - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(10.dp), - verticalAlignment = Alignment.CenterVertically - ) { - KaizenThemeId.values().forEach { theme -> - ThemeOptionPill( - themeId = theme, - selected = selectedTheme == theme, - onClick = { viewModel.selectTheme(theme) } - ) + SettingsOptionRow(title = stringResource(R.string.settings_accent_title), description = stringResource(R.string.settings_accent_desc), icon = Icons.Rounded.Palette) { + Row(horizontalArrangement = Arrangement.spacedBy(10.dp), verticalAlignment = Alignment.CenterVertically) { + KaizenThemeId.entries.forEach { theme -> + ThemeOptionPill(themeId = theme, selected = selectedTheme == theme, onClick = { viewModel.selectTheme(theme) }) } } } - Spacer(Modifier.height(12.dp)) - - // Light / Dark / System Appearance segmented toggle pills - SettingsOptionRow( - title = "Helligkeitsmodus", - description = "Passe das Hell/Dunkel-Erscheinungsbild an", - icon = Icons.Rounded.Settings - ) { - Row( - horizontalArrangement = Arrangement.spacedBy(6.dp), - verticalAlignment = Alignment.CenterVertically - ) { - AppAppearance.values().forEach { mode -> + SettingsOptionRow(title = stringResource(R.string.settings_appearance_title), description = stringResource(R.string.settings_appearance_desc), icon = Icons.Rounded.Settings) { + Row(horizontalArrangement = Arrangement.spacedBy(6.dp), verticalAlignment = Alignment.CenterVertically) { + AppAppearance.entries.forEach { mode -> val label = when (mode) { - AppAppearance.Light -> "Hell" - AppAppearance.Dark -> "Dunkel" - AppAppearance.System -> "System" + AppAppearance.Light -> stringResource(R.string.settings_appearance_light) + AppAppearance.Dark -> stringResource(R.string.settings_appearance_dark) + AppAppearance.System -> stringResource(R.string.settings_appearance_system) } - AppearanceTogglePill( - selected = appearanceMode == mode, - label = label, - onClick = { viewModel.selectAppearance(mode) } - ) + AppearanceTogglePill(selected = appearanceMode == mode, label = label, onClick = { viewModel.selectAppearance(mode) }) } } } } - // --- SEKTION 2: Profil & Konto --- - Text( - text = "PROFIL & KONTO", - color = cs.onSurfaceVariant.copy(alpha = 0.6f), - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) - ) - + Text(stringResource(R.string.settings_section_profile), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) + SettingsCard(modifier = Modifier.padding(bottom = 24.dp)) { - // Username editor row - SettingsOptionRow( - title = "Benutzername", - description = "Ändere deinen Anzeigenamen", - icon = Icons.Rounded.Person - ) { - SettingsInputCapsule( - value = userName, - onValueChange = { viewModel.updateUserName(it) } - ) + SettingsOptionRow(title = stringResource(R.string.settings_name_title), description = stringResource(R.string.settings_name_desc), icon = Icons.Rounded.Person) { + SettingsInputCapsule(value = userName, onValueChange = { viewModel.updateUserName(it) }) } - Spacer(Modifier.height(12.dp)) - - // Email display row (read-only in this screen view) - SettingsOptionRow( - title = "E-Mail-Adresse", - description = userEmail, - icon = Icons.Rounded.Settings - ) + SettingsOptionRow(title = stringResource(R.string.settings_email_title), description = userEmail, icon = Icons.Rounded.Settings) } - // --- SEKTION 3: KI-Profil & Kontext --- - Text( - text = "KI-PROFIL & KONTEXT", - color = cs.onSurfaceVariant.copy(alpha = 0.6f), - fontSize = 12.sp, - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(start = 8.dp, bottom = 8.dp) - ) - + Text(stringResource(R.string.settings_section_ai), color = cs.onSurfaceVariant.copy(alpha = 0.6f), fontSize = 12.sp, fontWeight = FontWeight.Bold, modifier = Modifier.padding(start = 8.dp, bottom = 8.dp)) + SettingsCard(modifier = Modifier.padding(bottom = 32.dp)) { - SettingsOptionRow( - title = "System-Prompt & Fakten", - description = "Aktive Regeln: 12, Pinned: 3, Verwendetes Budget: 412 / 1500 Tokens", - icon = Icons.Rounded.AutoAwesome - ) + SettingsOptionRow(title = stringResource(R.string.settings_ai_title), description = "", icon = Icons.Rounded.AutoAwesome) } } } diff --git a/app/src/main/java/dev/kaizen/app/settings/SettingsViewModel.kt b/app/src/main/java/dev/kaizen/app/settings/SettingsViewModel.kt index a5f4bcc..11ffff8 100644 --- a/app/src/main/java/dev/kaizen/app/settings/SettingsViewModel.kt +++ b/app/src/main/java/dev/kaizen/app/settings/SettingsViewModel.kt @@ -1,67 +1,40 @@ package dev.kaizen.app.settings -import androidx.compose.runtime.State -import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel +import dev.kaizen.app.ui.theme.themes.KaizenThemeId import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow -// Available Kaizen UI themes -enum class KaizenThemeId { Amber, Blue, Aurora, Mono } - -// Available Light/Dark/System appearance modes enum class AppAppearance { Light, Dark, System } -/** - * Clean MVVM ViewModel for the Settings module. - * - * Completely decoupled from Android UI classes, making it 100% unit-testable - * on the JVM. Manages the state of the Oklab Theme Picker, selected model, - * and user profile options. - */ class SettingsViewModel : ViewModel() { - // 1. Reactive State Flow for Theme - private val _selectedTheme = MutableStateFlow(KaizenThemeId.Amber) + private val _selectedTheme = MutableStateFlow(KaizenThemeId.AMBER) val selectedTheme: StateFlow = _selectedTheme.asStateFlow() - // 2. Reactive State Flow for Appearance - private val _appearanceMode = MutableStateFlow(AppAppearance.Dark) + private val _appearanceMode = MutableStateFlow(AppAppearance.System) val appearanceMode: StateFlow = _appearanceMode.asStateFlow() - // 3. User profile details private val _userName = MutableStateFlow("Bruno") val userName: StateFlow = _userName.asStateFlow() - private val _userEmail = MutableStateFlow("admin@kryptomrx.de") + private val _userEmail = MutableStateFlow("") val userEmail: StateFlow = _userEmail.asStateFlow() - // 4. Default Chat Model - private val _defaultModel = MutableStateFlow("gemini-2.5-flash") - val defaultModel: StateFlow = _defaultModel.asStateFlow() - - /** Selects a new Kaizen UI theme and triggers an Oklab transition stops recalculation */ fun selectTheme(themeId: KaizenThemeId) { _selectedTheme.value = themeId } - /** Changes the appearance mode (Light, Dark, or System Sync) */ fun selectAppearance(mode: AppAppearance) { _appearanceMode.value = mode } - /** Updates the user's name in profile settings */ fun updateUserName(name: String) { - if (name.isNotBlank()) { - _userName.value = name.trim() - } + if (name.isNotBlank()) _userName.value = name.trim() } - /** Updates the standard chat model */ - fun updateDefaultModel(modelId: String) { - if (modelId.isNotBlank()) { - _defaultModel.value = modelId.trim() - } + fun updateUserEmail(email: String) { + _userEmail.value = email } } diff --git a/app/src/main/java/dev/kaizen/app/ui/theme/Type.kt b/app/src/main/java/dev/kaizen/app/ui/theme/Type.kt index bad12c1..73ad3db 100644 --- a/app/src/main/java/dev/kaizen/app/ui/theme/Type.kt +++ b/app/src/main/java/dev/kaizen/app/ui/theme/Type.kt @@ -2,33 +2,123 @@ package dev.kaizen.app.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.Font import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontVariation import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp +import dev.kaizen.app.R + +@OptIn(androidx.compose.ui.text.ExperimentalTextApi::class) +val InterVariable = FontFamily( + Font( + R.font.inter_variable, + weight = FontWeight.W100, + variationSettings = FontVariation.Settings(FontVariation.weight(100)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W200, + variationSettings = FontVariation.Settings(FontVariation.weight(200)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W300, + variationSettings = FontVariation.Settings(FontVariation.weight(300)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W400, + variationSettings = FontVariation.Settings(FontVariation.weight(400)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W500, + variationSettings = FontVariation.Settings(FontVariation.weight(500)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W600, + variationSettings = FontVariation.Settings(FontVariation.weight(600)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W700, + variationSettings = FontVariation.Settings(FontVariation.weight(700)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W800, + variationSettings = FontVariation.Settings(FontVariation.weight(800)), + ), + Font( + R.font.inter_variable, + weight = FontWeight.W900, + variationSettings = FontVariation.Settings(FontVariation.weight(900)), + ), +) -// Set of Material typography styles to start with val Typography = Typography( + displayLarge = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W600, + fontSize = 32.sp, + lineHeight = 38.sp, + letterSpacing = (-0.32).sp, + ), + displayMedium = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W600, + fontSize = 24.sp, + lineHeight = 30.sp, + letterSpacing = (-0.12).sp, + ), + titleLarge = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W600, + fontSize = 20.sp, + lineHeight = 26.sp, + ), + titleMedium = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W500, + fontSize = 16.sp, + lineHeight = 22.sp, + letterSpacing = 0.05.sp, + ), bodyLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, + fontFamily = InterVariable, + fontWeight = FontWeight.W400, fontSize = 16.sp, lineHeight = 24.sp, - letterSpacing = 0.5.sp - ) - /* Other default text styles to override - titleLarge = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Normal, - fontSize = 22.sp, - lineHeight = 28.sp, - letterSpacing = 0.sp + letterSpacing = 0.08.sp, + ), + bodyMedium = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W400, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.11.sp, + ), + labelLarge = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W500, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.14.sp, + ), + labelMedium = TextStyle( + fontFamily = InterVariable, + fontWeight = FontWeight.W500, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.24.sp, ), labelSmall = TextStyle( - fontFamily = FontFamily.Default, - fontWeight = FontWeight.Medium, + fontFamily = InterVariable, + fontWeight = FontWeight.W500, fontSize = 11.sp, lineHeight = 16.sp, - letterSpacing = 0.5.sp - ) - */ -) \ No newline at end of file + letterSpacing = 0.33.sp, + ), +) diff --git a/app/src/main/res/font/inter_variable.ttf b/app/src/main/res/font/inter_variable.ttf new file mode 100644 index 0000000..4ab79e0 Binary files /dev/null and b/app/src/main/res/font/inter_variable.ttf differ diff --git a/app/src/test/java/dev/kaizen/app/SettingsViewModelTest.kt b/app/src/test/java/dev/kaizen/app/SettingsViewModelTest.kt index b26a839..6b19457 100644 --- a/app/src/test/java/dev/kaizen/app/SettingsViewModelTest.kt +++ b/app/src/test/java/dev/kaizen/app/SettingsViewModelTest.kt @@ -1,67 +1,55 @@ package dev.kaizen.app import dev.kaizen.app.settings.AppAppearance -import dev.kaizen.app.settings.KaizenThemeId import dev.kaizen.app.settings.SettingsViewModel +import dev.kaizen.app.ui.theme.themes.KaizenThemeId import org.junit.Assert.assertEquals import org.junit.Test -/** - * High-fidelity JVM Unit Test for the SettingsViewModel. - * - * Verifies that state transitions, user inputs, and theme selections are - * correctly and reactively managed inside the view model under clean MVVM. - */ class SettingsViewModelTest { @Test fun testInitialState() { val viewModel = SettingsViewModel() - - // Assert correct default states - assertEquals(KaizenThemeId.Amber, viewModel.selectedTheme.value) - assertEquals(AppAppearance.Dark, viewModel.appearanceMode.value) + assertEquals(KaizenThemeId.AMBER, viewModel.selectedTheme.value) + assertEquals(AppAppearance.System, viewModel.appearanceMode.value) assertEquals("Bruno", viewModel.userName.value) - assertEquals("gemini-2.5-flash", viewModel.defaultModel.value) } @Test fun testSelectTheme() { val viewModel = SettingsViewModel() - - // Transition theme to Aurora - viewModel.selectTheme(KaizenThemeId.Aurora) - - assertEquals(KaizenThemeId.Aurora, viewModel.selectedTheme.value) + viewModel.selectTheme(KaizenThemeId.AURORA) + assertEquals(KaizenThemeId.AURORA, viewModel.selectedTheme.value) + } + + @Test + fun testSelectAllThemes() { + val viewModel = SettingsViewModel() + KaizenThemeId.entries.forEach { id -> + viewModel.selectTheme(id) + assertEquals(id, viewModel.selectedTheme.value) + } } @Test fun testSelectAppearance() { val viewModel = SettingsViewModel() - - // Switch appearance to Light Mode viewModel.selectAppearance(AppAppearance.Light) - assertEquals(AppAppearance.Light, viewModel.appearanceMode.value) } @Test fun testUpdateUserName() { val viewModel = SettingsViewModel() - - // Update user's name - viewModel.updateUserName(" Giuseppe ") // Should be trimmed - + viewModel.updateUserName(" Giuseppe ") assertEquals("Giuseppe", viewModel.userName.value) } @Test fun testUpdateUserNameBlankIgnored() { val viewModel = SettingsViewModel() - - // Attempting to set an empty/blank name should be ignored viewModel.updateUserName(" ") - - assertEquals("Bruno", viewModel.userName.value) // Remains unchanged + assertEquals("Bruno", viewModel.userName.value) } }