# Agent B: Theme + Navigation ## Phase: 0 (Scaffolding) ## Depends on: Agent A (project structure must exist) ## Context You are building the theme system and navigation graph for "Kecalek" — an encrypted chat Android app. The app uses a Catppuccin Mocha dark theme (Signal-like appearance). ## Task 1. Implement Catppuccin Mocha dark theme in Jetpack Compose Material 3 2. Set up Compose Navigation with all app routes 3. Update MainActivity to use the theme and navigation ## Files to Create ### 1. ui/theme/Color.kt ```kotlin package com.kecalek.chat.ui.theme import androidx.compose.ui.graphics.Color // Catppuccin Mocha palette object CatppuccinMocha { val Rosewater = Color(0xFFF5E0DC) val Flamingo = Color(0xFFF2CDCD) val Pink = Color(0xFFF5C2E7) val Mauve = Color(0xFFCBA6F7) val Red = Color(0xFFF38BA8) val Maroon = Color(0xFFEBA0AC) val Peach = Color(0xFFFAB387) val Yellow = Color(0xFFF9E2AF) val Green = Color(0xFFA6E3A1) val Teal = Color(0xFF94E2D5) val Sky = Color(0xFF89DCEB) val Sapphire = Color(0xFF74C7EC) val Blue = Color(0xFF89B4FA) val Lavender = Color(0xFFB4BEFE) val Text = Color(0xFFCDD6F4) val Subtext1 = Color(0xFFBAC2DE) val Subtext0 = Color(0xFFA6ADC8) val Overlay2 = Color(0xFF9399B2) val Overlay1 = Color(0xFF7F849C) val Overlay0 = Color(0xFF6C7086) val Surface2 = Color(0xFF585B70) val Surface1 = Color(0xFF45475A) val Surface0 = Color(0xFF313244) val Base = Color(0xFF1E1E2E) val Mantle = Color(0xFF181825) val Crust = Color(0xFF11111B) } ``` ### 2. ui/theme/Type.kt ```kotlin package com.kecalek.chat.ui.theme import androidx.compose.material3.Typography import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp val KecalekTypography = Typography( headlineLarge = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Bold, fontSize = 28.sp, lineHeight = 36.sp, ), headlineMedium = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.SemiBold, fontSize = 22.sp, lineHeight = 28.sp, ), titleLarge = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.SemiBold, fontSize = 18.sp, lineHeight = 24.sp, ), titleMedium = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Medium, fontSize = 16.sp, lineHeight = 22.sp, ), bodyLarge = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Normal, fontSize = 16.sp, lineHeight = 24.sp, ), bodyMedium = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Normal, fontSize = 14.sp, lineHeight = 20.sp, ), bodySmall = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Normal, fontSize = 12.sp, lineHeight = 16.sp, ), labelLarge = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Medium, fontSize = 14.sp, lineHeight = 20.sp, ), labelSmall = TextStyle( fontFamily = FontFamily.SansSerif, fontWeight = FontWeight.Medium, fontSize = 11.sp, lineHeight = 16.sp, ), ) ``` ### 3. ui/theme/Theme.kt ```kotlin package com.kecalek.chat.ui.theme import androidx.compose.material3.MaterialTheme import androidx.compose.material3.darkColorScheme import androidx.compose.runtime.Composable private val DarkColorScheme = darkColorScheme( primary = CatppuccinMocha.Lavender, onPrimary = CatppuccinMocha.Base, primaryContainer = CatppuccinMocha.Lavender.copy(alpha = 0.3f), onPrimaryContainer = CatppuccinMocha.Text, secondary = CatppuccinMocha.Mauve, onSecondary = CatppuccinMocha.Base, secondaryContainer = CatppuccinMocha.Mauve.copy(alpha = 0.3f), onSecondaryContainer = CatppuccinMocha.Text, tertiary = CatppuccinMocha.Peach, onTertiary = CatppuccinMocha.Base, error = CatppuccinMocha.Red, onError = CatppuccinMocha.Base, errorContainer = CatppuccinMocha.Red.copy(alpha = 0.3f), background = CatppuccinMocha.Base, onBackground = CatppuccinMocha.Text, surface = CatppuccinMocha.Surface0, onSurface = CatppuccinMocha.Text, surfaceVariant = CatppuccinMocha.Surface1, onSurfaceVariant = CatppuccinMocha.Subtext1, outline = CatppuccinMocha.Overlay0, outlineVariant = CatppuccinMocha.Surface2, inverseSurface = CatppuccinMocha.Text, inverseOnSurface = CatppuccinMocha.Base, ) @Composable fun KecalekTheme(content: @Composable () -> Unit) { MaterialTheme( colorScheme = DarkColorScheme, typography = KecalekTypography, content = content, ) } ``` ### 4. ui/navigation/NavGraph.kt ```kotlin package com.kecalek.chat.ui.navigation import androidx.compose.runtime.Composable import androidx.navigation.NavHostController import androidx.navigation.NavType import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import androidx.navigation.navArgument /** * Navigation routes for the app. */ object Routes { const val LOGIN = "login" const val REGISTER = "register" const val PAIRING = "pairing" const val CONVERSATION_LIST = "conversations" const val CHAT = "chat/{conversationId}" const val GROUP_INFO = "group_info/{conversationId}" const val PROFILE = "profile/{userId}" const val EDIT_PROFILE = "edit_profile" const val VERIFICATION = "verification/{userId}" const val DEVICE_LIST = "devices" const val SETTINGS = "settings" const val IMAGE_VIEWER = "image_viewer/{imageUrl}" // Helper functions to build routes with arguments fun chat(conversationId: String) = "chat/$conversationId" fun groupInfo(conversationId: String) = "group_info/$conversationId" fun profile(userId: String) = "profile/$userId" fun verification(userId: String) = "verification/$userId" fun imageViewer(imageUrl: String) = "image_viewer/$imageUrl" } @Composable fun KecalekNavGraph( navController: NavHostController = rememberNavController(), startDestination: String = Routes.LOGIN, ) { NavHost( navController = navController, startDestination = startDestination, ) { composable(Routes.LOGIN) { // TODO: LoginScreen(navController) } composable(Routes.REGISTER) { // TODO: RegisterScreen(navController) } composable(Routes.PAIRING) { // TODO: PairingScreen(navController) } composable(Routes.CONVERSATION_LIST) { // TODO: ConversationListScreen(navController) } composable( route = Routes.CHAT, arguments = listOf(navArgument("conversationId") { type = NavType.StringType }) ) { backStackEntry -> val conversationId = backStackEntry.arguments?.getString("conversationId") ?: return@composable // TODO: ChatScreen(conversationId, navController) } composable( route = Routes.GROUP_INFO, arguments = listOf(navArgument("conversationId") { type = NavType.StringType }) ) { backStackEntry -> val conversationId = backStackEntry.arguments?.getString("conversationId") ?: return@composable // TODO: GroupInfoScreen(conversationId, navController) } composable( route = Routes.PROFILE, arguments = listOf(navArgument("userId") { type = NavType.StringType }) ) { backStackEntry -> val userId = backStackEntry.arguments?.getString("userId") ?: return@composable // TODO: ProfileScreen(userId, navController) } composable(Routes.EDIT_PROFILE) { // TODO: EditProfileScreen(navController) } composable( route = Routes.VERIFICATION, arguments = listOf(navArgument("userId") { type = NavType.StringType }) ) { backStackEntry -> val userId = backStackEntry.arguments?.getString("userId") ?: return@composable // TODO: SafetyNumberScreen(userId, navController) } composable(Routes.DEVICE_LIST) { // TODO: DeviceListScreen(navController) } composable(Routes.SETTINGS) { // TODO: SettingsScreen(navController) } composable( route = Routes.IMAGE_VIEWER, arguments = listOf(navArgument("imageUrl") { type = NavType.StringType }) ) { backStackEntry -> val imageUrl = backStackEntry.arguments?.getString("imageUrl") ?: return@composable // TODO: ImageViewer(imageUrl, navController) } } } ``` ### 5. Update MainActivity.kt ```kotlin package com.kecalek.chat import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.kecalek.chat.ui.navigation.KecalekNavGraph import com.kecalek.chat.ui.theme.KecalekTheme import dagger.hilt.android.AndroidEntryPoint @AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { KecalekTheme { Surface(modifier = Modifier.fillMaxSize()) { KecalekNavGraph() } } } } } ``` ## Color Usage Guide Use these mappings consistently across all screens: | UI Element | Color | |-----------|-------| | Own message bubble background | `CatppuccinMocha.Lavender.copy(alpha = 0.15f)` | | Other's message bubble background | `CatppuccinMocha.Surface0` | | App background | `CatppuccinMocha.Base` | | Card/surface background | `CatppuccinMocha.Surface0` | | Primary text | `CatppuccinMocha.Text` | | Secondary text | `CatppuccinMocha.Subtext1` | | Muted text (timestamps) | `CatppuccinMocha.Overlay1` | | Online indicator | `CatppuccinMocha.Green` | | Error/delete | `CatppuccinMocha.Red` | | Unread badge | `CatppuccinMocha.Lavender` | | Links/@mentions | `CatppuccinMocha.Blue` | | Verified checkmark | `CatppuccinMocha.Green` | | Warning | `CatppuccinMocha.Peach` | | Send button | `CatppuccinMocha.Lavender` | | Input field background | `CatppuccinMocha.Surface1` | | Dividers | `CatppuccinMocha.Surface2` | ## Constraints - Dark theme only (no light theme for now) - Use Material 3 components exclusively - Edge-to-edge display (enableEdgeToEdge) - All navigation routes defined with TODO placeholders for actual screens ## DO NOT - Implement any actual screen UI (only TODO placeholders in NavGraph) - Add business logic - Implement any cryptographic operations - Modify build.gradle files