Files
Kecalek/specs/agent-b-theme-navigation.md
filip fe861cfafa Initial commit: Kecalek Android client
Complete Android client for encrypted chat platform.
78+ Kotlin files: crypto (X3DH, Double Ratchet, AES-GCM, Ed25519, X25519,
RSA-PSS), network (TCP/TLS, 50 endpoints), Hilt DI, Room+SQLCipher DB,
Jetpack Compose UI with Catppuccin Mocha theme.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-11 01:19:17 +01:00

11 KiB

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

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

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

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

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

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