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

348 lines
11 KiB
Markdown

# 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