diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index b841e992f7..89c7dbb0a5 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -8,38 +8,61 @@ * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifos.android.library) - alias(libs.plugins.mifos.android.library.compose) - alias(libs.plugins.mifos.android.library.jacoco) + alias(libs.plugins.mifos.kmp.library) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.roborazzi) } android { - namespace = "com.mifos.core.designsystem" - defaultConfig { testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } + namespace = "com.mifos.core.designsystem" } -dependencies { - lintPublish(projects.lint) +kotlin { + sourceSets { + androidMain.dependencies { + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.activity.compose) + } + androidInstrumentedTest.dependencies { + implementation(libs.androidx.compose.ui.test) + } + androidUnitTest.dependencies { + implementation(libs.androidx.compose.ui.test) + } + commonMain.dependencies { + implementation(libs.coil.kt.compose) + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.materialIconsExtended) + implementation(compose.ui) + implementation(compose.uiUtil) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + api(libs.back.handler) + api(libs.window.size) + } - api(libs.androidx.compose.foundation) - api(libs.androidx.compose.foundation.layout) - api(libs.androidx.compose.material.iconsExtended) - api(libs.androidx.compose.material3) - api(libs.androidx.compose.runtime) - api(libs.androidx.compose.ui.util) - api(libs.androidx.activity.compose) + nativeMain.dependencies { + implementation(compose.runtime) + } - // coil - implementation(libs.coil.kt.compose) + jsMain.dependencies { + implementation(compose.runtime) + } - testImplementation(libs.androidx.compose.ui.test) - testImplementation(libs.androidx.compose.ui.test.manifest) - testImplementation(libs.hilt.android.testing) - testImplementation(projects.core.testing) - - androidTestImplementation(libs.androidx.compose.ui.test) - androidTestImplementation(projects.core.testing) + wasmJsMain.dependencies { + implementation(compose.runtime) + } + } } + +compose.resources { + publicResClass = true + generateResClass = always + packageOfResClass = "core.designsystem.generated.resources" +} \ No newline at end of file diff --git a/core/designsystem/src/main/AndroidManifest.xml b/core/designsystem/src/androidMain/AndroidManifest.xml similarity index 100% rename from core/designsystem/src/main/AndroidManifest.xml rename to core/designsystem/src/androidMain/AndroidManifest.xml diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosDrawingCanvas.kt b/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/component/MifosDrawingCanvas.kt similarity index 97% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosDrawingCanvas.kt rename to core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/component/MifosDrawingCanvas.kt index bcd3b1509d..3d82f2cf79 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosDrawingCanvas.kt +++ b/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/component/MifosDrawingCanvas.kt @@ -9,8 +9,6 @@ */ package com.mifos.core.designsystem.component -import DrawingState -import PathState import android.view.MotionEvent import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.fillMaxSize @@ -23,6 +21,7 @@ import androidx.compose.ui.geometry.Offset import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.input.pointer.pointerInteropFilter +import com.mifos.core.designsystem.utility.PathState @ExperimentalComposeUiApi @Composable diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt b/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/component/PermissionBox.kt similarity index 52% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt rename to core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/component/PermissionBox.kt index 0d3cd1a0a9..6145e2a314 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosPermissionBox.kt +++ b/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/component/PermissionBox.kt @@ -22,20 +22,24 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.compose.LocalLifecycleOwner +// TODO:: Support for compose multiplatform +@Suppress("LongMethod", "CyclomaticComplexMethod") @Composable fun PermissionBox( + title: String, + confirmButtonText: String, + dismissButtonText: String, requiredPermissions: List, - title: Int, - confirmButtonText: Int, - dismissButtonText: Int, - description: Int? = null, + modifier: Modifier = Modifier, + description: String? = null, onGranted: @Composable (() -> Unit)? = null, ) { val context = LocalContext.current @@ -56,7 +60,8 @@ fun PermissionBox( requiredPermissions.all { (context as? Activity)?.let { it1 -> ActivityCompat.shouldShowRequestPermissionRationale( - it1, it, + it1, + it, ) } == true } @@ -66,10 +71,10 @@ fun PermissionBox( } val decideCurrentPermissionStatus: (Boolean, Boolean) -> String = - { permissionGranted, shouldShowPermissionRationale -> - if (permissionGranted) { + { granted, rationale -> + if (granted) { "Granted" - } else if (shouldShowPermissionRationale) { + } else if (rationale) { "Rejected" } else { "Denied" @@ -85,62 +90,67 @@ fun PermissionBox( ) } - val multiplePermissionLauncher = rememberLauncherForActivityResult( - contract = ActivityResultContracts.RequestMultiplePermissions(), - onResult = { permissionResults -> - val isGranted = - requiredPermissions.all { permissionResults[it] ?: false } + val multiplePermissionLauncher = + rememberLauncherForActivityResult( + contract = ActivityResultContracts.RequestMultiplePermissions(), + onResult = { permissionResults -> + val isGranted = + requiredPermissions.all { permissionResults[it] ?: false } - permissionGranted = isGranted + permissionGranted = isGranted - if (!isGranted) { - shouldShowPermissionRationale = - requiredPermissions.all { - (context as? Activity)?.let { it1 -> + if (!isGranted) { + shouldShowPermissionRationale = + requiredPermissions.all { ActivityCompat.shouldShowRequestPermissionRationale( - it1, it, + context as Activity, + it, ) - } == false + } + } + shouldDirectUserToApplicationSettings = + !shouldShowPermissionRationale && + !permissionGranted + currentPermissionStatus = + decideCurrentPermissionStatus( + permissionGranted, + shouldShowPermissionRationale, + ) + }, + ) + + DisposableEffect( + key1 = lifecycleOwner, + effect = { + val observer = + LifecycleEventObserver { _, event -> + if (event == Lifecycle.Event.ON_START && + !permissionGranted && + !shouldShowPermissionRationale + ) { + multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) } + } + lifecycleOwner.lifecycle.addObserver(observer) + onDispose { + lifecycleOwner.lifecycle.removeObserver(observer) } - shouldDirectUserToApplicationSettings = - !shouldShowPermissionRationale && !permissionGranted - currentPermissionStatus = decideCurrentPermissionStatus( - permissionGranted, - shouldShowPermissionRationale, - ) }, ) - DisposableEffect(key1 = lifecycleOwner, effect = { - val observer = LifecycleEventObserver { _, event -> - if (event == Lifecycle.Event.ON_START && - !permissionGranted && - !shouldShowPermissionRationale - ) { - multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) - } - } - lifecycleOwner.lifecycle.addObserver(observer) - onDispose { - lifecycleOwner.lifecycle.removeObserver(observer) - } - }) - - if (shouldShowPermissionRationale) { - MifosDialogBox( - showDialogState = shouldShowPermissionRationale, - onDismiss = { shouldShowPermissionRationale = false }, - title = title, - message = description, - confirmButtonText = confirmButtonText, - onConfirm = { - shouldShowPermissionRationale = false - multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) - }, - dismissButtonText = dismissButtonText, - ) - } + MifosDialogBox( + showDialogState = shouldShowPermissionRationale, + onDismiss = { shouldShowPermissionRationale = false }, + title = title, + confirmButtonText = confirmButtonText, + onConfirm = { + shouldShowPermissionRationale = false + multiplePermissionLauncher.launch(requiredPermissions.toTypedArray()) + }, + dismissButtonText = dismissButtonText, + message = description, + modifier = modifier, + ) if (shouldDirectUserToApplicationSettings) { Intent( @@ -152,6 +162,8 @@ fun PermissionBox( } if (permissionGranted) { - onGranted?.invoke() + if (onGranted != null) { + onGranted() + } } } diff --git a/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/theme/Theme.android.kt b/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/theme/Theme.android.kt new file mode 100644 index 0000000000..141f0de5b5 --- /dev/null +++ b/core/designsystem/src/androidMain/kotlin/com/mifos/core/designsystem/theme/Theme.android.kt @@ -0,0 +1,30 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import android.os.Build +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.dynamicDarkColorScheme +import androidx.compose.material3.dynamicLightColorScheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext + +@Composable +actual fun colorScheme(useDarkTheme: Boolean, dynamicColor: Boolean): ColorScheme { + return when { + dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { + val context = LocalContext.current + if (useDarkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context) + } + + useDarkTheme -> darkScheme + else -> lightScheme + } +} diff --git a/core/designsystem/src/main/res/drawable/core_designsystem_ic_error_black_24dp.xml b/core/designsystem/src/commonMain/composeResources/drawable/core_designsystem_ic_error_black_24dp.xml similarity index 100% rename from core/designsystem/src/main/res/drawable/core_designsystem_ic_error_black_24dp.xml rename to core/designsystem/src/commonMain/composeResources/drawable/core_designsystem_ic_error_black_24dp.xml diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_black.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_black.ttf new file mode 100644 index 0000000000..71c0f995ee Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_black.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_bold.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_bold.ttf new file mode 100644 index 0000000000..00559eeb29 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_extra_bold.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_extra_bold.ttf new file mode 100644 index 0000000000..df7093608a Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_extra_bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_extra_light.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_extra_light.ttf new file mode 100644 index 0000000000..e76ec69a65 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_extra_light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_light.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_light.ttf new file mode 100644 index 0000000000..bc36bcc242 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_light.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_medium.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_medium.ttf new file mode 100644 index 0000000000..6bcdcc27f2 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_medium.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_regular.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_regular.ttf new file mode 100644 index 0000000000..9f0c71b70a Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_regular.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_semi_bold.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_semi_bold.ttf new file mode 100644 index 0000000000..74c726e327 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_semi_bold.ttf differ diff --git a/core/designsystem/src/commonMain/composeResources/font/poppins_thin.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_thin.ttf new file mode 100644 index 0000000000..03e736613a Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_thin.ttf differ diff --git a/core/designsystem/src/main/res/values/strings.xml b/core/designsystem/src/commonMain/composeResources/values/strings.xml similarity index 100% rename from core/designsystem/src/main/res/values/strings.xml rename to core/designsystem/src/commonMain/composeResources/values/strings.xml diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/DrawingState.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/DrawingState.kt similarity index 92% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/DrawingState.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/DrawingState.kt index d161fb3dd1..a274553566 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/DrawingState.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/DrawingState.kt @@ -7,12 +7,15 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ +package com.mifos.core.designsystem.component + import androidx.compose.runtime.MutableState import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Path +import com.mifos.core.designsystem.utility.PathState class DrawingState { var usedColors: MutableState> = mutableStateOf(setOf()) @@ -38,9 +41,3 @@ class DrawingState { currentPath.value = Path() } } - -data class PathState( - val path: Path, - val color: Color, - val stroke: Float, -) diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosAlertDialog.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosAlertDialog.kt new file mode 100644 index 0000000000..810789f72f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosAlertDialog.kt @@ -0,0 +1,71 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.BasicAlertDialog +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun MifosDialogBox( + title: String, + showDialogState: Boolean, + confirmButtonText: String, + dismissButtonText: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + message: String? = null, +) { + if (showDialogState) { + AlertDialog( + modifier = modifier, + onDismissRequest = onDismiss, + title = { Text(text = title) }, + text = { + if (message != null) { + Text(text = message) + } + }, + confirmButton = { + TextButton( + onClick = { + onConfirm() + }, + ) { + Text(text = confirmButtonText) + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text(text = dismissButtonText) + } + }, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosCustomDialog( + onDismiss: () -> Unit, + content: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + BasicAlertDialog( + onDismissRequest = onDismiss, + content = content, + modifier = modifier, + ) +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt similarity index 73% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt index b7a7364a20..4c7b8414c4 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosAndroidClientIcon.kt @@ -13,17 +13,14 @@ import androidx.compose.foundation.Image import androidx.compose.foundation.layout.size import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.graphics.vector.rememberVectorPainter import androidx.compose.ui.unit.dp -/** - * Created by Aditya Gupta on 21/02/24. - */ - @Composable -fun MifosAndroidClientIcon(id: Int, modifier: Modifier = Modifier) { +fun MifosAndroidClientIcon(imageVector: ImageVector, modifier: Modifier = Modifier) { Image( - painter = painterResource(id = id), + painter = rememberVectorPainter(imageVector), contentDescription = null, modifier = modifier.then(Modifier.size(200.dp, 100.dp)), ) diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBasicDialog.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBasicDialog.kt new file mode 100644 index 0000000000..ad54d5019f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBasicDialog.kt @@ -0,0 +1,142 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import com.mifos.core.designsystem.theme.MifosTheme +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun MifosBasicDialog( + visibilityState: BasicDialogState, + onDismissRequest: () -> Unit, +): Unit = when (visibilityState) { + BasicDialogState.Hidden -> Unit + is BasicDialogState.Shown -> { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + MifosTextButton( + content = { + Text(text = "Ok") + }, + onClick = onDismissRequest, + modifier = Modifier.testTag("AcceptAlertButton"), + ) + }, + title = visibilityState.title.let { + { + Text( + text = it, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.testTag("AlertTitleText"), + ) + } + }, + text = { + Text( + text = visibilityState.message, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.testTag("AlertContentText"), + ) + }, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier.semantics { + testTag = "AlertPopup" + }, + ) + } +} + +@Composable +fun MifosBasicDialog( + visibilityState: BasicDialogState, + onConfirm: () -> Unit, + onDismissRequest: () -> Unit, +): Unit = when (visibilityState) { + BasicDialogState.Hidden -> Unit + is BasicDialogState.Shown -> { + AlertDialog( + onDismissRequest = onDismissRequest, + confirmButton = { + MifosTextButton( + content = { + Text(text = "Ok") + }, + onClick = onConfirm, + modifier = Modifier.testTag("AcceptAlertButton"), + ) + }, + dismissButton = { + MifosTextButton( + content = { + Text(text = "Cancel") + }, + onClick = onDismissRequest, + modifier = Modifier.testTag("DismissAlertButton"), + ) + }, + title = visibilityState.title.let { + { + Text( + text = it, + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.testTag("AlertTitleText"), + ) + } + }, + text = { + Text( + text = visibilityState.message, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.testTag("AlertContentText"), + ) + }, + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + modifier = Modifier.semantics { + testTag = "AlertPopup" + }, + ) + } +} + +@Preview +@Composable +private fun MifosBasicDialog_preview() { + MifosTheme { + MifosBasicDialog( + visibilityState = BasicDialogState.Shown( + title = "An error has occurred.", + message = "Username or password is incorrect. Try again.", + ), + onDismissRequest = {}, + ) + } +} + +/** + * Models display of a [MifosBasicDialog]. + */ +sealed class BasicDialogState { + + data object Hidden : BasicDialogState() + + data class Shown( + val message: String, + val title: String = "An Error Occurred!", + ) : BasicDialogState() +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosBottomSheet.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBottomSheet.kt similarity index 86% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosBottomSheet.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBottomSheet.kt index 65fb8084b2..4cc83a740a 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosBottomSheet.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosBottomSheet.kt @@ -7,11 +7,8 @@ * * See https://github.com/openMF/android-client/blob/master/LICENSE.md */ -@file:OptIn(ExperimentalMaterial3Api::class) - package com.mifos.core.designsystem.component -import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.height @@ -26,15 +23,17 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.arkivanov.essenty.backhandler.BackCallback import kotlinx.coroutines.launch +import org.jetbrains.compose.ui.tooling.preview.Preview +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosBottomSheet( - content: @Composable () -> Unit, onDismiss: () -> Unit, modifier: Modifier = Modifier, + content: @Composable () -> Unit, ) { val coroutineScope = rememberCoroutineScope() val modalSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) @@ -49,14 +48,11 @@ fun MifosBottomSheet( onDismiss.invoke() } - BackHandler(modalSheetState.isVisible) { + BackCallback(modalSheetState.isVisible) { dismissSheet() } - AnimatedVisibility( - modifier = modifier, - visible = showBottomSheet, - ) { + AnimatedVisibility(visible = showBottomSheet) { ModalBottomSheet( containerColor = Color.White, onDismissRequest = { @@ -64,6 +60,7 @@ fun MifosBottomSheet( dismissSheet() }, sheetState = modalSheetState, + modifier = modifier, ) { content() } @@ -74,11 +71,11 @@ fun MifosBottomSheet( @Composable fun MifosBottomSheetPreview() { MifosBottomSheet( - { + content = { Box { Modifier.height(100.dp) } }, - {}, + onDismiss = {}, ) } diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt new file mode 100644 index 0000000000..1e7e46ee4c --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosButton.kt @@ -0,0 +1,239 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.BorderStroke +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.sizeIn +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.ButtonElevation +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.dp + +/** + * Mifos button with generic content slot. Wraps Material 3 [Button]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param contentPadding The spacing values to apply internally between the container and the + * content. + * @param content The button content. + */ +@Composable +fun MifosButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = ButtonDefaults.shape, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), + colors: ButtonColors = ButtonDefaults.buttonColors(), + content: @Composable RowScope.() -> Unit = {}, +) { + Button( + onClick = onClick, + modifier = modifier.height(48.dp), + enabled = enabled, + colors = colors, + shape = shape, + elevation = elevation, + contentPadding = contentPadding, + content = content, + ) +} + +/** + * Mifos button with generic content slot. Wraps Material 3 [Button]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param contentPadding The spacing values to apply internally between the container and the + * content. + * @param content The button content. + */ +@Composable +fun MifosButton( + onClick: () -> Unit, + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = ButtonDefaults.shape, + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), + colors: ButtonColors = ButtonDefaults.buttonColors(), +) { + Button( + onClick = onClick, + modifier = modifier.height(48.dp), + enabled = enabled, + colors = colors, + shape = shape, + elevation = elevation, + contentPadding = contentPadding, + content = { + text() + }, + ) +} + +/** + * Mifos outlined button with generic content slot. Wraps Material 3 [OutlinedButton]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param contentPadding The spacing values to apply internally between the container and the + * content. + * @param content The button content. + */ +@Composable +fun MifosOutlinedButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + shape: Shape = ButtonDefaults.outlinedShape, + border: BorderStroke? = ButtonDefaults.outlinedButtonBorder(enabled), + colors: ButtonColors = ButtonDefaults.outlinedButtonColors(), + contentPadding: PaddingValues = ButtonDefaults.ContentPadding, + content: @Composable RowScope.() -> Unit = {}, +) { + OutlinedButton( + onClick = onClick, + modifier = modifier + .height(48.dp), + enabled = enabled, + shape = shape, + colors = colors, + border = border, + contentPadding = contentPadding, + content = content, + ) +} + +/** + * Mifos text button with generic content slot. Wraps Material 3 [TextButton]. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param content The button content. + */ +@Composable +fun MifosTextButton( + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable RowScope.() -> Unit = {}, +) { + TextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + colors = ButtonDefaults.textButtonColors( + contentColor = MaterialTheme.colorScheme.onBackground, + ), + content = content, + ) +} + +/** + * Mifos text button with text and icon content slots. + * + * @param onClick Will be called when the user clicks the button. + * @param modifier Modifier to be applied to the button. + * @param enabled Controls the enabled state of the button. When `false`, this button will not be + * clickable and will appear disabled to accessibility services. + * @param text The button text label content. + * @param leadingIcon The button leading icon content. Pass `null` here for no leading icon. + */ +@Composable +fun MifosTextButton( + text: @Composable () -> Unit, + onClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + leadingIcon: @Composable (() -> Unit)? = null, +) { + MifosTextButton( + onClick = onClick, + modifier = modifier, + enabled = enabled, + ) { + MifosButtonContent( + text = text, + leadingIcon = leadingIcon, + ) + } +} + +/** + * Internal Mifos button content layout for arranging the text label and leading icon. + * + * @param text The button text label content. + * @param leadingIcon The button leading icon content. Default is `null` for no leading icon.Ï + */ +@Composable +private fun MifosButtonContent( + text: @Composable () -> Unit, + modifier: Modifier = Modifier, + leadingIcon: @Composable (() -> Unit)? = null, +) { + Row(modifier) { + if (leadingIcon != null) { + Box(Modifier.sizeIn(maxHeight = ButtonDefaults.IconSize)) { + leadingIcon() + } + } + Box( + Modifier + .padding( + start = if (leadingIcon != null) { + ButtonDefaults.IconSpacing + } else { + 0.dp + }, + ), + ) { + text() + } + } +} + +/** + * Mifos button default values. + */ +@Suppress("ForbiddenComment") +object MifosButtonDefaults { + // TODO: File bug + // OutlinedButton border color doesn't respect disabled state by default + const val DISABLED_OUTLINED_BUTTON_BORDER_ALPHA = 0.12f + + // TODO: File bug + // OutlinedButton default border width isn't exposed via ButtonDefaults + val OutlinedButtonBorderWidth = 1.dp +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosCard.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosCard.kt new file mode 100644 index 0000000000..2d3aac9c11 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosCard.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardColors +import androidx.compose.material3.CardDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@Composable +fun MifosCard( + modifier: Modifier = Modifier, + shape: Shape = RoundedCornerShape(8.dp), + elevation: Dp = 1.dp, + onClick: (() -> Unit)? = null, + colors: CardColors = CardDefaults.cardColors(), + content: @Composable ColumnScope.() -> Unit, +) { + Card( + shape = shape, + modifier = modifier + .fillMaxWidth() + .then(if (onClick != null) Modifier.clickable(onClick = onClick) else Modifier), + elevation = CardDefaults.cardElevation( + defaultElevation = elevation, + ), + colors = colors, + content = content, + ) +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosEditTextField.kt similarity index 87% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosEditTextField.kt index 989190c580..07f0d5f72c 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEditTextField.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosEditTextField.kt @@ -12,7 +12,6 @@ package com.mifos.core.designsystem.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.interaction.collectIsFocusedAsState -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions @@ -26,7 +25,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -35,7 +33,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.testTag -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.KeyboardCapitalization @@ -46,32 +43,24 @@ import androidx.compose.ui.text.input.VisualTransformation import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark -import com.mifos.core.designsystem.theme.DarkGray -import com.mifos.core.designsystem.theme.White - -/** - * Created by Aditya Gupta on 21/02/24. - */ @Composable fun MifosOutlinedTextField( value: TextFieldValue, onValueChange: (TextFieldValue) -> Unit, - label: Int, + label: String, modifier: Modifier = Modifier, maxLines: Int = 1, singleLine: Boolean = true, icon: ImageVector? = null, visualTransformation: VisualTransformation = VisualTransformation.None, trailingIcon: @Composable (() -> Unit)? = null, - error: Int? = null, + error: String? = null, ) { OutlinedTextField( value = value, onValueChange = onValueChange, - label = { Text(stringResource(id = label)) }, + label = { Text(label) }, modifier = modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp), @@ -80,7 +69,6 @@ fun MifosOutlinedTextField( Icon( imageVector = icon, contentDescription = null, - tint = if (isSystemInDarkTheme()) White else DarkGray, ) } } else { @@ -89,9 +77,6 @@ fun MifosOutlinedTextField( trailingIcon = trailingIcon, maxLines = maxLines, singleLine = singleLine, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, @@ -102,7 +87,7 @@ fun MifosOutlinedTextField( if (error != null) { Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = error), + text = error, color = MaterialTheme.colorScheme.error, ) } @@ -220,7 +205,7 @@ fun MifosOutlinedTextField( value: String, onValueChange: (String) -> Unit, label: String, - error: Int?, + error: String?, modifier: Modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), maxLines: Int = 1, readOnly: Boolean = false, @@ -243,7 +228,6 @@ fun MifosOutlinedTextField( Icon( imageVector = icon, contentDescription = null, - tint = if (isSystemInDarkTheme()) White else DarkGray, ) } } else { @@ -252,11 +236,6 @@ fun MifosOutlinedTextField( trailingIcon = trailingIcon, maxLines = maxLines, singleLine = singleLine, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - cursorColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, @@ -267,7 +246,7 @@ fun MifosOutlinedTextField( { Text( modifier = Modifier.fillMaxWidth(), - text = stringResource(id = error), + text = error, color = MaterialTheme.colorScheme.error, ) } @@ -331,20 +310,17 @@ private fun ClearIconButton( fun MifosDatePickerTextField( value: String, modifier: Modifier = Modifier.fillMaxWidth().padding(start = 16.dp, end = 16.dp), - label: Int? = null, + label: String? = null, labelString: String? = null, openDatePicker: () -> Unit, ) { OutlinedTextField( value = value, onValueChange = { }, - label = { Text(text = labelString ?: label?.let { stringResource(id = label) } ?: "") }, + label = { Text(text = labelString ?: label?.let { label } ?: "") }, readOnly = true, modifier = modifier, maxLines = 1, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEmptyContent.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosEmptyContent.kt similarity index 100% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosEmptyContent.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosEmptyContent.kt diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosLoadingDialog.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosLoadingDialog.kt new file mode 100644 index 0000000000..e6cfc4dc26 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosLoadingDialog.kt @@ -0,0 +1,107 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import com.mifos.core.designsystem.theme.MifosTheme +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun MifosLoadingDialog( + visibilityState: LoadingDialogState, +) { + when (visibilityState) { + is LoadingDialogState.Hidden -> Unit + is LoadingDialogState.Shown -> { + Dialog( + onDismissRequest = {}, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false, + ), + ) { + Card( + shape = RoundedCornerShape(28.dp), + colors = CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHigh, + ), + modifier = Modifier + .semantics { + testTag = "AlertPopup" + } + .fillMaxWidth() + .wrapContentHeight(), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.Center, + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Text( + text = "Loading..", + modifier = Modifier + .testTag("AlertTitleText") + .padding( + top = 24.dp, + bottom = 8.dp, + ), + ) + CircularProgressIndicator( + modifier = Modifier + .testTag("AlertProgressIndicator") + .padding( + top = 8.dp, + bottom = 24.dp, + ), + ) + } + } + } + } + } +} + +@Preview +@Composable +private fun MifosLoadingDialog_preview() { + MifosTheme { + MifosLoadingDialog( + visibilityState = LoadingDialogState.Shown, + ) + } +} + +/** + * Models display of a [MifosLoadingDialog]. + */ +sealed class LoadingDialogState { + data object Hidden : LoadingDialogState() + + data object Shown : LoadingDialogState() +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosMenuDropdown.kt similarity index 88% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosMenuDropdown.kt index 231ed54faf..75e8f4c655 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosMenuDropdown.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosMenuDropdown.kt @@ -11,12 +11,11 @@ package com.mifos.core.designsystem.component import androidx.compose.foundation.layout.padding import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp @Composable fun MifosMenuDropDownItem( @@ -29,7 +28,7 @@ fun MifosMenuDropDownItem( Text( modifier = Modifier.padding(6.dp), text = option, - style = TextStyle(fontSize = 17.sp), + style = MaterialTheme.typography.bodyLarge, ) }, onClick = { onClick() }, diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosNavigation.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosNavigation.kt new file mode 100644 index 0000000000..8c9e2b3543 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosNavigation.kt @@ -0,0 +1,246 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationRail +import androidx.compose.material3.NavigationRailItem +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.theme.MifosTheme +import org.jetbrains.compose.ui.tooling.preview.Preview + +/** + * Now in Android navigation bar item with icon and label content slots. Wraps Material 3 + * [NavigationBarItem]. + * + * @param selected Whether this item is selected. + * @param onClick The callback to be invoked when this item is selected. + * @param icon The item icon content. + * @param modifier Modifier to be applied to this item. + * @param selectedIcon The item icon content when selected. + * @param enabled controls the enabled state of this item. When `false`, this item will not be + * clickable and will appear disabled to accessibility services. + * @param label The item text label content. + * @param alwaysShowLabel Whether to always show the label for this item. If false, the label will + * only be shown when this item is selected. + */ +@Composable +fun RowScope.MifosNavigationBarItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + alwaysShowLabel: Boolean = true, + selectedIcon: @Composable () -> Unit = icon, + label: @Composable (() -> Unit)? = null, +) { + NavigationBarItem( + selected = selected, + onClick = onClick, + icon = if (selected) selectedIcon else icon, + modifier = modifier, + enabled = enabled, + label = label, + alwaysShowLabel = alwaysShowLabel, + ) +} + +/** + * Now in Android navigation bar with content slot. Wraps Material 3 [NavigationBar]. + * + * @param modifier Modifier to be applied to the navigation bar. + * @param content Destinations inside the navigation bar. This should contain multiple + * [NavigationBarItem]s. + */ +@Composable +fun MifosNavigationBar( + modifier: Modifier = Modifier, + content: @Composable RowScope.() -> Unit, +) { + NavigationBar( + modifier = modifier, + containerColor = MaterialTheme.colorScheme.background, + tonalElevation = 0.dp, + content = content, + ) +} + +/** + * Now in Android navigation rail item with icon and label content slots. Wraps Material 3 + * [NavigationRailItem]. + * + * @param selected Whether this item is selected. + * @param onClick The callback to be invoked when this item is selected. + * @param icon The item icon content. + * @param modifier Modifier to be applied to this item. + * @param selectedIcon The item icon content when selected. + * @param enabled controls the enabled state of this item. When `false`, this item will not be + * clickable and will appear disabled to accessibility services. + * @param label The item text label content. + * @param alwaysShowLabel Whether to always show the label for this item. If false, the label will + * only be shown when this item is selected. + */ +@Composable +fun MifosNavigationRailItem( + selected: Boolean, + onClick: () -> Unit, + icon: @Composable () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + alwaysShowLabel: Boolean = true, + selectedIcon: @Composable () -> Unit = icon, + label: @Composable (() -> Unit)? = null, +) { + NavigationRailItem( + selected = selected, + onClick = onClick, + icon = if (selected) selectedIcon else icon, + modifier = modifier, + enabled = enabled, + label = label, + alwaysShowLabel = alwaysShowLabel, + ) +} + +/** + * Now in Android navigation rail with header and content slots. Wraps Material 3 [NavigationRail]. + * + * @param modifier Modifier to be applied to the navigation rail. + * @param header Optional header that may hold a floating action button or a logo. + * @param content Destinations inside the navigation rail. This should contain multiple + * [NavigationRailItem]s. + */ +@Composable +fun MifosNavigationRail( + modifier: Modifier = Modifier, + header: @Composable (ColumnScope.() -> Unit)? = null, + content: @Composable ColumnScope.() -> Unit, +) { + NavigationRail( + modifier = modifier, + containerColor = Color.Transparent, + contentColor = MifosNavigationDefaults.navigationContentColor(), + header = header, + content = content, + ) +} + +@Preview +@Composable +fun MifosNavigationBarPreview() { + val items = listOf("Home", "Payments", "Finance", "Profile") + val icons = + listOf( + MifosIcons.Home, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.Profile, + ) + val selectedIcons = + listOf( + MifosIcons.HomeBoarder, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.ProfileBoarder, + ) + + MifosTheme { + MifosNavigationBar { + items.forEachIndexed { index, item -> + MifosNavigationBarItem( + selected = index == 0, + onClick = { }, + icon = { + Icon( + imageVector = icons[index], + contentDescription = item, + ) + }, + selectedIcon = { + Icon( + imageVector = selectedIcons[index], + contentDescription = item, + ) + }, + label = { Text(item) }, + ) + } + } + } +} + +@Preview +@Composable +fun MifosNavigationRailPreview() { + val items = listOf("Home", "Payments", "Finance", "Profile") + val icons = + listOf( + MifosIcons.Home, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.Profile, + ) + val selectedIcons = + listOf( + MifosIcons.HomeBoarder, + MifosIcons.Payment, + MifosIcons.Finance, + MifosIcons.ProfileBoarder, + ) + + MifosTheme { + MifosNavigationRail { + items.forEachIndexed { index, item -> + MifosNavigationRailItem( + selected = index == 0, + onClick = { }, + icon = { + Icon( + imageVector = icons[index], + contentDescription = item, + ) + }, + selectedIcon = { + Icon( + imageVector = selectedIcons[index], + contentDescription = item, + ) + }, + label = { Text(item) }, + ) + } + } + } +} + +/** + * Now in Android navigation default values. + */ +object MifosNavigationDefaults { + @Composable + fun navigationContentColor() = MaterialTheme.colorScheme.onSurfaceVariant + + @Composable + fun navigationSelectedItemColor() = MaterialTheme.colorScheme.onPrimaryContainer + + @Composable + fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosOtpTextField.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosOtpTextField.kt new file mode 100644 index 0000000000..9398a806e5 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosOtpTextField.kt @@ -0,0 +1,143 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +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.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.text.TextRange +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun MifosOtpTextField( + onOtpTextCorrectlyEntered: () -> Unit, + modifier: Modifier = Modifier, + realOtp: String = "", + otpCount: Int = 4, +) { + var otpText by remember { mutableStateOf("") } + var isError by remember { mutableStateOf(false) } + Column( + modifier = modifier + .fillMaxWidth() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + BasicTextField( + modifier = Modifier, + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), + onValueChange = { + otpText = it.text + isError = false + if (otpText.length == otpCount) { + if (otpText != realOtp) { + isError = true + } else { + onOtpTextCorrectlyEntered.invoke() + } + } + }, + keyboardActions = KeyboardActions( + onDone = { + if (otpText != realOtp) { + isError = true + } else { + onOtpTextCorrectlyEntered.invoke() + } + println("OTP: $otpText and $isError") + }, + ), + keyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number, + imeAction = ImeAction.Done, + ), + decorationBox = { + Row(horizontalArrangement = Arrangement.Center) { + repeat(otpCount) { index -> + CharView( + index = index, + text = otpText, + ) + Spacer(modifier = Modifier.width(8.dp)) + } + } + }, + ) + if (isError) { + // display error message in text + Text( + text = "Invalid OTP", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.error, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp), + ) + } + } +} + +@Composable +private fun CharView( + index: Int, + text: String, + modifier: Modifier = Modifier, +) { + val isFocused = text.length == index + val char = when { + index == text.length -> "_" + index > text.length -> "_" + else -> text[index].toString() + } + Text( + modifier = modifier + .width(40.dp) + .wrapContentHeight(align = Alignment.CenterVertically), + text = char, + style = MaterialTheme.typography.headlineSmall, + color = if (isFocused) { + MaterialTheme.colorScheme.secondary + } else { + MaterialTheme.colorScheme.tertiary + }, + textAlign = TextAlign.Center, + ) +} + +@Preview +@Composable +private fun PreviewOtpTextField() { + MifosOtpTextField( + onOtpTextCorrectlyEntered = {}, + realOtp = "1234", + otpCount = 4, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosPasswordField.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosPasswordField.kt new file mode 100644 index 0000000000..0d677ca622 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosPasswordField.kt @@ -0,0 +1,179 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.semantics.testTag +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.utils.nonLetterColorVisualTransformation +import com.mifos.core.designsystem.utils.tabNavigation +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +fun MifosPasswordField( + label: String, + value: String, + showPassword: Boolean, + showPasswordChange: (Boolean) -> Unit, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + readOnly: Boolean = false, + singleLine: Boolean = true, + hint: String? = null, + showPasswordTestTag: String? = null, + autoFocus: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Password, + imeAction: ImeAction = ImeAction.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + val focusRequester = remember { FocusRequester() } + MifosOutlinedTextField( + modifier = modifier + .tabNavigation() + .focusRequester(focusRequester), + label = label, + value = value, + onValueChange = onValueChange, + visualTransformation = when { + !showPassword -> PasswordVisualTransformation() + readOnly -> nonLetterColorVisualTransformation() + else -> VisualTransformation.None + }, + singleLine = singleLine, + readOnly = readOnly, + keyboardOptions = KeyboardOptions( + keyboardType = keyboardType, + imeAction = imeAction, + ), + keyboardActions = keyboardActions, + errorText = hint, + trailingIcon = { + IconButton( + onClick = { showPasswordChange.invoke(!showPassword) }, + ) { + val imageVector = if (showPassword) { + MifosIcons.OutlinedVisibilityOff + } else { + MifosIcons.OutlinedVisibility + } + + Icon( + modifier = Modifier.semantics { showPasswordTestTag?.let { testTag = it } }, + imageVector = imageVector, + contentDescription = "togglePassword", + ) + } + }, + ) + if (autoFocus) { + LaunchedEffect(Unit) { focusRequester.requestFocus() } + } +} + +@Composable +fun MifosPasswordField( + label: String, + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + readOnly: Boolean = false, + singleLine: Boolean = true, + hint: String? = null, + initialShowPassword: Boolean = false, + showPasswordTestTag: String? = null, + autoFocus: Boolean = false, + keyboardType: KeyboardType = KeyboardType.Password, + imeAction: ImeAction = ImeAction.Default, + keyboardActions: KeyboardActions = KeyboardActions.Default, +) { + var showPassword by rememberSaveable { mutableStateOf(initialShowPassword) } + MifosPasswordField( + modifier = modifier, + label = label, + value = value, + showPassword = showPassword, + showPasswordChange = { showPassword = !showPassword }, + onValueChange = onValueChange, + readOnly = readOnly, + singleLine = singleLine, + hint = hint, + showPasswordTestTag = showPasswordTestTag, + autoFocus = autoFocus, + keyboardType = keyboardType, + imeAction = imeAction, + keyboardActions = keyboardActions, + ) +} + +@Preview +@Composable +private fun MifosPasswordField_preview_withInput_hidePassword() { + MifosPasswordField( + label = "Label", + value = "Password", + onValueChange = {}, + initialShowPassword = false, + hint = "Hint", + ) +} + +@Preview +@Composable +private fun MifosPasswordField_preview_withInput_showPassword() { + MifosPasswordField( + label = "Label", + value = "Password", + onValueChange = {}, + initialShowPassword = true, + hint = "Hint", + ) +} + +@Preview +@Composable +private fun MifosPasswordField_preview_withoutInput_hidePassword() { + MifosPasswordField( + label = "Label", + value = "", + onValueChange = {}, + initialShowPassword = false, + hint = "Hint", + ) +} + +@Preview +@Composable +private fun MifosPasswordField_preview_withoutInput_showPassword() { + MifosPasswordField( + label = "Label", + value = "", + onValueChange = {}, + initialShowPassword = true, + hint = "Hint", + ) +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosProgressIndicator.kt similarity index 91% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosProgressIndicator.kt index 9b0d7a796c..8c828eecfe 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosProgressIndicator.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosProgressIndicator.kt @@ -19,6 +19,7 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment @@ -26,13 +27,8 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.platform.testTag import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp -import com.mifos.core.designsystem.theme.DarkGray - -/** - * Created by Aditya Gupta on 21/02/24. - */ +import org.jetbrains.compose.ui.tooling.preview.Preview @Composable fun MifosPagingAppendProgress(modifier: Modifier = Modifier) { @@ -52,7 +48,7 @@ fun MifosPagingAppendProgress(modifier: Modifier = Modifier) { } } -@Preview(showBackground = true) +@Preview @Composable fun MifosCircularProgress( modifier: Modifier = Modifier @@ -74,7 +70,7 @@ fun MifosCircularProgress( .height(60.dp) .padding(8.dp), strokeWidth = 4.dp, - color = DarkGray, + color = MaterialTheme.colorScheme.secondary, ) text?.let { Text(text = text) diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosScaffold.kt new file mode 100644 index 0000000000..8b376ee624 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosScaffold.kt @@ -0,0 +1,177 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.imePadding +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FabPosition +import androidx.compose.material3.FloatingActionButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Scaffold +import androidx.compose.material3.ScaffoldDefaults +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.pulltorefresh.PullToRefreshDefaults +import androidx.compose.material3.pulltorefresh.pullToRefresh +import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosScaffold( + backPress: () -> Unit, + modifier: Modifier = Modifier, + topBarTitle: String? = null, + floatingActionButtonContent: FloatingActionButtonContent? = null, + pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(), + snackbarHost: @Composable () -> Unit = {}, + actions: @Composable RowScope.() -> Unit = {}, + content: @Composable (PaddingValues) -> Unit = {}, +) { + Scaffold( + topBar = { + if (topBarTitle != null) { + MifosTopBar( + topBarTitle = topBarTitle, + backPress = backPress, + actions = actions, + ) + } + }, + floatingActionButton = { + floatingActionButtonContent?.let { content -> + FloatingActionButton( + onClick = content.onClick, + contentColor = content.contentColor, + content = content.content, + ) + } + }, + snackbarHost = snackbarHost, + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0f), + content = { paddingValues -> + val internalPullToRefreshState = rememberPullToRefreshState() + Box( + modifier = Modifier.pullToRefresh( + state = internalPullToRefreshState, + isRefreshing = pullToRefreshState.isRefreshing, + onRefresh = pullToRefreshState.onRefresh, + enabled = pullToRefreshState.isEnabled, + ), + ) { + content(paddingValues) + + PullToRefreshDefaults.Indicator( + modifier = Modifier + .padding(paddingValues) + .align(Alignment.TopCenter), + isRefreshing = pullToRefreshState.isRefreshing, + state = internalPullToRefreshState, + ) + } + }, + modifier = modifier + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosScaffold( + modifier: Modifier = Modifier, + topBar: @Composable () -> Unit = {}, + bottomBar: @Composable () -> Unit = {}, + floatingActionButton: @Composable () -> Unit = {}, + snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, + pullToRefreshState: MifosPullToRefreshState = rememberMifosPullToRefreshState(), + floatingActionButtonPosition: FabPosition = FabPosition.End, + containerColor: Color = MaterialTheme.colorScheme.surface.copy(alpha = 0f), + contentColor: Color = MaterialTheme.colorScheme.onSurface, + contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets, + content: @Composable (PaddingValues) -> Unit, +) { + Scaffold( + modifier = modifier + .fillMaxSize() + .navigationBarsPadding() + .imePadding(), + topBar = topBar, + bottomBar = bottomBar, + snackbarHost = { SnackbarHost(snackbarHostState) }, + floatingActionButton = { + Box(modifier = Modifier.navigationBarsPadding()) { + floatingActionButton() + } + }, + floatingActionButtonPosition = floatingActionButtonPosition, + containerColor = containerColor, + contentColor = contentColor, + contentWindowInsets = contentWindowInsets, + content = { paddingValues -> + val internalPullToRefreshState = rememberPullToRefreshState() + Box( + modifier = Modifier.pullToRefresh( + state = internalPullToRefreshState, + isRefreshing = pullToRefreshState.isRefreshing, + onRefresh = pullToRefreshState.onRefresh, + enabled = pullToRefreshState.isEnabled, + ), + ) { + content(paddingValues) + + PullToRefreshDefaults.Indicator( + modifier = Modifier + .padding(paddingValues) + .align(Alignment.TopCenter), + isRefreshing = pullToRefreshState.isRefreshing, + state = internalPullToRefreshState, + ) + } + }, + ) +} + +data class FloatingActionButtonContent( + val onClick: (() -> Unit), + val contentColor: Color, + val content: (@Composable () -> Unit), +) + +data class MifosPullToRefreshState( + val isEnabled: Boolean, + val isRefreshing: Boolean, + val onRefresh: () -> Unit, +) + +@Composable +fun rememberMifosPullToRefreshState( + isEnabled: Boolean = false, + isRefreshing: Boolean = false, + onRefresh: () -> Unit = { }, +): MifosPullToRefreshState = remember(isEnabled, isRefreshing, onRefresh) { + MifosPullToRefreshState( + isEnabled = isEnabled, + isRefreshing = isRefreshing, + onRefresh = onRefresh, + ) +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosSweetError.kt similarity index 61% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosSweetError.kt index c4566bc160..5af67fba3c 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosSweetError.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosSweetError.kt @@ -9,7 +9,6 @@ */ package com.mifos.core.designsystem.component -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues @@ -22,27 +21,23 @@ import androidx.compose.foundation.layout.size import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Info import androidx.compose.material3.Button -import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.res.stringResource import androidx.compose.ui.semantics.contentDescription import androidx.compose.ui.semantics.semantics -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import coil.compose.AsyncImage -import com.mifos.core.designsystem.R -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark -import com.mifos.core.designsystem.theme.DarkGray +import coil3.compose.AsyncImage +import core.designsystem.generated.resources.Res +import core.designsystem.generated.resources.core_designsystem_ic_error_black_24dp +import core.designsystem.generated.resources.core_designsystem_try_again +import core.designsystem.generated.resources.core_designsystem_unable_to_load +import org.jetbrains.compose.resources.stringResource @Composable fun MifosSweetError( @@ -52,7 +47,7 @@ fun MifosSweetError( .padding(18.dp) .semantics { contentDescription = "MifosSweetError" }, isRetryEnabled: Boolean = true, - buttonText: String = stringResource(id = R.string.core_designsystem_try_again), + buttonText: String = stringResource(Res.string.core_designsystem_try_again), onclick: () -> Unit = {}, ) { Column( @@ -62,42 +57,31 @@ fun MifosSweetError( ) { AsyncImage( modifier = Modifier.size(70.dp), - model = R.drawable.core_designsystem_ic_error_black_24dp, + model = Res.drawable.core_designsystem_ic_error_black_24dp, contentDescription = null, colorFilter = ColorFilter.tint(Color.Gray), ) Spacer(modifier = Modifier.height(20.dp)) Text( - text = stringResource(id = R.string.core_designsystem_unable_to_load), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), + text = stringResource(Res.string.core_designsystem_unable_to_load), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary, ) Text( text = message, - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary, ) if (isRetryEnabled) { Spacer(modifier = Modifier.height(20.dp)) Button( onClick = { onclick() }, contentPadding = PaddingValues(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), ) { Text( modifier = Modifier.padding(start = 20.dp, end = 20.dp), text = buttonText, - fontSize = 15.sp, + style = MaterialTheme.typography.labelLarge, ) } } @@ -119,29 +103,21 @@ fun MifosPaginationSweetError( Icon( imageVector = Icons.Default.Info, contentDescription = "Info Image", - tint = Color.Gray, ) Text( - text = stringResource(id = R.string.core_designsystem_unable_to_load), - style = TextStyle( - fontSize = 14.sp, - fontWeight = FontWeight.Normal, - fontStyle = FontStyle.Normal, - color = DarkGray, - ), + text = stringResource(Res.string.core_designsystem_unable_to_load), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.secondary, ) Button( onClick = { onclick() }, contentPadding = PaddingValues(), - colors = ButtonDefaults.buttonColors( - containerColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), ) { Text( modifier = Modifier .padding(start = 20.dp, end = 20.dp), - text = stringResource(id = R.string.core_designsystem_try_again), - fontSize = 15.sp, + text = stringResource(Res.string.core_designsystem_try_again), + style = MaterialTheme.typography.bodyLarge, ) } } diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTab.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTab.kt new file mode 100644 index 0000000000..8b46b28290 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTab.kt @@ -0,0 +1,47 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Tab +import androidx.compose.material3.Text +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +@Composable +fun MifosTab( + text: String, + selected: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, + selectedColor: Color = MaterialTheme.colorScheme.primary, + unselectedColor: Color = MaterialTheme.colorScheme.primaryContainer, +) { + Tab( + text = { + Text(text = text) + }, + selected = selected, + onClick = onClick, + selectedContentColor = contentColorFor(selectedColor), + unselectedContentColor = contentColorFor(unselectedColor), + modifier = modifier + .clip(RoundedCornerShape(25.dp)) + .background(if (selected) selectedColor else unselectedColor) + .padding(horizontal = 20.dp), + ) +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTabRow.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTabRow.kt similarity index 89% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTabRow.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTabRow.kt index 6b6e6916cd..60fdc9ef1a 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTabRow.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTabRow.kt @@ -16,6 +16,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.pager.HorizontalPager import androidx.compose.foundation.pager.PagerState +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.Text @@ -23,7 +24,6 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color -import com.mifos.core.designsystem.theme.BluePrimary import com.mifos.core.designsystem.utility.TabContent import kotlinx.coroutines.launch @@ -32,9 +32,9 @@ fun MifosTabRow( tabContents: List, pagerState: PagerState, modifier: Modifier = Modifier, - containerColor: Color = Color.White, - selectedContentColor: Color = BluePrimary, - unselectedContentColor: Color = Color.LightGray, + containerColor: Color = MaterialTheme.colorScheme.surface, + selectedContentColor: Color = MaterialTheme.colorScheme.primary, + unselectedContentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant, ) { val scope = rememberCoroutineScope() diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextField.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextField.kt new file mode 100644 index 0000000000..3914c3ec8b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextField.kt @@ -0,0 +1,206 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.animation.AnimatedContent +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.text.KeyboardActions +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LocalTextStyle +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.semantics.contentDescription +import androidx.compose.ui.semantics.semantics +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.VisualTransformation +import com.mifos.core.designsystem.icon.MifosIcons + +@Composable +fun MifosOutlinedTextField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + showClearIcon: Boolean = true, + readOnly: Boolean = false, + clearIcon: ImageVector = MifosIcons.Close, + isError: Boolean = false, + errorText: String? = null, + onClickClearIcon: () -> Unit = { onValueChange("") }, + textStyle: TextStyle = LocalTextStyle.current, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = true, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, +) { + val isFocused by interactionSource.collectIsFocusedAsState() + val showIcon by rememberUpdatedState(value.isNotEmpty()) + + OutlinedTextField( + value = value, + label = { Text(text = label) }, + onValueChange = onValueChange, + textStyle = textStyle, + modifier = modifier.fillMaxWidth(), + enabled = enabled, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + leadingIcon = leadingIcon, + isError = isError, + trailingIcon = @Composable { + AnimatedContent( + targetState = showClearIcon && isFocused && showIcon, + ) { + if (it) { + ClearIconButton( + showClearIcon = true, + clearIcon = clearIcon, + onClickClearIcon = onClickClearIcon, + ) + } else { + trailingIcon?.invoke() + } + } + }, + supportingText = errorText?.let { + { + Text( + modifier = Modifier.testTag("errorTag"), + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + ) + } + }, + ) +} + +@Composable +fun MifosTextField( + value: String, + onValueChange: (String) -> Unit, + label: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + showClearIcon: Boolean = true, + readOnly: Boolean = false, + clearIcon: ImageVector = MifosIcons.Close, + isError: Boolean = false, + errorText: String? = null, + onClickClearIcon: () -> Unit = { onValueChange("") }, + textStyle: TextStyle = LocalTextStyle.current, + visualTransformation: VisualTransformation = VisualTransformation.None, + keyboardActions: KeyboardActions = KeyboardActions.Default, + singleLine: Boolean = true, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + minLines: Int = 1, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + keyboardOptions: KeyboardOptions = KeyboardOptions(imeAction = ImeAction.Next), + trailingIcon: @Composable (() -> Unit)? = null, + leadingIcon: @Composable (() -> Unit)? = null, +) { + val isFocused by interactionSource.collectIsFocusedAsState() + val showIcon by rememberUpdatedState(value.isNotEmpty()) + + OutlinedTextField( + value = value, + label = { Text(text = label) }, + onValueChange = onValueChange, + textStyle = textStyle, + modifier = modifier.fillMaxWidth(), + enabled = enabled, + readOnly = readOnly, + visualTransformation = visualTransformation, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + interactionSource = interactionSource, + singleLine = singleLine, + maxLines = maxLines, + minLines = minLines, + leadingIcon = leadingIcon, + isError = isError, + trailingIcon = @Composable { + AnimatedContent( + targetState = showClearIcon && isFocused && showIcon, + ) { + if (it) { + ClearIconButton( + showClearIcon = true, + clearIcon = clearIcon, + onClickClearIcon = onClickClearIcon, + ) + } else { + trailingIcon?.invoke() + } + } + }, + supportingText = errorText?.let { + { + Text( + modifier = Modifier.testTag("errorTag"), + text = it, + style = MaterialTheme.typography.labelSmall, + color = MaterialTheme.colorScheme.error, + ) + } + }, + ) +} + +@Composable +private fun ClearIconButton( + showClearIcon: Boolean, + clearIcon: ImageVector, + onClickClearIcon: () -> Unit, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = showClearIcon, + modifier = modifier, + ) { + IconButton( + onClick = onClickClearIcon, + modifier = Modifier.semantics { + contentDescription = "clearIcon" + }, + ) { + Icon( + imageVector = clearIcon, + contentDescription = "trailingIcon", + ) + } + } +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt similarity index 81% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt index 3865a01cb6..4b6e416193 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTextFieldDropdown.kt @@ -11,7 +11,6 @@ package com.mifos.core.designsystem.component -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.text.KeyboardOptions @@ -20,7 +19,6 @@ import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExposedDropdownMenuBox import androidx.compose.material3.ExposedDropdownMenuDefaults import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -29,14 +27,12 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp -import com.mifos.core.designsystem.theme.BluePrimary -import com.mifos.core.designsystem.theme.BluePrimaryDark +@OptIn(ExperimentalMaterial3Api::class) @Composable fun MifosTextFieldDropdown( value: String, @@ -59,13 +55,13 @@ fun MifosTextFieldDropdown( OutlinedTextField( value = value, onValueChange = { onValueChanged(it) }, - label = { Text(text = labelString ?: label?.let { stringResource(id = label) } ?: "") }, + label = { + if (labelString != null) { + Text(text = labelString) + } + }, modifier = modifier.menuAnchor(), maxLines = 1, - colors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - focusedLabelColor = if (isSystemInDarkTheme()) BluePrimaryDark else BluePrimary, - ), textStyle = LocalDensity.current.run { TextStyle(fontSize = 18.sp) }, diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTopAppBar.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTopAppBar.kt new file mode 100644 index 0000000000..9a58dc59e5 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTopAppBar.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package org.mifos.core.designsystem.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.LargeTopAppBar +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MediumTopAppBar +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TopAppBarScrollBehavior +import androidx.compose.material3.rememberTopAppBarState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import com.mifos.core.designsystem.icon.MifosIcons +import com.mifos.core.designsystem.theme.MifosTheme +import com.mifos.core.designsystem.utils.mirrorIfRtl +import org.jetbrains.compose.ui.tooling.preview.Preview + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosTopAppBar( + title: String, + scrollBehavior: TopAppBarScrollBehavior, + navigationIcon: ImageVector, + navigationIconContentDescription: String, + onNavigationIconClick: () -> Unit, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = { }, +) { + MifosTopAppBar( + title = title, + scrollBehavior = scrollBehavior, + navigationIcon = NavigationIcon( + navigationIcon = navigationIcon, + navigationIconContentDescription = navigationIconContentDescription, + onNavigationIconClick = onNavigationIconClick, + ), + modifier = modifier, + actions = actions, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Suppress("LongMethod") +@Composable +fun MifosTopAppBar( + title: String, + scrollBehavior: TopAppBarScrollBehavior, + navigationIcon: NavigationIcon?, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = {}, +) { + var titleTextHasOverflow by remember { + mutableStateOf(false) + } + + val navigationIconContent: @Composable () -> Unit = remember(navigationIcon) { + { + navigationIcon?.let { + IconButton( + onClick = it.onNavigationIconClick, + modifier = Modifier.testTag("CloseButton"), + ) { + Icon( + modifier = Modifier.mirrorIfRtl(), + imageVector = it.navigationIcon, + contentDescription = it.navigationIconContentDescription, + ) + } + } + } + } + + val topAppBarColors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onSurface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + if (titleTextHasOverflow) { + MediumTopAppBar( + colors = topAppBarColors, + scrollBehavior = scrollBehavior, + navigationIcon = navigationIconContent, + title = { + // The height of the component is controlled and will only allow for 1 extra row, + // making adding any arguments for softWrap and minLines superfluous. + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag("PageTitleLabel"), + ) + }, + modifier = modifier.testTag("HeaderBarComponent"), + actions = actions, + ) + } else { + TopAppBar( + colors = topAppBarColors, + scrollBehavior = scrollBehavior, + navigationIcon = navigationIconContent, + title = { + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + maxLines = 1, + softWrap = false, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag("PageTitleLabel"), + onTextLayout = { + titleTextHasOverflow = it.hasVisualOverflow + }, + ) + }, + modifier = modifier.testTag("HeaderBarComponent"), + actions = actions, + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Suppress("LongMethod") +@Composable +fun MifosTopAppBar( + title: String, + subtitle: String, + scrollBehavior: TopAppBarScrollBehavior, + navigationIcon: ImageVector, + navigationIconContentDescription: String, + onNavigationIconClick: () -> Unit, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = {}, +) { + val navigationIconContent: @Composable () -> Unit = remember(navigationIcon) { + { + IconButton( + onClick = onNavigationIconClick, + modifier = Modifier.testTag("CloseButton"), + ) { + Icon( + modifier = Modifier.mirrorIfRtl(), + imageVector = navigationIcon, + contentDescription = navigationIconContentDescription, + ) + } + } + } + + val topAppBarColors = TopAppBarDefaults.largeTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceContainer, + navigationIconContentColor = MaterialTheme.colorScheme.onSurface, + titleContentColor = MaterialTheme.colorScheme.onSurface, + actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + LargeTopAppBar( + colors = topAppBarColors, + scrollBehavior = scrollBehavior, + navigationIcon = navigationIconContent, + title = { + Column( + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + // The height of the component is controlled and will only allow for 1 extra row, + // making adding any arguments for softWrap and minLines superfluous. + Text( + text = title, + style = MaterialTheme.typography.titleLarge, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.testTag("PageTitleLabel"), + ) + + Text( + text = subtitle, + style = MaterialTheme.typography.bodyMedium, + modifier = Modifier.testTag("PageTitleSubTitle"), + ) + } + }, + modifier = modifier.testTag("HeaderBarComponent"), + actions = actions, + ) +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun MifosTopAppBar_preview() { + MifosTheme { + MifosTopAppBar( + title = "Title", + scrollBehavior = TopAppBarDefaults + .exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + ), + navigationIcon = NavigationIcon( + navigationIcon = MifosIcons.Back, + navigationIconContentDescription = "Back", + onNavigationIconClick = { }, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun MifosTopAppBarOverflow_preview() { + MifosTheme { + MifosTopAppBar( + title = "Title that is too long for the top line", + scrollBehavior = TopAppBarDefaults + .exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + ), + navigationIcon = NavigationIcon( + navigationIcon = MifosIcons.Close, + navigationIconContentDescription = "Close", + onNavigationIconClick = { }, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun MifosTopAppBarOverflowCutoff_preview() { + MifosTheme { + MifosTopAppBar( + title = "Title that is too long for the top line and the bottom line", + scrollBehavior = TopAppBarDefaults + .exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + ), + navigationIcon = NavigationIcon( + navigationIcon = MifosIcons.Close, + navigationIconContentDescription = "Close", + onNavigationIconClick = { }, + ), + ) + } +} + +data class NavigationIcon( + val navigationIcon: ImageVector, + val navigationIconContentDescription: String, + val onNavigationIconClick: () -> Unit, +) diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTopBar.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTopBar.kt new file mode 100644 index 0000000000..66f50881eb --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/MifosTopBar.kt @@ -0,0 +1,55 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import com.mifos.core.designsystem.icon.MifosIcons + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun MifosTopBar( + topBarTitle: String, + backPress: () -> Unit, + modifier: Modifier = Modifier, + actions: @Composable RowScope.() -> Unit = {}, +) { + CenterAlignedTopAppBar( + title = { + Text( + text = topBarTitle, + style = MaterialTheme.typography.titleMedium, + ) + }, + navigationIcon = { + IconButton( + onClick = backPress, + ) { + Icon( + imageVector = MifosIcons.ArrowBack2, + contentDescription = "Back", + ) + } + }, + colors = TopAppBarDefaults.centerAlignedTopAppBarColors( + containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0f), + ), + actions = actions, + modifier = modifier, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/AppScrollbars.kt new file mode 100644 index 0000000000..e6efc9b3fa --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/AppScrollbars.kt @@ -0,0 +1,248 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.component.scrollbar + +import androidx.compose.animation.animateColorAsState +import androidx.compose.animation.core.Spring +import androidx.compose.animation.core.SpringSpec +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.Orientation.Horizontal +import androidx.compose.foundation.gestures.Orientation.Vertical +import androidx.compose.foundation.gestures.ScrollableState +import androidx.compose.foundation.interaction.InteractionSource +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsDraggedAsState +import androidx.compose.foundation.interaction.collectIsHoveredAsState +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.State +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.ColorProducer +import androidx.compose.ui.graphics.Outline +import androidx.compose.ui.graphics.drawOutline +import androidx.compose.ui.graphics.drawscope.ContentDrawScope +import androidx.compose.ui.node.DrawModifierNode +import androidx.compose.ui.node.ModifierNodeElement +import androidx.compose.ui.node.invalidateDraw +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import com.mifos.core.designsystem.component.scrollbar.ThumbState.Active +import com.mifos.core.designsystem.component.scrollbar.ThumbState.Dormant +import com.mifos.core.designsystem.component.scrollbar.ThumbState.Inactive +import kotlinx.coroutines.delay +import org.mifos.core.designsystem.component.scrollbar.Scrollbar +import org.mifos.core.designsystem.component.scrollbar.ScrollbarState + +/** + * The time period for showing the scrollbar thumb after interacting with it, before it fades away + */ +private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L + +/** + * A [Scrollbar] that allows for fast scrolling of content by dragging its thumb. + * Its thumb disappears when the scrolling container is dormant. + * @param modifier a [Modifier] for the [Scrollbar] + * @param state the driving state for the [Scrollbar] + * @param orientation the orientation of the scrollbar + * @param onThumbMoved the fast scroll implementation + */ +@Composable +fun ScrollableState.DraggableScrollbar( + state: ScrollbarState, + orientation: Orientation, + onThumbMoved: (Float) -> Unit, + modifier: Modifier = Modifier, +) { + val interactionSource = remember { MutableInteractionSource() } + Scrollbar( + modifier = modifier, + orientation = orientation, + interactionSource = interactionSource, + state = state, + thumb = { + DraggableScrollbarThumb( + interactionSource = interactionSource, + orientation = orientation, + ) + }, + onThumbMoved = onThumbMoved, + ) +} + +/** + * A simple [Scrollbar]. + * Its thumb disappears when the scrolling container is dormant. + * @param modifier a [Modifier] for the [Scrollbar] + * @param state the driving state for the [Scrollbar] + * @param orientation the orientation of the scrollbar + */ +@Composable +fun ScrollableState.DecorativeScrollbar( + state: ScrollbarState, + orientation: Orientation, + modifier: Modifier = Modifier, +) { + val interactionSource = remember { MutableInteractionSource() } + Scrollbar( + modifier = modifier, + orientation = orientation, + interactionSource = interactionSource, + state = state, + thumb = { + DecorativeScrollbarThumb( + interactionSource = interactionSource, + orientation = orientation, + ) + }, + ) +} + +/** + * A scrollbar thumb that is intended to also be a touch target for fast scrolling. + */ +@Composable +private fun ScrollableState.DraggableScrollbarThumb( + interactionSource: InteractionSource, + orientation: Orientation, +) { + Box( + modifier = Modifier + .run { + when (orientation) { + Vertical -> width(12.dp).fillMaxHeight() + Horizontal -> height(12.dp).fillMaxWidth() + } + } + .scrollThumb(this, interactionSource), + ) +} + +/** + * A decorative scrollbar thumb used solely for communicating a user's position in a list. + */ +@Composable +private fun ScrollableState.DecorativeScrollbarThumb( + interactionSource: InteractionSource, + orientation: Orientation, +) { + Box( + modifier = Modifier + .run { + when (orientation) { + Vertical -> width(2.dp).fillMaxHeight() + Horizontal -> height(2.dp).fillMaxWidth() + } + } + .scrollThumb(this, interactionSource), + ) +} + +@Composable +private fun Modifier.scrollThumb( + scrollableState: ScrollableState, + interactionSource: InteractionSource, +): Modifier { + val colorState = scrollbarThumbColor(scrollableState, interactionSource) + return this then ScrollThumbElement { colorState.value } +} + +private data class ScrollThumbElement(val colorProducer: ColorProducer) : + ModifierNodeElement() { + override fun create(): ScrollThumbNode = ScrollThumbNode(colorProducer) + override fun update(node: ScrollThumbNode) { + node.colorProducer = colorProducer + node.invalidateDraw() + } +} + +private class ScrollThumbNode(var colorProducer: ColorProducer) : DrawModifierNode, Modifier.Node() { + private val shape = RoundedCornerShape(16.dp) + + // naive cache outline calculation if size is the same + private var lastSize: Size? = null + private var lastLayoutDirection: LayoutDirection? = null + private var lastOutline: Outline? = null + + override fun ContentDrawScope.draw() { + val color = colorProducer() + val outline = + if (size == lastSize && layoutDirection == lastLayoutDirection) { + lastOutline!! + } else { + shape.createOutline(size, layoutDirection, this) + } + if (color != Color.Unspecified) drawOutline(outline, color = color) + + lastOutline = outline + lastSize = size + lastLayoutDirection = layoutDirection + } +} + +/** + * The color of the scrollbar thumb as a function of its interaction state. + * @param interactionSource source of interactions in the scrolling container + */ +@Composable +private fun scrollbarThumbColor( + scrollableState: ScrollableState, + interactionSource: InteractionSource, +): State { + var state by remember { mutableStateOf(Dormant) } + val pressed by interactionSource.collectIsPressedAsState() + val hovered by interactionSource.collectIsHoveredAsState() + val dragged by interactionSource.collectIsDraggedAsState() + val active = (scrollableState.canScrollForward || scrollableState.canScrollBackward) && + (pressed || hovered || dragged || scrollableState.isScrollInProgress) + + val color = animateColorAsState( + targetValue = when (state) { + Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f) + Inactive -> MaterialTheme.colorScheme.onSurface.copy(alpha = 0.2f) + Dormant -> Color.Transparent + }, + animationSpec = SpringSpec( + stiffness = Spring.StiffnessLow, + ), + label = "Scrollbar thumb color", + ) + LaunchedEffect(active) { + when (active) { + true -> state = Active + false -> if (state == Active) { + state = Inactive + delay(SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS) + state = Dormant + } + } + } + + return color +} + +private enum class ThumbState { + Active, + Inactive, + Dormant, +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt new file mode 100644 index 0000000000..f3ca7b2606 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package org.mifos.core.designsystem.component.scrollbar + +import androidx.compose.foundation.gestures.ScrollableState +import kotlin.math.abs + +/** + * Linearly interpolates the index for the first item in [visibleItems] for smooth scrollbar + * progression. + * @param visibleItems a list of items currently visible in the layout. + * @param itemSize a lookup function for the size of an item in the layout. + * @param offset a lookup function for the offset of an item relative to the start of the view port. + * @param nextItemOnMainAxis a lookup function for the next item on the main axis in the direction + * of the scroll. + * @param itemIndex a lookup function for index of an item in the layout relative to + * the total amount of items available. + * + * @return a [Float] in the range [firstItemPosition..nextItemPosition) where nextItemPosition + * is the index of the consecutive item along the major axis. + * */ +internal inline fun LazyState.interpolateFirstItemIndex( + visibleItems: List, + crossinline itemSize: LazyState.(LazyStateItem) -> Int, + crossinline offset: LazyState.(LazyStateItem) -> Int, + crossinline nextItemOnMainAxis: LazyState.(LazyStateItem) -> LazyStateItem?, + crossinline itemIndex: (LazyStateItem) -> Int, +): Float { + if (visibleItems.isEmpty()) return 0f + + val firstItem = visibleItems.first() + val firstItemIndex = itemIndex(firstItem) + + if (firstItemIndex < 0) return Float.NaN + + val firstItemSize = itemSize(firstItem) + if (firstItemSize == 0) return Float.NaN + + val itemOffset = offset(firstItem).toFloat() + val offsetPercentage = abs(itemOffset) / firstItemSize + + val nextItem = nextItemOnMainAxis(firstItem) ?: return firstItemIndex + offsetPercentage + + val nextItemIndex = itemIndex(nextItem) + + return firstItemIndex + ((nextItemIndex - firstItemIndex) * offsetPercentage) +} + +/** + * Returns the percentage of an item that is currently visible in the view port. + * @param itemSize the size of the item + * @param itemStartOffset the start offset of the item relative to the view port start + * @param viewportStartOffset the start offset of the view port + * @param viewportEndOffset the end offset of the view port + */ +internal fun itemVisibilityPercentage( + itemSize: Int, + itemStartOffset: Int, + viewportStartOffset: Int, + viewportEndOffset: Int, +): Float { + if (itemSize == 0) return 0f + val itemEnd = itemStartOffset + itemSize + val startOffset = when { + itemStartOffset > viewportStartOffset -> 0 + else -> abs(abs(viewportStartOffset) - abs(itemStartOffset)) + } + val endOffset = when { + itemEnd < viewportEndOffset -> 0 + else -> abs(abs(itemEnd) - abs(viewportEndOffset)) + } + val size = itemSize.toFloat() + return (size - startOffset - endOffset) / size +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/Scrollbar.kt new file mode 100644 index 0000000000..fac126822b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/Scrollbar.kt @@ -0,0 +1,412 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package org.mifos.core.designsystem.component.scrollbar + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.Orientation.Horizontal +import androidx.compose.foundation.gestures.Orientation.Vertical +import androidx.compose.foundation.gestures.detectHorizontalDragGestures +import androidx.compose.foundation.gestures.detectTapGestures +import androidx.compose.foundation.gestures.detectVerticalDragGestures +import androidx.compose.foundation.hoverable +import androidx.compose.foundation.interaction.DragInteraction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.PressInteraction +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshotFlow +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.pointer.PointerInputChange +import androidx.compose.ui.input.pointer.pointerInput +import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.onGloballyPositioned +import androidx.compose.ui.layout.positionInRoot +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import androidx.compose.ui.util.packFloats +import androidx.compose.ui.util.unpackFloat1 +import androidx.compose.ui.util.unpackFloat2 +import kotlinx.coroutines.TimeoutCancellationException +import kotlinx.coroutines.delay +import kotlinx.coroutines.withTimeout +import kotlin.jvm.JvmInline +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll + * instead of dragging the scrollbar thumb. + */ +private const val SCROLLBAR_PRESS_DELAY_MS = 10L + +/** + * The percentage displacement of the scrollbar when scrolled by long presses on the scrollbar + * track. + */ +private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f + +class ScrollbarState { + private var packedValue by mutableLongStateOf(0L) + + internal fun onScroll(stateValue: ScrollbarStateValue) { + packedValue = stateValue.packedValue + } + + /** + * Returns the thumb size of the scrollbar as a percentage of the total track size + */ + val thumbSizePercent + get() = unpackFloat1(packedValue) + + /** + * Returns the distance the thumb has traveled as a percentage of total track size + */ + val thumbMovedPercent + get() = unpackFloat2(packedValue) + + /** + * Returns the max distance the thumb can travel as a percentage of total track size + */ + val thumbTrackSizePercent + get() = 1f - thumbSizePercent +} + +/** + * Returns the size of the scrollbar track in pixels + */ +private val ScrollbarTrack.size + get() = unpackFloat2(packedValue) - unpackFloat1(packedValue) + +/** + * Returns the position of the scrollbar thumb on the track as a percentage + */ +private fun ScrollbarTrack.thumbPosition( + dimension: Float, +): Float = max( + a = min( + a = dimension / size, + b = 1f, + ), + b = 0f, +) + +/** + * Class definition for the core properties of a scroll bar + */ +@Immutable +@JvmInline +value class ScrollbarStateValue internal constructor( + internal val packedValue: Long, +) + +/** + * Class definition for the core properties of a scroll bar track + */ +@Immutable +@JvmInline +private value class ScrollbarTrack( + val packedValue: Long, +) { + constructor( + max: Float, + min: Float, + ) : this(packFloats(max, min)) +} + +/** + * Creates a [ScrollbarStateValue] with the listed properties + * @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size. + * Refers to either the thumb width (for horizontal scrollbars) + * or height (for vertical scrollbars). + * @param thumbMovedPercent the distance the thumb has traveled as a percentage of total + * track size. + */ +fun scrollbarStateValue( + thumbSizePercent: Float, + thumbMovedPercent: Float, +) = ScrollbarStateValue( + packFloats( + val1 = thumbSizePercent, + val2 = thumbMovedPercent, + ), +) + +/** + * Returns the value of [offset] along the axis specified by [this] + */ +internal fun Orientation.valueOf(offset: Offset) = when (this) { + Orientation.Horizontal -> offset.x + Orientation.Vertical -> offset.y +} + +/** + * Returns the value of [intSize] along the axis specified by [this] + */ +internal fun Orientation.valueOf(intSize: IntSize) = when (this) { + Orientation.Horizontal -> intSize.width + Orientation.Vertical -> intSize.height +} + +/** + * Returns the value of [intOffset] along the axis specified by [this] + */ +internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) { + Orientation.Horizontal -> intOffset.x + Orientation.Vertical -> intOffset.y +} + +/** + * A Composable for drawing a scrollbar + * @param orientation the scroll direction of the scrollbar + * @param state the state describing the position of the scrollbar + * @param minThumbSize the minimum size of the scrollbar thumb + * @param interactionSource allows for observing the state of the scroll bar + * @param thumb a composable for drawing the scrollbar thumb + * @param onThumbMoved an function for reacting to scroll bar displacements caused by direct + * interactions on the scrollbar thumb by the user, for example implementing a fast scroll + */ +@Composable +fun Scrollbar( + orientation: Orientation, + state: ScrollbarState, + modifier: Modifier = Modifier, + interactionSource: MutableInteractionSource? = null, + minThumbSize: Dp = 40.dp, + onThumbMoved: ((Float) -> Unit)? = null, + thumb: @Composable () -> Unit, +) { + // Using Offset.Unspecified and Float.NaN instead of null + // to prevent unnecessary boxing of primitives + var pressedOffset by remember { mutableStateOf(Offset.Unspecified) } + var draggedOffset by remember { mutableStateOf(Offset.Unspecified) } + + // Used to immediately show drag feedback in the UI while the scrolling implementation + // catches up + var interactionThumbTravelPercent by remember { mutableFloatStateOf(Float.NaN) } + + var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) } + + // scrollbar track container + Box( + modifier = modifier + .run { + val withHover = interactionSource?.let(::hoverable) ?: this + when (orientation) { + Orientation.Vertical -> withHover.fillMaxHeight() + Orientation.Horizontal -> withHover.fillMaxWidth() + } + } + .onGloballyPositioned { coordinates -> + val scrollbarStartCoordinate = orientation.valueOf(coordinates.positionInRoot()) + track = ScrollbarTrack( + max = scrollbarStartCoordinate, + min = scrollbarStartCoordinate + orientation.valueOf(coordinates.size), + ) + } + // Process scrollbar presses + .pointerInput(Unit) { + detectTapGestures( + onPress = { offset -> + try { + // Wait for a long press before scrolling + withTimeout(viewConfiguration.longPressTimeoutMillis) { + tryAwaitRelease() + } + } catch (e: TimeoutCancellationException) { + // Start the press triggered scroll + val initialPress = PressInteraction.Press(offset) + interactionSource?.tryEmit(initialPress) + + pressedOffset = offset + interactionSource?.tryEmit( + when { + tryAwaitRelease() -> PressInteraction.Release(initialPress) + else -> PressInteraction.Cancel(initialPress) + }, + ) + + // End the press + pressedOffset = Offset.Unspecified + } + }, + ) + } + // Process scrollbar drags + .pointerInput(Unit) { + var dragInteraction: DragInteraction.Start? = null + val onDragStart: (Offset) -> Unit = { offset -> + val start = DragInteraction.Start() + dragInteraction = start + interactionSource?.tryEmit(start) + draggedOffset = offset + } + val onDragEnd: () -> Unit = { + dragInteraction?.let { interactionSource?.tryEmit(DragInteraction.Stop(it)) } + draggedOffset = Offset.Unspecified + } + val onDragCancel: () -> Unit = { + dragInteraction?.let { interactionSource?.tryEmit(DragInteraction.Cancel(it)) } + draggedOffset = Offset.Unspecified + } + val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit = + onDrag@{ _, delta -> + if (draggedOffset == Offset.Unspecified) return@onDrag + draggedOffset = when (orientation) { + Orientation.Vertical -> draggedOffset.copy( + y = draggedOffset.y + delta, + ) + + Orientation.Horizontal -> draggedOffset.copy( + x = draggedOffset.x + delta, + ) + } + } + + when (orientation) { + Orientation.Horizontal -> detectHorizontalDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onHorizontalDrag = onDrag, + ) + + Orientation.Vertical -> detectVerticalDragGestures( + onDragStart = onDragStart, + onDragEnd = onDragEnd, + onDragCancel = onDragCancel, + onVerticalDrag = onDrag, + ) + } + }, + ) { + // scrollbar thumb container + Layout(content = { thumb() }) { measurables, constraints -> + val measurable = measurables.first() + + val thumbSizePx = max( + a = state.thumbSizePercent * track.size, + b = minThumbSize.toPx(), + ) + + val trackSizePx = when (state.thumbTrackSizePercent) { + 0f -> track.size + else -> (track.size - thumbSizePx) / state.thumbTrackSizePercent + } + + val thumbTravelPercent = max( + a = min( + a = when { + interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent + else -> interactionThumbTravelPercent + }, + b = state.thumbTrackSizePercent, + ), + b = 0f, + ) + + val thumbMovedPx = trackSizePx * thumbTravelPercent + + val y = when (orientation) { + Horizontal -> 0 + Vertical -> thumbMovedPx.roundToInt() + } + val x = when (orientation) { + Horizontal -> thumbMovedPx.roundToInt() + Vertical -> 0 + } + + val updatedConstraints = when (orientation) { + Horizontal -> { + constraints.copy( + minWidth = thumbSizePx.roundToInt(), + maxWidth = thumbSizePx.roundToInt(), + ) + } + Vertical -> { + constraints.copy( + minHeight = thumbSizePx.roundToInt(), + maxHeight = thumbSizePx.roundToInt(), + ) + } + } + + val placeable = measurable.measure(updatedConstraints) + layout(placeable.width, placeable.height) { + placeable.place(x, y) + } + } + } + + if (onThumbMoved == null) return + + // Process presses + LaunchedEffect(Unit) { + snapshotFlow { pressedOffset }.collect { pressedOffset -> + // Press ended, reset interactionThumbTravelPercent + if (pressedOffset == Offset.Unspecified) { + interactionThumbTravelPercent = Float.NaN + return@collect + } + + var currentThumbMovedPercent = state.thumbMovedPercent + val destinationThumbMovedPercent = track.thumbPosition( + dimension = orientation.valueOf(pressedOffset), + ) + val isPositive = currentThumbMovedPercent < destinationThumbMovedPercent + val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f + + while (currentThumbMovedPercent != destinationThumbMovedPercent) { + currentThumbMovedPercent = when { + isPositive -> min( + a = currentThumbMovedPercent + delta, + b = destinationThumbMovedPercent, + ) + + else -> max( + a = currentThumbMovedPercent + delta, + b = destinationThumbMovedPercent, + ) + } + onThumbMoved(currentThumbMovedPercent) + interactionThumbTravelPercent = currentThumbMovedPercent + delay(SCROLLBAR_PRESS_DELAY_MS) + } + } + } + + // Process drags + LaunchedEffect(Unit) { + snapshotFlow { draggedOffset }.collect { draggedOffset -> + if (draggedOffset == Offset.Unspecified) { + interactionThumbTravelPercent = Float.NaN + return@collect + } + val currentTravel = track.thumbPosition( + dimension = orientation.valueOf(draggedOffset), + ) + onThumbMoved(currentTravel) + interactionThumbTravelPercent = currentTravel + } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/ScrollbarExt.kt new file mode 100644 index 0000000000..f1ff9ee383 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/ScrollbarExt.kt @@ -0,0 +1,227 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package org.mifos.core.designsystem.component.scrollbar + +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.lazy.LazyListItemInfo +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridItemInfo +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlin.math.min + +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyListState]. + * + * @param itemsAvailable the total amount of items available to scroll in the lazy list. + * @param itemIndex a lookup function for index of an item in the list relative to [itemsAvailable]. + */ +@Composable +fun LazyListState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index, +): ScrollbarState { + val state = remember { ScrollbarState() } + LaunchedEffect(this, itemsAvailable) { + snapshotFlow { + if (itemsAvailable == 0) return@snapshotFlow null + + val visibleItemsInfo = layoutInfo.visibleItemsInfo + if (visibleItemsInfo.isEmpty()) return@snapshotFlow null + + val firstIndex = min( + a = interpolateFirstItemIndex( + visibleItems = visibleItemsInfo, + itemSize = { it.size }, + offset = { it.offset }, + nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } }, + itemIndex = itemIndex, + ), + b = itemsAvailable.toFloat(), + ) + if (firstIndex.isNaN()) return@snapshotFlow null + + val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> + itemVisibilityPercentage( + itemSize = itemInfo.size, + itemStartOffset = itemInfo.offset, + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + } + + val thumbTravelPercent = min( + a = firstIndex / itemsAvailable, + b = 1f, + ) + val thumbSizePercent = min( + a = itemsVisible / itemsAvailable, + b = 1f, + ) + scrollbarStateValue( + thumbSizePercent = thumbSizePercent, + thumbMovedPercent = when { + layoutInfo.reverseLayout -> 1f - thumbTravelPercent + else -> thumbTravelPercent + }, + ) + } + .filterNotNull() + .distinctUntilChanged() + .collect { state.onScroll(it) } + } + return state +} + +/** + * Calculates a [ScrollbarState] driven by the changes in a [LazyGridState] + * + * @param itemsAvailable the total amount of items available to scroll in the grid. + * @param itemIndex a lookup function for index of an item in the grid relative to [itemsAvailable]. + */ +@Composable +fun LazyGridState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index, +): ScrollbarState { + val state = remember { ScrollbarState() } + LaunchedEffect(this, itemsAvailable) { + snapshotFlow { + if (itemsAvailable == 0) return@snapshotFlow null + + val visibleItemsInfo = layoutInfo.visibleItemsInfo + if (visibleItemsInfo.isEmpty()) return@snapshotFlow null + + val firstIndex = min( + a = interpolateFirstItemIndex( + visibleItems = visibleItemsInfo, + itemSize = { layoutInfo.orientation.valueOf(it.size) }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + when (layoutInfo.orientation) { + Orientation.Vertical -> visibleItemsInfo.find { + it != first && it.row != first.row + } + + Orientation.Horizontal -> visibleItemsInfo.find { + it != first && it.column != first.column + } + } + }, + itemIndex = itemIndex, + ), + b = itemsAvailable.toFloat(), + ) + if (firstIndex.isNaN()) return@snapshotFlow null + + val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> + itemVisibilityPercentage( + itemSize = layoutInfo.orientation.valueOf(itemInfo.size), + itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + } + + val thumbTravelPercent = min( + a = firstIndex / itemsAvailable, + b = 1f, + ) + val thumbSizePercent = min( + a = itemsVisible / itemsAvailable, + b = 1f, + ) + scrollbarStateValue( + thumbSizePercent = thumbSizePercent, + thumbMovedPercent = when { + layoutInfo.reverseLayout -> 1f - thumbTravelPercent + else -> thumbTravelPercent + }, + ) + } + .filterNotNull() + .distinctUntilChanged() + .collect { state.onScroll(it) } + } + return state +} + +/** + * Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState] + * + * @param itemsAvailable the total amount of items available to scroll in the staggered grid. + * @param itemIndex a lookup function for index of an item in the staggered grid relative + * to [itemsAvailable]. + */ +@Composable +fun LazyStaggeredGridState.scrollbarState( + itemsAvailable: Int, + itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index, +): ScrollbarState { + val state = remember { ScrollbarState() } + LaunchedEffect(this, itemsAvailable) { + snapshotFlow { + if (itemsAvailable == 0) return@snapshotFlow null + + val visibleItemsInfo = layoutInfo.visibleItemsInfo + if (visibleItemsInfo.isEmpty()) return@snapshotFlow null + + val firstIndex = min( + a = interpolateFirstItemIndex( + visibleItems = visibleItemsInfo, + itemSize = { layoutInfo.orientation.valueOf(it.size) }, + offset = { layoutInfo.orientation.valueOf(it.offset) }, + nextItemOnMainAxis = { first -> + visibleItemsInfo.find { it != first && it.lane == first.lane } + }, + itemIndex = itemIndex, + ), + b = itemsAvailable.toFloat(), + ) + if (firstIndex.isNaN()) return@snapshotFlow null + + val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo -> + itemVisibilityPercentage( + itemSize = layoutInfo.orientation.valueOf(itemInfo.size), + itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset), + viewportStartOffset = layoutInfo.viewportStartOffset, + viewportEndOffset = layoutInfo.viewportEndOffset, + ) + } + + val thumbTravelPercent = min( + a = firstIndex / itemsAvailable, + b = 1f, + ) + val thumbSizePercent = min( + a = itemsVisible / itemsAvailable, + b = 1f, + ) + scrollbarStateValue( + thumbSizePercent = thumbSizePercent, + thumbMovedPercent = thumbTravelPercent, + ) + } + .filterNotNull() + .distinctUntilChanged() + .collect { state.onScroll(it) } + } + return state +} + +private inline fun List.floatSumOf(selector: (T) -> Float): Float = + fold(initial = 0f) { accumulator, listItem -> accumulator + selector(listItem) } diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/ThumbExt.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/ThumbExt.kt new file mode 100644 index 0000000000..2a5e40a7be --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/component/scrollbar/ThumbExt.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package org.mifos.core.designsystem.component.scrollbar + +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.grid.LazyGridState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberUpdatedState +import androidx.compose.runtime.setValue +import kotlin.math.roundToInt + +/** + * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyListState] + * @param itemsAvailable the amount of items in the list. + */ +@Composable +fun LazyListState.rememberDraggableScroller( + itemsAvailable: Int, +): (Float) -> Unit = rememberDraggableScroller( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem, +) + +/** + * Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyGridState] + * @param itemsAvailable the amount of items in the grid. + */ +@Composable +fun LazyGridState.rememberDraggableScroller( + itemsAvailable: Int, +): (Float) -> Unit = rememberDraggableScroller( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem, +) + +/** + * Remembers a function to react to [Scrollbar] thumb position displacements for a + * [LazyStaggeredGridState] + * @param itemsAvailable the amount of items in the staggered grid. + */ +@Composable +fun LazyStaggeredGridState.rememberDraggableScroller( + itemsAvailable: Int, +): (Float) -> Unit = rememberDraggableScroller( + itemsAvailable = itemsAvailable, + scroll = ::scrollToItem, +) + +/** + * Generic function to react to [Scrollbar] thumb displacements in a lazy layout. + * @param itemsAvailable the total amount of items available to scroll in the layout. + * @param scroll a function to be invoked when an index has been identified to scroll to. + */ +@Composable +private inline fun rememberDraggableScroller( + itemsAvailable: Int, + crossinline scroll: suspend (index: Int) -> Unit, +): (Float) -> Unit { + var percentage by remember { mutableFloatStateOf(Float.NaN) } + val itemCount by rememberUpdatedState(itemsAvailable) + + LaunchedEffect(percentage) { + if (percentage.isNaN()) return@LaunchedEffect + val indexToFind = (itemCount * percentage).roundToInt() + scroll(indexToFind) + } + return remember { + { newPercentage -> percentage = newPercentage } + } +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt new file mode 100644 index 0000000000..bcf01607c2 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/icon/MifosIcons.kt @@ -0,0 +1,154 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.icon + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.Assignment +import androidx.compose.material.icons.automirrored.filled.CompareArrows +import androidx.compose.material.icons.automirrored.filled.Help +import androidx.compose.material.icons.automirrored.filled.Label +import androidx.compose.material.icons.automirrored.filled.Logout +import androidx.compose.material.icons.automirrored.outlined.ArrowBack +import androidx.compose.material.icons.filled.AccountBalance +import androidx.compose.material.icons.filled.AccountBalanceWallet +import androidx.compose.material.icons.filled.Add +import androidx.compose.material.icons.filled.ArrowDropDown +import androidx.compose.material.icons.filled.ArrowDropUp +import androidx.compose.material.icons.filled.AssignmentTurnedIn +import androidx.compose.material.icons.filled.ChevronLeft +import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.CloudDownload +import androidx.compose.material.icons.filled.Edit +import androidx.compose.material.icons.filled.Error +import androidx.compose.material.icons.filled.FilterList +import androidx.compose.material.icons.filled.FlashOff +import androidx.compose.material.icons.filled.FlashOn +import androidx.compose.material.icons.filled.Image +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.LocationOn +import androidx.compose.material.icons.filled.Menu +import androidx.compose.material.icons.filled.MoreVert +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Paid +import androidx.compose.material.icons.filled.People +import androidx.compose.material.icons.filled.Phone +import androidx.compose.material.icons.filled.RealEstateAgent +import androidx.compose.material.icons.filled.Search +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Share +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material.icons.filled.WifiOff +import androidx.compose.material.icons.outlined.AccountCircle +import androidx.compose.material.icons.outlined.DateRange +import androidx.compose.material.icons.outlined.EventRepeat +import androidx.compose.material.icons.outlined.Group +import androidx.compose.material.icons.outlined.Home +import androidx.compose.material.icons.outlined.Mail +import androidx.compose.material.icons.outlined.Visibility +import androidx.compose.material.icons.outlined.VisibilityOff +import androidx.compose.material.icons.outlined.Wallet +import androidx.compose.material.icons.rounded.AccountCircle +import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.AddLocation +import androidx.compose.material.icons.rounded.ArrowBackIosNew +import androidx.compose.material.icons.rounded.Bedtime +import androidx.compose.material.icons.rounded.Cancel +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material.icons.rounded.Close +import androidx.compose.material.icons.rounded.Delete +import androidx.compose.material.icons.rounded.Download +import androidx.compose.material.icons.rounded.Error +import androidx.compose.material.icons.rounded.FileUpload +import androidx.compose.material.icons.rounded.FilterList +import androidx.compose.material.icons.rounded.Home +import androidx.compose.material.icons.rounded.KeyboardArrowDown +import androidx.compose.material.icons.rounded.KeyboardArrowUp +import androidx.compose.material.icons.rounded.Lock +import androidx.compose.material.icons.rounded.MoreVert +import androidx.compose.material.icons.rounded.PersonOutline +import androidx.compose.material.icons.rounded.Search +import androidx.compose.material.icons.rounded.SwapHoriz +import androidx.compose.material.icons.rounded.Sync +import androidx.compose.material.icons.rounded.Translate +import androidx.compose.ui.graphics.vector.ImageVector + +object MifosIcons { + val Add = Icons.Rounded.Add + val Person = Icons.Rounded.PersonOutline + val Group = Icons.Outlined.Group + val EventRepeat = Icons.Outlined.EventRepeat + val Date = Icons.Outlined.DateRange + val ArrowBack1 = Icons.Rounded.ArrowBackIosNew + val Search = Icons.Rounded.Search + val Filter = Icons.Rounded.FilterList + val Sync = Icons.Rounded.Sync + val Check = Icons.Rounded.Check + val Close = Icons.Rounded.Close + val Cancel = Icons.Rounded.Cancel + val Error = Icons.Rounded.Error + val Delete = Icons.Rounded.Delete + val ArrowUp = Icons.Rounded.KeyboardArrowUp + val ArrowDown = Icons.Rounded.KeyboardArrowDown + val MoreVert = Icons.Rounded.MoreVert + val FileTask = Icons.Default.AssignmentTurnedIn + val AddLocation = Icons.Rounded.AddLocation + val CloudDownload = Icons.Default.CloudDownload + val Password = Icons.Rounded.Lock + val Theme = Icons.Rounded.Bedtime + val Language = Icons.Rounded.Translate + val Save = Icons.Rounded.Download + val WifiOff = Icons.Default.WifiOff + val Upload = Icons.Rounded.FileUpload + val Gallery = Icons.Filled.Image + val ArrowDropDown = Icons.Default.ArrowDropDown + val AssignmentTurnedIn = Icons.Default.AssignmentTurnedIn + + // Recently added + val ArrowBack2 = Icons.Filled.ChevronLeft + val Back = Icons.AutoMirrored.Outlined.ArrowBack + val Home = Icons.Outlined.Home + val HomeBoarder = Icons.Rounded.Home + val Payment = Icons.Rounded.SwapHoriz + val Finance = Icons.Outlined.Wallet + val Profile = Icons.Outlined.AccountCircle + val ProfileBoarder = Icons.Rounded.AccountCircle + + val Paid: ImageVector = Icons.Default.Paid + val Logout: ImageVector = Icons.AutoMirrored.Filled.Logout + val Help: ImageVector = Icons.AutoMirrored.Filled.Help + val Settings: ImageVector = Icons.Default.Settings + val Label: ImageVector = Icons.AutoMirrored.Filled.Label + val Assignment: ImageVector = Icons.AutoMirrored.Filled.Assignment + val People: ImageVector = Icons.Filled.People + val RealEstateAgent: ImageVector = Icons.Filled.RealEstateAgent + val AccountBalanceWallet: ImageVector = Icons.Filled.AccountBalanceWallet + val CompareArrows: ImageVector = Icons.AutoMirrored.Filled.CompareArrows + val AccountBalance: ImageVector = Icons.Filled.AccountBalance + val Share: ImageVector = Icons.Default.Share + val Mail: ImageVector = Icons.Outlined.Mail + val LocationOn: ImageVector = Icons.Filled.LocationOn + val Phone: ImageVector = Icons.Default.Phone + val VisibilityOff: ImageVector = Icons.Filled.VisibilityOff + val Visibility: ImageVector = Icons.Filled.Visibility + val Info: ImageVector = Icons.Default.Info + val ArrowDropUp: ImageVector = Icons.Default.ArrowDropUp + val OutlinedVisibilityOff: ImageVector = Icons.Outlined.VisibilityOff + val OutlinedVisibility: ImageVector = Icons.Outlined.Visibility + val ArrowBack = Icons.AutoMirrored.Default.ArrowBack + val Edit = Icons.Default.Edit + val FilterList = Icons.Filled.FilterList + val FlashOn = Icons.Default.FlashOn + val FlashOff = Icons.Default.FlashOff + val Error2 = Icons.Filled.Error + val Notifications = Icons.Filled.Notifications + val NavigationDrawer = Icons.Default.Menu +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/BackgroundTheme.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/BackgroundTheme.kt new file mode 100644 index 0000000000..bf21d50c47 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/BackgroundTheme.kt @@ -0,0 +1,29 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.staticCompositionLocalOf +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp + +/** + * A class to model background color and tonal elevation values for Now in Android. + */ +@Immutable +data class BackgroundTheme( + val color: Color = Color.Unspecified, + val tonalElevation: Dp = Dp.Unspecified, +) + +/** + * A composition local for [BackgroundTheme]. + */ +val LocalBackgroundTheme = staticCompositionLocalOf { BackgroundTheme() } diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt new file mode 100644 index 0000000000..18bc14d64d --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Color.kt @@ -0,0 +1,242 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +val primaryLight = Color(0xFF445E91) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFFD8E2FF) +val onPrimaryContainerLight = Color(0xFF2B4678) +val secondaryLight = Color(0xFF575E71) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFDBE2F9) +val onSecondaryContainerLight = Color(0xFF3F4759) +val tertiaryLight = Color(0xFF715573) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFFFBD7FC) +val onTertiaryContainerLight = Color(0xFF583E5B) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF93000A) +val backgroundLight = Color(0xFFF9F9FF) +val onBackgroundLight = Color(0xFF1A1B20) +val surfaceLight = Color(0xFFF9F9FF) +val onSurfaceLight = Color(0xFF1A1B20) +val surfaceVariantLight = Color(0xFFE1E2EC) +val onSurfaceVariantLight = Color(0xFF44474F) +val outlineLight = Color(0xFF74777F) +val outlineVariantLight = Color(0xFFC4C6D0) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF2F3036) +val inverseOnSurfaceLight = Color(0xFFF0F0F7) +val inversePrimaryLight = Color(0xFFADC6FF) +val surfaceDimLight = Color(0xFFD9D9E0) +val surfaceBrightLight = Color(0xFFF9F9FF) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFF3F3FA) +val surfaceContainerLight = Color(0xFFEDEDF4) +val surfaceContainerHighLight = Color(0xFFE8E7EE) +val surfaceContainerHighestLight = Color(0xFFE2E2E9) + +val primaryLightMediumContrast = Color(0xFF183566) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF536DA1) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF2E3647) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF656D80) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF462D49) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF806383) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF740006) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFCF2C27) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFF9F9FF) +val onBackgroundLightMediumContrast = Color(0xFF1A1B20) +val surfaceLightMediumContrast = Color(0xFFF9F9FF) +val onSurfaceLightMediumContrast = Color(0xFF0F1116) +val surfaceVariantLightMediumContrast = Color(0xFFE1E2EC) +val onSurfaceVariantLightMediumContrast = Color(0xFF33363E) +val outlineLightMediumContrast = Color(0xFF50525A) +val outlineVariantLightMediumContrast = Color(0xFF6A6D75) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF2F3036) +val inverseOnSurfaceLightMediumContrast = Color(0xFFF0F0F7) +val inversePrimaryLightMediumContrast = Color(0xFFADC6FF) +val surfaceDimLightMediumContrast = Color(0xFFC6C6CD) +val surfaceBrightLightMediumContrast = Color(0xFFF9F9FF) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFF3F3FA) +val surfaceContainerLightMediumContrast = Color(0xFFE8E7EE) +val surfaceContainerHighLightMediumContrast = Color(0xFFDCDCE3) +val surfaceContainerHighestLightMediumContrast = Color(0xFFD1D1D8) + +val primaryLightHighContrast = Color(0xFF0A2B5B) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF2E487A) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF242C3D) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF41495B) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF3B233F) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF5A405D) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF600004) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF98000A) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFF9F9FF) +val onBackgroundLightHighContrast = Color(0xFF1A1B20) +val surfaceLightHighContrast = Color(0xFFF9F9FF) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFE1E2EC) +val onSurfaceVariantLightHighContrast = Color(0xFF000000) +val outlineLightHighContrast = Color(0xFF292C33) +val outlineVariantLightHighContrast = Color(0xFF464951) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF2F3036) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFADC6FF) +val surfaceDimLightHighContrast = Color(0xFFB8B8BF) +val surfaceBrightLightHighContrast = Color(0xFFF9F9FF) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFF0F0F7) +val surfaceContainerLightHighContrast = Color(0xFFE2E2E9) +val surfaceContainerHighLightHighContrast = Color(0xFFD4D4DB) +val surfaceContainerHighestLightHighContrast = Color(0xFFC6C6CD) + +val primaryDark = Color(0xFFADC6FF) +val onPrimaryDark = Color(0xFF102F60) +val primaryContainerDark = Color(0xFF2B4678) +val onPrimaryContainerDark = Color(0xFFD8E2FF) +val secondaryDark = Color(0xFFBFC6DC) +val onSecondaryDark = Color(0xFF293041) +val secondaryContainerDark = Color(0xFF3F4759) +val onSecondaryContainerDark = Color(0xFFDBE2F9) +val tertiaryDark = Color(0xFFDEBCDF) +val onTertiaryDark = Color(0xFF402843) +val tertiaryContainerDark = Color(0xFF583E5B) +val onTertiaryContainerDark = Color(0xFFFBD7FC) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF111318) +val onBackgroundDark = Color(0xFFE2E2E9) +val surfaceDark = Color(0xFF111318) +val onSurfaceDark = Color(0xFFE2E2E9) +val surfaceVariantDark = Color(0xFF44474F) +val onSurfaceVariantDark = Color(0xFFC4C6D0) +val outlineDark = Color(0xFF8E9099) +val outlineVariantDark = Color(0xFF44474F) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFE2E2E9) +val inverseOnSurfaceDark = Color(0xFF2F3036) +val inversePrimaryDark = Color(0xFF445E91) +val surfaceDimDark = Color(0xFF111318) +val surfaceBrightDark = Color(0xFF37393E) +val surfaceContainerLowestDark = Color(0xFF0C0E13) +val surfaceContainerLowDark = Color(0xFF1A1B20) +val surfaceContainerDark = Color(0xFF1E1F25) +val surfaceContainerHighDark = Color(0xFF282A2F) +val surfaceContainerHighestDark = Color(0xFF33353A) + +val primaryDarkMediumContrast = Color(0xFFCFDCFF) +val onPrimaryDarkMediumContrast = Color(0xFF002454) +val primaryContainerDarkMediumContrast = Color(0xFF7790C7) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFD5DCF3) +val onSecondaryDarkMediumContrast = Color(0xFF1E2636) +val secondaryContainerDarkMediumContrast = Color(0xFF8991A5) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFF5D1F5) +val onTertiaryDarkMediumContrast = Color(0xFF341D38) +val tertiaryContainerDarkMediumContrast = Color(0xFFA687A8) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFD2CC) +val onErrorDarkMediumContrast = Color(0xFF540003) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF111318) +val onBackgroundDarkMediumContrast = Color(0xFFE2E2E9) +val surfaceDarkMediumContrast = Color(0xFF111318) +val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkMediumContrast = Color(0xFF44474F) +val onSurfaceVariantDarkMediumContrast = Color(0xFFDADCE6) +val outlineDarkMediumContrast = Color(0xFFB0B1BB) +val outlineVariantDarkMediumContrast = Color(0xFF8E9099) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFE2E2E9) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF282A2F) +val inversePrimaryDarkMediumContrast = Color(0xFF2C4779) +val surfaceDimDarkMediumContrast = Color(0xFF111318) +val surfaceBrightDarkMediumContrast = Color(0xFF43444A) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF06070C) +val surfaceContainerLowDarkMediumContrast = Color(0xFF1C1D22) +val surfaceContainerDarkMediumContrast = Color(0xFF26282D) +val surfaceContainerHighDarkMediumContrast = Color(0xFF313238) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3C3D43) + +val primaryDarkHighContrast = Color(0xFFECEFFF) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFA8C2FC) +val onPrimaryContainerDarkHighContrast = Color(0xFF000A22) +val secondaryDarkHighContrast = Color(0xFFECEFFF) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFBBC2D8) +val onSecondaryContainerDarkHighContrast = Color(0xFF040B1B) +val tertiaryDarkHighContrast = Color(0xFFFFEAFC) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFDAB8DB) +val onTertiaryContainerDarkHighContrast = Color(0xFF17031C) +val errorDarkHighContrast = Color(0xFFFFECE9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFAEA4) +val onErrorContainerDarkHighContrast = Color(0xFF220001) +val backgroundDarkHighContrast = Color(0xFF111318) +val onBackgroundDarkHighContrast = Color(0xFFE2E2E9) +val surfaceDarkHighContrast = Color(0xFF111318) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF44474F) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF) +val outlineDarkHighContrast = Color(0xFFEEEFF9) +val outlineVariantDarkHighContrast = Color(0xFFC1C2CC) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFE2E2E9) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF2C4779) +val surfaceDimDarkHighContrast = Color(0xFF111318) +val surfaceBrightDarkHighContrast = Color(0xFF4E5056) +val surfaceContainerLowestDarkHighContrast = Color(0xFF000000) +val surfaceContainerLowDarkHighContrast = Color(0xFF1E1F25) +val surfaceContainerDarkHighContrast = Color(0xFF2F3036) +val surfaceContainerHighDarkHighContrast = Color(0xFF3A3B41) +val surfaceContainerHighestDarkHighContrast = Color(0xFF45474C) + +/* +Older colors + +val White = Color(0xFFFFFFFF) +val Black = Color(0xFF000000) +val DarkGray = Color(0xFF696969) +val LightGray = Color(0xFFD3D3D3) +val BluePrimary = Color(0xFF2D5BA8) +val BluePrimaryDark = Color(0xFF9BB1E3) +val BlueSecondary = Color(0xFFD7E2FC) +val LightGreen = Color(0xFF99CC00) +val SummerSky = Color(0xFF29B6F6) + */ diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/MifosTextStyle.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/MifosTextStyle.kt similarity index 97% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/theme/MifosTextStyle.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/MifosTextStyle.kt index 181291048b..7c359dcc41 100644 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/MifosTextStyle.kt +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/MifosTextStyle.kt @@ -37,7 +37,7 @@ val identifierTextStyleDark = TextStyle( ) val identifierTextStyleLight = TextStyle( - color = DarkGray, + color = secondaryLight, textAlign = TextAlign.Start, fontSize = 16.sp, fontWeight = FontWeight.Normal, diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Theme.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Theme.kt new file mode 100644 index 0000000000..02000c8852 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Theme.kt @@ -0,0 +1,264 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.material3.ColorScheme +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.darkColorScheme +import androidx.compose.material3.lightColorScheme +import androidx.compose.runtime.Composable + +val lightScheme = lightColorScheme( + primary = primaryLight, + onPrimary = onPrimaryLight, + primaryContainer = primaryContainerLight, + onPrimaryContainer = onPrimaryContainerLight, + secondary = secondaryLight, + onSecondary = onSecondaryLight, + secondaryContainer = secondaryContainerLight, + onSecondaryContainer = onSecondaryContainerLight, + tertiary = tertiaryLight, + onTertiary = onTertiaryLight, + tertiaryContainer = tertiaryContainerLight, + onTertiaryContainer = onTertiaryContainerLight, + error = errorLight, + onError = onErrorLight, + errorContainer = errorContainerLight, + onErrorContainer = onErrorContainerLight, + background = backgroundLight, + onBackground = onBackgroundLight, + surface = surfaceLight, + onSurface = onSurfaceLight, + surfaceVariant = surfaceVariantLight, + onSurfaceVariant = onSurfaceVariantLight, + outline = outlineLight, + outlineVariant = outlineVariantLight, + scrim = scrimLight, + inverseSurface = inverseSurfaceLight, + inverseOnSurface = inverseOnSurfaceLight, + inversePrimary = inversePrimaryLight, + surfaceDim = surfaceDimLight, + surfaceBright = surfaceBrightLight, + surfaceContainerLowest = surfaceContainerLowestLight, + surfaceContainerLow = surfaceContainerLowLight, + surfaceContainer = surfaceContainerLight, + surfaceContainerHigh = surfaceContainerHighLight, + surfaceContainerHighest = surfaceContainerHighestLight, +) + +val darkScheme = darkColorScheme( + primary = primaryDark, + onPrimary = onPrimaryDark, + primaryContainer = primaryContainerDark, + onPrimaryContainer = onPrimaryContainerDark, + secondary = secondaryDark, + onSecondary = onSecondaryDark, + secondaryContainer = secondaryContainerDark, + onSecondaryContainer = onSecondaryContainerDark, + tertiary = tertiaryDark, + onTertiary = onTertiaryDark, + tertiaryContainer = tertiaryContainerDark, + onTertiaryContainer = onTertiaryContainerDark, + error = errorDark, + onError = onErrorDark, + errorContainer = errorContainerDark, + onErrorContainer = onErrorContainerDark, + background = backgroundDark, + onBackground = onBackgroundDark, + surface = surfaceDark, + onSurface = onSurfaceDark, + surfaceVariant = surfaceVariantDark, + onSurfaceVariant = onSurfaceVariantDark, + outline = outlineDark, + outlineVariant = outlineVariantDark, + scrim = scrimDark, + inverseSurface = inverseSurfaceDark, + inverseOnSurface = inverseOnSurfaceDark, + inversePrimary = inversePrimaryDark, + surfaceDim = surfaceDimDark, + surfaceBright = surfaceBrightDark, + surfaceContainerLowest = surfaceContainerLowestDark, + surfaceContainerLow = surfaceContainerLowDark, + surfaceContainer = surfaceContainerDark, + surfaceContainerHigh = surfaceContainerHighDark, + surfaceContainerHighest = surfaceContainerHighestDark, +) + +val mediumContrastLightColorScheme = lightColorScheme( + primary = primaryLightMediumContrast, + onPrimary = onPrimaryLightMediumContrast, + primaryContainer = primaryContainerLightMediumContrast, + onPrimaryContainer = onPrimaryContainerLightMediumContrast, + secondary = secondaryLightMediumContrast, + onSecondary = onSecondaryLightMediumContrast, + secondaryContainer = secondaryContainerLightMediumContrast, + onSecondaryContainer = onSecondaryContainerLightMediumContrast, + tertiary = tertiaryLightMediumContrast, + onTertiary = onTertiaryLightMediumContrast, + tertiaryContainer = tertiaryContainerLightMediumContrast, + onTertiaryContainer = onTertiaryContainerLightMediumContrast, + error = errorLightMediumContrast, + onError = onErrorLightMediumContrast, + errorContainer = errorContainerLightMediumContrast, + onErrorContainer = onErrorContainerLightMediumContrast, + background = backgroundLightMediumContrast, + onBackground = onBackgroundLightMediumContrast, + surface = surfaceLightMediumContrast, + onSurface = onSurfaceLightMediumContrast, + surfaceVariant = surfaceVariantLightMediumContrast, + onSurfaceVariant = onSurfaceVariantLightMediumContrast, + outline = outlineLightMediumContrast, + outlineVariant = outlineVariantLightMediumContrast, + scrim = scrimLightMediumContrast, + inverseSurface = inverseSurfaceLightMediumContrast, + inverseOnSurface = inverseOnSurfaceLightMediumContrast, + inversePrimary = inversePrimaryLightMediumContrast, + surfaceDim = surfaceDimLightMediumContrast, + surfaceBright = surfaceBrightLightMediumContrast, + surfaceContainerLowest = surfaceContainerLowestLightMediumContrast, + surfaceContainerLow = surfaceContainerLowLightMediumContrast, + surfaceContainer = surfaceContainerLightMediumContrast, + surfaceContainerHigh = surfaceContainerHighLightMediumContrast, + surfaceContainerHighest = surfaceContainerHighestLightMediumContrast, +) + +val highContrastLightColorScheme = lightColorScheme( + primary = primaryLightHighContrast, + onPrimary = onPrimaryLightHighContrast, + primaryContainer = primaryContainerLightHighContrast, + onPrimaryContainer = onPrimaryContainerLightHighContrast, + secondary = secondaryLightHighContrast, + onSecondary = onSecondaryLightHighContrast, + secondaryContainer = secondaryContainerLightHighContrast, + onSecondaryContainer = onSecondaryContainerLightHighContrast, + tertiary = tertiaryLightHighContrast, + onTertiary = onTertiaryLightHighContrast, + tertiaryContainer = tertiaryContainerLightHighContrast, + onTertiaryContainer = onTertiaryContainerLightHighContrast, + error = errorLightHighContrast, + onError = onErrorLightHighContrast, + errorContainer = errorContainerLightHighContrast, + onErrorContainer = onErrorContainerLightHighContrast, + background = backgroundLightHighContrast, + onBackground = onBackgroundLightHighContrast, + surface = surfaceLightHighContrast, + onSurface = onSurfaceLightHighContrast, + surfaceVariant = surfaceVariantLightHighContrast, + onSurfaceVariant = onSurfaceVariantLightHighContrast, + outline = outlineLightHighContrast, + outlineVariant = outlineVariantLightHighContrast, + scrim = scrimLightHighContrast, + inverseSurface = inverseSurfaceLightHighContrast, + inverseOnSurface = inverseOnSurfaceLightHighContrast, + inversePrimary = inversePrimaryLightHighContrast, + surfaceDim = surfaceDimLightHighContrast, + surfaceBright = surfaceBrightLightHighContrast, + surfaceContainerLowest = surfaceContainerLowestLightHighContrast, + surfaceContainerLow = surfaceContainerLowLightHighContrast, + surfaceContainer = surfaceContainerLightHighContrast, + surfaceContainerHigh = surfaceContainerHighLightHighContrast, + surfaceContainerHighest = surfaceContainerHighestLightHighContrast, +) + +val mediumContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkMediumContrast, + onPrimary = onPrimaryDarkMediumContrast, + primaryContainer = primaryContainerDarkMediumContrast, + onPrimaryContainer = onPrimaryContainerDarkMediumContrast, + secondary = secondaryDarkMediumContrast, + onSecondary = onSecondaryDarkMediumContrast, + secondaryContainer = secondaryContainerDarkMediumContrast, + onSecondaryContainer = onSecondaryContainerDarkMediumContrast, + tertiary = tertiaryDarkMediumContrast, + onTertiary = onTertiaryDarkMediumContrast, + tertiaryContainer = tertiaryContainerDarkMediumContrast, + onTertiaryContainer = onTertiaryContainerDarkMediumContrast, + error = errorDarkMediumContrast, + onError = onErrorDarkMediumContrast, + errorContainer = errorContainerDarkMediumContrast, + onErrorContainer = onErrorContainerDarkMediumContrast, + background = backgroundDarkMediumContrast, + onBackground = onBackgroundDarkMediumContrast, + surface = surfaceDarkMediumContrast, + onSurface = onSurfaceDarkMediumContrast, + surfaceVariant = surfaceVariantDarkMediumContrast, + onSurfaceVariant = onSurfaceVariantDarkMediumContrast, + outline = outlineDarkMediumContrast, + outlineVariant = outlineVariantDarkMediumContrast, + scrim = scrimDarkMediumContrast, + inverseSurface = inverseSurfaceDarkMediumContrast, + inverseOnSurface = inverseOnSurfaceDarkMediumContrast, + inversePrimary = inversePrimaryDarkMediumContrast, + surfaceDim = surfaceDimDarkMediumContrast, + surfaceBright = surfaceBrightDarkMediumContrast, + surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast, + surfaceContainerLow = surfaceContainerLowDarkMediumContrast, + surfaceContainer = surfaceContainerDarkMediumContrast, + surfaceContainerHigh = surfaceContainerHighDarkMediumContrast, + surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast, +) + +val highContrastDarkColorScheme = darkColorScheme( + primary = primaryDarkHighContrast, + onPrimary = onPrimaryDarkHighContrast, + primaryContainer = primaryContainerDarkHighContrast, + onPrimaryContainer = onPrimaryContainerDarkHighContrast, + secondary = secondaryDarkHighContrast, + onSecondary = onSecondaryDarkHighContrast, + secondaryContainer = secondaryContainerDarkHighContrast, + onSecondaryContainer = onSecondaryContainerDarkHighContrast, + tertiary = tertiaryDarkHighContrast, + onTertiary = onTertiaryDarkHighContrast, + tertiaryContainer = tertiaryContainerDarkHighContrast, + onTertiaryContainer = onTertiaryContainerDarkHighContrast, + error = errorDarkHighContrast, + onError = onErrorDarkHighContrast, + errorContainer = errorContainerDarkHighContrast, + onErrorContainer = onErrorContainerDarkHighContrast, + background = backgroundDarkHighContrast, + onBackground = onBackgroundDarkHighContrast, + surface = surfaceDarkHighContrast, + onSurface = onSurfaceDarkHighContrast, + surfaceVariant = surfaceVariantDarkHighContrast, + onSurfaceVariant = onSurfaceVariantDarkHighContrast, + outline = outlineDarkHighContrast, + outlineVariant = outlineVariantDarkHighContrast, + scrim = scrimDarkHighContrast, + inverseSurface = inverseSurfaceDarkHighContrast, + inverseOnSurface = inverseOnSurfaceDarkHighContrast, + inversePrimary = inversePrimaryDarkHighContrast, + surfaceDim = surfaceDimDarkHighContrast, + surfaceBright = surfaceBrightDarkHighContrast, + surfaceContainerLowest = surfaceContainerLowestDarkHighContrast, + surfaceContainerLow = surfaceContainerLowDarkHighContrast, + surfaceContainer = surfaceContainerDarkHighContrast, + surfaceContainerHigh = surfaceContainerHighDarkHighContrast, + surfaceContainerHighest = surfaceContainerHighestDarkHighContrast, +) + +// TODO:: Configure more themes for the app like medium contrast, high contrast, etc. + +@Composable +fun MifosTheme( + useDarkTheme: Boolean = isSystemInDarkTheme(), + // Dynamic color is available on Android 12+ + dynamicColor: Boolean = true, + content: @Composable () -> Unit, +) { + MaterialTheme( + colorScheme = colorScheme(useDarkTheme, dynamicColor), + content = content, + typography = mifosTypography(), + ) +} + +@Composable +expect fun colorScheme(useDarkTheme: Boolean, dynamicColor: Boolean): ColorScheme diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Type.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Type.kt new file mode 100644 index 0000000000..2c302d443b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/theme/Type.kt @@ -0,0 +1,173 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.material3.Typography +import androidx.compose.runtime.Composable +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.LineHeightStyle +import androidx.compose.ui.unit.sp +import core.designsystem.generated.resources.Res +import core.designsystem.generated.resources.poppins_black +import core.designsystem.generated.resources.poppins_bold +import core.designsystem.generated.resources.poppins_extra_bold +import core.designsystem.generated.resources.poppins_extra_light +import core.designsystem.generated.resources.poppins_light +import core.designsystem.generated.resources.poppins_medium +import core.designsystem.generated.resources.poppins_regular +import core.designsystem.generated.resources.poppins_semi_bold +import core.designsystem.generated.resources.poppins_thin +import org.jetbrains.compose.resources.Font + +@Composable +private fun appFontFamily(): FontFamily { + return FontFamily( + Font(Res.font.poppins_black, FontWeight.Black), + Font(Res.font.poppins_bold, FontWeight.Bold), + Font(Res.font.poppins_semi_bold, FontWeight.SemiBold), + Font(Res.font.poppins_medium, FontWeight.Medium), + Font(Res.font.poppins_regular, FontWeight.Normal), + Font(Res.font.poppins_light, FontWeight.Light), + Font(Res.font.poppins_thin, FontWeight.Thin), + Font(Res.font.poppins_extra_light, FontWeight.ExtraLight), + Font(Res.font.poppins_extra_bold, FontWeight.ExtraBold), + ) +} + +// Set of Material typography styles to start with +@Composable +internal fun mifosTypography() = Typography( + displayLarge = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 57.sp, + lineHeight = 64.sp, + letterSpacing = (-0.25).sp, + ), + displayMedium = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 45.sp, + lineHeight = 52.sp, + letterSpacing = 0.sp, + ), + displaySmall = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 36.sp, + lineHeight = 44.sp, + letterSpacing = 0.sp, + ), + headlineLarge = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 32.sp, + lineHeight = 40.sp, + letterSpacing = 0.sp, + ), + headlineMedium = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 28.sp, + lineHeight = 36.sp, + letterSpacing = 0.sp, + ), + headlineSmall = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 24.sp, + lineHeight = 32.sp, + letterSpacing = 0.sp, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Bottom, + trim = LineHeightStyle.Trim.None, + ), + ), + titleLarge = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.SemiBold, + fontSize = 24.sp, + lineHeight = 30.24.sp, + ), + titleMedium = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.SemiBold, + fontSize = 20.sp, + lineHeight = 28.sp, + letterSpacing = 0.1.sp, + ), + titleSmall = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + // Default text style + bodyLarge = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 16.sp, + lineHeight = 24.sp, + letterSpacing = 0.5.sp, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.None, + ), + ), + bodyMedium = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 20.sp, + letterSpacing = 0.25.sp, + ), + bodySmall = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.4.sp, + ), + // Used for Button + labelLarge = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Medium, + fontSize = 16.sp, + lineHeight = 20.sp, + letterSpacing = 0.1.sp, + ), + // Used for Navigation items + labelMedium = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Medium, + fontSize = 12.sp, + lineHeight = 16.sp, + letterSpacing = 0.5.sp, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.LastLineBottom, + ), + ), + // Used for Tag + labelSmall = TextStyle( + fontFamily = appFontFamily(), + fontWeight = FontWeight.Medium, + fontSize = 10.sp, + lineHeight = 14.sp, + letterSpacing = 0.sp, + lineHeightStyle = LineHeightStyle( + alignment = LineHeightStyle.Alignment.Center, + trim = LineHeightStyle.Trim.LastLineBottom, + ), + ), +) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/utility/PathState.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utility/PathState.kt similarity index 100% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/utility/PathState.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utility/PathState.kt diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/utility/TabContent.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utility/TabContent.kt similarity index 100% rename from core/designsystem/src/main/java/com/mifos/core/designsystem/utility/TabContent.kt rename to core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utility/TabContent.kt diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utils/ModifierExt.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utils/ModifierExt.kt new file mode 100644 index 0000000000..550c47d13f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utils/ModifierExt.kt @@ -0,0 +1,85 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.utils + +import androidx.compose.foundation.Indication +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.composed +import androidx.compose.ui.draw.scale +import androidx.compose.ui.focus.FocusDirection +import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isShiftPressed +import androidx.compose.ui.input.key.key +import androidx.compose.ui.input.key.onPreviewKeyEvent +import androidx.compose.ui.input.key.type +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.platform.debugInspectorInfo +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.LayoutDirection + +@Stable +@Composable +fun Modifier.mirrorIfRtl(): Modifier = + if (LocalLayoutDirection.current == LayoutDirection.Rtl) { + scale(scaleX = -1f, scaleY = 1f) + } else { + this + } + +@Stable +@Composable +fun Modifier.tabNavigation(): Modifier { + val focusManager = LocalFocusManager.current + return onPreviewKeyEvent { keyEvent -> + if (keyEvent.key == Key.Tab && keyEvent.type == KeyEventType.KeyDown) { + focusManager.moveFocus( + if (keyEvent.isShiftPressed) { + FocusDirection.Previous + } else { + FocusDirection.Next + }, + ) + true + } else { + false + } + } +} + +fun Modifier.onClick( + indication: Indication? = null, + enabled: Boolean = true, + onClickLabel: String? = null, + role: Role? = null, + onClick: () -> Unit, +) = this.composed( + inspectorInfo = debugInspectorInfo { + name = "onClickModifier" + value = enabled + }, +) { + val interactionSource = remember { MutableInteractionSource() } + clickable( + indication = indication, + interactionSource = interactionSource, + enabled = enabled, + onClickLabel = onClickLabel, + role = role, + ) { + onClick.invoke() + } +} diff --git a/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utils/NonLetterColorVisualTransformation.kt b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utils/NonLetterColorVisualTransformation.kt new file mode 100644 index 0000000000..766be6a806 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/com/mifos/core/designsystem/utils/NonLetterColorVisualTransformation.kt @@ -0,0 +1,61 @@ +/* + * Copyright 2024 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.utils + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.AnnotatedString +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.input.OffsetMapping +import androidx.compose.ui.text.input.TransformedText +import androidx.compose.ui.text.input.VisualTransformation +import androidx.compose.ui.text.withStyle + +@Composable +fun nonLetterColorVisualTransformation(): VisualTransformation { + val digitColor = MaterialTheme.colorScheme.primary + val specialCharacterColor = MaterialTheme.colorScheme.error + return remember(digitColor, specialCharacterColor) { + NonLetterColorVisualTransformation( + digitColor = digitColor, + specialCharacterColor = specialCharacterColor, + ) + } +} + +private class NonLetterColorVisualTransformation( + private val digitColor: Color, + private val specialCharacterColor: Color, +) : VisualTransformation { + + override fun filter(text: AnnotatedString): TransformedText = + TransformedText( + buildTransformedAnnotatedString(text.toString()), + OffsetMapping.Identity, + ) + + private fun buildTransformedAnnotatedString(text: String): AnnotatedString { + val builder = AnnotatedString.Builder() + text.toCharArray().forEach { char -> + when { + char.isDigit() -> builder.withStyle(SpanStyle(color = digitColor)) { append(char) } + + !char.isLetter() -> { + builder.withStyle(SpanStyle(color = specialCharacterColor)) { append(char) } + } + + else -> builder.append(char) + } + } + return builder.toAnnotatedString() + } +} diff --git a/core/designsystem/src/desktopMain/kotlin/com/mifos/core/designsystem/theme/Theme.desktop.kt b/core/designsystem/src/desktopMain/kotlin/com/mifos/core/designsystem/theme/Theme.desktop.kt new file mode 100644 index 0000000000..f16a50af32 --- /dev/null +++ b/core/designsystem/src/desktopMain/kotlin/com/mifos/core/designsystem/theme/Theme.desktop.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +actual fun colorScheme( + useDarkTheme: Boolean, + dynamicColor: Boolean, +): ColorScheme { + return when { + useDarkTheme -> darkScheme + else -> lightScheme + } +} diff --git a/core/designsystem/src/jsMain/kotlin/com/mifos/core/designsystem/theme/Theme.js.kt b/core/designsystem/src/jsMain/kotlin/com/mifos/core/designsystem/theme/Theme.js.kt new file mode 100644 index 0000000000..f16a50af32 --- /dev/null +++ b/core/designsystem/src/jsMain/kotlin/com/mifos/core/designsystem/theme/Theme.js.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +actual fun colorScheme( + useDarkTheme: Boolean, + dynamicColor: Boolean, +): ColorScheme { + return when { + useDarkTheme -> darkScheme + else -> lightScheme + } +} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt deleted file mode 100644 index d43fcadd44..0000000000 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosAlertDailog.kt +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.designsystem.component - -import androidx.compose.foundation.clickable -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.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.itemsIndexed -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Card -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.RadioButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.saveable.rememberSaveable -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog -import com.mifos.core.designsystem.R - -@Composable -fun MifosDialogBox( - showDialogState: Boolean, - onDismiss: () -> Unit, - title: Int, - confirmButtonText: Int, - onConfirm: () -> Unit, - dismissButtonText: Int, - message: Int? = null, -) { - if (showDialogState) { - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(text = stringResource(id = title)) }, - text = { - if (message != null) { - Text(text = stringResource(id = message)) - } - }, - confirmButton = { - TextButton( - onClick = { - onConfirm() - }, - ) { - Text(stringResource(id = confirmButtonText)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(id = dismissButtonText)) - } - }, - ) - } -} - -@Composable -fun MifosRadioButtonDialog( - titleResId: Int, - selectedItem: String, - items: Array, - selectItem: (item: String, index: Int) -> Unit, - onDismissRequest: () -> Unit, - modifier: Modifier = Modifier, -) { - Dialog( - onDismissRequest = { onDismissRequest.invoke() }, - ) { - Card(modifier = modifier) { - Column(modifier = Modifier.padding(20.dp)) { - Text(text = stringResource(id = titleResId)) - LazyColumn( - modifier = Modifier - .fillMaxWidth() - .heightIn(max = 500.dp), - ) { - itemsIndexed(items = items) { index, item -> - Row( - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier - .clickable { - onDismissRequest.invoke() - selectItem.invoke(item, index) - } - .fillMaxWidth(), - ) { - RadioButton( - selected = (item == selectedItem), - onClick = { - onDismissRequest.invoke() - selectItem.invoke(item, index) - }, - ) - Text( - text = item, - modifier = Modifier.padding(start = 4.dp), - ) - } - } - } - } - } - } -} - -@Composable -fun UpdateEndpointDialogScreen( - initialBaseURL: String?, - initialTenant: String?, - onDismissRequest: () -> Unit, - handleEndpointUpdate: (baseURL: String, tenant: String) -> Unit, - modifier: Modifier = Modifier, -) { - var baseURL by rememberSaveable { mutableStateOf(initialBaseURL) } - var tenant by rememberSaveable { mutableStateOf(initialTenant) } - - Dialog( - onDismissRequest = { onDismissRequest.invoke() }, - ) { - Card { - Column( - modifier = modifier - .fillMaxWidth() - .padding(20.dp), - ) { - Text(text = stringResource(id = R.string.core_designsystem_pref_base_url_title)) - Spacer(modifier = Modifier.height(8.dp)) - - baseURL?.let { - OutlinedTextField( - value = it, - onValueChange = { baseURL = it }, - label = { Text(text = stringResource(id = R.string.core_designsystem_enter_base_url)) }, - ) - } - - Spacer(modifier = Modifier.height(8.dp)) - - tenant?.let { - OutlinedTextField( - value = it, - onValueChange = { tenant = it }, - label = { Text(text = stringResource(id = R.string.core_designsystem_enter_tenant)) }, - ) - } - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.End, - ) { - TextButton( - onClick = { onDismissRequest.invoke() }, - ) { - Text(text = stringResource(id = R.string.core_designsystem_cancel)) - } - TextButton( - onClick = { - if (baseURL != null && tenant != null) { - handleEndpointUpdate.invoke(baseURL ?: "", tenant ?: "") - } - }, - ) { - Text(text = stringResource(id = R.string.core_designsystem_dialog_action_ok)) - } - } - } - } - } -} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt deleted file mode 100644 index 62bbb00f94..0000000000 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/component/MifosScaffold.kt +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -@file:OptIn(ExperimentalMaterial3Api::class) - -package com.mifos.core.designsystem.component - -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.WindowInsets -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.FabPosition -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton -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.material3.TopAppBarDefaults -import androidx.compose.material3.contentColorFor -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontStyle -import androidx.compose.ui.text.font.FontWeight -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.unit.sp -import com.mifos.core.designsystem.theme.Black -import com.mifos.core.designsystem.theme.White - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MifosScaffold( - snackbarHostState: SnackbarHostState?, - modifier: Modifier = Modifier, - isAppBarPresent: Boolean = true, - icon: ImageVector? = null, - title: String? = null, - fontsizeInSp: Int = 24, - onBackPressed: () -> Unit = {}, - actions: @Composable () -> Unit = {}, - bottomBar: @Composable () -> Unit = {}, - floatingActionButton: @Composable () -> Unit = {}, - content: @Composable (PaddingValues) -> Unit, -) { - Scaffold( - modifier = modifier, - topBar = { - if (isAppBarPresent) { - TopAppBar( - colors = TopAppBarDefaults.mediumTopAppBarColors(containerColor = White), - navigationIcon = { - if (icon != null) { - IconButton( - onClick = { onBackPressed() }, - ) { - Icon( - imageVector = icon, - contentDescription = null, - tint = Black, - ) - } - } - }, - title = { - title?.let { - Text( - text = it, - style = TextStyle( - fontSize = fontsizeInSp.sp, - fontWeight = FontWeight.Medium, - fontStyle = FontStyle.Normal, - ), - color = Black, - textAlign = TextAlign.Start, - ) - } - }, - actions = { actions() }, - ) - } - }, - snackbarHost = { snackbarHostState?.let { SnackbarHost(it) } }, - containerColor = White, - bottomBar = bottomBar, - floatingActionButton = floatingActionButton, - ) { padding -> - content(padding) - } -} - -@Composable -fun MifosScaffold( - modifier: Modifier = Modifier, - topBar: @Composable () -> Unit = {}, - bottomBar: @Composable () -> Unit = {}, - floatingActionButton: @Composable () -> Unit = {}, - snackbarHostState: SnackbarHostState = remember { SnackbarHostState() }, - floatingActionButtonPosition: FabPosition = FabPosition.End, - containerColor: Color = Color.White, - contentColor: Color = contentColorFor(containerColor), - content: @Composable (PaddingValues) -> Unit, -) { - Scaffold( - modifier = modifier, - topBar = topBar, - floatingActionButton = floatingActionButton, - floatingActionButtonPosition = floatingActionButtonPosition, - snackbarHost = { SnackbarHost(snackbarHostState) }, - bottomBar = bottomBar, - containerColor = containerColor, - contentColor = contentColor, - contentWindowInsets = WindowInsets(0, 0, 0, 0), - ) { padding -> - content(padding) - } -} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt deleted file mode 100644 index 78bd2e26c1..0000000000 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/icon/MifosIcons.kt +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.designsystem.icon - -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowDropDown -import androidx.compose.material.icons.filled.AssignmentTurnedIn -import androidx.compose.material.icons.filled.CloudDownload -import androidx.compose.material.icons.filled.Image -import androidx.compose.material.icons.filled.WifiOff -import androidx.compose.material.icons.outlined.DateRange -import androidx.compose.material.icons.outlined.EventRepeat -import androidx.compose.material.icons.outlined.Group -import androidx.compose.material.icons.rounded.Add -import androidx.compose.material.icons.rounded.AddLocation -import androidx.compose.material.icons.rounded.ArrowBackIosNew -import androidx.compose.material.icons.rounded.Bedtime -import androidx.compose.material.icons.rounded.Cancel -import androidx.compose.material.icons.rounded.Check -import androidx.compose.material.icons.rounded.Close -import androidx.compose.material.icons.rounded.Delete -import androidx.compose.material.icons.rounded.Download -import androidx.compose.material.icons.rounded.Error -import androidx.compose.material.icons.rounded.FileUpload -import androidx.compose.material.icons.rounded.FilterList -import androidx.compose.material.icons.rounded.KeyboardArrowDown -import androidx.compose.material.icons.rounded.KeyboardArrowUp -import androidx.compose.material.icons.rounded.Lock -import androidx.compose.material.icons.rounded.MoreVert -import androidx.compose.material.icons.rounded.PersonOutline -import androidx.compose.material.icons.rounded.Search -import androidx.compose.material.icons.rounded.Sync -import androidx.compose.material.icons.rounded.Translate - -object MifosIcons { - val Add = Icons.Rounded.Add - val person = Icons.Rounded.PersonOutline - val group = Icons.Outlined.Group - val eventRepeat = Icons.Outlined.EventRepeat - val date = Icons.Outlined.DateRange - val arrowBack = Icons.Rounded.ArrowBackIosNew - val search = Icons.Rounded.Search - val filter = Icons.Rounded.FilterList - val sync = Icons.Rounded.Sync - val check = Icons.Rounded.Check - val close = Icons.Rounded.Close - val cancel = Icons.Rounded.Cancel - val error = Icons.Rounded.Error - val delete = Icons.Rounded.Delete - val arrowUp = Icons.Rounded.KeyboardArrowUp - val arrowDown = Icons.Rounded.KeyboardArrowDown - val moreVert = Icons.Rounded.MoreVert - val fileTask = Icons.Default.AssignmentTurnedIn - val addLocation = Icons.Rounded.AddLocation - val cloudDownload = Icons.Default.CloudDownload - val password = Icons.Rounded.Lock - val theme = Icons.Rounded.Bedtime - val language = Icons.Rounded.Translate - val save = Icons.Rounded.Download - val WifiOff = Icons.Default.WifiOff - val upload = Icons.Rounded.FileUpload - val gallery = Icons.Filled.Image - val ArrowDropDown = Icons.Default.ArrowDropDown - val assignmentTurnedIn = Icons.Default.AssignmentTurnedIn -} diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt deleted file mode 100644 index 6df456f2c0..0000000000 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Color.kt +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.designsystem.theme - -import androidx.compose.ui.graphics.Color - -val White = Color(0xFFFFFFFF) -val Black = Color(0xFF000000) -val DarkGray = Color(0xFF696969) -val LightGray = Color(0xFFD3D3D3) -val BluePrimary = Color(0xFF2D5BA8) -val BluePrimaryDark = Color(0xFF9BB1E3) -val BlueSecondary = Color(0xFFD7E2FC) -val LightGreen = Color(0xFF99CC00) -val SummerSky = Color(0xFF29B6F6) diff --git a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt deleted file mode 100644 index 6a1f86c35b..0000000000 --- a/core/designsystem/src/main/java/com/mifos/core/designsystem/theme/Theme.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.designsystem.theme - -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.darkColorScheme -import androidx.compose.material3.lightColorScheme -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color - -private val LightThemeColors = lightColorScheme( - primary = BluePrimary, - onPrimary = Color.White, - error = Color.Red, - background = Color.White, - onSurface = BlueSecondary, - onSecondary = Color.Gray, - outlineVariant = Color.Gray, - surfaceTint = BlueSecondary, -) - -private val DarkThemeColors = darkColorScheme( - primary = BluePrimaryDark, - onPrimary = Color.White, - secondary = Black, - error = Color.Red, - background = Color.Black, - surface = BlueSecondary, - onSurface = Color.White, - onSecondary = Color.White, - outlineVariant = Color.White, - surfaceTint = BlueSecondary, -) - -@Composable -fun MifosTheme( - useDarkTheme: Boolean = isSystemInDarkTheme(), - content: @Composable () -> Unit, -) { - val colors = when { - useDarkTheme -> DarkThemeColors - else -> LightThemeColors - } - - MaterialTheme( - colorScheme = colors, - content = content, - ) -} diff --git a/core/designsystem/src/nativeMain/kotlin/com/mifos/core/designsystem/theme/Theme.native.kt b/core/designsystem/src/nativeMain/kotlin/com/mifos/core/designsystem/theme/Theme.native.kt new file mode 100644 index 0000000000..c8c48dbec8 --- /dev/null +++ b/core/designsystem/src/nativeMain/kotlin/com/mifos/core/designsystem/theme/Theme.native.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +actual fun colorScheme(useDarkTheme: Boolean, dynamicColor: Boolean): ColorScheme { + return when { + useDarkTheme -> darkScheme + else -> lightScheme + } +} diff --git a/core/designsystem/src/test/java/com/mifos/core/designsystem/ExampleUnitTest.kt b/core/designsystem/src/test/java/com/mifos/core/designsystem/ExampleUnitTest.kt deleted file mode 100644 index ced7220e88..0000000000 --- a/core/designsystem/src/test/java/com/mifos/core/designsystem/ExampleUnitTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -/* - * Copyright 2024 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at https://mozilla.org/MPL/2.0/. - * - * See https://github.com/openMF/android-client/blob/master/LICENSE.md - */ -package com.mifos.core.designsystem - -import org.junit.Assert.assertEquals -import org.junit.Test - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} diff --git a/core/designsystem/src/wasmJsMain/kotlin/com/mifos/core/designsystem/theme/Theme.wasmJs.kt b/core/designsystem/src/wasmJsMain/kotlin/com/mifos/core/designsystem/theme/Theme.wasmJs.kt new file mode 100644 index 0000000000..f16a50af32 --- /dev/null +++ b/core/designsystem/src/wasmJsMain/kotlin/com/mifos/core/designsystem/theme/Theme.wasmJs.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at https://mozilla.org/MPL/2.0/. + * + * See https://github.com/openMF/android-client/blob/master/LICENSE.md + */ +package com.mifos.core.designsystem.theme + +import androidx.compose.material3.ColorScheme +import androidx.compose.runtime.Composable + +@Composable +actual fun colorScheme( + useDarkTheme: Boolean, + dynamicColor: Boolean, +): ColorScheme { + return when { + useDarkTheme -> darkScheme + else -> lightScheme + } +} diff --git a/gradle.properties b/gradle.properties index 3a2fbdd27c..d93e3fe88f 100644 --- a/gradle.properties +++ b/gradle.properties @@ -25,4 +25,6 @@ org.gradle.parallel=true # The developer environment is optimized for speed and feedback so we nearly always run Gradle jobs with the daemon. org.gradle.daemon=true android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true + +org.jetbrains.compose.experimental.jscanvas.enabled=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 67ab31e3b3..57755bab3c 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -9,10 +9,10 @@ androidJob = "1.2.6" androidMapsUtils = "0.4.2" androidGradlePlugin = "8.8.0" androidTools = "31.8.0" -androidxActivity = "1.9.3" +androidxActivity = "1.10.0" androidxAppCompat = "1.7.0" androidxArchCore = '2.2.0' -androidxComposeBom = "2024.12.01" +androidxComposeBom = "2025.01.00" androidxComposeCompiler = "1.5.8" androidxComposeUi = "1.7.6" androidxComposeRuntime = "1.7.6" @@ -52,7 +52,7 @@ dbflowProcessor = "4.2.4" espressoIdlingResource = "3.6.1" espressoIntents = "3.6.1" fineractClientVersion = "0.0.1" -firebaseBom = "33.7.0" +firebaseBom = "33.8.0" firebaseCrashlyticsPlugin = "3.0.2" firebasePerfPlugin = "1.4.2" fliptables = "1.0.1" @@ -101,7 +101,8 @@ playUpdate = "2.1.0" playScanner = "16.1.0" playService = "18.5.0" realmVersion = "1.13.0" -recyclerview = "1.3.2" +recyclerview = "1.4.0" +roborazzi = "1.26.0" room = "2.7.0-alpha12" roomKtxVersion = "2.7.0-alpha12" roomRuntimeVersion = "2.7.0-alpha12" @@ -308,7 +309,7 @@ converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref converter-scalars = { module = "com.squareup.retrofit2:converter-scalars", version.ref = "converterScalars" } # Coil Compose -coil-kt-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil-compose" } +coil-kt-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } # DBFlow dbflow = { module = "com.github.raizlabs.dbflow.dbflow:dbflow", version.ref = "dbflow" } @@ -579,6 +580,7 @@ ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } spotless = { id = "com.diffplug.spotless", version.ref = "spotlessVersion" } module-graph = { id = "com.jraska.module.graph.assertion", version.ref = "moduleGraph" } dependencyGuard = { id = "com.dropbox.dependency-guard", version.ref = "dependencyGuard" } +roborazzi = { id = "io.github.takahirom.roborazzi", version.ref = "roborazzi" } # Plugins defined by this project diff --git a/mifosng-android/dependencies/demoDebugCompileClasspath.txt b/mifosng-android/dependencies/demoDebugCompileClasspath.txt index ff66c0f566..657ca2727f 100644 --- a/mifosng-android/dependencies/demoDebugCompileClasspath.txt +++ b/mifosng-android/dependencies/demoDebugCompileClasspath.txt @@ -1,6 +1,6 @@ -androidx.activity:activity-compose:1.9.3 -androidx.activity:activity-ktx:1.9.3 -androidx.activity:activity:1.9.3 +androidx.activity:activity-compose:1.10.0 +androidx.activity:activity-ktx:1.10.0 +androidx.activity:activity:1.10.0 androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 @@ -59,7 +59,7 @@ androidx.compose.ui:ui-unit:1.7.6 androidx.compose.ui:ui-util-android:1.7.6 androidx.compose.ui:ui-util:1.7.6 androidx.compose.ui:ui:1.7.6 -androidx.compose:compose-bom:2024.12.01 +androidx.compose:compose-bom:2025.01.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-solver:2.0.1 androidx.constraintlayout:constraintlayout:2.0.1 @@ -128,7 +128,7 @@ androidx.paging:paging-runtime:3.3.5 androidx.preference:preference-ktx:1.2.1 androidx.preference:preference:1.2.1 androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 +androidx.recyclerview:recyclerview:1.4.0 androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 @@ -147,6 +147,8 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 com.evernote:android-job:1.2.6 com.facebook.stetho:stetho-okhttp3:1.3.1 com.facebook.stetho:stetho:1.3.1 @@ -182,23 +184,41 @@ com.joanzapata.iconify:android-iconify-material:2.2.2 com.joanzapata.iconify:android-iconify:2.2.2 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.8.0 -com.squareup.okio:okio:3.8.0 +com.squareup.okio:okio-jvm:3.9.1 +com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:converter-scalars:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.6.0 -io.coil-kt:coil-compose-base:2.6.0 -io.coil-kt:coil-compose:2.6.0 -io.coil-kt:coil:2.6.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 +io.coil-kt.coil3:coil-android:3.0.4 +io.coil-kt.coil3:coil-compose-android:3.0.4 +io.coil-kt.coil3:coil-compose-core-android:3.0.4 +io.coil-kt.coil3:coil-compose-core:3.0.4 +io.coil-kt.coil3:coil-compose:3.0.4 +io.coil-kt.coil3:coil-core-android:3.0.4 +io.coil-kt.coil3:coil-core:3.0.4 +io.coil-kt.coil3:coil:3.0.4 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 junit:junit:4.13.2 org.hamcrest:hamcrest-core:1.3 +org.jetbrains.compose.animation:animation-core:1.7.0-rc01 +org.jetbrains.compose.animation:animation:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime:1.7.0-rc01 +org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 +org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 +org.jetbrains.compose.ui:ui-text:1.7.0-rc01 +org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 +org.jetbrains.compose.ui:ui-util:1.7.0-rc01 +org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/mifosng-android/dependencies/demoReleaseCompileClasspath.txt b/mifosng-android/dependencies/demoReleaseCompileClasspath.txt index 257d4a83ff..88c5ddbe51 100644 --- a/mifosng-android/dependencies/demoReleaseCompileClasspath.txt +++ b/mifosng-android/dependencies/demoReleaseCompileClasspath.txt @@ -1,6 +1,6 @@ -androidx.activity:activity-compose:1.9.3 -androidx.activity:activity-ktx:1.9.3 -androidx.activity:activity:1.9.3 +androidx.activity:activity-compose:1.10.0 +androidx.activity:activity-ktx:1.10.0 +androidx.activity:activity:1.10.0 androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 @@ -54,7 +54,7 @@ androidx.compose.ui:ui-unit:1.7.6 androidx.compose.ui:ui-util-android:1.7.6 androidx.compose.ui:ui-util:1.7.6 androidx.compose.ui:ui:1.7.6 -androidx.compose:compose-bom:2024.12.01 +androidx.compose:compose-bom:2025.01.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-solver:2.0.1 androidx.constraintlayout:constraintlayout:2.0.1 @@ -123,7 +123,7 @@ androidx.paging:paging-runtime:3.3.5 androidx.preference:preference-ktx:1.2.1 androidx.preference:preference:1.2.1 androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 +androidx.recyclerview:recyclerview:1.4.0 androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 @@ -142,6 +142,8 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 com.evernote:android-job:1.2.6 com.facebook.stetho:stetho-okhttp3:1.3.1 com.facebook.stetho:stetho:1.3.1 @@ -177,23 +179,41 @@ com.joanzapata.iconify:android-iconify-material:2.2.2 com.joanzapata.iconify:android-iconify:2.2.2 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.8.0 -com.squareup.okio:okio:3.8.0 +com.squareup.okio:okio-jvm:3.9.1 +com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:converter-scalars:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.6.0 -io.coil-kt:coil-compose-base:2.6.0 -io.coil-kt:coil-compose:2.6.0 -io.coil-kt:coil:2.6.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 +io.coil-kt.coil3:coil-android:3.0.4 +io.coil-kt.coil3:coil-compose-android:3.0.4 +io.coil-kt.coil3:coil-compose-core-android:3.0.4 +io.coil-kt.coil3:coil-compose-core:3.0.4 +io.coil-kt.coil3:coil-compose:3.0.4 +io.coil-kt.coil3:coil-core-android:3.0.4 +io.coil-kt.coil3:coil-core:3.0.4 +io.coil-kt.coil3:coil:3.0.4 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 junit:junit:4.13.2 org.hamcrest:hamcrest-core:1.3 +org.jetbrains.compose.animation:animation-core:1.7.0-rc01 +org.jetbrains.compose.animation:animation:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime:1.7.0-rc01 +org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 +org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 +org.jetbrains.compose.ui:ui-text:1.7.0-rc01 +org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 +org.jetbrains.compose.ui:ui-util:1.7.0-rc01 +org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/mifosng-android/dependencies/prodDebugCompileClasspath.txt b/mifosng-android/dependencies/prodDebugCompileClasspath.txt index ff66c0f566..657ca2727f 100644 --- a/mifosng-android/dependencies/prodDebugCompileClasspath.txt +++ b/mifosng-android/dependencies/prodDebugCompileClasspath.txt @@ -1,6 +1,6 @@ -androidx.activity:activity-compose:1.9.3 -androidx.activity:activity-ktx:1.9.3 -androidx.activity:activity:1.9.3 +androidx.activity:activity-compose:1.10.0 +androidx.activity:activity-ktx:1.10.0 +androidx.activity:activity:1.10.0 androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 @@ -59,7 +59,7 @@ androidx.compose.ui:ui-unit:1.7.6 androidx.compose.ui:ui-util-android:1.7.6 androidx.compose.ui:ui-util:1.7.6 androidx.compose.ui:ui:1.7.6 -androidx.compose:compose-bom:2024.12.01 +androidx.compose:compose-bom:2025.01.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-solver:2.0.1 androidx.constraintlayout:constraintlayout:2.0.1 @@ -128,7 +128,7 @@ androidx.paging:paging-runtime:3.3.5 androidx.preference:preference-ktx:1.2.1 androidx.preference:preference:1.2.1 androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 +androidx.recyclerview:recyclerview:1.4.0 androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 @@ -147,6 +147,8 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 com.evernote:android-job:1.2.6 com.facebook.stetho:stetho-okhttp3:1.3.1 com.facebook.stetho:stetho:1.3.1 @@ -182,23 +184,41 @@ com.joanzapata.iconify:android-iconify-material:2.2.2 com.joanzapata.iconify:android-iconify:2.2.2 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.8.0 -com.squareup.okio:okio:3.8.0 +com.squareup.okio:okio-jvm:3.9.1 +com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:converter-scalars:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.6.0 -io.coil-kt:coil-compose-base:2.6.0 -io.coil-kt:coil-compose:2.6.0 -io.coil-kt:coil:2.6.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 +io.coil-kt.coil3:coil-android:3.0.4 +io.coil-kt.coil3:coil-compose-android:3.0.4 +io.coil-kt.coil3:coil-compose-core-android:3.0.4 +io.coil-kt.coil3:coil-compose-core:3.0.4 +io.coil-kt.coil3:coil-compose:3.0.4 +io.coil-kt.coil3:coil-core-android:3.0.4 +io.coil-kt.coil3:coil-core:3.0.4 +io.coil-kt.coil3:coil:3.0.4 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 junit:junit:4.13.2 org.hamcrest:hamcrest-core:1.3 +org.jetbrains.compose.animation:animation-core:1.7.0-rc01 +org.jetbrains.compose.animation:animation:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime:1.7.0-rc01 +org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 +org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 +org.jetbrains.compose.ui:ui-text:1.7.0-rc01 +org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 +org.jetbrains.compose.ui:ui-util:1.7.0-rc01 +org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/mifosng-android/dependencies/prodReleaseCompileClasspath.txt b/mifosng-android/dependencies/prodReleaseCompileClasspath.txt index 257d4a83ff..88c5ddbe51 100644 --- a/mifosng-android/dependencies/prodReleaseCompileClasspath.txt +++ b/mifosng-android/dependencies/prodReleaseCompileClasspath.txt @@ -1,6 +1,6 @@ -androidx.activity:activity-compose:1.9.3 -androidx.activity:activity-ktx:1.9.3 -androidx.activity:activity:1.9.3 +androidx.activity:activity-compose:1.10.0 +androidx.activity:activity-ktx:1.10.0 +androidx.activity:activity:1.10.0 androidx.annotation:annotation-experimental:1.4.1 androidx.annotation:annotation-jvm:1.9.1 androidx.annotation:annotation:1.9.1 @@ -54,7 +54,7 @@ androidx.compose.ui:ui-unit:1.7.6 androidx.compose.ui:ui-util-android:1.7.6 androidx.compose.ui:ui-util:1.7.6 androidx.compose.ui:ui:1.7.6 -androidx.compose:compose-bom:2024.12.01 +androidx.compose:compose-bom:2025.01.00 androidx.concurrent:concurrent-futures:1.1.0 androidx.constraintlayout:constraintlayout-solver:2.0.1 androidx.constraintlayout:constraintlayout:2.0.1 @@ -123,7 +123,7 @@ androidx.paging:paging-runtime:3.3.5 androidx.preference:preference-ktx:1.2.1 androidx.preference:preference:1.2.1 androidx.print:print:1.0.0 -androidx.recyclerview:recyclerview:1.3.2 +androidx.recyclerview:recyclerview:1.4.0 androidx.resourceinspection:resourceinspection-annotation:1.0.1 androidx.savedstate:savedstate-ktx:1.2.1 androidx.savedstate:savedstate:1.2.1 @@ -142,6 +142,8 @@ androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager2:viewpager2:1.1.0-beta02 androidx.viewpager:viewpager:1.0.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 com.evernote:android-job:1.2.6 com.facebook.stetho:stetho-okhttp3:1.3.1 com.facebook.stetho:stetho:1.3.1 @@ -177,23 +179,41 @@ com.joanzapata.iconify:android-iconify-material:2.2.2 com.joanzapata.iconify:android-iconify:2.2.2 com.squareup.okhttp3:logging-interceptor:4.12.0 com.squareup.okhttp3:okhttp:4.12.0 -com.squareup.okio:okio-jvm:3.8.0 -com.squareup.okio:okio:3.8.0 +com.squareup.okio:okio-jvm:3.9.1 +com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:adapter-rxjava:2.9.0 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:converter-scalars:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 commons-cli:commons-cli:1.2 -io.coil-kt:coil-base:2.6.0 -io.coil-kt:coil-compose-base:2.6.0 -io.coil-kt:coil-compose:2.6.0 -io.coil-kt:coil:2.6.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 +io.coil-kt.coil3:coil-android:3.0.4 +io.coil-kt.coil3:coil-compose-android:3.0.4 +io.coil-kt.coil3:coil-compose-core-android:3.0.4 +io.coil-kt.coil3:coil-compose-core:3.0.4 +io.coil-kt.coil3:coil-compose:3.0.4 +io.coil-kt.coil3:coil-core-android:3.0.4 +io.coil-kt.coil3:coil-core:3.0.4 +io.coil-kt.coil3:coil:3.0.4 io.reactivex:rxandroid:1.1.0 io.reactivex:rxjava:1.3.8 jakarta.inject:jakarta.inject-api:2.0.1 javax.inject:javax.inject:1 junit:junit:4.13.2 org.hamcrest:hamcrest-core:1.3 +org.jetbrains.compose.animation:animation-core:1.7.0-rc01 +org.jetbrains.compose.animation:animation:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation-layout:1.7.0-rc01 +org.jetbrains.compose.foundation:foundation:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 +org.jetbrains.compose.runtime:runtime:1.7.0-rc01 +org.jetbrains.compose.ui:ui-geometry:1.7.0-rc01 +org.jetbrains.compose.ui:ui-graphics:1.7.0-rc01 +org.jetbrains.compose.ui:ui-text:1.7.0-rc01 +org.jetbrains.compose.ui:ui-unit:1.7.0-rc01 +org.jetbrains.compose.ui:ui-util:1.7.0-rc01 +org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.1.0 org.jetbrains.kotlin:kotlin-stdlib:2.1.0