From 14e4e13b2265ac057f170010ec65db53689da31c Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 15 May 2025 09:55:36 +0300 Subject: [PATCH 01/23] implemented OTP --- design/api/current.api | 51 ++++++ .../design/ui/otpContainer/OtpContainer.kt | 163 ++++++++++++++++++ .../ui/otpContainer/models/OtpCellStyle.kt | 24 +++ 3 files changed, 238 insertions(+) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt diff --git a/design/api/current.api b/design/api/current.api index 0e9da44d..844dceb4 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -203,6 +203,57 @@ package com.urlaunched.android.design.ui.modifiers { } +package com.urlaunched.android.design.ui.otpContainer { + + public final class OtpContainerKt { + method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, androidx.compose.ui.text.input.TextFieldValue otpTextFieldValue, kotlin.jvm.functions.Function1 onOtpTextFieldValueChange, optional float elementsSpacing, optional boolean useSpaceBetween, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle); + method @androidx.compose.runtime.Composable @androidx.compose.ui.tooling.preview.Preview(showBackground=true) public static void OtpTextFieldPreview(); + } + +} + +package com.urlaunched.android.design.ui.otpContainer.models { + + public final class OtpCellStyle { + ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional long backgroundColor, optional long filledBorderColor, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); + method public androidx.compose.ui.text.TextStyle component1(); + method public long component10-0d7_KjU(); + method public long component11-0d7_KjU(); + method public androidx.compose.ui.graphics.Shape component2(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public long component5-0d7_KjU(); + method public long component6-0d7_KjU(); + method public float component7-D9Ej5fM(); + method public float component8-D9Ej5fM(); + method public long component9-0d7_KjU(); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-fxvIEpE(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, long backgroundColor, long filledBorderColor, float focusedBorderWidth, float unfocusedBorderWidth, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); + method public long getBackgroundColor(); + method public long getEmptyBorderColor(); + method public long getErrorBorderColor(); + method public long getFilledBorderColor(); + method public long getFocusedBorderColor(); + method public float getFocusedBorderWidth(); + method public float getHeight(); + method public androidx.compose.ui.graphics.Shape getShape(); + method public androidx.compose.ui.text.TextStyle getTextStyle(); + method public float getUnfocusedBorderWidth(); + method public float getWidth(); + property public final long backgroundColor; + property public final long emptyBorderColor; + property public final long errorBorderColor; + property public final long filledBorderColor; + property public final long focusedBorderColor; + property public final float focusedBorderWidth; + property public final float height; + property public final androidx.compose.ui.graphics.Shape shape; + property public final androidx.compose.ui.text.TextStyle textStyle; + property public final float unfocusedBorderWidth; + property public final float width; + } + +} + package com.urlaunched.android.design.ui.paging { public final class PagingColumnKt { diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt new file mode 100644 index 00000000..163ba170 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -0,0 +1,163 @@ +package com.urlaunched.android.design.ui.otpContainer + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardOptions +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight +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.tooling.preview.Preview +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle + +/** + * A composable OTP container input field where each character is shown in an individual box. + * + * @param modifier Modifier applied to the root of the text field. + * @param otpLength The total number of characters expected in the OTP code. Default is 6. + * @param hasError If true, the OTP boxes will show an error state (e.g., red border). + * @param otpTextFieldValue The current text input as a [TextFieldValue], including cursor position. + * @param onOtpTextFieldValueChange Callback invoked when the OTP value changes. + * @param elementsSpacing The spacing between individual character boxes. Ignored if [useSpaceBetween] is true. + * @param useSpaceBetween If true, distributes the boxes evenly across the available width using [Arrangement.SpaceBetween]. + * @param cellsStyle Visual configuration for the individual character boxes, defined by [OtpCellStyle]. + */ + +@Composable +fun OtpTextField( + modifier: Modifier, + otpLength: Int = 6, + hasError: Boolean, + otpTextFieldValue: TextFieldValue, + onOtpTextFieldValueChange: (TextFieldValue) -> Unit, + elementsSpacing: Dp = 8.dp, + useSpaceBetween: Boolean = false, + cellsStyle: OtpCellStyle = OtpCellStyle() +) { + + BasicTextField( + modifier = modifier, + value = otpTextFieldValue, + onValueChange = { + if (it.text.length <= otpLength) { + onOtpTextFieldValueChange(it) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + decorationBox = { + Row( + modifier = Modifier, + horizontalArrangement = if (useSpaceBetween) Arrangement.SpaceBetween else Arrangement.spacedBy(elementsSpacing), + verticalAlignment = Alignment.CenterVertically + ) { + repeat(otpLength) { index -> + OtpCellView( + index = index, + text = otpTextFieldValue.text, + isError = hasError, + isFocused = otpTextFieldValue.selection.start == index, + style = cellsStyle + ) + } + } + } + ) +} + +@Composable +private fun OtpCellView( + index: Int, + text: String, + isError: Boolean, + isFocused: Boolean, + style: OtpCellStyle +) { + val char = text.getOrNull(index)?.toString().orEmpty() + val currentBorderColor = when { + isError -> style.errorBorderColor + isFocused -> style.focusedBorderColor + char.isNotBlank() -> style.filledBorderColor + else -> style.emptyBorderColor + } + + + val borderWidth = if (isFocused) { + style.focusedBorderWidth + } else { + style.unfocusedBorderWidth + } + + Box( + Modifier + .size(width = style.width, height = style.height) + .border(borderWidth, currentBorderColor, style.shape) + .clip(style.shape) + .padding(borderWidth) + .background(style.backgroundColor), + contentAlignment = Alignment.Center + ) { + Text( + text = char, + modifier = Modifier, + style = style.textStyle, + textAlign = TextAlign.Center + ) + } +} + + +@Preview(showBackground = true) +@Composable +fun OtpTextFieldPreview() { + var textFieldValue by remember { + mutableStateOf(TextFieldValue("45", selection = TextRange(2))) + } + Box(Modifier.fillMaxSize().background(Color.Gray), contentAlignment = Alignment.TopCenter) { + OtpTextField( + modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp).fillMaxWidth(), + otpTextFieldValue = textFieldValue, + onOtpTextFieldValueChange = { textFieldValue = it }, + useSpaceBetween = true, + hasError = false, + cellsStyle = OtpCellStyle().copy( + backgroundColor = Color.White, + shape = RoundedCornerShape(12.dp), + focusedBorderColor = Color(0xFF1e7e9c), + filledBorderColor = Color(0xFFD8D9DF), + textStyle = TextStyle( + fontWeight = FontWeight(700), + fontSize = 22.sp, color = Color.Black + ), + focusedBorderWidth = 1.5.dp, + unfocusedBorderWidth = 1.5.dp + + ) + ) + } +} + + diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt new file mode 100644 index 00000000..abfa36f1 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt @@ -0,0 +1,24 @@ +package com.urlaunched.android.design.ui.otpContainer.models + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +data class OtpCellStyle( + val textStyle: TextStyle = TextStyle(fontSize = 20.sp, textAlign = TextAlign.Center), + val shape: Shape = RoundedCornerShape(8.dp), + val width: Dp = 40.dp, + val height: Dp = 56.dp, + val backgroundColor: Color = Color.White, + val filledBorderColor: Color = Color.LightGray, + val focusedBorderWidth: Dp = 1.dp, + val unfocusedBorderWidth: Dp = 1.dp, + val focusedBorderColor: Color = Color.Black, + val errorBorderColor: Color = Color.Red, + val emptyBorderColor: Color = Color.White, +) \ No newline at end of file From 6292ee8377e79685ea55f8c17470dafbfa591b68 Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 15 May 2025 09:57:22 +0300 Subject: [PATCH 02/23] implemented OTP --- .../design/ui/otpContainer/OtpContainer.kt | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index 163ba170..24ca784b 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -56,7 +56,7 @@ fun OtpTextField( onOtpTextFieldValueChange: (TextFieldValue) -> Unit, elementsSpacing: Dp = 8.dp, useSpaceBetween: Boolean = false, - cellsStyle: OtpCellStyle = OtpCellStyle() + cellsStyle: OtpCellStyle = OtpCellStyle(), ) { BasicTextField( @@ -71,7 +71,9 @@ fun OtpTextField( decorationBox = { Row( modifier = Modifier, - horizontalArrangement = if (useSpaceBetween) Arrangement.SpaceBetween else Arrangement.spacedBy(elementsSpacing), + horizontalArrangement = if (useSpaceBetween) Arrangement.SpaceBetween else Arrangement.spacedBy( + elementsSpacing + ), verticalAlignment = Alignment.CenterVertically ) { repeat(otpLength) { index -> @@ -94,9 +96,10 @@ private fun OtpCellView( text: String, isError: Boolean, isFocused: Boolean, - style: OtpCellStyle + style: OtpCellStyle, ) { val char = text.getOrNull(index)?.toString().orEmpty() + val currentBorderColor = when { isError -> style.errorBorderColor isFocused -> style.focusedBorderColor @@ -104,7 +107,6 @@ private fun OtpCellView( else -> style.emptyBorderColor } - val borderWidth = if (isFocused) { style.focusedBorderWidth } else { @@ -129,16 +131,19 @@ private fun OtpCellView( } } - @Preview(showBackground = true) @Composable -fun OtpTextFieldPreview() { +private fun OtpTextFieldPreview() { var textFieldValue by remember { mutableStateOf(TextFieldValue("45", selection = TextRange(2))) } - Box(Modifier.fillMaxSize().background(Color.Gray), contentAlignment = Alignment.TopCenter) { + Box(Modifier + .fillMaxSize() + .background(Color.Gray), contentAlignment = Alignment.TopCenter) { OtpTextField( - modifier = Modifier.padding(vertical = 12.dp, horizontal = 16.dp).fillMaxWidth(), + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), otpTextFieldValue = textFieldValue, onOtpTextFieldValueChange = { textFieldValue = it }, useSpaceBetween = true, From 142e366a549646f8e71d940e8d594613aea08abd Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 15 May 2025 11:04:32 +0300 Subject: [PATCH 03/23] added emptyBackgroundColor --- design/api/current.api | 12 +++++++----- .../android/design/ui/otpContainer/OtpContainer.kt | 8 +++++++- .../design/ui/otpContainer/models/OtpCellStyle.kt | 1 + 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index 844dceb4..2ad664d1 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -207,7 +207,6 @@ package com.urlaunched.android.design.ui.otpContainer { public final class OtpContainerKt { method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, androidx.compose.ui.text.input.TextFieldValue otpTextFieldValue, kotlin.jvm.functions.Function1 onOtpTextFieldValueChange, optional float elementsSpacing, optional boolean useSpaceBetween, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle); - method @androidx.compose.runtime.Composable @androidx.compose.ui.tooling.preview.Preview(showBackground=true) public static void OtpTextFieldPreview(); } } @@ -215,20 +214,22 @@ package com.urlaunched.android.design.ui.otpContainer { package com.urlaunched.android.design.ui.otpContainer.models { public final class OtpCellStyle { - ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional long backgroundColor, optional long filledBorderColor, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); + ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional long backgroundColor, optional long emptyBackgroundColor, optional long filledBorderColor, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); method public androidx.compose.ui.text.TextStyle component1(); method public long component10-0d7_KjU(); method public long component11-0d7_KjU(); + method public long component12-0d7_KjU(); method public androidx.compose.ui.graphics.Shape component2(); method public float component3-D9Ej5fM(); method public float component4-D9Ej5fM(); method public long component5-0d7_KjU(); method public long component6-0d7_KjU(); - method public float component7-D9Ej5fM(); + method public long component7-0d7_KjU(); method public float component8-D9Ej5fM(); - method public long component9-0d7_KjU(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-fxvIEpE(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, long backgroundColor, long filledBorderColor, float focusedBorderWidth, float unfocusedBorderWidth, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); + method public float component9-D9Ej5fM(); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-d4pJHjE(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, long backgroundColor, long emptyBackgroundColor, long filledBorderColor, float focusedBorderWidth, float unfocusedBorderWidth, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); method public long getBackgroundColor(); + method public long getEmptyBackgroundColor(); method public long getEmptyBorderColor(); method public long getErrorBorderColor(); method public long getFilledBorderColor(); @@ -240,6 +241,7 @@ package com.urlaunched.android.design.ui.otpContainer.models { method public float getUnfocusedBorderWidth(); method public float getWidth(); property public final long backgroundColor; + property public final long emptyBackgroundColor; property public final long emptyBorderColor; property public final long errorBorderColor; property public final long filledBorderColor; diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index 24ca784b..bf10a4e1 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -113,13 +113,19 @@ private fun OtpCellView( style.unfocusedBorderWidth } + val backgroundColor = if(char.isNotBlank()){ + style.backgroundColor + }else{ + style.emptyBackgroundColor + } + Box( Modifier .size(width = style.width, height = style.height) .border(borderWidth, currentBorderColor, style.shape) .clip(style.shape) .padding(borderWidth) - .background(style.backgroundColor), + .background(backgroundColor), contentAlignment = Alignment.Center ) { Text( diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt index abfa36f1..b8868722 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt @@ -15,6 +15,7 @@ data class OtpCellStyle( val width: Dp = 40.dp, val height: Dp = 56.dp, val backgroundColor: Color = Color.White, + val emptyBackgroundColor: Color = Color.White, val filledBorderColor: Color = Color.LightGray, val focusedBorderWidth: Dp = 1.dp, val unfocusedBorderWidth: Dp = 1.dp, From 6b16c788f0761bb667823cba4d83ba6cbb84b077 Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 15 May 2025 13:36:01 +0300 Subject: [PATCH 04/23] fixed OTP`s issues and all ktlint issues --- .../design/ui/otpContainer/OtpContainer.kt | 76 +++++++++---------- .../constants/OtpContainerDimens.kt | 9 +++ .../ui/otpContainer/models/OtpCellColors.kt | 12 +++ .../ui/otpContainer/models/OtpCellStyle.kt | 25 +++--- .../models/OtpCellsArrangement.kt | 8 ++ 5 files changed, 72 insertions(+), 58 deletions(-) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index bf10a4e1..690706ff 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -9,7 +9,6 @@ import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text @@ -23,16 +22,17 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.font.FontWeight 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.tooling.preview.Preview -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle +import com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement + +private const val DEFAULT_OTP_LENGTH = 6 /** * A composable OTP container input field where each character is shown in an individual box. @@ -50,14 +50,18 @@ import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle @Composable fun OtpTextField( modifier: Modifier, - otpLength: Int = 6, + otpLength: Int = DEFAULT_OTP_LENGTH, hasError: Boolean, otpTextFieldValue: TextFieldValue, - onOtpTextFieldValueChange: (TextFieldValue) -> Unit, - elementsSpacing: Dp = 8.dp, - useSpaceBetween: Boolean = false, + onOtpTextFieldValueChange: (textFiledValue: TextFieldValue) -> Unit, + cellsArrangement: OtpCellsArrangement = OtpCellsArrangement.Spaced(Dimens.spacingSmall), cellsStyle: OtpCellStyle = OtpCellStyle(), + cellsColors: OtpCellColors = OtpCellColors() ) { + val horizontalArrangement = when (cellsArrangement) { + is OtpCellsArrangement.SpaceBetween -> Arrangement.SpaceBetween + is OtpCellsArrangement.Spaced -> Arrangement.spacedBy(cellsArrangement.spacing) + } BasicTextField( modifier = modifier, @@ -71,9 +75,7 @@ fun OtpTextField( decorationBox = { Row( modifier = Modifier, - horizontalArrangement = if (useSpaceBetween) Arrangement.SpaceBetween else Arrangement.spacedBy( - elementsSpacing - ), + horizontalArrangement = horizontalArrangement, verticalAlignment = Alignment.CenterVertically ) { repeat(otpLength) { index -> @@ -82,7 +84,8 @@ fun OtpTextField( text = otpTextFieldValue.text, isError = hasError, isFocused = otpTextFieldValue.selection.start == index, - style = cellsStyle + style = cellsStyle, + colors = cellsColors ) } } @@ -97,14 +100,15 @@ private fun OtpCellView( isError: Boolean, isFocused: Boolean, style: OtpCellStyle, + colors: OtpCellColors ) { val char = text.getOrNull(index)?.toString().orEmpty() val currentBorderColor = when { - isError -> style.errorBorderColor - isFocused -> style.focusedBorderColor - char.isNotBlank() -> style.filledBorderColor - else -> style.emptyBorderColor + isError -> colors.errorBorderColor + isFocused -> colors.focusedBorderColor + char.isNotBlank() -> colors.filledBorderColor + else -> colors.emptyBorderColor } val borderWidth = if (isFocused) { @@ -113,10 +117,10 @@ private fun OtpCellView( style.unfocusedBorderWidth } - val backgroundColor = if(char.isNotBlank()){ - style.backgroundColor - }else{ - style.emptyBackgroundColor + val backgroundColor = if (char.isNotBlank() || isFocused) { + colors.backgroundColor + } else { + colors.emptyBackgroundColor } Box( @@ -143,32 +147,20 @@ private fun OtpTextFieldPreview() { var textFieldValue by remember { mutableStateOf(TextFieldValue("45", selection = TextRange(2))) } - Box(Modifier - .fillMaxSize() - .background(Color.Gray), contentAlignment = Alignment.TopCenter) { + Box( + Modifier + .fillMaxSize() + .background(Color.Gray), + contentAlignment = Alignment.TopCenter + ) { OtpTextField( modifier = Modifier .padding(vertical = 12.dp, horizontal = 16.dp) .fillMaxWidth(), otpTextFieldValue = textFieldValue, onOtpTextFieldValueChange = { textFieldValue = it }, - useSpaceBetween = true, - hasError = false, - cellsStyle = OtpCellStyle().copy( - backgroundColor = Color.White, - shape = RoundedCornerShape(12.dp), - focusedBorderColor = Color(0xFF1e7e9c), - filledBorderColor = Color(0xFFD8D9DF), - textStyle = TextStyle( - fontWeight = FontWeight(700), - fontSize = 22.sp, color = Color.Black - ), - focusedBorderWidth = 1.5.dp, - unfocusedBorderWidth = 1.5.dp - - ) + cellsArrangement = OtpCellsArrangement.SpaceBetween, + hasError = false ) } -} - - +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt new file mode 100644 index 00000000..9883678b --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt @@ -0,0 +1,9 @@ +package com.urlaunched.android.design.ui.otpContainer.constants + +import androidx.compose.ui.unit.dp + +internal object OtpContainerDimens { + val defaultOtpCellHeight = 48.dp + val defaultOtpCellWidth = 40.dp + val defaultBorderWidth = 1.dp +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt new file mode 100644 index 00000000..eba13b86 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt @@ -0,0 +1,12 @@ +package com.urlaunched.android.design.ui.otpContainer.models + +import androidx.compose.ui.graphics.Color + +data class OtpCellColors( + val backgroundColor: Color = Color.White, + val emptyBackgroundColor: Color = Color.White, + val filledBorderColor: Color = Color.LightGray, + val focusedBorderColor: Color = Color.Black, + val errorBorderColor: Color = Color.Red, + val emptyBorderColor: Color = Color.White +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt index b8868722..869a9453 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt @@ -1,25 +1,18 @@ package com.urlaunched.android.design.ui.otpContainer.models import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Dp -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.otpContainer.constants.OtpContainerDimens data class OtpCellStyle( - val textStyle: TextStyle = TextStyle(fontSize = 20.sp, textAlign = TextAlign.Center), - val shape: Shape = RoundedCornerShape(8.dp), - val width: Dp = 40.dp, - val height: Dp = 56.dp, - val backgroundColor: Color = Color.White, - val emptyBackgroundColor: Color = Color.White, - val filledBorderColor: Color = Color.LightGray, - val focusedBorderWidth: Dp = 1.dp, - val unfocusedBorderWidth: Dp = 1.dp, - val focusedBorderColor: Color = Color.Black, - val errorBorderColor: Color = Color.Red, - val emptyBorderColor: Color = Color.White, + val textStyle: TextStyle = androidx.compose.material3.Typography().bodyLarge, + val shape: Shape = RoundedCornerShape(Dimens.cornerRadiusSmall), + val width: Dp = OtpContainerDimens.defaultOtpCellWidth, + val height: Dp = OtpContainerDimens.defaultOtpCellHeight, + val focusedBorderWidth: Dp = OtpContainerDimens.defaultBorderWidth, + val unfocusedBorderWidth: Dp = OtpContainerDimens.defaultBorderWidth, + val colors: OtpCellColors = OtpCellColors() ) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt new file mode 100644 index 00000000..dc6ab420 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt @@ -0,0 +1,8 @@ +package com.urlaunched.android.design.ui.otpContainer.models + +import androidx.compose.ui.unit.Dp + +sealed class OtpCellsArrangement { + data class Spaced(val spacing: Dp) : OtpCellsArrangement() + data object SpaceBetween : OtpCellsArrangement() +} \ No newline at end of file From 665e70489dbf1b34330469f67f4d167a9de350ad Mon Sep 17 00:00:00 2001 From: BohunD Date: Thu, 15 May 2025 13:59:43 +0300 Subject: [PATCH 05/23] changed OTP value parameter from TextFieldValue to String --- cdn/models/domainn/build.gradle | 4 ++ design/api/current.api | 63 +++++++++++++------ .../design/ui/otpContainer/OtpContainer.kt | 31 ++++----- 3 files changed, 63 insertions(+), 35 deletions(-) diff --git a/cdn/models/domainn/build.gradle b/cdn/models/domainn/build.gradle index 8c16f045..9addcb70 100644 --- a/cdn/models/domainn/build.gradle +++ b/cdn/models/domainn/build.gradle @@ -20,4 +20,8 @@ dependencies { // Kotlin implementation(platform(libs.kotlinDependencies.bom)) implementation libs.kotlinDependencies.serialization +} + +kotlin { + jvmToolchain(17) } \ No newline at end of file diff --git a/design/api/current.api b/design/api/current.api index 2ad664d1..4780d281 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -206,46 +206,54 @@ package com.urlaunched.android.design.ui.modifiers { package com.urlaunched.android.design.ui.otpContainer { public final class OtpContainerKt { - method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, androidx.compose.ui.text.input.TextFieldValue otpTextFieldValue, kotlin.jvm.functions.Function1 onOtpTextFieldValueChange, optional float elementsSpacing, optional boolean useSpaceBetween, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle); + method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement cellsArrangement, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors cellsColors); } } package com.urlaunched.android.design.ui.otpContainer.models { - public final class OtpCellStyle { - ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional long backgroundColor, optional long emptyBackgroundColor, optional long filledBorderColor, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); - method public androidx.compose.ui.text.TextStyle component1(); - method public long component10-0d7_KjU(); - method public long component11-0d7_KjU(); - method public long component12-0d7_KjU(); - method public androidx.compose.ui.graphics.Shape component2(); - method public float component3-D9Ej5fM(); - method public float component4-D9Ej5fM(); + public final class OtpCellColors { + ctor public OtpCellColors(optional long backgroundColor, optional long emptyBackgroundColor, optional long filledBorderColor, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); + method public long component1-0d7_KjU(); + method public long component2-0d7_KjU(); + method public long component3-0d7_KjU(); + method public long component4-0d7_KjU(); method public long component5-0d7_KjU(); method public long component6-0d7_KjU(); - method public long component7-0d7_KjU(); - method public float component8-D9Ej5fM(); - method public float component9-D9Ej5fM(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-d4pJHjE(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, long backgroundColor, long emptyBackgroundColor, long filledBorderColor, float focusedBorderWidth, float unfocusedBorderWidth, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors copy-tNS2XkQ(long backgroundColor, long emptyBackgroundColor, long filledBorderColor, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); method public long getBackgroundColor(); method public long getEmptyBackgroundColor(); method public long getEmptyBorderColor(); method public long getErrorBorderColor(); method public long getFilledBorderColor(); method public long getFocusedBorderColor(); - method public float getFocusedBorderWidth(); - method public float getHeight(); - method public androidx.compose.ui.graphics.Shape getShape(); - method public androidx.compose.ui.text.TextStyle getTextStyle(); - method public float getUnfocusedBorderWidth(); - method public float getWidth(); property public final long backgroundColor; property public final long emptyBackgroundColor; property public final long emptyBorderColor; property public final long errorBorderColor; property public final long filledBorderColor; property public final long focusedBorderColor; + } + + public final class OtpCellStyle { + ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors colors); + method public androidx.compose.ui.text.TextStyle component1(); + method public androidx.compose.ui.graphics.Shape component2(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public float component5-D9Ej5fM(); + method public float component6-D9Ej5fM(); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors component7(); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-8ZKsbrE(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors colors); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors getColors(); + method public float getFocusedBorderWidth(); + method public float getHeight(); + method public androidx.compose.ui.graphics.Shape getShape(); + method public androidx.compose.ui.text.TextStyle getTextStyle(); + method public float getUnfocusedBorderWidth(); + method public float getWidth(); + property public final com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors colors; property public final float focusedBorderWidth; property public final float height; property public final androidx.compose.ui.graphics.Shape shape; @@ -254,6 +262,21 @@ package com.urlaunched.android.design.ui.otpContainer.models { property public final float width; } + public abstract sealed class OtpCellsArrangement { + } + + public static final class OtpCellsArrangement.SpaceBetween extends com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement { + field public static final com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement.SpaceBetween INSTANCE; + } + + public static final class OtpCellsArrangement.Spaced extends com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement { + ctor public OtpCellsArrangement.Spaced(float spacing); + method public float component1-D9Ej5fM(); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement.Spaced copy-0680j_4(float spacing); + method public float getSpacing(); + property public final float spacing; + } + } package com.urlaunched.android.design.ui.paging { diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index 690706ff..efcdfc96 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -39,12 +39,13 @@ private const val DEFAULT_OTP_LENGTH = 6 * * @param modifier Modifier applied to the root of the text field. * @param otpLength The total number of characters expected in the OTP code. Default is 6. - * @param hasError If true, the OTP boxes will show an error state (e.g., red border). - * @param otpTextFieldValue The current text input as a [TextFieldValue], including cursor position. - * @param onOtpTextFieldValueChange Callback invoked when the OTP value changes. - * @param elementsSpacing The spacing between individual character boxes. Ignored if [useSpaceBetween] is true. - * @param useSpaceBetween If true, distributes the boxes evenly across the available width using [Arrangement.SpaceBetween]. - * @param cellsStyle Visual configuration for the individual character boxes, defined by [OtpCellStyle]. + * @param hasError If true, the OTP boxes will show an error state (e.g., red border color). + * @param otpText The current OTP input as a plain [String]. The cursor will automatically be placed at the end. + * @param onOtpTextChange Callback invoked when the OTP value changes. + * @param cellsArrangement Defines how the OTP boxes are arranged horizontally. Use [OtpCellsArrangement.SpaceBetween] + * for evenly distributed boxes, or [OtpCellsArrangement.Spaced] to specify exact spacing between them. + * @param cellsStyle Configuration of the visual properties for each OTP cell, such as size, shape, text style, and border widths. + * @param cellsColors Configuration of the color states (focused, error, empty, filled) and backgrounds for OTP cells. */ @Composable @@ -52,8 +53,8 @@ fun OtpTextField( modifier: Modifier, otpLength: Int = DEFAULT_OTP_LENGTH, hasError: Boolean, - otpTextFieldValue: TextFieldValue, - onOtpTextFieldValueChange: (textFiledValue: TextFieldValue) -> Unit, + otpText: String, + onOtpTextChange: (text: String) -> Unit, cellsArrangement: OtpCellsArrangement = OtpCellsArrangement.Spaced(Dimens.spacingSmall), cellsStyle: OtpCellStyle = OtpCellStyle(), cellsColors: OtpCellColors = OtpCellColors() @@ -65,10 +66,10 @@ fun OtpTextField( BasicTextField( modifier = modifier, - value = otpTextFieldValue, + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), onValueChange = { if (it.text.length <= otpLength) { - onOtpTextFieldValueChange(it) + onOtpTextChange(it.text) } }, keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), @@ -81,9 +82,9 @@ fun OtpTextField( repeat(otpLength) { index -> OtpCellView( index = index, - text = otpTextFieldValue.text, + text = otpText, isError = hasError, - isFocused = otpTextFieldValue.selection.start == index, + isFocused = otpText.length == index, style = cellsStyle, colors = cellsColors ) @@ -145,7 +146,7 @@ private fun OtpCellView( @Composable private fun OtpTextFieldPreview() { var textFieldValue by remember { - mutableStateOf(TextFieldValue("45", selection = TextRange(2))) + mutableStateOf("") } Box( Modifier @@ -157,8 +158,8 @@ private fun OtpTextFieldPreview() { modifier = Modifier .padding(vertical = 12.dp, horizontal = 16.dp) .fillMaxWidth(), - otpTextFieldValue = textFieldValue, - onOtpTextFieldValueChange = { textFieldValue = it }, + otpText = textFieldValue, + onOtpTextChange = { textFieldValue = it }, cellsArrangement = OtpCellsArrangement.SpaceBetween, hasError = false ) From e6d6351b91709182c0ef9bddf9d8a8ab47a392d9 Mon Sep 17 00:00:00 2001 From: BohunD Date: Mon, 19 May 2025 21:23:13 +0300 Subject: [PATCH 06/23] added otpError text --- design/api/current.api | 15 +++-- .../design/ui/otpContainer/OtpContainer.kt | 63 +++++++++++-------- .../ui/otpContainer/models/OtpCellStyle.kt | 3 +- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index 4780d281..2e7ca7ca 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -206,7 +206,7 @@ package com.urlaunched.android.design.ui.modifiers { package com.urlaunched.android.design.ui.otpContainer { public final class OtpContainerKt { - method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement cellsArrangement, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors cellsColors); + method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, String otpText, String errorText, kotlin.jvm.functions.Function1 onOtpTextChange, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement cellsArrangement, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors cellsColors); } } @@ -237,23 +237,26 @@ package com.urlaunched.android.design.ui.otpContainer.models { } public final class OtpCellStyle { - ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors colors); + ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional float errorTextTopPadding, optional androidx.compose.ui.text.TextStyle errorTextStyle); method public androidx.compose.ui.text.TextStyle component1(); method public androidx.compose.ui.graphics.Shape component2(); method public float component3-D9Ej5fM(); method public float component4-D9Ej5fM(); method public float component5-D9Ej5fM(); method public float component6-D9Ej5fM(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors component7(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-8ZKsbrE(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors colors); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors getColors(); + method public float component7-D9Ej5fM(); + method public androidx.compose.ui.text.TextStyle component8(); + method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-F0d9qfw(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, float errorTextTopPadding, androidx.compose.ui.text.TextStyle errorTextStyle); + method public androidx.compose.ui.text.TextStyle getErrorTextStyle(); + method public float getErrorTextTopPadding(); method public float getFocusedBorderWidth(); method public float getHeight(); method public androidx.compose.ui.graphics.Shape getShape(); method public androidx.compose.ui.text.TextStyle getTextStyle(); method public float getUnfocusedBorderWidth(); method public float getWidth(); - property public final com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors colors; + property public final androidx.compose.ui.text.TextStyle errorTextStyle; + property public final float errorTextTopPadding; property public final float focusedBorderWidth; property public final float height; property public final androidx.compose.ui.graphics.Shape shape; diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index efcdfc96..6b4ba178 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -4,9 +4,12 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.BasicTextField @@ -54,6 +57,7 @@ fun OtpTextField( otpLength: Int = DEFAULT_OTP_LENGTH, hasError: Boolean, otpText: String, + errorText: String, onOtpTextChange: (text: String) -> Unit, cellsArrangement: OtpCellsArrangement = OtpCellsArrangement.Spaced(Dimens.spacingSmall), cellsStyle: OtpCellStyle = OtpCellStyle(), @@ -63,35 +67,41 @@ fun OtpTextField( is OtpCellsArrangement.SpaceBetween -> Arrangement.SpaceBetween is OtpCellsArrangement.Spaced -> Arrangement.spacedBy(cellsArrangement.spacing) } - - BasicTextField( - modifier = modifier, - value = TextFieldValue(otpText, selection = TextRange(otpText.length)), - onValueChange = { - if (it.text.length <= otpLength) { - onOtpTextChange(it.text) - } - }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), - decorationBox = { - Row( - modifier = Modifier, - horizontalArrangement = horizontalArrangement, - verticalAlignment = Alignment.CenterVertically - ) { - repeat(otpLength) { index -> - OtpCellView( - index = index, - text = otpText, - isError = hasError, - isFocused = otpText.length == index, - style = cellsStyle, - colors = cellsColors - ) + Column(modifier) { + BasicTextField( + modifier = Modifier.fillMaxWidth(), + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), + onValueChange = { + if (it.text.length <= otpLength) { + onOtpTextChange(it.text) + } + }, + keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + decorationBox = { + Row( + modifier = Modifier, + horizontalArrangement = horizontalArrangement, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(otpLength) { index -> + OtpCellView( + index = index, + text = otpText, + isError = hasError, + isFocused = otpText.length == index, + style = cellsStyle, + colors = cellsColors + ) + } } } + + ) + Spacer(Modifier.height(cellsStyle.errorTextTopPadding)) + if (hasError) { + Text(text = errorText, style = cellsStyle.errorTextStyle, color = cellsColors.errorBorderColor) } - ) + } } @Composable @@ -160,6 +170,7 @@ private fun OtpTextFieldPreview() { .fillMaxWidth(), otpText = textFieldValue, onOtpTextChange = { textFieldValue = it }, + errorText = "ERROR", cellsArrangement = OtpCellsArrangement.SpaceBetween, hasError = false ) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt index 869a9453..5a611bf9 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt @@ -14,5 +14,6 @@ data class OtpCellStyle( val height: Dp = OtpContainerDimens.defaultOtpCellHeight, val focusedBorderWidth: Dp = OtpContainerDimens.defaultBorderWidth, val unfocusedBorderWidth: Dp = OtpContainerDimens.defaultBorderWidth, - val colors: OtpCellColors = OtpCellColors() + val errorTextTopPadding: Dp = Dimens.spacingTiny, + val errorTextStyle: TextStyle = TextStyle() ) \ No newline at end of file From 8c490f26afece8a9e1b6d0102cf58ce34299d086 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 10:59:45 +0300 Subject: [PATCH 07/23] Replace cells arrangement with compose horizontal arrangement --- .../design/ui/otpContainer/OtpContainer.kt | 35 ++++++------------- .../models/OtpCellsArrangement.kt | 8 ----- 2 files changed, 11 insertions(+), 32 deletions(-) delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index 6b4ba178..ae5c053c 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -7,7 +7,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -33,7 +32,6 @@ import androidx.compose.ui.unit.dp import com.urlaunched.android.design.resources.dimens.Dimens import com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle -import com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement private const val DEFAULT_OTP_LENGTH = 6 @@ -45,12 +43,10 @@ private const val DEFAULT_OTP_LENGTH = 6 * @param hasError If true, the OTP boxes will show an error state (e.g., red border color). * @param otpText The current OTP input as a plain [String]. The cursor will automatically be placed at the end. * @param onOtpTextChange Callback invoked when the OTP value changes. - * @param cellsArrangement Defines how the OTP boxes are arranged horizontally. Use [OtpCellsArrangement.SpaceBetween] - * for evenly distributed boxes, or [OtpCellsArrangement.Spaced] to specify exact spacing between them. + * @param cellsArrangement Defines how the OTP boxes are arranged horizontally. * @param cellsStyle Configuration of the visual properties for each OTP cell, such as size, shape, text style, and border widths. * @param cellsColors Configuration of the color states (focused, error, empty, filled) and backgrounds for OTP cells. */ - @Composable fun OtpTextField( modifier: Modifier, @@ -59,14 +55,10 @@ fun OtpTextField( otpText: String, errorText: String, onOtpTextChange: (text: String) -> Unit, - cellsArrangement: OtpCellsArrangement = OtpCellsArrangement.Spaced(Dimens.spacingSmall), + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), cellsStyle: OtpCellStyle = OtpCellStyle(), cellsColors: OtpCellColors = OtpCellColors() ) { - val horizontalArrangement = when (cellsArrangement) { - is OtpCellsArrangement.SpaceBetween -> Arrangement.SpaceBetween - is OtpCellsArrangement.Spaced -> Arrangement.spacedBy(cellsArrangement.spacing) - } Column(modifier) { BasicTextField( modifier = Modifier.fillMaxWidth(), @@ -80,7 +72,7 @@ fun OtpTextField( decorationBox = { Row( modifier = Modifier, - horizontalArrangement = horizontalArrangement, + horizontalArrangement = cellsArrangement, verticalAlignment = Alignment.CenterVertically ) { repeat(otpLength) { index -> @@ -137,15 +129,13 @@ private fun OtpCellView( Box( Modifier .size(width = style.width, height = style.height) - .border(borderWidth, currentBorderColor, style.shape) .clip(style.shape) - .padding(borderWidth) - .background(backgroundColor), + .background(backgroundColor) + .border(borderWidth, currentBorderColor, style.shape), contentAlignment = Alignment.Center ) { Text( text = char, - modifier = Modifier, style = style.textStyle, textAlign = TextAlign.Center ) @@ -155,13 +145,10 @@ private fun OtpCellView( @Preview(showBackground = true) @Composable private fun OtpTextFieldPreview() { - var textFieldValue by remember { - mutableStateOf("") - } + var textFieldValue by remember { mutableStateOf("12") } + Box( - Modifier - .fillMaxSize() - .background(Color.Gray), + modifier = Modifier.background(Color.Black), contentAlignment = Alignment.TopCenter ) { OtpTextField( @@ -170,9 +157,9 @@ private fun OtpTextFieldPreview() { .fillMaxWidth(), otpText = textFieldValue, onOtpTextChange = { textFieldValue = it }, - errorText = "ERROR", - cellsArrangement = OtpCellsArrangement.SpaceBetween, - hasError = false + errorText = "Otp is incorrect, try another one", + cellsArrangement = Arrangement.SpaceBetween, + hasError = true ) } } \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt deleted file mode 100644 index dc6ab420..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellsArrangement.kt +++ /dev/null @@ -1,8 +0,0 @@ -package com.urlaunched.android.design.ui.otpContainer.models - -import androidx.compose.ui.unit.Dp - -sealed class OtpCellsArrangement { - data class Spaced(val spacing: Dp) : OtpCellsArrangement() - data object SpaceBetween : OtpCellsArrangement() -} \ No newline at end of file From 59f9b08a6f7e6d60beccb888cbf25eb5306b9faa Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 11:12:21 +0300 Subject: [PATCH 08/23] Hide error padding if there is no error --- .../android/design/ui/otpContainer/OtpContainer.kt | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index ae5c053c..5ab03fc9 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -89,9 +89,15 @@ fun OtpTextField( } ) - Spacer(Modifier.height(cellsStyle.errorTextTopPadding)) + if (hasError) { - Text(text = errorText, style = cellsStyle.errorTextStyle, color = cellsColors.errorBorderColor) + Spacer(Modifier.height(cellsStyle.errorTextTopPadding)) + + Text( + text = errorText, + style = cellsStyle.errorTextStyle, + color = cellsColors.errorBorderColor + ) } } } @@ -159,7 +165,7 @@ private fun OtpTextFieldPreview() { onOtpTextChange = { textFieldValue = it }, errorText = "Otp is incorrect, try another one", cellsArrangement = Arrangement.SpaceBetween, - hasError = true + hasError = false ) } } \ No newline at end of file From 45242cc161f29a3cb498de42d47ebd74f452bca9 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 11:23:24 +0300 Subject: [PATCH 09/23] Combine hasError and errorText into a single parameter --- .../design/ui/otpContainer/OtpContainer.kt | 67 +++++++++++++------ 1 file changed, 46 insertions(+), 21 deletions(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index 5ab03fc9..10f085cb 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -35,26 +35,13 @@ import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle private const val DEFAULT_OTP_LENGTH = 6 -/** - * A composable OTP container input field where each character is shown in an individual box. - * - * @param modifier Modifier applied to the root of the text field. - * @param otpLength The total number of characters expected in the OTP code. Default is 6. - * @param hasError If true, the OTP boxes will show an error state (e.g., red border color). - * @param otpText The current OTP input as a plain [String]. The cursor will automatically be placed at the end. - * @param onOtpTextChange Callback invoked when the OTP value changes. - * @param cellsArrangement Defines how the OTP boxes are arranged horizontally. - * @param cellsStyle Configuration of the visual properties for each OTP cell, such as size, shape, text style, and border widths. - * @param cellsColors Configuration of the color states (focused, error, empty, filled) and backgrounds for OTP cells. - */ @Composable fun OtpTextField( modifier: Modifier, - otpLength: Int = DEFAULT_OTP_LENGTH, - hasError: Boolean, otpText: String, - errorText: String, onOtpTextChange: (text: String) -> Unit, + error: String? = null, + otpLength: Int = DEFAULT_OTP_LENGTH, cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), cellsStyle: OtpCellStyle = OtpCellStyle(), cellsColors: OtpCellColors = OtpCellColors() @@ -79,7 +66,7 @@ fun OtpTextField( OtpCellView( index = index, text = otpText, - isError = hasError, + isError = error != null, isFocused = otpText.length == index, style = cellsStyle, colors = cellsColors @@ -90,11 +77,11 @@ fun OtpTextField( ) - if (hasError) { + if (!error.isNullOrEmpty()) { Spacer(Modifier.height(cellsStyle.errorTextTopPadding)) Text( - text = errorText, + text = error, style = cellsStyle.errorTextStyle, color = cellsColors.errorBorderColor ) @@ -153,6 +140,45 @@ private fun OtpCellView( private fun OtpTextFieldPreview() { var textFieldValue by remember { mutableStateOf("12") } + Box( + modifier = Modifier.background(Color.Black) + ) { + OtpTextField( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun OtpTextFieldErrorPreview() { + var textFieldValue by remember { mutableStateOf("123") } + + Box( + modifier = Modifier.background(Color.Black) + ) { + OtpTextField( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + onOtpTextChange = { textFieldValue = it }, + error = "Otp is incorrect, try another one", + cellsArrangement = Arrangement.SpaceBetween + ) + } +} + +@Preview(showBackground = true) +@Composable +private fun OtpTextFieldErrorWithoutMessagePreview() { + var textFieldValue by remember { mutableStateOf("123456") } + Box( modifier = Modifier.background(Color.Black), contentAlignment = Alignment.TopCenter @@ -163,9 +189,8 @@ private fun OtpTextFieldPreview() { .fillMaxWidth(), otpText = textFieldValue, onOtpTextChange = { textFieldValue = it }, - errorText = "Otp is incorrect, try another one", - cellsArrangement = Arrangement.SpaceBetween, - hasError = false + error = "", + cellsArrangement = Arrangement.SpaceBetween ) } } \ No newline at end of file From aed10648b431749ec4f1364b95b7ec4a5a92c956 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 11:57:11 +0300 Subject: [PATCH 10/23] Add keyboard actions and options parameter --- .../android/design/ui/otpContainer/OtpContainer.kt | 10 ++++++---- .../ui/otpContainer/constants/OtpTextFieldDefaults.kt | 11 +++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt index 10f085cb..4c530c11 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.text.BasicTextField +import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,12 +25,12 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextRange -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.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.otpContainer.constants.OtpTextFieldDefaults import com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle @@ -43,6 +44,8 @@ fun OtpTextField( error: String? = null, otpLength: Int = DEFAULT_OTP_LENGTH, cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + keyboardOptions: KeyboardOptions = OtpTextFieldDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default, cellsStyle: OtpCellStyle = OtpCellStyle(), cellsColors: OtpCellColors = OtpCellColors() ) { @@ -55,10 +58,10 @@ fun OtpTextField( onOtpTextChange(it.text) } }, - keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, decorationBox = { Row( - modifier = Modifier, horizontalArrangement = cellsArrangement, verticalAlignment = Alignment.CenterVertically ) { @@ -74,7 +77,6 @@ fun OtpTextField( } } } - ) if (!error.isNullOrEmpty()) { diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt new file mode 100644 index 00000000..9ddd3fd0 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt @@ -0,0 +1,11 @@ +package com.urlaunched.android.design.ui.otpContainer.constants + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.KeyboardType + +object OtpTextFieldDefaults { + + val DefaultKeyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.Number + ) +} \ No newline at end of file From cd3997625e83a17e6a60bb13f1bcb039cd976609 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 15:45:02 +0300 Subject: [PATCH 11/23] Rename to opt text field --- .../OtpContainer.kt => otptextfield/OtpTextField.kt} | 8 ++++---- .../constants/OtpTextFieldDefaults.kt | 2 +- .../constants/OtpTextFieldDimens.kt} | 4 ++-- .../models/OtpCellColors.kt | 2 +- .../models/OtpCellStyle.kt | 12 ++++++------ 5 files changed, 14 insertions(+), 14 deletions(-) rename design/src/main/java/com/urlaunched/android/design/ui/{otpContainer/OtpContainer.kt => otptextfield/OtpTextField.kt} (96%) rename design/src/main/java/com/urlaunched/android/design/ui/{otpContainer => otptextfield}/constants/OtpTextFieldDefaults.kt (79%) rename design/src/main/java/com/urlaunched/android/design/ui/{otpContainer/constants/OtpContainerDimens.kt => otptextfield/constants/OtpTextFieldDimens.kt} (58%) rename design/src/main/java/com/urlaunched/android/design/ui/{otpContainer => otptextfield}/models/OtpCellColors.kt (85%) rename design/src/main/java/com/urlaunched/android/design/ui/{otpContainer => otptextfield}/models/OtpCellStyle.kt (57%) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt similarity index 96% rename from design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt rename to design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt index 4c530c11..c013d244 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/OtpContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt @@ -1,4 +1,4 @@ -package com.urlaunched.android.design.ui.otpContainer +package com.urlaunched.android.design.ui.otptextfield import androidx.compose.foundation.background import androidx.compose.foundation.border @@ -30,9 +30,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.urlaunched.android.design.resources.dimens.Dimens -import com.urlaunched.android.design.ui.otpContainer.constants.OtpTextFieldDefaults -import com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors -import com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle +import com.urlaunched.android.design.ui.otptextfield.constants.OtpTextFieldDefaults +import com.urlaunched.android.design.ui.otptextfield.models.OtpCellColors +import com.urlaunched.android.design.ui.otptextfield.models.OtpCellStyle private const val DEFAULT_OTP_LENGTH = 6 diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt similarity index 79% rename from design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt rename to design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt index 9ddd3fd0..776f73f1 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpTextFieldDefaults.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt @@ -1,4 +1,4 @@ -package com.urlaunched.android.design.ui.otpContainer.constants +package com.urlaunched.android.design.ui.otptextfield.constants import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.ui.text.input.KeyboardType diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt similarity index 58% rename from design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt rename to design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt index 9883678b..9135e399 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/constants/OtpContainerDimens.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt @@ -1,8 +1,8 @@ -package com.urlaunched.android.design.ui.otpContainer.constants +package com.urlaunched.android.design.ui.otptextfield.constants import androidx.compose.ui.unit.dp -internal object OtpContainerDimens { +internal object OtpTextFieldDimens { val defaultOtpCellHeight = 48.dp val defaultOtpCellWidth = 40.dp val defaultBorderWidth = 1.dp diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt similarity index 85% rename from design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt rename to design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt index eba13b86..82091cee 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellColors.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt @@ -1,4 +1,4 @@ -package com.urlaunched.android.design.ui.otpContainer.models +package com.urlaunched.android.design.ui.otptextfield.models import androidx.compose.ui.graphics.Color diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt similarity index 57% rename from design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt rename to design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt index 5a611bf9..9b06b418 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpContainer/models/OtpCellStyle.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt @@ -1,19 +1,19 @@ -package com.urlaunched.android.design.ui.otpContainer.models +package com.urlaunched.android.design.ui.otptextfield.models import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.ui.graphics.Shape import androidx.compose.ui.text.TextStyle import androidx.compose.ui.unit.Dp import com.urlaunched.android.design.resources.dimens.Dimens -import com.urlaunched.android.design.ui.otpContainer.constants.OtpContainerDimens +import com.urlaunched.android.design.ui.otptextfield.constants.OtpTextFieldDimens data class OtpCellStyle( val textStyle: TextStyle = androidx.compose.material3.Typography().bodyLarge, val shape: Shape = RoundedCornerShape(Dimens.cornerRadiusSmall), - val width: Dp = OtpContainerDimens.defaultOtpCellWidth, - val height: Dp = OtpContainerDimens.defaultOtpCellHeight, - val focusedBorderWidth: Dp = OtpContainerDimens.defaultBorderWidth, - val unfocusedBorderWidth: Dp = OtpContainerDimens.defaultBorderWidth, + val width: Dp = OtpTextFieldDimens.defaultOtpCellWidth, + val height: Dp = OtpTextFieldDimens.defaultOtpCellHeight, + val focusedBorderWidth: Dp = OtpTextFieldDimens.defaultBorderWidth, + val unfocusedBorderWidth: Dp = OtpTextFieldDimens.defaultBorderWidth, val errorTextTopPadding: Dp = Dimens.spacingTiny, val errorTextStyle: TextStyle = TextStyle() ) \ No newline at end of file From 8cc5747cb0a7e673e7aaea7e6e10b53f219e3025 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 15:49:51 +0300 Subject: [PATCH 12/23] Make modifier parameter optional --- .../urlaunched/android/design/ui/otptextfield/OtpTextField.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt index c013d244..c6572830 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt @@ -38,7 +38,7 @@ private const val DEFAULT_OTP_LENGTH = 6 @Composable fun OtpTextField( - modifier: Modifier, + modifier: Modifier = Modifier, otpText: String, onOtpTextChange: (text: String) -> Unit, error: String? = null, From 9be5377684aa1fa11ef8c0675868f6988cdf1f73 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 16:13:54 +0300 Subject: [PATCH 13/23] Change opt default keyboard type to NumberPassword --- .../design/ui/otptextfield/constants/OtpTextFieldDefaults.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt index 776f73f1..70a7ba70 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt @@ -6,6 +6,6 @@ import androidx.compose.ui.text.input.KeyboardType object OtpTextFieldDefaults { val DefaultKeyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.Number + keyboardType = KeyboardType.NumberPassword ) } \ No newline at end of file From b0aa1a21d9fe2f3799a6891966117d4d328b42f6 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 16:37:46 +0300 Subject: [PATCH 14/23] Remove color from the error text --- .../urlaunched/android/design/ui/otptextfield/OtpTextField.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt index c6572830..807a8b40 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt @@ -84,8 +84,7 @@ fun OtpTextField( Text( text = error, - style = cellsStyle.errorTextStyle, - color = cellsColors.errorBorderColor + style = cellsStyle.errorTextStyle ) } } From c2cb80639abb10056839f1182e80ec4a206af1b7 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 16:39:10 +0300 Subject: [PATCH 15/23] Update api --- design/api/current.api | 37 ++++++++++++++++--------------------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index 2e7ca7ca..eca42c77 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -203,15 +203,25 @@ package com.urlaunched.android.design.ui.modifiers { } -package com.urlaunched.android.design.ui.otpContainer { +package com.urlaunched.android.design.ui.otptextfield { - public final class OtpContainerKt { - method @androidx.compose.runtime.Composable public static void OtpTextField(androidx.compose.ui.Modifier modifier, optional int otpLength, boolean hasError, String otpText, String errorText, kotlin.jvm.functions.Function1 onOtpTextChange, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement cellsArrangement, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle cellsStyle, optional com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors cellsColors); + public final class OtpTextFieldKt { + method @androidx.compose.runtime.Composable public static void OtpTextField(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional String? error, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional com.urlaunched.android.design.ui.otptextfield.models.OtpCellStyle cellsStyle, optional com.urlaunched.android.design.ui.otptextfield.models.OtpCellColors cellsColors); } } -package com.urlaunched.android.design.ui.otpContainer.models { +package com.urlaunched.android.design.ui.otptextfield.constants { + + public final class OtpTextFieldDefaults { + method public androidx.compose.foundation.text.KeyboardOptions getDefaultKeyboardOptions(); + property public final androidx.compose.foundation.text.KeyboardOptions DefaultKeyboardOptions; + field public static final com.urlaunched.android.design.ui.otptextfield.constants.OtpTextFieldDefaults INSTANCE; + } + +} + +package com.urlaunched.android.design.ui.otptextfield.models { public final class OtpCellColors { ctor public OtpCellColors(optional long backgroundColor, optional long emptyBackgroundColor, optional long filledBorderColor, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); @@ -221,7 +231,7 @@ package com.urlaunched.android.design.ui.otpContainer.models { method public long component4-0d7_KjU(); method public long component5-0d7_KjU(); method public long component6-0d7_KjU(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellColors copy-tNS2XkQ(long backgroundColor, long emptyBackgroundColor, long filledBorderColor, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); + method public com.urlaunched.android.design.ui.otptextfield.models.OtpCellColors copy-tNS2XkQ(long backgroundColor, long emptyBackgroundColor, long filledBorderColor, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); method public long getBackgroundColor(); method public long getEmptyBackgroundColor(); method public long getEmptyBorderColor(); @@ -246,7 +256,7 @@ package com.urlaunched.android.design.ui.otpContainer.models { method public float component6-D9Ej5fM(); method public float component7-D9Ej5fM(); method public androidx.compose.ui.text.TextStyle component8(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellStyle copy-F0d9qfw(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, float errorTextTopPadding, androidx.compose.ui.text.TextStyle errorTextStyle); + method public com.urlaunched.android.design.ui.otptextfield.models.OtpCellStyle copy-F0d9qfw(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, float errorTextTopPadding, androidx.compose.ui.text.TextStyle errorTextStyle); method public androidx.compose.ui.text.TextStyle getErrorTextStyle(); method public float getErrorTextTopPadding(); method public float getFocusedBorderWidth(); @@ -265,21 +275,6 @@ package com.urlaunched.android.design.ui.otpContainer.models { property public final float width; } - public abstract sealed class OtpCellsArrangement { - } - - public static final class OtpCellsArrangement.SpaceBetween extends com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement { - field public static final com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement.SpaceBetween INSTANCE; - } - - public static final class OtpCellsArrangement.Spaced extends com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement { - ctor public OtpCellsArrangement.Spaced(float spacing); - method public float component1-D9Ej5fM(); - method public com.urlaunched.android.design.ui.otpContainer.models.OtpCellsArrangement.Spaced copy-0680j_4(float spacing); - method public float getSpacing(); - property public final float spacing; - } - } package com.urlaunched.android.design.ui.paging { From b43501dfc315c4e5f3e5be782977b0718f9286b4 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 29 Sep 2025 16:55:55 +0300 Subject: [PATCH 16/23] Change error color on preview --- .../android/design/ui/otptextfield/OtpTextField.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt index 807a8b40..c2308758 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt @@ -25,6 +25,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview @@ -170,7 +171,10 @@ private fun OtpTextFieldErrorPreview() { otpText = textFieldValue, onOtpTextChange = { textFieldValue = it }, error = "Otp is incorrect, try another one", - cellsArrangement = Arrangement.SpaceBetween + cellsArrangement = Arrangement.SpaceBetween, + cellsStyle = OtpCellStyle( + errorTextStyle = TextStyle(color = Color.Red) + ) ) } } From d0c40a7240388d736e52c2233e3c51e6321fa016 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Tue, 30 Sep 2025 14:29:55 +0300 Subject: [PATCH 17/23] Simplify otp container to accept cellContent --- design/api/current.api | 79 ++----- .../design/ui/otpcontainer/OTPCellScope.kt | 6 + .../design/ui/otpcontainer/OTPContainer.kt | 127 +++++++++++ .../constants/OTPContainerDefaults.kt | 12 ++ .../design/ui/otptextfield/OtpTextField.kt | 201 ------------------ .../constants/OtpTextFieldDefaults.kt | 11 - .../constants/OtpTextFieldDimens.kt | 9 - .../ui/otptextfield/models/OtpCellColors.kt | 12 -- .../ui/otptextfield/models/OtpCellStyle.kt | 19 -- 9 files changed, 162 insertions(+), 314 deletions(-) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt delete mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt diff --git a/design/api/current.api b/design/api/current.api index eca42c77..2e1f233b 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -203,76 +203,31 @@ package com.urlaunched.android.design.ui.modifiers { } -package com.urlaunched.android.design.ui.otptextfield { +package com.urlaunched.android.design.ui.otpcontainer { - public final class OtpTextFieldKt { - method @androidx.compose.runtime.Composable public static void OtpTextField(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional String? error, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, optional com.urlaunched.android.design.ui.otptextfield.models.OtpCellStyle cellsStyle, optional com.urlaunched.android.design.ui.otptextfield.models.OtpCellColors cellsColors); + public final class OTPCellScope { + ctor public OTPCellScope(String char, boolean isFocused); + method public String component1(); + method public boolean component2(); + method public com.urlaunched.android.design.ui.otpcontainer.OTPCellScope copy(String char, boolean isFocused); + method public String getChar(); + method public boolean isFocused(); + property public final String char; + property public final boolean isFocused; } -} - -package com.urlaunched.android.design.ui.otptextfield.constants { - - public final class OtpTextFieldDefaults { - method public androidx.compose.foundation.text.KeyboardOptions getDefaultKeyboardOptions(); - property public final androidx.compose.foundation.text.KeyboardOptions DefaultKeyboardOptions; - field public static final com.urlaunched.android.design.ui.otptextfield.constants.OtpTextFieldDefaults INSTANCE; + public final class OTPContainerKt { + method @androidx.compose.runtime.Composable public static void OTPContainer(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, kotlin.jvm.functions.Function1 cellContent); } } -package com.urlaunched.android.design.ui.otptextfield.models { +package com.urlaunched.android.design.ui.otpcontainer.constants { - public final class OtpCellColors { - ctor public OtpCellColors(optional long backgroundColor, optional long emptyBackgroundColor, optional long filledBorderColor, optional long focusedBorderColor, optional long errorBorderColor, optional long emptyBorderColor); - method public long component1-0d7_KjU(); - method public long component2-0d7_KjU(); - method public long component3-0d7_KjU(); - method public long component4-0d7_KjU(); - method public long component5-0d7_KjU(); - method public long component6-0d7_KjU(); - method public com.urlaunched.android.design.ui.otptextfield.models.OtpCellColors copy-tNS2XkQ(long backgroundColor, long emptyBackgroundColor, long filledBorderColor, long focusedBorderColor, long errorBorderColor, long emptyBorderColor); - method public long getBackgroundColor(); - method public long getEmptyBackgroundColor(); - method public long getEmptyBorderColor(); - method public long getErrorBorderColor(); - method public long getFilledBorderColor(); - method public long getFocusedBorderColor(); - property public final long backgroundColor; - property public final long emptyBackgroundColor; - property public final long emptyBorderColor; - property public final long errorBorderColor; - property public final long filledBorderColor; - property public final long focusedBorderColor; - } - - public final class OtpCellStyle { - ctor public OtpCellStyle(optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional float errorTextTopPadding, optional androidx.compose.ui.text.TextStyle errorTextStyle); - method public androidx.compose.ui.text.TextStyle component1(); - method public androidx.compose.ui.graphics.Shape component2(); - method public float component3-D9Ej5fM(); - method public float component4-D9Ej5fM(); - method public float component5-D9Ej5fM(); - method public float component6-D9Ej5fM(); - method public float component7-D9Ej5fM(); - method public androidx.compose.ui.text.TextStyle component8(); - method public com.urlaunched.android.design.ui.otptextfield.models.OtpCellStyle copy-F0d9qfw(androidx.compose.ui.text.TextStyle textStyle, androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, float errorTextTopPadding, androidx.compose.ui.text.TextStyle errorTextStyle); - method public androidx.compose.ui.text.TextStyle getErrorTextStyle(); - method public float getErrorTextTopPadding(); - method public float getFocusedBorderWidth(); - method public float getHeight(); - method public androidx.compose.ui.graphics.Shape getShape(); - method public androidx.compose.ui.text.TextStyle getTextStyle(); - method public float getUnfocusedBorderWidth(); - method public float getWidth(); - property public final androidx.compose.ui.text.TextStyle errorTextStyle; - property public final float errorTextTopPadding; - property public final float focusedBorderWidth; - property public final float height; - property public final androidx.compose.ui.graphics.Shape shape; - property public final androidx.compose.ui.text.TextStyle textStyle; - property public final float unfocusedBorderWidth; - property public final float width; + public final class OTPContainerDefaults { + method public androidx.compose.foundation.text.KeyboardOptions getDefaultKeyboardOptions(); + property public final androidx.compose.foundation.text.KeyboardOptions DefaultKeyboardOptions; + field public static final com.urlaunched.android.design.ui.otpcontainer.constants.OTPContainerDefaults INSTANCE; } } diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt new file mode 100644 index 00000000..ea5cfccd --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt @@ -0,0 +1,6 @@ +package com.urlaunched.android.design.ui.otpcontainer + +data class OTPCellScope( + val char: String, + val isFocused: Boolean +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt new file mode 100644 index 00000000..f39cc99c --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt @@ -0,0 +1,127 @@ +package com.urlaunched.android.design.ui.otpcontainer + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.input.TextFieldValue +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.otpcontainer.constants.OTPContainerDefaults +import com.urlaunched.android.design.ui.shadow.shadow + +private const val DEFAULT_OTP_LENGTH = 6 + +@Composable +fun OTPContainer( + modifier: Modifier = Modifier, + otpText: String, + onOtpTextChange: (text: String) -> Unit, + otpLength: Int = DEFAULT_OTP_LENGTH, + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + keyboardOptions: KeyboardOptions = OTPContainerDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default, + cellContent: @Composable OTPCellScope.() -> Unit +) { + BasicTextField( + modifier = modifier, + value = TextFieldValue(otpText, selection = TextRange(otpText.length)), + onValueChange = { + if (it.text.length <= otpLength) { + onOtpTextChange(it.text) + } + }, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + decorationBox = { + Row( + horizontalArrangement = cellsArrangement, + verticalAlignment = Alignment.CenterVertically + ) { + repeat(otpLength) { index -> + cellContent( + OTPCellScope( + char = otpText.getOrNull(index)?.toString().orEmpty(), + isFocused = otpText.length == index + ) + ) + } + } + } + ) +} + +@Preview(showBackground = true) +@Composable +private fun OtpTextFieldPreview() { + var textFieldValue by remember { mutableStateOf("12") } + + Box( + modifier = Modifier.background(Color.LightGray) + ) { + OTPContainer( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) { + Text( + text = char, + textAlign = TextAlign.Center, + modifier = Modifier + .size(40.dp, 48.dp) + .shadow( + color = Color.Black, + shadowBlurRadius = 6.dp, + offset = DpOffset(x = 0.dp, y = 2.dp), + alpha = 0.20f + ) + .clip(RoundedCornerShape(Dimens.cornerRadiusSmall)) + .background(Color.White) + .run { + when { + isFocused -> this.border( + width = 1.dp, + color = MaterialTheme.colorScheme.primary, + shape = RoundedCornerShape(Dimens.cornerRadiusSmall) + ) + + char.isNotBlank() -> this.border( + width = 1.dp, + color = MaterialTheme.colorScheme.outline, + shape = RoundedCornerShape(Dimens.cornerRadiusSmall) + ) + + else -> this + } + } + .padding(vertical = Dimens.spacingNormal) + ) + } + } +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt new file mode 100644 index 00000000..635cb792 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPContainerDefaults.kt @@ -0,0 +1,12 @@ +package com.urlaunched.android.design.ui.otpcontainer.constants + +import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.input.KeyboardType + +object OTPContainerDefaults { + val DefaultKeyboardOptions = KeyboardOptions.Default.copy( + keyboardType = KeyboardType.NumberPassword, + imeAction = ImeAction.Done + ) +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt deleted file mode 100644 index c2308758..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/OtpTextField.kt +++ /dev/null @@ -1,201 +0,0 @@ -package com.urlaunched.android.design.ui.otptextfield - -import androidx.compose.foundation.background -import androidx.compose.foundation.border -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.BasicTextField -import androidx.compose.foundation.text.KeyboardActions -import androidx.compose.foundation.text.KeyboardOptions -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.draw.clip -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.text.TextRange -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.text.input.TextFieldValue -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview -import androidx.compose.ui.unit.dp -import com.urlaunched.android.design.resources.dimens.Dimens -import com.urlaunched.android.design.ui.otptextfield.constants.OtpTextFieldDefaults -import com.urlaunched.android.design.ui.otptextfield.models.OtpCellColors -import com.urlaunched.android.design.ui.otptextfield.models.OtpCellStyle - -private const val DEFAULT_OTP_LENGTH = 6 - -@Composable -fun OtpTextField( - modifier: Modifier = Modifier, - otpText: String, - onOtpTextChange: (text: String) -> Unit, - error: String? = null, - otpLength: Int = DEFAULT_OTP_LENGTH, - cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), - keyboardOptions: KeyboardOptions = OtpTextFieldDefaults.DefaultKeyboardOptions, - keyboardActions: KeyboardActions = KeyboardActions.Default, - cellsStyle: OtpCellStyle = OtpCellStyle(), - cellsColors: OtpCellColors = OtpCellColors() -) { - Column(modifier) { - BasicTextField( - modifier = Modifier.fillMaxWidth(), - value = TextFieldValue(otpText, selection = TextRange(otpText.length)), - onValueChange = { - if (it.text.length <= otpLength) { - onOtpTextChange(it.text) - } - }, - keyboardOptions = keyboardOptions, - keyboardActions = keyboardActions, - decorationBox = { - Row( - horizontalArrangement = cellsArrangement, - verticalAlignment = Alignment.CenterVertically - ) { - repeat(otpLength) { index -> - OtpCellView( - index = index, - text = otpText, - isError = error != null, - isFocused = otpText.length == index, - style = cellsStyle, - colors = cellsColors - ) - } - } - } - ) - - if (!error.isNullOrEmpty()) { - Spacer(Modifier.height(cellsStyle.errorTextTopPadding)) - - Text( - text = error, - style = cellsStyle.errorTextStyle - ) - } - } -} - -@Composable -private fun OtpCellView( - index: Int, - text: String, - isError: Boolean, - isFocused: Boolean, - style: OtpCellStyle, - colors: OtpCellColors -) { - val char = text.getOrNull(index)?.toString().orEmpty() - - val currentBorderColor = when { - isError -> colors.errorBorderColor - isFocused -> colors.focusedBorderColor - char.isNotBlank() -> colors.filledBorderColor - else -> colors.emptyBorderColor - } - - val borderWidth = if (isFocused) { - style.focusedBorderWidth - } else { - style.unfocusedBorderWidth - } - - val backgroundColor = if (char.isNotBlank() || isFocused) { - colors.backgroundColor - } else { - colors.emptyBackgroundColor - } - - Box( - Modifier - .size(width = style.width, height = style.height) - .clip(style.shape) - .background(backgroundColor) - .border(borderWidth, currentBorderColor, style.shape), - contentAlignment = Alignment.Center - ) { - Text( - text = char, - style = style.textStyle, - textAlign = TextAlign.Center - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OtpTextFieldPreview() { - var textFieldValue by remember { mutableStateOf("12") } - - Box( - modifier = Modifier.background(Color.Black) - ) { - OtpTextField( - modifier = Modifier - .padding(vertical = 12.dp, horizontal = 16.dp) - .fillMaxWidth(), - otpText = textFieldValue, - onOtpTextChange = { textFieldValue = it }, - cellsArrangement = Arrangement.SpaceBetween - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OtpTextFieldErrorPreview() { - var textFieldValue by remember { mutableStateOf("123") } - - Box( - modifier = Modifier.background(Color.Black) - ) { - OtpTextField( - modifier = Modifier - .padding(vertical = 12.dp, horizontal = 16.dp) - .fillMaxWidth(), - otpText = textFieldValue, - onOtpTextChange = { textFieldValue = it }, - error = "Otp is incorrect, try another one", - cellsArrangement = Arrangement.SpaceBetween, - cellsStyle = OtpCellStyle( - errorTextStyle = TextStyle(color = Color.Red) - ) - ) - } -} - -@Preview(showBackground = true) -@Composable -private fun OtpTextFieldErrorWithoutMessagePreview() { - var textFieldValue by remember { mutableStateOf("123456") } - - Box( - modifier = Modifier.background(Color.Black), - contentAlignment = Alignment.TopCenter - ) { - OtpTextField( - modifier = Modifier - .padding(vertical = 12.dp, horizontal = 16.dp) - .fillMaxWidth(), - otpText = textFieldValue, - onOtpTextChange = { textFieldValue = it }, - error = "", - cellsArrangement = Arrangement.SpaceBetween - ) - } -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt deleted file mode 100644 index 70a7ba70..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDefaults.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.urlaunched.android.design.ui.otptextfield.constants - -import androidx.compose.foundation.text.KeyboardOptions -import androidx.compose.ui.text.input.KeyboardType - -object OtpTextFieldDefaults { - - val DefaultKeyboardOptions = KeyboardOptions.Default.copy( - keyboardType = KeyboardType.NumberPassword - ) -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt deleted file mode 100644 index 9135e399..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/constants/OtpTextFieldDimens.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.urlaunched.android.design.ui.otptextfield.constants - -import androidx.compose.ui.unit.dp - -internal object OtpTextFieldDimens { - val defaultOtpCellHeight = 48.dp - val defaultOtpCellWidth = 40.dp - val defaultBorderWidth = 1.dp -} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt deleted file mode 100644 index 82091cee..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellColors.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.urlaunched.android.design.ui.otptextfield.models - -import androidx.compose.ui.graphics.Color - -data class OtpCellColors( - val backgroundColor: Color = Color.White, - val emptyBackgroundColor: Color = Color.White, - val filledBorderColor: Color = Color.LightGray, - val focusedBorderColor: Color = Color.Black, - val errorBorderColor: Color = Color.Red, - val emptyBorderColor: Color = Color.White -) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt deleted file mode 100644 index 9b06b418..00000000 --- a/design/src/main/java/com/urlaunched/android/design/ui/otptextfield/models/OtpCellStyle.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.urlaunched.android.design.ui.otptextfield.models - -import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.ui.graphics.Shape -import androidx.compose.ui.text.TextStyle -import androidx.compose.ui.unit.Dp -import com.urlaunched.android.design.resources.dimens.Dimens -import com.urlaunched.android.design.ui.otptextfield.constants.OtpTextFieldDimens - -data class OtpCellStyle( - val textStyle: TextStyle = androidx.compose.material3.Typography().bodyLarge, - val shape: Shape = RoundedCornerShape(Dimens.cornerRadiusSmall), - val width: Dp = OtpTextFieldDimens.defaultOtpCellWidth, - val height: Dp = OtpTextFieldDimens.defaultOtpCellHeight, - val focusedBorderWidth: Dp = OtpTextFieldDimens.defaultBorderWidth, - val unfocusedBorderWidth: Dp = OtpTextFieldDimens.defaultBorderWidth, - val errorTextTopPadding: Dp = Dimens.spacingTiny, - val errorTextStyle: TextStyle = TextStyle() -) \ No newline at end of file From bdbbb3acfb7746cf80657861fc9f1783d01086ec Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Thu, 2 Oct 2025 18:44:39 +0300 Subject: [PATCH 18/23] Create shadow style with overload --- .../urlaunched/android/design/ui/shadow/Shadow.kt | 9 +++++++++ .../android/design/ui/shadow/models/ShadowStyle.kt | 14 ++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt b/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt index 1a82bf7b..afb142e9 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/shadow/Shadow.kt @@ -9,6 +9,15 @@ import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle + +fun Modifier.shadow(style: ShadowStyle) = this.shadow( + color = style.color, + alpha = style.alpha, + cornersRadius = style.cornersRadius, + shadowBlurRadius = style.blurRadius, + offset = style.offset +) fun Modifier.shadow( color: Color = Color.Black, diff --git a/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt new file mode 100644 index 00000000..ed8979bb --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/shadow/models/ShadowStyle.kt @@ -0,0 +1,14 @@ +package com.urlaunched.android.design.ui.shadow.models + +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp + +data class ShadowStyle( + val color: Color = Color.Black, + val alpha: Float = 1f, + val cornersRadius: Dp = 0.dp, + val blurRadius: Dp = 0.dp, + val offset: DpOffset = DpOffset.Zero +) \ No newline at end of file From aa518ddcc03fec3ed7d5faf4bed2b3dd795b8265 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 6 Oct 2025 17:01:32 +0300 Subject: [PATCH 19/23] Add overloads without otp cell --- .../design/ui/otpcontainer/OTPCellScope.kt | 2 +- .../design/ui/otpcontainer/OTPContainer.kt | 223 +++++++++++++++--- .../otpcontainer/constants/OTPCellDimens.kt | 9 + .../ui/otpcontainer/models/OTPCellColors.kt | 13 + .../ui/otpcontainer/models/OTPCellStyle.kt | 17 ++ 5 files changed, 225 insertions(+), 39 deletions(-) create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt create mode 100644 design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt index ea5cfccd..d78ac651 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPCellScope.kt @@ -1,6 +1,6 @@ package com.urlaunched.android.design.ui.otpcontainer data class OTPCellScope( - val char: String, + val char: Char?, val isFocused: Boolean ) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt index f39cc99c..64882ef1 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt @@ -4,14 +4,16 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardOptions +import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -24,17 +26,102 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.text.TextRange +import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.DpOffset import androidx.compose.ui.unit.dp import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.modifiers.ifNotNull import com.urlaunched.android.design.ui.otpcontainer.constants.OTPContainerDefaults +import com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors +import com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle import com.urlaunched.android.design.ui.shadow.shadow private const val DEFAULT_OTP_LENGTH = 6 +@Composable +fun OTPContainerWithError( + modifier: Modifier = Modifier, + otpText: String, + onOtpTextChange: (text: String) -> Unit, + error: String? = null, + textStyle: TextStyle = LocalTextStyle.current, + errorTextStyle: TextStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.error), + errorContentPadding: PaddingValues = PaddingValues(top = Dimens.spacingTiny), + otpLength: Int = DEFAULT_OTP_LENGTH, + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + cellStyle: OTPCellStyle = OTPCellStyle(), + cellColors: OTPCellColors = OTPCellColors(), + keyboardOptions: KeyboardOptions = OTPContainerDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + Column( + modifier = modifier + ) { + OTPContainer( + modifier = Modifier.fillMaxWidth(), + otpText = otpText, + onOtpTextChange = onOtpTextChange, + otpLength = otpLength, + cellsArrangement = cellsArrangement, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cellContent = { + OTPCellView( + isError = error != null, + style = cellStyle, + colors = cellColors, + textStyle = textStyle + ) + } + ) + + if (!error.isNullOrEmpty()) { + Text( + text = error, + style = errorTextStyle, + modifier = Modifier.padding(errorContentPadding) + ) + } + } +} + +@Composable +fun OTPContainer( + modifier: Modifier = Modifier, + otpText: String, + onOtpTextChange: (text: String) -> Unit, + textStyle: TextStyle = LocalTextStyle.current, + otpLength: Int = DEFAULT_OTP_LENGTH, + cellsArrangement: Arrangement.Horizontal = Arrangement.spacedBy(Dimens.spacingSmall), + isError: Boolean = false, + cellStyle: OTPCellStyle = OTPCellStyle(), + cellColors: OTPCellColors = OTPCellColors(), + keyboardOptions: KeyboardOptions = OTPContainerDefaults.DefaultKeyboardOptions, + keyboardActions: KeyboardActions = KeyboardActions.Default +) { + OTPContainer( + modifier = modifier, + otpText = otpText, + onOtpTextChange = onOtpTextChange, + otpLength = otpLength, + cellsArrangement = cellsArrangement, + keyboardOptions = keyboardOptions, + keyboardActions = keyboardActions, + cellContent = { + OTPCellView( + isError = isError, + style = cellStyle, + colors = cellColors, + textStyle = textStyle + ) + } + ) +} + @Composable fun OTPContainer( modifier: Modifier = Modifier, @@ -64,7 +151,7 @@ fun OTPContainer( repeat(otpLength) { index -> cellContent( OTPCellScope( - char = otpText.getOrNull(index)?.toString().orEmpty(), + char = otpText.getOrNull(index), isFocused = otpText.length == index ) ) @@ -74,9 +161,74 @@ fun OTPContainer( ) } -@Preview(showBackground = true) @Composable -private fun OtpTextFieldPreview() { +private fun OTPCellScope.OTPCellView( + modifier: Modifier = Modifier, + isError: Boolean = false, + textStyle: TextStyle = LocalTextStyle.current, + style: OTPCellStyle = OTPCellStyle(), + colors: OTPCellColors = OTPCellColors() +) { + val char = char?.toString().orEmpty() + + val currentBorderColor = when { + isError -> colors.errorBorderColor + isFocused -> colors.focusedBorderColor + char.isNotBlank() -> colors.borderColor + else -> colors.emptyBorderColor + } + + val borderWidth = if (isFocused) { + style.focusedBorderWidth + } else { + style.unfocusedBorderWidth + } + + val backgroundColor = when { + isFocused -> colors.focusedBackgroundColor + char.isNotBlank() -> colors.backgroundColor + else -> colors.emptyBackgroundColor + } + + Box( + modifier = modifier + .size(width = style.width, height = style.height) + .ifNotNull(style.shadowStyle) { Modifier.shadow(it) } + .clip(style.shape) + .background(backgroundColor) + .border(borderWidth, currentBorderColor, style.shape), + contentAlignment = Alignment.Center + ) { + Text( + text = char, + style = textStyle, + textAlign = TextAlign.Center + ) + } +} + +@Preview +@Composable +private fun OTPContainerPreview() { + var textFieldValue by remember { mutableStateOf("1234") } + + Box( + modifier = Modifier.background(Color.LightGray) + ) { + OTPContainer( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) + } +} + +@Preview +@Composable +private fun OTPContainerShadowPreview() { var textFieldValue by remember { mutableStateOf("12") } Box( @@ -87,41 +239,36 @@ private fun OtpTextFieldPreview() { .padding(vertical = 12.dp, horizontal = 16.dp) .fillMaxWidth(), otpText = textFieldValue, + cellStyle = OTPCellStyle( + shadowStyle = ShadowStyle( + color = Color.Black, + blurRadius = 6.dp, + offset = DpOffset(x = 0.dp, y = 2.dp), + alpha = 0.20f + ) + ), onOtpTextChange = { textFieldValue = it }, cellsArrangement = Arrangement.SpaceBetween - ) { - Text( - text = char, - textAlign = TextAlign.Center, - modifier = Modifier - .size(40.dp, 48.dp) - .shadow( - color = Color.Black, - shadowBlurRadius = 6.dp, - offset = DpOffset(x = 0.dp, y = 2.dp), - alpha = 0.20f - ) - .clip(RoundedCornerShape(Dimens.cornerRadiusSmall)) - .background(Color.White) - .run { - when { - isFocused -> this.border( - width = 1.dp, - color = MaterialTheme.colorScheme.primary, - shape = RoundedCornerShape(Dimens.cornerRadiusSmall) - ) - - char.isNotBlank() -> this.border( - width = 1.dp, - color = MaterialTheme.colorScheme.outline, - shape = RoundedCornerShape(Dimens.cornerRadiusSmall) - ) - - else -> this - } - } - .padding(vertical = Dimens.spacingNormal) - ) - } + ) + } +} + +@Preview +@Composable +private fun OTPContainerErrorPreview() { + var textFieldValue by remember { mutableStateOf("123456") } + + Box( + modifier = Modifier.background(Color.LightGray) + ) { + OTPContainerWithError( + modifier = Modifier + .padding(vertical = 12.dp, horizontal = 16.dp) + .fillMaxWidth(), + otpText = textFieldValue, + error = "Something went wrong, try another one", + onOtpTextChange = { textFieldValue = it }, + cellsArrangement = Arrangement.SpaceBetween + ) } } \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt new file mode 100644 index 00000000..bbe7241a --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/constants/OTPCellDimens.kt @@ -0,0 +1,9 @@ +package com.urlaunched.android.design.ui.otpcontainer.constants + +import androidx.compose.ui.unit.dp + +internal object OTPCellDimens { + val defaultOtpCellHeight = 48.dp + val defaultOtpCellWidth = 40.dp + val defaultBorderWidth = 1.dp +} \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt new file mode 100644 index 00000000..e85d180f --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellColors.kt @@ -0,0 +1,13 @@ +package com.urlaunched.android.design.ui.otpcontainer.models + +import androidx.compose.ui.graphics.Color + +data class OTPCellColors( + val backgroundColor: Color = Color.White, + val emptyBackgroundColor: Color = backgroundColor, + val focusedBackgroundColor: Color = backgroundColor, + val borderColor: Color = Color.LightGray, + val emptyBorderColor: Color = borderColor, + val focusedBorderColor: Color = borderColor, + val errorBorderColor: Color = Color.Red +) \ No newline at end of file diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt new file mode 100644 index 00000000..b05af9f7 --- /dev/null +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/models/OTPCellStyle.kt @@ -0,0 +1,17 @@ +package com.urlaunched.android.design.ui.otpcontainer.models + +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.unit.Dp +import com.urlaunched.android.design.resources.dimens.Dimens +import com.urlaunched.android.design.ui.otpcontainer.constants.OTPCellDimens +import com.urlaunched.android.design.ui.shadow.models.ShadowStyle + +data class OTPCellStyle( + val shape: Shape = RoundedCornerShape(Dimens.cornerRadiusSmall), + val width: Dp = OTPCellDimens.defaultOtpCellWidth, + val height: Dp = OTPCellDimens.defaultOtpCellHeight, + val focusedBorderWidth: Dp = OTPCellDimens.defaultBorderWidth, + val unfocusedBorderWidth: Dp = OTPCellDimens.defaultBorderWidth, + val shadowStyle: ShadowStyle? = null +) \ No newline at end of file From 33a0c0f21e5368aeb4c34e137a9f5aea52846cb9 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 6 Oct 2025 17:04:29 +0300 Subject: [PATCH 20/23] Update api --- design/api/current.api | 90 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 85 insertions(+), 5 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index 2e1f233b..2516ed05 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -206,18 +206,20 @@ package com.urlaunched.android.design.ui.modifiers { package com.urlaunched.android.design.ui.otpcontainer { public final class OTPCellScope { - ctor public OTPCellScope(String char, boolean isFocused); - method public String component1(); + ctor public OTPCellScope(Character? char, boolean isFocused); + method public Character? component1(); method public boolean component2(); - method public com.urlaunched.android.design.ui.otpcontainer.OTPCellScope copy(String char, boolean isFocused); - method public String getChar(); + method public com.urlaunched.android.design.ui.otpcontainer.OTPCellScope copy(Character? char, boolean isFocused); + method public Character? getChar(); method public boolean isFocused(); - property public final String char; + property public final Character? char; property public final boolean isFocused; } public final class OTPContainerKt { + method @androidx.compose.runtime.Composable public static void OTPContainer(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional androidx.compose.ui.text.TextStyle textStyle, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional boolean isError, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle cellStyle, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors cellColors, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions); method @androidx.compose.runtime.Composable public static void OTPContainer(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, kotlin.jvm.functions.Function1 cellContent); + method @androidx.compose.runtime.Composable public static void OTPContainerWithError(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional String? error, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.text.TextStyle errorTextStyle, optional androidx.compose.foundation.layout.PaddingValues errorContentPadding, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle cellStyle, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors cellColors, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions); } } @@ -232,6 +234,59 @@ package com.urlaunched.android.design.ui.otpcontainer.constants { } +package com.urlaunched.android.design.ui.otpcontainer.models { + + public final class OTPCellColors { + ctor public OTPCellColors(optional long backgroundColor, optional long emptyBackgroundColor, optional long focusedBackgroundColor, optional long borderColor, optional long emptyBorderColor, optional long focusedBorderColor, optional long errorBorderColor); + method public long component1-0d7_KjU(); + method public long component2-0d7_KjU(); + method public long component3-0d7_KjU(); + method public long component4-0d7_KjU(); + method public long component5-0d7_KjU(); + method public long component6-0d7_KjU(); + method public long component7-0d7_KjU(); + method public com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors copy-4JmcsL4(long backgroundColor, long emptyBackgroundColor, long focusedBackgroundColor, long borderColor, long emptyBorderColor, long focusedBorderColor, long errorBorderColor); + method public long getBackgroundColor(); + method public long getBorderColor(); + method public long getEmptyBackgroundColor(); + method public long getEmptyBorderColor(); + method public long getErrorBorderColor(); + method public long getFocusedBackgroundColor(); + method public long getFocusedBorderColor(); + property public final long backgroundColor; + property public final long borderColor; + property public final long emptyBackgroundColor; + property public final long emptyBorderColor; + property public final long errorBorderColor; + property public final long focusedBackgroundColor; + property public final long focusedBorderColor; + } + + public final class OTPCellStyle { + ctor public OTPCellStyle(optional androidx.compose.ui.graphics.Shape shape, optional float width, optional float height, optional float focusedBorderWidth, optional float unfocusedBorderWidth, optional com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadowStyle); + method public androidx.compose.ui.graphics.Shape component1(); + method public float component2-D9Ej5fM(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public float component5-D9Ej5fM(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle? component6(); + method public com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle copy-YLPp7PM(androidx.compose.ui.graphics.Shape shape, float width, float height, float focusedBorderWidth, float unfocusedBorderWidth, com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadowStyle); + method public float getFocusedBorderWidth(); + method public float getHeight(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle? getShadowStyle(); + method public androidx.compose.ui.graphics.Shape getShape(); + method public float getUnfocusedBorderWidth(); + method public float getWidth(); + property public final float focusedBorderWidth; + property public final float height; + property public final com.urlaunched.android.design.ui.shadow.models.ShadowStyle? shadowStyle; + property public final androidx.compose.ui.graphics.Shape shape; + property public final float unfocusedBorderWidth; + property public final float width; + } + +} + package com.urlaunched.android.design.ui.paging { public final class PagingColumnKt { @@ -457,11 +512,36 @@ package com.urlaunched.android.design.ui.scrollbar.controller { package com.urlaunched.android.design.ui.shadow { public final class ShadowKt { + method public static androidx.compose.ui.Modifier shadow(androidx.compose.ui.Modifier, com.urlaunched.android.design.ui.shadow.models.ShadowStyle style); method public static androidx.compose.ui.Modifier shadow(androidx.compose.ui.Modifier, optional long color, optional float alpha, optional float cornersRadius, optional float shadowBlurRadius, optional long offset); } } +package com.urlaunched.android.design.ui.shadow.models { + + public final class ShadowStyle { + ctor public ShadowStyle(optional long color, optional float alpha, optional float cornersRadius, optional float blurRadius, optional long offset); + method public long component1-0d7_KjU(); + method public float component2(); + method public float component3-D9Ej5fM(); + method public float component4-D9Ej5fM(); + method public long component5-RKDOV3M(); + method public com.urlaunched.android.design.ui.shadow.models.ShadowStyle copy-w1ByDHw(long color, float alpha, float cornersRadius, float blurRadius, long offset); + method public float getAlpha(); + method public float getBlurRadius(); + method public long getColor(); + method public float getCornersRadius(); + method public long getOffset(); + property public final float alpha; + property public final float blurRadius; + property public final long color; + property public final float cornersRadius; + property public final long offset; + } + +} + package com.urlaunched.android.design.ui.shimmer { public final class ShimmerKt { From 58f37087b7f82e40f450db564dc0a11c5df92c6c Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 20 Oct 2025 16:20:41 +0300 Subject: [PATCH 21/23] Remove jvmToolchain --- cdn/models/domainn/build.gradle | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cdn/models/domainn/build.gradle b/cdn/models/domainn/build.gradle index 9addcb70..8c16f045 100644 --- a/cdn/models/domainn/build.gradle +++ b/cdn/models/domainn/build.gradle @@ -20,8 +20,4 @@ dependencies { // Kotlin implementation(platform(libs.kotlinDependencies.bom)) implementation libs.kotlinDependencies.serialization -} - -kotlin { - jvmToolchain(17) } \ No newline at end of file From 84133313b478bc04ceb5192fb21a63bf0cb4f927 Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 20 Oct 2025 16:22:03 +0300 Subject: [PATCH 22/23] Rename OTPCellView to OTPCell --- .../android/design/ui/otpcontainer/OTPContainer.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt index 64882ef1..8b9ed68a 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt @@ -70,7 +70,7 @@ fun OTPContainerWithError( keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, cellContent = { - OTPCellView( + OTPCell( isError = error != null, style = cellStyle, colors = cellColors, @@ -112,7 +112,7 @@ fun OTPContainer( keyboardOptions = keyboardOptions, keyboardActions = keyboardActions, cellContent = { - OTPCellView( + OTPCell( isError = isError, style = cellStyle, colors = cellColors, @@ -162,7 +162,7 @@ fun OTPContainer( } @Composable -private fun OTPCellScope.OTPCellView( +private fun OTPCellScope.OTPCell( modifier: Modifier = Modifier, isError: Boolean = false, textStyle: TextStyle = LocalTextStyle.current, From a308ff1f2886ca4ec19b755147327140bc95703d Mon Sep 17 00:00:00 2001 From: Khotych Mykola Date: Mon, 20 Oct 2025 17:09:07 +0300 Subject: [PATCH 23/23] Make base OTPContainer private --- design/api/current.api | 1 - .../urlaunched/android/design/ui/otpcontainer/OTPContainer.kt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/design/api/current.api b/design/api/current.api index 2516ed05..29aa3ce7 100644 --- a/design/api/current.api +++ b/design/api/current.api @@ -218,7 +218,6 @@ package com.urlaunched.android.design.ui.otpcontainer { public final class OTPContainerKt { method @androidx.compose.runtime.Composable public static void OTPContainer(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional androidx.compose.ui.text.TextStyle textStyle, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional boolean isError, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle cellStyle, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors cellColors, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions); - method @androidx.compose.runtime.Composable public static void OTPContainer(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions, kotlin.jvm.functions.Function1 cellContent); method @androidx.compose.runtime.Composable public static void OTPContainerWithError(optional androidx.compose.ui.Modifier modifier, String otpText, kotlin.jvm.functions.Function1 onOtpTextChange, optional String? error, optional androidx.compose.ui.text.TextStyle textStyle, optional androidx.compose.ui.text.TextStyle errorTextStyle, optional androidx.compose.foundation.layout.PaddingValues errorContentPadding, optional int otpLength, optional androidx.compose.foundation.layout.Arrangement.Horizontal cellsArrangement, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellStyle cellStyle, optional com.urlaunched.android.design.ui.otpcontainer.models.OTPCellColors cellColors, optional androidx.compose.foundation.text.KeyboardOptions keyboardOptions, optional androidx.compose.foundation.text.KeyboardActions keyboardActions); } diff --git a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt index 8b9ed68a..c54dae37 100644 --- a/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt +++ b/design/src/main/java/com/urlaunched/android/design/ui/otpcontainer/OTPContainer.kt @@ -123,7 +123,7 @@ fun OTPContainer( } @Composable -fun OTPContainer( +private fun OTPContainer( modifier: Modifier = Modifier, otpText: String, onOtpTextChange: (text: String) -> Unit,