diff --git a/androidApp/dependencies/demoDebugRuntimeClasspath.txt b/androidApp/dependencies/demoDebugRuntimeClasspath.txt index be9a20c67..8a10a41f0 100644 --- a/androidApp/dependencies/demoDebugRuntimeClasspath.txt +++ b/androidApp/dependencies/demoDebugRuntimeClasspath.txt @@ -127,6 +127,8 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window:1.2.0 app.cash.turbine:turbine-jvm:1.1.0 app.cash.turbine:turbine:1.1.0 co.touchlab:kermit-android-debug:2.0.4 @@ -139,7 +141,12 @@ co.touchlab:stately-concurrent-collections-jvm:2.1.0 co.touchlab:stately-concurrent-collections:2.1.0 co.touchlab:stately-strict-jvm:2.1.0 co.touchlab:stately-strict:2.1.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 +com.arkivanov.essenty:utils-internal-android:2.1.0 +com.arkivanov.essenty:utils-internal:2.1.0 com.caverock:androidsvg-aar:1.4 +com.google.accompanist:accompanist-drawablepainter:0.36.0 com.google.accompanist:accompanist-pager:0.34.0 com.google.accompanist:accompanist-permissions:0.34.0 com.google.android.gms:play-services-ads-identifier:18.0.0 @@ -193,8 +200,12 @@ com.squareup.okio:okio-jvm:3.9.1 com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 dev.chrisbanes.snapper:snapper:0.2.2 io.coil-kt.coil3:coil-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-core-android:3.0.4 io.coil-kt.coil3:coil-core:3.0.4 io.coil-kt.coil3:coil-network-core-android:3.0.4 @@ -255,10 +266,15 @@ org.jetbrains.compose.animation:animation-core:1.7.0-rc01 org.jetbrains.compose.animation:animation:1.7.0-rc01 org.jetbrains.compose.annotation-internal:annotation:1.7.3 org.jetbrains.compose.collection-internal:collection:1.7.3 +org.jetbrains.compose.components:components-resources-android:1.7.0-rc01 +org.jetbrains.compose.components:components-resources:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview-android:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview: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.material3:material3:1.7.0-rc01 org.jetbrains.compose.material:material-icons-core:1.7.0-rc01 +org.jetbrains.compose.material:material-icons-extended:1.7.0-rc01 org.jetbrains.compose.material:material-ripple:1.7.0-rc01 org.jetbrains.compose.material:material:1.7.0-rc01 org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 @@ -272,7 +288,6 @@ org.jetbrains.compose.ui:ui-util:1.7.0-rc01 org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.1.0 org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 -org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.23 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/androidApp/dependencies/demoReleaseRuntimeClasspath.txt b/androidApp/dependencies/demoReleaseRuntimeClasspath.txt index 8f6afb06d..96c9c7de9 100644 --- a/androidApp/dependencies/demoReleaseRuntimeClasspath.txt +++ b/androidApp/dependencies/demoReleaseRuntimeClasspath.txt @@ -122,6 +122,8 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window:1.2.0 app.cash.turbine:turbine-jvm:1.1.0 app.cash.turbine:turbine:1.1.0 co.touchlab:kermit-android:2.0.4 @@ -134,7 +136,12 @@ co.touchlab:stately-concurrent-collections-jvm:2.1.0 co.touchlab:stately-concurrent-collections:2.1.0 co.touchlab:stately-strict-jvm:2.1.0 co.touchlab:stately-strict:2.1.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 +com.arkivanov.essenty:utils-internal-android:2.1.0 +com.arkivanov.essenty:utils-internal:2.1.0 com.caverock:androidsvg-aar:1.4 +com.google.accompanist:accompanist-drawablepainter:0.36.0 com.google.accompanist:accompanist-pager:0.34.0 com.google.accompanist:accompanist-permissions:0.34.0 com.google.android.gms:play-services-ads-identifier:18.0.0 @@ -188,8 +195,12 @@ com.squareup.okio:okio-jvm:3.9.1 com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 dev.chrisbanes.snapper:snapper:0.2.2 io.coil-kt.coil3:coil-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-core-android:3.0.4 io.coil-kt.coil3:coil-core:3.0.4 io.coil-kt.coil3:coil-network-core-android:3.0.4 @@ -250,10 +261,15 @@ org.jetbrains.compose.animation:animation-core:1.7.0-rc01 org.jetbrains.compose.animation:animation:1.7.0-rc01 org.jetbrains.compose.annotation-internal:annotation:1.7.3 org.jetbrains.compose.collection-internal:collection:1.7.3 +org.jetbrains.compose.components:components-resources-android:1.7.0-rc01 +org.jetbrains.compose.components:components-resources:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview-android:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview: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.material3:material3:1.7.0-rc01 org.jetbrains.compose.material:material-icons-core:1.7.0-rc01 +org.jetbrains.compose.material:material-icons-extended:1.7.0-rc01 org.jetbrains.compose.material:material-ripple:1.7.0-rc01 org.jetbrains.compose.material:material:1.7.0-rc01 org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 @@ -267,7 +283,6 @@ org.jetbrains.compose.ui:ui-util:1.7.0-rc01 org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.1.0 org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 -org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.23 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/androidApp/dependencies/prodDebugRuntimeClasspath.txt b/androidApp/dependencies/prodDebugRuntimeClasspath.txt index be9a20c67..8a10a41f0 100644 --- a/androidApp/dependencies/prodDebugRuntimeClasspath.txt +++ b/androidApp/dependencies/prodDebugRuntimeClasspath.txt @@ -127,6 +127,8 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window:1.2.0 app.cash.turbine:turbine-jvm:1.1.0 app.cash.turbine:turbine:1.1.0 co.touchlab:kermit-android-debug:2.0.4 @@ -139,7 +141,12 @@ co.touchlab:stately-concurrent-collections-jvm:2.1.0 co.touchlab:stately-concurrent-collections:2.1.0 co.touchlab:stately-strict-jvm:2.1.0 co.touchlab:stately-strict:2.1.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 +com.arkivanov.essenty:utils-internal-android:2.1.0 +com.arkivanov.essenty:utils-internal:2.1.0 com.caverock:androidsvg-aar:1.4 +com.google.accompanist:accompanist-drawablepainter:0.36.0 com.google.accompanist:accompanist-pager:0.34.0 com.google.accompanist:accompanist-permissions:0.34.0 com.google.android.gms:play-services-ads-identifier:18.0.0 @@ -193,8 +200,12 @@ com.squareup.okio:okio-jvm:3.9.1 com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 dev.chrisbanes.snapper:snapper:0.2.2 io.coil-kt.coil3:coil-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-core-android:3.0.4 io.coil-kt.coil3:coil-core:3.0.4 io.coil-kt.coil3:coil-network-core-android:3.0.4 @@ -255,10 +266,15 @@ org.jetbrains.compose.animation:animation-core:1.7.0-rc01 org.jetbrains.compose.animation:animation:1.7.0-rc01 org.jetbrains.compose.annotation-internal:annotation:1.7.3 org.jetbrains.compose.collection-internal:collection:1.7.3 +org.jetbrains.compose.components:components-resources-android:1.7.0-rc01 +org.jetbrains.compose.components:components-resources:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview-android:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview: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.material3:material3:1.7.0-rc01 org.jetbrains.compose.material:material-icons-core:1.7.0-rc01 +org.jetbrains.compose.material:material-icons-extended:1.7.0-rc01 org.jetbrains.compose.material:material-ripple:1.7.0-rc01 org.jetbrains.compose.material:material:1.7.0-rc01 org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 @@ -272,7 +288,6 @@ org.jetbrains.compose.ui:ui-util:1.7.0-rc01 org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.1.0 org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 -org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.23 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/androidApp/dependencies/prodReleaseRuntimeClasspath.txt b/androidApp/dependencies/prodReleaseRuntimeClasspath.txt index 8f6afb06d..96c9c7de9 100644 --- a/androidApp/dependencies/prodReleaseRuntimeClasspath.txt +++ b/androidApp/dependencies/prodReleaseRuntimeClasspath.txt @@ -122,6 +122,8 @@ androidx.vectordrawable:vectordrawable-animated:1.1.0 androidx.vectordrawable:vectordrawable:1.1.0 androidx.versionedparcelable:versionedparcelable:1.1.1 androidx.viewpager:viewpager:1.0.0 +androidx.window.extensions.core:core:1.0.0 +androidx.window:window:1.2.0 app.cash.turbine:turbine-jvm:1.1.0 app.cash.turbine:turbine:1.1.0 co.touchlab:kermit-android:2.0.4 @@ -134,7 +136,12 @@ co.touchlab:stately-concurrent-collections-jvm:2.1.0 co.touchlab:stately-concurrent-collections:2.1.0 co.touchlab:stately-strict-jvm:2.1.0 co.touchlab:stately-strict:2.1.0 +com.arkivanov.essenty:back-handler-android:2.1.0 +com.arkivanov.essenty:back-handler:2.1.0 +com.arkivanov.essenty:utils-internal-android:2.1.0 +com.arkivanov.essenty:utils-internal:2.1.0 com.caverock:androidsvg-aar:1.4 +com.google.accompanist:accompanist-drawablepainter:0.36.0 com.google.accompanist:accompanist-pager:0.34.0 com.google.accompanist:accompanist-permissions:0.34.0 com.google.android.gms:play-services-ads-identifier:18.0.0 @@ -188,8 +195,12 @@ com.squareup.okio:okio-jvm:3.9.1 com.squareup.okio:okio:3.9.1 com.squareup.retrofit2:converter-gson:2.11.0 com.squareup.retrofit2:retrofit:2.11.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform-android:0.5.0 +dev.chrisbanes.material3:material3-window-size-class-multiplatform:0.5.0 dev.chrisbanes.snapper:snapper:0.2.2 io.coil-kt.coil3:coil-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-core-android:3.0.4 io.coil-kt.coil3:coil-core:3.0.4 io.coil-kt.coil3:coil-network-core-android:3.0.4 @@ -250,10 +261,15 @@ org.jetbrains.compose.animation:animation-core:1.7.0-rc01 org.jetbrains.compose.animation:animation:1.7.0-rc01 org.jetbrains.compose.annotation-internal:annotation:1.7.3 org.jetbrains.compose.collection-internal:collection:1.7.3 +org.jetbrains.compose.components:components-resources-android:1.7.0-rc01 +org.jetbrains.compose.components:components-resources:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview-android:1.7.0-rc01 +org.jetbrains.compose.components:components-ui-tooling-preview: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.material3:material3:1.7.0-rc01 org.jetbrains.compose.material:material-icons-core:1.7.0-rc01 +org.jetbrains.compose.material:material-icons-extended:1.7.0-rc01 org.jetbrains.compose.material:material-ripple:1.7.0-rc01 org.jetbrains.compose.material:material:1.7.0-rc01 org.jetbrains.compose.runtime:runtime-saveable:1.7.0-rc01 @@ -267,7 +283,6 @@ org.jetbrains.compose.ui:ui-util:1.7.0-rc01 org.jetbrains.compose.ui:ui:1.7.0-rc01 org.jetbrains.kotlin:kotlin-android-extensions-runtime:2.1.0 org.jetbrains.kotlin:kotlin-parcelize-runtime:2.1.0 -org.jetbrains.kotlin:kotlin-stdlib-common:2.1.0 org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.23 org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.22 org.jetbrains.kotlin:kotlin-stdlib:2.1.0 diff --git a/core/designsystem/build.gradle.kts b/core/designsystem/build.gradle.kts index fe662c31a..d5dbbb23f 100644 --- a/core/designsystem/build.gradle.kts +++ b/core/designsystem/build.gradle.kts @@ -8,8 +8,10 @@ * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ plugins { - alias(libs.plugins.mifos.android.library) - alias(libs.plugins.mifos.android.library.compose) + alias(libs.plugins.mifos.kmp.library) + alias(libs.plugins.jetbrainsCompose) + alias(libs.plugins.compose.compiler) + alias(libs.plugins.roborazzi) } android { @@ -19,19 +21,48 @@ android { namespace = "org.mifos.mobile.core.designsystem" } -dependencies { - api(libs.androidx.compose.ui) - 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) +kotlin { + sourceSets { + androidMain.dependencies { + implementation(libs.androidx.compose.ui.tooling.preview) + implementation(libs.androidx.activity.compose) + implementation(projects.core.model) + } + 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) + } - // Accompanist Pager Library - implementation(libs.accompanist.pager) + nativeMain.dependencies { + implementation(compose.runtime) + } - testImplementation(libs.androidx.compose.ui.test) - androidTestImplementation(libs.androidx.compose.ui.test) + jsMain.dependencies { + implementation(compose.runtime) + } + + wasmJsMain.dependencies { + implementation(compose.runtime) + } + } +} + +compose.resources { + publicResClass = true + generateResClass = always } 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/androidMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.android.kt b/core/designsystem/src/androidMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.android.kt new file mode 100644 index 000000000..267dbff8b --- /dev/null +++ b/core/designsystem/src/androidMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/commonMain/composeResources/font/poppins_black.ttf b/core/designsystem/src/commonMain/composeResources/font/poppins_black.ttf new file mode 100644 index 000000000..71c0f995e 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 000000000..00559eeb2 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 000000000..df7093608 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 000000000..e76ec69a6 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 000000000..bc36bcc24 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 000000000..6bcdcc27f 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 000000000..9f0c71b70 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 000000000..74c726e32 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 000000000..03e736613 Binary files /dev/null and b/core/designsystem/src/commonMain/composeResources/font/poppins_thin.ttf differ diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosAlertDialog.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosAlertDialog.kt new file mode 100644 index 000000000..f29294345 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosBasicDialog.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosBasicDialog.kt new file mode 100644 index 000000000..ed55e3a5c --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme + +@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() { + MifosMobileTheme { + 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/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosBottomSheet.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosBottomSheet.kt new file mode 100644 index 000000000..c077caf9f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosBottomSheet.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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.designsystem.component + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.height +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ModalBottomSheet +import androidx.compose.material3.rememberModalBottomSheetState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +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.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( + onDismiss: () -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val coroutineScope = rememberCoroutineScope() + val modalSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) + var showBottomSheet by remember { mutableStateOf(true) } + + fun dismissSheet() { + coroutineScope.launch { modalSheetState.hide() }.invokeOnCompletion { + if (!modalSheetState.isVisible) { + showBottomSheet = false + } + } + onDismiss.invoke() + } + + BackCallback(modalSheetState.isVisible) { + dismissSheet() + } + + AnimatedVisibility(visible = showBottomSheet) { + ModalBottomSheet( + containerColor = Color.White, + onDismissRequest = { + showBottomSheet = false + dismissSheet() + }, + sheetState = modalSheetState, + modifier = modifier, + ) { + content() + } + } +} + +@Preview +@Composable +fun MifosBottomSheetPreview() { + MifosBottomSheet( + content = { + Box { + Modifier.height(100.dp) + } + }, + onDismiss = {}, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosButton.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosButton.kt new file mode 100644 index 000000000..33eacf63d --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/component/MifosCard.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosCard.kt new file mode 100644 index 000000000..b329ce70a --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosLoadingDialog.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosLoadingDialog.kt new file mode 100644 index 000000000..71614a3b8 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme + +@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() { + MifosMobileTheme { + 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/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosNavigation.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosNavigation.kt new file mode 100644 index 000000000..5e1600be0 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.icon.MifosIcons +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme + +/** + * 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, + ) + + MifosMobileTheme { + 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, + ) + + MifosMobileTheme { + 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/org/mifos/mobile/core/designsystem/component/MifosOtpTextField.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosOtpTextField.kt new file mode 100644 index 000000000..da22af6c6 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosOtpTextField.kt @@ -0,0 +1,144 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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.graphics.Color +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 erro message in text + Text( + text = "Invalid OTP", + style = MaterialTheme.typography.bodyMedium, + color = Color.Red, + 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) { + Color.DarkGray + } else { + Color.LightGray + }, + textAlign = TextAlign.Center, + ) +} + +@Preview +@Composable +private fun PreviewOtpTextField() { + MifosOtpTextField( + onOtpTextCorrectlyEntered = {}, + realOtp = "1234", + otpCount = 4, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosPasswordField.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosPasswordField.kt new file mode 100644 index 000000000..666b7ca1f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.icon.MifosIcons +import org.mifos.mobile.core.designsystem.utils.nonLetterColorVisualTransformation +import org.mifos.mobile.core.designsystem.utils.tabNavigation + +@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/kotlin/org/mifos/mobile/core/designsystem/components/MifosRadioButton.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosRadioButton.kt similarity index 80% rename from core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosRadioButton.kt rename to core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosRadioButton.kt index 548df9b1c..4351de758 100644 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosRadioButton.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosRadioButton.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ -package org.mifos.mobile.core.designsystem.components +package org.mifos.mobile.core.designsystem.component import androidx.compose.foundation.layout.Row import androidx.compose.material3.RadioButton @@ -15,15 +15,14 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.tooling.preview.Preview +import org.jetbrains.compose.ui.tooling.preview.Preview import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme @Composable fun MifosRadioButton( + label: String, selected: Boolean, onClick: () -> Unit, - textResId: Int, modifier: Modifier = Modifier, ) { Row( @@ -34,18 +33,18 @@ fun MifosRadioButton( selected = selected, onClick = onClick, ) - Text(text = stringResource(id = textResId)) + Text(text = label) } } -@Preview(showSystemUi = true) +@Preview @Composable private fun MifosRadioButtonPreview() { MifosMobileTheme { MifosRadioButton( + label = "Test Label", selected = false, onClick = {}, - textResId = 1, ) } } diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosScaffold.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosScaffold.kt new file mode 100644 index 000000000..f3910b295 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 = Color.Transparent, + 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 = Color.Transparent, + 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/kotlin/org/mifos/mobile/core/designsystem/components/MifosSearchTextField.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosSearchTextField.kt similarity index 96% rename from core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosSearchTextField.kt rename to core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosSearchTextField.kt index ab7fba6af..a45efcf35 100644 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosSearchTextField.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosSearchTextField.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ -package org.mifos.mobile.core.designsystem.components +package org.mifos.mobile.core.designsystem.component import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.isSystemInDarkTheme @@ -28,7 +28,6 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.tooling.preview.Preview import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme @Composable @@ -82,7 +81,6 @@ fun MifosSearchTextField( ) } -@Preview @Composable private fun MifosSearchTextFieldPreview( modifier: Modifier = Modifier, diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTab.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTab.kt new file mode 100644 index 000000000..7d35bc2e3 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/kotlin/org/mifos/mobile/core/designsystem/components/MifosTabPager.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTabPager.kt similarity index 72% rename from core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosTabPager.kt rename to core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTabPager.kt index f7d16f63e..cf7419fb1 100644 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosTabPager.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTabPager.kt @@ -7,13 +7,13 @@ * * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ -package org.mifos.mobile.core.designsystem.components +package org.mifos.mobile.core.designsystem.component -import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding -import androidx.compose.material3.MaterialTheme +import androidx.compose.foundation.pager.HorizontalPager +import androidx.compose.foundation.pager.PagerState import androidx.compose.material3.Tab import androidx.compose.material3.TabRow import androidx.compose.material3.TabRowDefaults @@ -21,12 +21,8 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset 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.google.accompanist.pager.HorizontalPager -import com.google.accompanist.pager.PagerState -@Suppress("DEPRECATION") @Composable fun MifosTabPager( pagerState: PagerState, @@ -40,22 +36,17 @@ fun MifosTabPager( TabRow( modifier = Modifier.fillMaxWidth(), selectedTabIndex = currentPage, - containerColor = MaterialTheme.colorScheme.background, - contentColor = MaterialTheme.colorScheme.surfaceTint, indicator = { tabPositions -> TabRowDefaults.SecondaryIndicator( modifier = Modifier .tabIndicatorOffset(tabPositions[currentPage]) .padding(start = 36.dp, end = 36.dp), - color = MaterialTheme.colorScheme.surfaceTint, ) }, ) { tabs.forEachIndexed { index, tabTitle -> Tab( modifier = Modifier.padding(all = 16.dp), - selectedContentColor = MaterialTheme.colorScheme.surfaceTint, - unselectedContentColor = if (isSystemInDarkTheme()) Color.White else Color.Black, selected = currentPage == index, onClick = { setCurrentPage(index) }, ) { @@ -66,9 +57,8 @@ fun MifosTabPager( HorizontalPager( state = pagerState, - count = tabs.size, modifier = Modifier.fillMaxWidth(), - content = { page -> + pageContent = { page -> content(page) }, ) diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTextField.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTextField.kt new file mode 100644 index 000000000..49c190f29 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 org.mifos.mobile.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/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTopAppBar.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTopAppBar.kt new file mode 100644 index 000000000..6d8fd4abf --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 org.jetbrains.compose.ui.tooling.preview.Preview +import org.mifos.mobile.core.designsystem.icon.MifosIcons +import org.mifos.mobile.core.designsystem.theme.MifosMobileTheme +import org.mifos.mobile.core.designsystem.utils.mirrorIfRtl + +@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() { + MifosMobileTheme { + MifosTopAppBar( + title = "Title", + scrollBehavior = TopAppBarDefaults + .exitUntilCollapsedScrollBehavior( + rememberTopAppBarState(), + ), + navigationIcon = NavigationIcon( + navigationIcon = MifosIcons.Back, + navigationIconContentDescription = "Back", + onNavigationIconClick = { }, + ), + ) + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Preview +@Composable +private fun MifosTopAppBarOverflow_preview() { + MifosMobileTheme { + 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() { + MifosMobileTheme { + 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/org/mifos/mobile/core/designsystem/component/MifosTopBar.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTopBar.kt new file mode 100644 index 000000000..d2763a12f --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/MifosTopBar.kt @@ -0,0 +1,56 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 androidx.compose.ui.graphics.Color +import org.mifos.mobile.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 = Color.Transparent, + ), + actions = actions, + modifier = modifier, + ) +} diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/AppScrollbars.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/AppScrollbars.kt new file mode 100644 index 000000000..805bc66b5 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/AppScrollbars.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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 kotlinx.coroutines.delay +import org.mifos.mobile.core.designsystem.component.scrollbar.ThumbState.Active +import org.mifos.mobile.core.designsystem.component.scrollbar.ThumbState.Dormant +import org.mifos.mobile.core.designsystem.component.scrollbar.ThumbState.Inactive + +/** + * 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/org/mifos/mobile/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/LazyScrollbarUtilities.kt new file mode 100644 index 000000000..7ab10c674 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/component/scrollbar/Scrollbar.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/Scrollbar.kt new file mode 100644 index 000000000..5b70290a4 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/component/scrollbar/ScrollbarExt.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/ScrollbarExt.kt new file mode 100644 index 000000000..e74b46ca7 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/component/scrollbar/ThumbExt.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/component/scrollbar/ThumbExt.kt new file mode 100644 index 000000000..13c071adb --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/main/kotlin/org/mifos/mobile/core/designsystem/icons/MifosIcons.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt similarity index 76% rename from core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/icons/MifosIcons.kt rename to core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt index 5738de8be..35e0085e1 100644 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/icons/MifosIcons.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/icon/MifosIcons.kt @@ -7,7 +7,7 @@ * * See https://github.com/openMF/mobile-mobile/blob/master/LICENSE.md */ -package org.mifos.mobile.core.designsystem.icons +package org.mifos.mobile.core.designsystem.icon import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack @@ -16,11 +16,14 @@ 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.ChevronLeft +import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Edit import androidx.compose.material.icons.filled.Error import androidx.compose.material.icons.filled.FilterList @@ -41,7 +44,15 @@ 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.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.Home +import androidx.compose.material.icons.rounded.SwapHoriz import androidx.compose.ui.graphics.vector.ImageVector object MifosIcons { @@ -66,6 +77,9 @@ object MifosIcons { val Info: ImageVector = Icons.Default.Info val ArrowDropUp: ImageVector = Icons.Default.ArrowDropUp val ArrowDropDown: ImageVector = Icons.Default.ArrowDropDown + val Close: ImageVector = Icons.Filled.Close + 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 @@ -77,4 +91,14 @@ object MifosIcons { val Error = Icons.Filled.Error val Notifications = Icons.Filled.Notifications val NavigationDrawer = Icons.Default.Menu + + // 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 } diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/BackgroundTheme.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/BackgroundTheme.kt similarity index 100% rename from core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/BackgroundTheme.kt rename to core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/BackgroundTheme.kt diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/Color.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/Color.kt new file mode 100644 index 000000000..1e070e4c6 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/Color.kt @@ -0,0 +1,228 @@ +/* + * 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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.core.designsystem.theme + +import androidx.compose.ui.graphics.Color + +val primaryLight = Color(0xFF4A0059) +val onPrimaryLight = Color(0xFFFFFFFF) +val primaryContainerLight = Color(0xFF791C8C) +val onPrimaryContainerLight = Color(0xFFFFD9FF) +val secondaryLight = Color(0xFF78507C) +val onSecondaryLight = Color(0xFFFFFFFF) +val secondaryContainerLight = Color(0xFFFED0FF) +val onSecondaryContainerLight = Color(0xFF5D3862) +val tertiaryLight = Color(0xFF57002B) +val onTertiaryLight = Color(0xFFFFFFFF) +val tertiaryContainerLight = Color(0xFF8D174D) +val onTertiaryContainerLight = Color(0xFFFFDDE5) +val errorLight = Color(0xFFBA1A1A) +val onErrorLight = Color(0xFFFFFFFF) +val errorContainerLight = Color(0xFFFFDAD6) +val onErrorContainerLight = Color(0xFF410002) +val backgroundLight = Color(0xFFFFF7FA) +val onBackgroundLight = Color(0xFF201920) +val surfaceLight = Color(0xFFFFF7FA) +val onSurfaceLight = Color(0xFF201920) +val surfaceVariantLight = Color(0xFFF0DDED) +val onSurfaceVariantLight = Color(0xFF50434F) +val outlineLight = Color(0xFF827281) +val outlineVariantLight = Color(0xFFD3C1D1) +val scrimLight = Color(0xFF000000) +val inverseSurfaceLight = Color(0xFF362E35) +val inverseOnSurfaceLight = Color(0xFFFBEDF7) +val inversePrimaryLight = Color(0xFFF7ACFF) +val surfaceDimLight = Color(0xFFE4D6E0) +val surfaceBrightLight = Color(0xFFFFF7FA) +val surfaceContainerLowestLight = Color(0xFFFFFFFF) +val surfaceContainerLowLight = Color(0xFFFEF0FA) +val surfaceContainerLight = Color(0xFFF8EAF4) +val surfaceContainerHighLight = Color(0xFFF2E4EE) +val surfaceContainerHighestLight = Color(0xFFECDFE9) + +val primaryLightMediumContrast = Color(0xFF4A0059) +val onPrimaryLightMediumContrast = Color(0xFFFFFFFF) +val primaryContainerLightMediumContrast = Color(0xFF791C8C) +val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val secondaryLightMediumContrast = Color(0xFF5A355F) +val onSecondaryLightMediumContrast = Color(0xFFFFFFFF) +val secondaryContainerLightMediumContrast = Color(0xFF906694) +val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryLightMediumContrast = Color(0xFF57002B) +val onTertiaryLightMediumContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightMediumContrast = Color(0xFF8D174D) +val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF) +val errorLightMediumContrast = Color(0xFF8C0009) +val onErrorLightMediumContrast = Color(0xFFFFFFFF) +val errorContainerLightMediumContrast = Color(0xFFDA342E) +val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF) +val backgroundLightMediumContrast = Color(0xFFFFF7FA) +val onBackgroundLightMediumContrast = Color(0xFF201920) +val surfaceLightMediumContrast = Color(0xFFFFF7FA) +val onSurfaceLightMediumContrast = Color(0xFF201920) +val surfaceVariantLightMediumContrast = Color(0xFFF0DDED) +val onSurfaceVariantLightMediumContrast = Color(0xFF4C3F4B) +val outlineLightMediumContrast = Color(0xFF695B68) +val outlineVariantLightMediumContrast = Color(0xFF857684) +val scrimLightMediumContrast = Color(0xFF000000) +val inverseSurfaceLightMediumContrast = Color(0xFF362E35) +val inverseOnSurfaceLightMediumContrast = Color(0xFFFBEDF7) +val inversePrimaryLightMediumContrast = Color(0xFFF7ACFF) +val surfaceDimLightMediumContrast = Color(0xFFE4D6E0) +val surfaceBrightLightMediumContrast = Color(0xFFFFF7FA) +val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightMediumContrast = Color(0xFFFEF0FA) +val surfaceContainerLightMediumContrast = Color(0xFFF8EAF4) +val surfaceContainerHighLightMediumContrast = Color(0xFFF2E4EE) +val surfaceContainerHighestLightMediumContrast = Color(0xFFECDFE9) + +val primaryLightHighContrast = Color(0xFF40004D) +val onPrimaryLightHighContrast = Color(0xFFFFFFFF) +val primaryContainerLightHighContrast = Color(0xFF711084) +val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF) +val secondaryLightHighContrast = Color(0xFF36143C) +val onSecondaryLightHighContrast = Color(0xFFFFFFFF) +val secondaryContainerLightHighContrast = Color(0xFF5A355F) +val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF) +val tertiaryLightHighContrast = Color(0xFF4B0024) +val onTertiaryLightHighContrast = Color(0xFFFFFFFF) +val tertiaryContainerLightHighContrast = Color(0xFF830C46) +val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF) +val errorLightHighContrast = Color(0xFF4E0002) +val onErrorLightHighContrast = Color(0xFFFFFFFF) +val errorContainerLightHighContrast = Color(0xFF8C0009) +val onErrorContainerLightHighContrast = Color(0xFFFFFFFF) +val backgroundLightHighContrast = Color(0xFFFFF7FA) +val onBackgroundLightHighContrast = Color(0xFF201920) +val surfaceLightHighContrast = Color(0xFFFFF7FA) +val onSurfaceLightHighContrast = Color(0xFF000000) +val surfaceVariantLightHighContrast = Color(0xFFF0DDED) +val onSurfaceVariantLightHighContrast = Color(0xFF2B202C) +val outlineLightHighContrast = Color(0xFF4C3F4B) +val outlineVariantLightHighContrast = Color(0xFF4C3F4B) +val scrimLightHighContrast = Color(0xFF000000) +val inverseSurfaceLightHighContrast = Color(0xFF362E35) +val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF) +val inversePrimaryLightHighContrast = Color(0xFFFFE4FD) +val surfaceDimLightHighContrast = Color(0xFFE4D6E0) +val surfaceBrightLightHighContrast = Color(0xFFFFF7FA) +val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF) +val surfaceContainerLowLightHighContrast = Color(0xFFFEF0FA) +val surfaceContainerLightHighContrast = Color(0xFFF8EAF4) +val surfaceContainerHighLightHighContrast = Color(0xFFF2E4EE) +val surfaceContainerHighestLightHighContrast = Color(0xFFECDFE9) + +val primaryDark = Color(0xFFF7ACFF) +val onPrimaryDark = Color(0xFF560067) +val primaryContainerDark = Color(0xFF5B006D) +val onPrimaryContainerDark = Color(0xFFF6A6FF) +val secondaryDark = Color(0xFFE7B7E9) +val onSecondaryDark = Color(0xFF46234B) +val secondaryContainerDark = Color(0xFF59355E) +val onSecondaryContainerDark = Color(0xFFFAC9FC) +val tertiaryDark = Color(0xFFFFB1C8) +val onTertiaryDark = Color(0xFF650033) +val tertiaryContainerDark = Color(0xFF690035) +val onTertiaryContainerDark = Color(0xFFFFA9C3) +val errorDark = Color(0xFFFFB4AB) +val onErrorDark = Color(0xFF690005) +val errorContainerDark = Color(0xFF93000A) +val onErrorContainerDark = Color(0xFFFFDAD6) +val backgroundDark = Color(0xFF181118) +val onBackgroundDark = Color(0xFFECDFE9) +val surfaceDark = Color(0xFF181118) +val onSurfaceDark = Color(0xFFECDFE9) +val surfaceVariantDark = Color(0xFF50434F) +val onSurfaceVariantDark = Color(0xFFD3C1D1) +val outlineDark = Color(0xFF9C8C9A) +val outlineVariantDark = Color(0xFF50434F) +val scrimDark = Color(0xFF000000) +val inverseSurfaceDark = Color(0xFFECDFE9) +val inverseOnSurfaceDark = Color(0xFF362E35) +val inversePrimaryDark = Color(0xFF9135A3) +val surfaceDimDark = Color(0xFF181118) +val surfaceBrightDark = Color(0xFF3F373E) +val surfaceContainerLowestDark = Color(0xFF120C13) +val surfaceContainerLowDark = Color(0xFF201920) +val surfaceContainerDark = Color(0xFF241D24) +val surfaceContainerHighDark = Color(0xFF2F282F) +val surfaceContainerHighestDark = Color(0xFF3A323A) + +val primaryDarkMediumContrast = Color(0xFFF8B3FF) +val onPrimaryDarkMediumContrast = Color(0xFF2C0036) +val primaryContainerDarkMediumContrast = Color(0xFFCA6BDA) +val onPrimaryContainerDarkMediumContrast = Color(0xFF000000) +val secondaryDarkMediumContrast = Color(0xFFEBBBEE) +val onSecondaryDarkMediumContrast = Color(0xFF28072F) +val secondaryContainerDarkMediumContrast = Color(0xFFAE82B1) +val onSecondaryContainerDarkMediumContrast = Color(0xFF000000) +val tertiaryDarkMediumContrast = Color(0xFFFFB7CC) +val onTertiaryDarkMediumContrast = Color(0xFF350018) +val tertiaryContainerDarkMediumContrast = Color(0xFFE96294) +val onTertiaryContainerDarkMediumContrast = Color(0xFF000000) +val errorDarkMediumContrast = Color(0xFFFFBAB1) +val onErrorDarkMediumContrast = Color(0xFF370001) +val errorContainerDarkMediumContrast = Color(0xFFFF5449) +val onErrorContainerDarkMediumContrast = Color(0xFF000000) +val backgroundDarkMediumContrast = Color(0xFF181118) +val onBackgroundDarkMediumContrast = Color(0xFFECDFE9) +val surfaceDarkMediumContrast = Color(0xFF181118) +val onSurfaceDarkMediumContrast = Color(0xFFFFF9FA) +val surfaceVariantDarkMediumContrast = Color(0xFF50434F) +val onSurfaceVariantDarkMediumContrast = Color(0xFFD8C5D5) +val outlineDarkMediumContrast = Color(0xFFAF9EAD) +val outlineVariantDarkMediumContrast = Color(0xFF8E7E8D) +val scrimDarkMediumContrast = Color(0xFF000000) +val inverseSurfaceDarkMediumContrast = Color(0xFFECDFE9) +val inverseOnSurfaceDarkMediumContrast = Color(0xFF2F282F) +val inversePrimaryDarkMediumContrast = Color(0xFF77198A) +val surfaceDimDarkMediumContrast = Color(0xFF181118) +val surfaceBrightDarkMediumContrast = Color(0xFF3F373E) +val surfaceContainerLowestDarkMediumContrast = Color(0xFF120C13) +val surfaceContainerLowDarkMediumContrast = Color(0xFF201920) +val surfaceContainerDarkMediumContrast = Color(0xFF241D24) +val surfaceContainerHighDarkMediumContrast = Color(0xFF2F282F) +val surfaceContainerHighestDarkMediumContrast = Color(0xFF3A323A) + +val primaryDarkHighContrast = Color(0xFFFFF9FA) +val onPrimaryDarkHighContrast = Color(0xFF000000) +val primaryContainerDarkHighContrast = Color(0xFFF8B3FF) +val onPrimaryContainerDarkHighContrast = Color(0xFF000000) +val secondaryDarkHighContrast = Color(0xFFFFF9FA) +val onSecondaryDarkHighContrast = Color(0xFF000000) +val secondaryContainerDarkHighContrast = Color(0xFFEBBBEE) +val onSecondaryContainerDarkHighContrast = Color(0xFF000000) +val tertiaryDarkHighContrast = Color(0xFFFFF9F9) +val onTertiaryDarkHighContrast = Color(0xFF000000) +val tertiaryContainerDarkHighContrast = Color(0xFFFFB7CC) +val onTertiaryContainerDarkHighContrast = Color(0xFF000000) +val errorDarkHighContrast = Color(0xFFFFF9F9) +val onErrorDarkHighContrast = Color(0xFF000000) +val errorContainerDarkHighContrast = Color(0xFFFFBAB1) +val onErrorContainerDarkHighContrast = Color(0xFF000000) +val backgroundDarkHighContrast = Color(0xFF181118) +val onBackgroundDarkHighContrast = Color(0xFFECDFE9) +val surfaceDarkHighContrast = Color(0xFF181118) +val onSurfaceDarkHighContrast = Color(0xFFFFFFFF) +val surfaceVariantDarkHighContrast = Color(0xFF50434F) +val onSurfaceVariantDarkHighContrast = Color(0xFFFFF9FA) +val outlineDarkHighContrast = Color(0xFFD8C5D5) +val outlineVariantDarkHighContrast = Color(0xFFD8C5D5) +val scrimDarkHighContrast = Color(0xFF000000) +val inverseSurfaceDarkHighContrast = Color(0xFFECDFE9) +val inverseOnSurfaceDarkHighContrast = Color(0xFF000000) +val inversePrimaryDarkHighContrast = Color(0xFF4C005B) +val surfaceDimDarkHighContrast = Color(0xFF181118) +val surfaceBrightDarkHighContrast = Color(0xFF3F373E) +val surfaceContainerLowestDarkHighContrast = Color(0xFF120C13) +val surfaceContainerLowDarkHighContrast = Color(0xFF201920) +val surfaceContainerDarkHighContrast = Color(0xFF241D24) +val surfaceContainerHighDarkHighContrast = Color(0xFF2F282F) +val surfaceContainerHighestDarkHighContrast = Color(0xFF3A323A) diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/MifosBackground.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/MifosBackground.kt similarity index 90% rename from core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/MifosBackground.kt rename to core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/MifosBackground.kt index 8559cee6c..d0cb2ab24 100644 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/MifosBackground.kt +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/MifosBackground.kt @@ -9,7 +9,6 @@ */ package org.mifos.mobile.core.designsystem.theme -import android.content.res.Configuration import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.size import androidx.compose.material3.LocalAbsoluteTonalElevation @@ -18,9 +17,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider 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 androidx.compose.ui.unit.dp +import org.jetbrains.compose.ui.tooling.preview.Preview /** * The main background for the app. @@ -51,8 +50,7 @@ fun MifosBackground( * Multipreview annotation that represents light and dark themes. Add this annotation to a * composable to render the both themes. */ -@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme") -@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme") +@Preview annotation class ThemePreviews @ThemePreviews diff --git a/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.kt new file mode 100644 index 000000000..428e8ca57 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 MifosMobileTheme( + 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/org/mifos/mobile/core/designsystem/theme/Type.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/theme/Type.kt new file mode 100644 index 000000000..6e97bc41a --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 mifos_mobile.core.designsystem.generated.resources.Res +import mifos_mobile.core.designsystem.generated.resources.poppins_black +import mifos_mobile.core.designsystem.generated.resources.poppins_bold +import mifos_mobile.core.designsystem.generated.resources.poppins_extra_bold +import mifos_mobile.core.designsystem.generated.resources.poppins_extra_light +import mifos_mobile.core.designsystem.generated.resources.poppins_light +import mifos_mobile.core.designsystem.generated.resources.poppins_medium +import mifos_mobile.core.designsystem.generated.resources.poppins_regular +import mifos_mobile.core.designsystem.generated.resources.poppins_semi_bold +import mifos_mobile.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/commonMain/kotlin/org/mifos/mobile/core/designsystem/utils/ModifierExt.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/utils/ModifierExt.kt new file mode 100644 index 000000000..50cf9835b --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/utils/NonLetterColorVisualTransformation.kt b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/core/designsystem/utils/NonLetterColorVisualTransformation.kt new file mode 100644 index 000000000..7fcbabe31 --- /dev/null +++ b/core/designsystem/src/commonMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/theme/Theme.desktop.kt b/core/designsystem/src/desktopMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.desktop.kt new file mode 100644 index 000000000..91add704d --- /dev/null +++ b/core/designsystem/src/desktopMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/org/mifos/mobile/core/designsystem/theme/Theme.js.kt b/core/designsystem/src/jsMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.js.kt new file mode 100644 index 000000000..91add704d --- /dev/null +++ b/core/designsystem/src/jsMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/kotlin/org/mifos/mobile/core/designsystem/components/MifosButton.kt b/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosButton.kt deleted file mode 100644 index 2871e2a90..000000000 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosButton.kt +++ /dev/null @@ -1,181 +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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.core.designsystem.components - -import androidx.annotation.StringRes -import androidx.compose.foundation.BorderStroke -import androidx.compose.foundation.interaction.MutableInteractionSource -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.RowScope -import androidx.compose.material3.Button -import androidx.compose.material3.ButtonColors -import androidx.compose.material3.ButtonDefaults -import androidx.compose.material3.ButtonElevation -import androidx.compose.material3.Icon -import androidx.compose.material3.OutlinedButton -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -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.Shape -import androidx.compose.ui.graphics.vector.ImageVector -import androidx.compose.ui.res.stringResource - -@Composable -fun MifosButton( - @StringRes - textResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - shape: Shape = ButtonDefaults.shape, - colors: ButtonColors = ButtonDefaults.buttonColors(), - elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), - border: BorderStroke? = null, - contentPadding: PaddingValues = ButtonDefaults.ContentPadding, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, -) { - Button( - modifier = modifier, - onClick = onClick, - enabled = enabled, - shape = shape, - colors = colors, - elevation = elevation, - border = border, - contentPadding = contentPadding, - interactionSource = interactionSource, - content = { - Text(text = stringResource(id = textResId)) - }, - ) -} - -@Composable -fun MifosButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - shape: Shape = ButtonDefaults.shape, - colors: ButtonColors = ButtonDefaults.buttonColors(), - elevation: ButtonElevation? = ButtonDefaults.buttonElevation(), - border: BorderStroke? = null, - contentPadding: PaddingValues = ButtonDefaults.ContentPadding, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - content: @Composable (RowScope.() -> Unit), -) { - Button( - modifier = modifier, - onClick = onClick, - enabled = enabled, - shape = shape, - colors = colors, - elevation = elevation, - border = border, - contentPadding = contentPadding, - interactionSource = interactionSource, - content = content, - ) -} - -@Composable -fun MifosTextButton( - text: String, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TextButton( - modifier = modifier, - onClick = onClick, - content = { - Text(text = text) - }, - ) -} - -@Composable -fun MifosTextButton( - onClick: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - shape: Shape = ButtonDefaults.textShape, - colors: ButtonColors = ButtonDefaults.textButtonColors(), - elevation: ButtonElevation? = null, - border: BorderStroke? = null, - contentPadding: PaddingValues = ButtonDefaults.TextButtonContentPadding, - interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, - content: @Composable (RowScope.() -> Unit), -) { - TextButton( - modifier = modifier, - onClick = onClick, - enabled = enabled, - shape = shape, - colors = colors, - elevation = elevation, - border = border, - contentPadding = contentPadding, - interactionSource = interactionSource, - content = content, - ) -} - -@Composable -fun MifosOutlinedTextButton( - textResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - Button( - onClick = onClick, - modifier = modifier, - content = { - Text(text = stringResource(id = textResId)) - }, - ) -} - -@Composable -fun MifosOutlinedButton( - textResId: Int, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - OutlinedButton( - onClick = onClick, - modifier = modifier, - content = { - Text(text = stringResource(id = textResId)) - }, - ) -} - -@Composable -fun MifosIconTextButton( - text: String, - enabled: Boolean, - imageVector: ImageVector, - onClick: () -> Unit, - modifier: Modifier = Modifier, -) { - TextButton( - onClick = onClick, - modifier = modifier, - enabled = enabled, - ) { - Row(verticalAlignment = Alignment.CenterVertically) { - Icon(imageVector = imageVector, contentDescription = null) - Text(text = text) - } - } -} diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosOutlinedTextField.kt b/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosOutlinedTextField.kt deleted file mode 100644 index 258fce217..000000000 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosOutlinedTextField.kt +++ /dev/null @@ -1,103 +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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.core.designsystem.components - -import androidx.compose.foundation.Image -import androidx.compose.foundation.isSystemInDarkTheme -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.OutlinedTextFieldDefaults -import androidx.compose.material3.Text -import androidx.compose.material3.TextFieldColors -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.graphics.ColorFilter -import androidx.compose.ui.platform.LocalDensity -import androidx.compose.ui.res.painterResource -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.KeyboardType -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.input.VisualTransformation -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp - -@Composable -fun MifosOutlinedTextField( - label: Int, - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - modifier: Modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), - maxLines: Int = 1, - singleLine: Boolean = true, - icon: Int? = null, - visualTransformation: VisualTransformation = VisualTransformation.None, - trailingIcon: @Composable (() -> Unit)? = null, - error: Boolean = false, - supportingText: String? = null, - keyboardType: KeyboardType = KeyboardType.Text, - enabled: Boolean = true, - readOnly: Boolean = false, - imeAction: ImeAction = ImeAction.Next, - colors: TextFieldColors = OutlinedTextFieldDefaults.colors( - focusedBorderColor = MaterialTheme.colorScheme.primary, - ), -) { - OutlinedTextField( - value = value, - onValueChange = onValueChange, - label = { Text(stringResource(id = label)) }, - modifier = modifier, - leadingIcon = if (icon != null) { - { - Image( - painter = painterResource(id = icon), - contentDescription = null, - colorFilter = if (isSystemInDarkTheme()) { - ColorFilter.tint(Color.White) - } else { - ColorFilter.tint(Color.Black) - }, - ) - } - } else { - null - }, - trailingIcon = trailingIcon, - maxLines = maxLines, - singleLine = singleLine, - colors = colors, - enabled = enabled, - readOnly = readOnly, - textStyle = LocalDensity.current.run { - TextStyle(fontSize = 18.sp) - }, - keyboardOptions = KeyboardOptions.Default.copy( - imeAction = imeAction, - keyboardType = keyboardType, - ), - visualTransformation = visualTransformation, - isError = error, - supportingText = { - if (error) { - Text( - modifier = Modifier.fillMaxWidth(), - text = supportingText ?: "", - color = MaterialTheme.colorScheme.error, - ) - } - }, - ) -} diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosScaffold.kt b/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosScaffold.kt deleted file mode 100644 index a7e09154c..000000000 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosScaffold.kt +++ /dev/null @@ -1,105 +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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.core.designsystem.components - -import androidx.annotation.StringRes -import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.calculateEndPadding -import androidx.compose.foundation.layout.padding -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Scaffold -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.Color -import androidx.compose.ui.unit.LayoutDirection - -@Composable -fun MifosScaffold( - @StringRes - topBarTitleResId: Int, - navigateBack: () -> Unit, - modifier: Modifier = Modifier, - floatingActionButtonContent: FloatingActionButtonContent? = null, - snackbarHost: @Composable () -> Unit = {}, - content: @Composable (PaddingValues) -> Unit, -) { - var padding by remember { mutableStateOf(PaddingValues()) } - Scaffold( - topBar = { - MifosTopBarTitle( - topBarTitleResId = topBarTitleResId, - navigateBack = navigateBack, - ) - }, - floatingActionButton = { - floatingActionButtonContent?.let { content -> - FloatingActionButton( - modifier = Modifier.padding( - end = padding.calculateEndPadding(LayoutDirection.Ltr), - ), - onClick = content.onClick, - contentColor = content.contentColor, - containerColor = MaterialTheme.colorScheme.primary, - content = content.content, - ) - } - }, - snackbarHost = snackbarHost, - modifier = modifier, - content = { - padding = it - content(it) - }, - ) -} - -@Composable -fun MifosScaffold( - topBar: @Composable () -> Unit, - modifier: Modifier = Modifier, - floatingActionButtonContent: FloatingActionButtonContent? = null, - snackbarHost: @Composable () -> Unit = {}, - content: @Composable (PaddingValues) -> Unit, -) { - var padding by remember { mutableStateOf(PaddingValues()) } - Scaffold( - topBar = topBar, - floatingActionButton = { - floatingActionButtonContent?.let { content -> - FloatingActionButton( - modifier = Modifier.padding( - end = padding.calculateEndPadding(LayoutDirection.Ltr), - ), - onClick = content.onClick, - contentColor = content.contentColor, - containerColor = MaterialTheme.colorScheme.primary, - content = content.content, - ) - } - }, - snackbarHost = snackbarHost, - modifier = modifier, - content = { - padding = it - content(it) - }, - ) -} - -data class FloatingActionButtonContent( - val onClick: (() -> Unit), - val contentColor: Color, - val content: (@Composable () -> Unit), -) diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosTopBar.kt b/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosTopBar.kt deleted file mode 100644 index 6fc08fed0..000000000 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/components/MifosTopBar.kt +++ /dev/null @@ -1,90 +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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.core.designsystem.components - -import androidx.annotation.StringRes -import androidx.compose.foundation.isSystemInDarkTheme -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.Text -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.runtime.Composable -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.res.stringResource -import org.mifos.mobile.core.designsystem.icons.MifosIcons - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MifosTopBar( - navigateBack: () -> Unit, - title: @Composable () -> Unit, - modifier: Modifier = Modifier, - actions: @Composable RowScope.() -> Unit = {}, -) { - TopAppBar( - modifier = modifier, - title = title, - navigationIcon = { - IconButton( - onClick = navigateBack, - ) { - Icon( - imageVector = MifosIcons.ArrowBack, - contentDescription = "Back Arrow", - tint = if (isSystemInDarkTheme()) Color.White else Color.Black, - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = if (isSystemInDarkTheme()) { - Color(0xFF1B1B1F) - } else { - Color(0xFFFEFBFF) - }, - ), - actions = actions, - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun MifosTopBarTitle( - @StringRes - topBarTitleResId: Int, - navigateBack: () -> Unit, - modifier: Modifier = Modifier, -) { - TopAppBar( - modifier = modifier, - title = { Text(text = stringResource(id = topBarTitleResId)) }, - navigationIcon = { - IconButton( - onClick = navigateBack, - ) { - Icon( - imageVector = MifosIcons.ArrowBack, - contentDescription = "Back Arrow", - tint = if (isSystemInDarkTheme()) Color.White else Color.Black, - ) - } - }, - colors = TopAppBarDefaults.topAppBarColors( - containerColor = if (isSystemInDarkTheme()) { - Color(0xFF1B1B1F) - } else { - Color(0xFFFEFBFF) - }, - ), - ) -} diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/Color.kt b/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/Color.kt deleted file mode 100644 index dae68a6ce..000000000 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/Color.kt +++ /dev/null @@ -1,40 +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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.core.designsystem.theme - -import androidx.compose.ui.graphics.Color - -val Blue600 = Color(0xFF1E88E5) -val Blue700 = Color(0xFF1976D2) - -val Black1 = Color(0xFF222222) -val Black2 = Color(0xFF000000) -val BackgroundLight = Color(0xFFFEFBFF) -val BackgroundDark = Color(0xFF1B1B1F) - -val RedErrorDark = Color(0xFFB00020) - -val LightPrimary = Color(0xFF325ca8) -val DarkPrimary = Color(0xFF9bb1e3) - -val DepositGreen = Color(0xff14c416) -val Blue = Color(0xFF003FFF) -val RedLight = Color(0xFFFF4444) -val LightYellow = Color(0xFFF9AC06) - -val Primary = Color(0xFF3F51B5) -val DarkGray = Color(0xBB666666) - -val GreenSuccess = Color(0xff14c416) -val LightSurfaceTint = Color(0xFF325CA8) -val DarkSurfaceTint = Color(0xFFAEC6FF) - -val lightScrim = Color(0x80FFFFFF) -val darkScrim = Color(0x80000000) diff --git a/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.kt b/core/designsystem/src/main/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.kt deleted file mode 100644 index b3e2da1f8..000000000 --- a/core/designsystem/src/main/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md - */ -package org.mifos.mobile.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 = LightPrimary, - onPrimary = Color.White, - error = RedErrorDark, - background = BackgroundLight, - onSurface = Black2, - onSecondary = Color.Gray, - outlineVariant = Color.Gray, - surfaceTint = LightSurfaceTint, -) - -private val DarkThemeColors = darkColorScheme( - primary = DarkPrimary, - onPrimary = Color.White, - secondary = Black1, - error = RedErrorDark, - background = BackgroundDark, - surface = Black1, - onSurface = Color.White, - onSecondary = Color.White, - outlineVariant = Color.White, - surfaceTint = DarkSurfaceTint, -) - -@Composable -fun MifosMobileTheme( - 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/org/mifos/mobile/core/designsystem/theme/Theme.native.kt b/core/designsystem/src/nativeMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.native.kt new file mode 100644 index 000000000..5294d0f28 --- /dev/null +++ b/core/designsystem/src/nativeMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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/wasmJsMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.wasmJs.kt b/core/designsystem/src/wasmJsMain/kotlin/org/mifos/mobile/core/designsystem/theme/Theme.wasmJs.kt new file mode 100644 index 000000000..91add704d --- /dev/null +++ b/core/designsystem/src/wasmJsMain/kotlin/org/mifos/mobile/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/mobile-mobile/blob/master/LICENSE.md + */ +package org.mifos.mobile.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 f95c904a9..c5553b7cd 100644 --- a/gradle.properties +++ b/gradle.properties @@ -45,4 +45,5 @@ kotlin.code.style=official # Disable build features that are enabled by default, # https://developer.android.com/build/releases/gradle-plugin#default-changes android.defaults.buildfeatures.resvalues=false -android.defaults.buildfeatures.shaders=false \ No newline at end of file +android.defaults.buildfeatures.shaders=false +org.jetbrains.compose.experimental.jscanvas.enabled=true \ No newline at end of file