feat: initial project structure with Settings screen

- Set up Kotlin + Jetpack Compose Android project
- Configure Gradle with version catalog and all dependencies
- Implement Settings screen for TeslamateApi server configuration
- Add Material Design 3 theming with Tesla-inspired colors
- Set up Hilt dependency injection
- Add DataStore for settings persistence
- Include navigation component with Compose integration
- Add GPLv3 license

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Davide Ferrari
2025-12-14 12:03:25 +01:00
commit d6cb3b42f2
34 changed files with 3231 additions and 0 deletions

View File

@@ -0,0 +1,31 @@
package com.matedroid
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.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import com.matedroid.ui.navigation.NavGraph
import com.matedroid.ui.theme.MateDroidTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
MateDroidTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
NavGraph()
}
}
}
}
}

View File

@@ -0,0 +1,7 @@
package com.matedroid
import android.app.Application
import dagger.hilt.android.HiltAndroidApp
@HiltAndroidApp
class MateDroidApp : Application()

View File

@@ -0,0 +1,51 @@
package com.matedroid.data.local
import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "matedroid_settings")
data class AppSettings(
val serverUrl: String = "",
val apiToken: String = ""
) {
val isConfigured: Boolean
get() = serverUrl.isNotBlank()
}
@Singleton
class SettingsDataStore @Inject constructor(
@ApplicationContext private val context: Context
) {
private val serverUrlKey = stringPreferencesKey("server_url")
private val apiTokenKey = stringPreferencesKey("api_token")
val settings: Flow<AppSettings> = context.dataStore.data.map { preferences ->
AppSettings(
serverUrl = preferences[serverUrlKey] ?: "",
apiToken = preferences[apiTokenKey] ?: ""
)
}
suspend fun saveSettings(serverUrl: String, apiToken: String) {
context.dataStore.edit { preferences ->
preferences[serverUrlKey] = serverUrl
preferences[apiTokenKey] = apiToken
}
}
suspend fun clearSettings() {
context.dataStore.edit { preferences ->
preferences.clear()
}
}
}

View File

@@ -0,0 +1,23 @@
package com.matedroid.di
import android.content.Context
import com.matedroid.data.local.SettingsDataStore
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object AppModule {
@Provides
@Singleton
fun provideSettingsDataStore(
@ApplicationContext context: Context
): SettingsDataStore {
return SettingsDataStore(context)
}
}

View File

@@ -0,0 +1,44 @@
package com.matedroid.ui.navigation
import androidx.compose.runtime.Composable
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.matedroid.ui.screens.settings.SettingsScreen
sealed class Screen(val route: String) {
data object Settings : Screen("settings")
data object Dashboard : Screen("dashboard")
data object Charges : Screen("charges")
data object ChargeDetail : Screen("charges/{chargeId}") {
fun createRoute(chargeId: Int) = "charges/$chargeId"
}
data object Drives : Screen("drives")
data object DriveDetail : Screen("drives/{driveId}") {
fun createRoute(driveId: Int) = "drives/$driveId"
}
data object Battery : Screen("battery")
data object Updates : Screen("updates")
}
@Composable
fun NavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = Screen.Settings.route
) {
composable(Screen.Settings.route) {
SettingsScreen(
onNavigateToDashboard = {
navController.navigate(Screen.Dashboard.route) {
popUpTo(Screen.Settings.route) { inclusive = true }
}
}
)
}
// Dashboard and other screens will be added in subsequent phases
}
}

View File

@@ -0,0 +1,318 @@
package com.matedroid.ui.screens.settings
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Scaffold
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.matedroid.ui.theme.MateDroidTheme
import com.matedroid.ui.theme.StatusError
import com.matedroid.ui.theme.StatusSuccess
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(
onNavigateToDashboard: () -> Unit,
viewModel: SettingsViewModel = hiltViewModel()
) {
val uiState by viewModel.uiState.collectAsState()
val snackbarHostState = remember { SnackbarHostState() }
LaunchedEffect(uiState.error) {
uiState.error?.let {
snackbarHostState.showSnackbar(it)
viewModel.clearError()
}
}
Scaffold(
topBar = {
TopAppBar(
title = { Text("MateDroid Settings") }
)
},
snackbarHost = { SnackbarHost(snackbarHostState) }
) { paddingValues ->
if (uiState.isLoading) {
LoadingContent(modifier = Modifier.padding(paddingValues))
} else {
SettingsContent(
modifier = Modifier.padding(paddingValues),
uiState = uiState,
onServerUrlChange = viewModel::updateServerUrl,
onApiTokenChange = viewModel::updateApiToken,
onTestConnection = viewModel::testConnection,
onSave = { viewModel.saveSettings(onNavigateToDashboard) }
)
}
}
}
@Composable
private fun LoadingContent(modifier: Modifier = Modifier) {
Column(
modifier = modifier.fillMaxSize(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
CircularProgressIndicator()
Spacer(modifier = Modifier.height(16.dp))
Text("Loading settings...")
}
}
@Composable
private fun SettingsContent(
modifier: Modifier = Modifier,
uiState: SettingsUiState,
onServerUrlChange: (String) -> Unit,
onApiTokenChange: (String) -> Unit,
onTestConnection: () -> Unit,
onSave: () -> Unit
) {
var passwordVisible by remember { mutableStateOf(false) }
Column(
modifier = modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(16.dp)
) {
Text(
text = "Connect to TeslamateApi",
style = MaterialTheme.typography.headlineSmall
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Enter your TeslamateApi server URL and optional API token to connect.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
OutlinedTextField(
value = uiState.serverUrl,
onValueChange = onServerUrlChange,
label = { Text("Server URL") },
placeholder = { Text("https://teslamate.example.com") },
modifier = Modifier
.fillMaxWidth()
.testTag("urlInput"),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
enabled = !uiState.isTesting && !uiState.isSaving
)
Spacer(modifier = Modifier.height(16.dp))
OutlinedTextField(
value = uiState.apiToken,
onValueChange = onApiTokenChange,
label = { Text("API Token (optional)") },
placeholder = { Text("Your API token") },
modifier = Modifier
.fillMaxWidth()
.testTag("tokenInput"),
singleLine = true,
visualTransformation = if (passwordVisible) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
},
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
trailingIcon = {
IconButton(onClick = { passwordVisible = !passwordVisible }) {
Icon(
imageVector = if (passwordVisible) {
Icons.Filled.VisibilityOff
} else {
Icons.Filled.Visibility
},
contentDescription = if (passwordVisible) {
"Hide token"
} else {
"Show token"
}
)
}
},
enabled = !uiState.isTesting && !uiState.isSaving
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Leave empty if your TeslamateApi doesn't require authentication.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(24.dp))
// Test result card
uiState.testResult?.let { result ->
TestResultCard(result = result)
Spacer(modifier = Modifier.height(16.dp))
}
// Action buttons
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
OutlinedButton(
onClick = onTestConnection,
enabled = uiState.serverUrl.isNotBlank() && !uiState.isTesting && !uiState.isSaving,
modifier = Modifier.weight(1f)
) {
if (uiState.isTesting) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp
)
Spacer(modifier = Modifier.width(8.dp))
}
Text("Test Connection")
}
Button(
onClick = onSave,
enabled = uiState.serverUrl.isNotBlank() && !uiState.isTesting && !uiState.isSaving,
modifier = Modifier
.weight(1f)
.testTag("saveButton")
) {
if (uiState.isSaving) {
CircularProgressIndicator(
modifier = Modifier.size(18.dp),
strokeWidth = 2.dp,
color = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.width(8.dp))
}
Text("Save & Continue")
}
}
}
}
@Composable
private fun TestResultCard(result: TestResult) {
val (icon, color, text) = when (result) {
is TestResult.Success -> Triple(
Icons.Filled.CheckCircle,
StatusSuccess,
"Connection successful!"
)
is TestResult.Failure -> Triple(
Icons.Filled.Error,
StatusError,
"Connection failed: ${result.message}"
)
}
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = color.copy(alpha = 0.1f)
)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
imageVector = icon,
contentDescription = null,
tint = color
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = text,
color = color,
style = MaterialTheme.typography.bodyMedium
)
}
}
}
@Preview(showBackground = true)
@Composable
private fun SettingsScreenPreview() {
MateDroidTheme {
SettingsContent(
uiState = SettingsUiState(isLoading = false),
onServerUrlChange = {},
onApiTokenChange = {},
onTestConnection = {},
onSave = {}
)
}
}
@Preview(showBackground = true)
@Composable
private fun SettingsScreenWithResultPreview() {
MateDroidTheme {
SettingsContent(
uiState = SettingsUiState(
isLoading = false,
serverUrl = "https://teslamate.example.com",
testResult = TestResult.Success
),
onServerUrlChange = {},
onApiTokenChange = {},
onTestConnection = {},
onSave = {}
)
}
}

View File

@@ -0,0 +1,144 @@
package com.matedroid.ui.screens.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.matedroid.data.local.AppSettings
import com.matedroid.data.local.SettingsDataStore
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import javax.inject.Inject
data class SettingsUiState(
val serverUrl: String = "",
val apiToken: String = "",
val isLoading: Boolean = true,
val isTesting: Boolean = false,
val isSaving: Boolean = false,
val testResult: TestResult? = null,
val error: String? = null
)
sealed class TestResult {
data object Success : TestResult()
data class Failure(val message: String) : TestResult()
}
@HiltViewModel
class SettingsViewModel @Inject constructor(
private val settingsDataStore: SettingsDataStore
) : ViewModel() {
private val _uiState = MutableStateFlow(SettingsUiState())
val uiState: StateFlow<SettingsUiState> = _uiState.asStateFlow()
init {
loadSettings()
}
private fun loadSettings() {
viewModelScope.launch {
val settings = settingsDataStore.settings.first()
_uiState.value = _uiState.value.copy(
serverUrl = settings.serverUrl,
apiToken = settings.apiToken,
isLoading = false
)
}
}
fun updateServerUrl(url: String) {
_uiState.value = _uiState.value.copy(
serverUrl = url,
testResult = null,
error = null
)
}
fun updateApiToken(token: String) {
_uiState.value = _uiState.value.copy(
apiToken = token,
testResult = null,
error = null
)
}
fun testConnection() {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isTesting = true, testResult = null, error = null)
try {
val url = _uiState.value.serverUrl.trimEnd('/')
if (url.isBlank()) {
_uiState.value = _uiState.value.copy(
isTesting = false,
testResult = TestResult.Failure("Server URL is required")
)
return@launch
}
// TODO: Implement actual API ping test in Phase 2
// For now, just validate URL format
if (!url.startsWith("http://") && !url.startsWith("https://")) {
_uiState.value = _uiState.value.copy(
isTesting = false,
testResult = TestResult.Failure("URL must start with http:// or https://")
)
return@launch
}
// Simulate successful test for now
_uiState.value = _uiState.value.copy(
isTesting = false,
testResult = TestResult.Success
)
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isTesting = false,
testResult = TestResult.Failure(e.message ?: "Unknown error")
)
}
}
}
fun saveSettings(onSuccess: () -> Unit) {
viewModelScope.launch {
_uiState.value = _uiState.value.copy(isSaving = true, error = null)
try {
val url = _uiState.value.serverUrl.trimEnd('/')
if (url.isBlank()) {
_uiState.value = _uiState.value.copy(
isSaving = false,
error = "Server URL is required"
)
return@launch
}
settingsDataStore.saveSettings(
serverUrl = url,
apiToken = _uiState.value.apiToken
)
_uiState.value = _uiState.value.copy(isSaving = false)
onSuccess()
} catch (e: Exception) {
_uiState.value = _uiState.value.copy(
isSaving = false,
error = e.message ?: "Failed to save settings"
)
}
}
}
fun clearTestResult() {
_uiState.value = _uiState.value.copy(testResult = null)
}
fun clearError() {
_uiState.value = _uiState.value.copy(error = null)
}
}

View File

@@ -0,0 +1,53 @@
package com.matedroid.ui.theme
import androidx.compose.ui.graphics.Color
// Tesla-inspired colors
val TeslaRed = Color(0xFFE31937)
val TeslaDark = Color(0xFF171A20)
val TeslaBlue = Color(0xFF3E6AE1)
// Status colors
val StatusSuccess = Color(0xFF4CAF50)
val StatusWarning = Color(0xFFFF9800)
val StatusError = Color(0xFFF44336)
// Light theme colors
val PrimaryLight = TeslaRed
val OnPrimaryLight = Color.White
val PrimaryContainerLight = Color(0xFFFFDAD6)
val OnPrimaryContainerLight = Color(0xFF410002)
val SecondaryLight = TeslaDark
val OnSecondaryLight = Color.White
val SecondaryContainerLight = Color(0xFFE2E2E2)
val OnSecondaryContainerLight = Color(0xFF1B1B1B)
val TertiaryLight = TeslaBlue
val OnTertiaryLight = Color.White
val BackgroundLight = Color(0xFFFFFBFF)
val OnBackgroundLight = Color(0xFF1C1B1F)
val SurfaceLight = Color(0xFFFFFBFF)
val OnSurfaceLight = Color(0xFF1C1B1F)
val SurfaceVariantLight = Color(0xFFF5DDDA)
val OnSurfaceVariantLight = Color(0xFF534341)
val ErrorLight = StatusError
val OnErrorLight = Color.White
// Dark theme colors
val PrimaryDark = Color(0xFFFFB4AB)
val OnPrimaryDark = Color(0xFF690005)
val PrimaryContainerDark = TeslaRed
val OnPrimaryContainerDark = Color.White
val SecondaryDark = Color(0xFFC6C6C6)
val OnSecondaryDark = Color(0xFF303030)
val SecondaryContainerDark = Color(0xFF474747)
val OnSecondaryContainerDark = Color(0xFFE2E2E2)
val TertiaryDark = Color(0xFFADC6FF)
val OnTertiaryDark = Color(0xFF002E69)
val BackgroundDark = Color(0xFF1C1B1F)
val OnBackgroundDark = Color(0xFFE6E1E5)
val SurfaceDark = Color(0xFF1C1B1F)
val OnSurfaceDark = Color(0xFFE6E1E5)
val SurfaceVariantDark = Color(0xFF534341)
val OnSurfaceVariantDark = Color(0xFFD8C2BF)
val ErrorDark = Color(0xFFFFB4AB)
val OnErrorDark = Color(0xFF690005)

View File

@@ -0,0 +1,75 @@
package com.matedroid.ui.theme
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
private val DarkColorScheme = darkColorScheme(
primary = PrimaryDark,
onPrimary = OnPrimaryDark,
primaryContainer = PrimaryContainerDark,
onPrimaryContainer = OnPrimaryContainerDark,
secondary = SecondaryDark,
onSecondary = OnSecondaryDark,
secondaryContainer = SecondaryContainerDark,
onSecondaryContainer = OnSecondaryContainerDark,
tertiary = TertiaryDark,
onTertiary = OnTertiaryDark,
background = BackgroundDark,
onBackground = OnBackgroundDark,
surface = SurfaceDark,
onSurface = OnSurfaceDark,
surfaceVariant = SurfaceVariantDark,
onSurfaceVariant = OnSurfaceVariantDark,
error = ErrorDark,
onError = OnErrorDark
)
private val LightColorScheme = lightColorScheme(
primary = PrimaryLight,
onPrimary = OnPrimaryLight,
primaryContainer = PrimaryContainerLight,
onPrimaryContainer = OnPrimaryContainerLight,
secondary = SecondaryLight,
onSecondary = OnSecondaryLight,
secondaryContainer = SecondaryContainerLight,
onSecondaryContainer = OnSecondaryContainerLight,
tertiary = TertiaryLight,
onTertiary = OnTertiaryLight,
background = BackgroundLight,
onBackground = OnBackgroundLight,
surface = SurfaceLight,
onSurface = OnSurfaceLight,
surfaceVariant = SurfaceVariantLight,
onSurfaceVariant = OnSurfaceVariantLight,
error = ErrorLight,
onError = OnErrorLight
)
@Composable
fun MateDroidTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,115 @@
package com.matedroid.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 Typography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 57.sp,
lineHeight = 64.sp,
letterSpacing = (-0.25).sp
),
displayMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 45.sp,
lineHeight = 52.sp,
letterSpacing = 0.sp
),
displaySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 36.sp,
lineHeight = 44.sp,
letterSpacing = 0.sp
),
headlineLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 32.sp,
lineHeight = 40.sp,
letterSpacing = 0.sp
),
headlineMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 28.sp,
lineHeight = 36.sp,
letterSpacing = 0.sp
),
headlineSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 24.sp,
lineHeight = 32.sp,
letterSpacing = 0.sp
),
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
titleMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.15.sp
),
titleSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
),
bodyMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.25.sp
),
bodySmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.4.sp
),
labelLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 14.sp,
lineHeight = 20.sp,
letterSpacing = 0.1.sp
),
labelMedium = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 12.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
)