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>
This commit is contained in:
347
specs/agent-b-theme-navigation.md
Normal file
347
specs/agent-b-theme-navigation.md
Normal file
@@ -0,0 +1,347 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user