diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt index 93369d7b46..f135158d06 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/GhostTangemButton.kt @@ -80,6 +80,7 @@ fun GhostTangemButton( contentColor = contentColor, isEnabled = isEnabled, isLoading = isLoading, + hasPadding = false, size = size, iconPosition = iconPosition, iconRes = iconRes, diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt index a4574014be..af41dd86cc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/button/TangemButtonInternal.kt @@ -43,8 +43,9 @@ private const val LOADING_ANIMATION_DURATION = 150 * @param text TextReference for the button label. * @param iconRes Drawable resource ID for the icon to be displayed in the button. * @param iconPosition Position of the icon (Start or End). - * @param isEnabled Boolean indicating whether the button is enabled. + * @param isEnabled Boolean indicating whether the button is enabled. * @param isLoading Boolean indicating whether the button is in a loading state. + * @param hasPadding Boolean indicating whether the button should have padding around its content. * @param contentColor Color of the button content (text and icon). * @param size TangemButtonSize defining the size of the button. * @@ -60,6 +61,7 @@ internal fun TangemButtonInternal( iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start, isEnabled: Boolean = true, isLoading: Boolean = false, + hasPadding: Boolean = true, contentColor: Color = TangemTheme.colors2.text.neutral.primary, size: TangemButtonSize = TangemButtonSize.X15, ) { @@ -72,7 +74,7 @@ internal fun TangemButtonInternal( .conditionalCompose(text == null) { width(size.toHeightDp()) } - .conditionalCompose(text != null) { + .conditionalCompose(text != null && hasPadding) { padding(size.toPaddingDp()) } .animateContentSize(), diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt new file mode 100644 index 0000000000..1635ee15f2 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/field/search/TangemSearchField.kt @@ -0,0 +1,346 @@ +package com.tangem.core.ui.ds.field.search + +import android.content.res.Configuration +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.animation.expandHorizontally +import androidx.compose.animation.shrinkHorizontally +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsFocusedAsState +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +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.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.ripple +import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.ui.Alignment +import androidx.compose.ui.BiasAlignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.alpha +import androidx.compose.ui.draw.clip +import androidx.compose.ui.focus.FocusManager +import androidx.compose.ui.focus.FocusRequester +import androidx.compose.ui.focus.focusRequester +import androidx.compose.ui.focus.onFocusChanged +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.SolidColor +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.platform.LocalFocusManager +import androidx.compose.ui.platform.LocalSoftwareKeyboardController +import androidx.compose.ui.platform.SoftwareKeyboardController +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.input.ImeAction +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.util.fastForEach +import com.tangem.core.ui.R +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.button.GhostTangemButton +import com.tangem.core.ui.ds.button.TangemButtonSize +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.BaseSearchBarTestTags +import com.tangem.core.ui.test.SearchBarTestTags + +/** + * Shape options for [TangemSearchField]. + */ +enum class TangemFieldShape { + RoundedCorners, + Circle, + ; + + @ReadOnlyComposable + @Composable + internal fun toShape(): Shape { + return when (this) { + RoundedCorners -> RoundedCornerShape(TangemTheme.dimens2.x4) + Circle -> CircleShape + } + } +} + +/** + * Custom search field component that provides a user-friendly interface for searching and filtering content. + * + * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8445-19217&m=dev) + * + * @param state The state of the search field, including the current query, placeholder text, and active status. + * @param shape The shape of the search field, which can be either rounded corners or a circle. + * @param modifier The modifier to be applied to the search field. + * @param enabled Whether the search field is enabled or not. + * @param focusRequester The [FocusRequester] used to request focus on the search field when it becomes active. + */ +@Composable +fun TangemSearchField( + state: SearchBarUM, + shape: TangemFieldShape, + modifier: Modifier = Modifier, + enabled: Boolean = true, + focusRequester: FocusRequester = remember { FocusRequester() }, +) { + val keyboardController = LocalSoftwareKeyboardController.current + val focusManager = LocalFocusManager.current + val interactionSource = remember { MutableInteractionSource() } + var isInitialComposition by rememberSaveable { mutableStateOf(true) } + LaunchedEffect(Unit) { + isInitialComposition = false + } + BasicTextField( + modifier = modifier + .heightIn(min = TangemTheme.dimens2.x11) + .onFocusChanged { focusState -> + if (!isInitialComposition) { + if (focusState.isFocused) { + state.onActiveChange(true) + } else { + state.onActiveChange(false) + } + } + } + .focusRequester(focusRequester) + .testTag(BaseSearchBarTestTags.SEARCH_BAR), + enabled = enabled, + value = state.query, + onValueChange = state.onQueryChange, + interactionSource = interactionSource, + keyboardOptions = KeyboardOptions( + imeAction = ImeAction.Search, + ), + keyboardActions = KeyboardActions( + onSearch = { + keyboardController?.hide() + focusManager.clearFocus() + }, + ), + singleLine = true, + maxLines = 1, + textStyle = TangemTheme.typography2.bodySemibold16.copy( + color = TangemTheme.colors2.text.neutral.primary, + ), + cursorBrush = SolidColor(TangemTheme.colors2.graphic.neutral.primary), + decorationBox = @Composable { innerTextField -> + DecorationBox( + state = state, + shape = shape, + innerTextField = innerTextField, + focusManager = focusManager, + interactionSource = interactionSource, + color = TangemTheme.colors2.field.backgroundDefault, + keyboardController = keyboardController, + ) + }, + ) +} + +@Suppress("LongParameterList") +@Composable +@OptIn(ExperimentalMaterial3Api::class) +private fun DecorationBox( + state: SearchBarUM, + shape: TangemFieldShape, + focusManager: FocusManager, + interactionSource: MutableInteractionSource, + color: Color, + keyboardController: SoftwareKeyboardController?, + innerTextField: @Composable () -> Unit, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth(), + ) { + val isFocused by interactionSource.collectIsFocusedAsState() + val alignmentBias by animateFloatAsState( + targetValue = if (isFocused) -1f else 0f, + animationSpec = tween(), + label = "AlignmentBias", + ) + + Box( + contentAlignment = BiasAlignment(horizontalBias = alignmentBias, verticalBias = 0f), + modifier = Modifier + .weight(1f) + .background(color, shape.toShape()) + .padding(TangemTheme.dimens2.x3), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1), + ) { + Icon( + modifier = Modifier + .size(TangemTheme.dimens2.x5) + .testTag(SearchBarTestTags.ICON), + painter = painterResource(id = R.drawable.ic_search_default_24), + tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant, + contentDescription = null, + ) + Field( + state = state, + innerTextField = innerTextField, + ) + ClearButton( + state = state, + ) + } + } + CancelButton( + state = state, + keyboardController = keyboardController, + focusManager = focusManager, + isActive = isFocused, + ) + } +} + +@Composable +private fun Field(state: SearchBarUM, innerTextField: @Composable () -> Unit) { + Box( + contentAlignment = Alignment.CenterStart, + modifier = Modifier + .heightIn(min = TangemTheme.dimens2.x5) + .width(IntrinsicSize.Max), + ) { + innerTextField() + + val placeholderOpacity by remember(state.query) { + derivedStateOf { + if (state.query.isNotEmpty()) { + 0f + } else { + 1f + } + } + } + + Text( + text = state.placeholderText.resolveReference(), + color = TangemTheme.colors2.text.neutral.tertiary, + style = TangemTheme.typography2.bodySemibold16, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + modifier = Modifier + .testTag(SearchBarTestTags.PLACEHOLDER_TEXT) + .alpha(placeholderOpacity), + ) + } +} + +@Composable +private fun ClearButton(state: SearchBarUM) { + if (state.query.isNotEmpty()) { + Box(modifier = Modifier.fillMaxWidth()) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.ic_close_new_20), + tint = TangemTheme.colors2.graphic.neutral.tertiary, + contentDescription = null, + modifier = Modifier + .align(Alignment.CenterEnd) + .size(TangemTheme.dimens2.x5) + .clip(CircleShape) + .clickable( + onClick = state.onClearClick, + interactionSource = remember { MutableInteractionSource() }, + indication = ripple(bounded = false), + ) + .testTag(SearchBarTestTags.CLEAR_BUTTON), + ) + } + } +} + +@Composable +private fun CancelButton( + state: SearchBarUM, + isActive: Boolean, + focusManager: FocusManager, + keyboardController: SoftwareKeyboardController?, + modifier: Modifier = Modifier, +) { + AnimatedVisibility( + visible = isActive, + enter = expandHorizontally(), + exit = shrinkHorizontally(), + modifier = modifier, + ) { + GhostTangemButton( + text = resourceReference(R.string.common_cancel), + size = TangemButtonSize.X9, + modifier = Modifier.padding(start = TangemTheme.dimens2.x3), + onClick = { + if (state.query.isNotEmpty()) { + state.onQueryChange("") + } + focusManager.clearFocus() + keyboardController?.hide() + state.onActiveChange(false) + state.onClearClick() + }, + ) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemSearchField_Preview(@PreviewParameter(TangemSearchFieldPreviewProvider::class) params: SearchBarUM) { + var state by remember { mutableStateOf(params) } + TangemThemePreviewRedesign { + Column( + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x4), + modifier = Modifier + .background(TangemTheme.colors2.surface.level1) + .padding(TangemTheme.dimens2.x4), + ) { + TangemFieldShape.entries.fastForEach { shape -> + TangemSearchField( + state = state.copy( + onActiveChange = { state = state.copy(isActive = it) }, + onQueryChange = { state = state.copy(query = it) }, + onClearClick = { state = state.copy(query = "") }, + ), + shape = shape, + ) + } + } + } +} + +private class TangemSearchFieldPreviewProvider : PreviewParameterProvider { + override val values: Sequence + get() = sequenceOf( + SearchBarUM( + placeholderText = stringReference("Crypto, news and more"), + query = "BTC", + onQueryChange = {}, + isActive = true, + onActiveChange = {}, + ), + SearchBarUM( + placeholderText = stringReference("Crypto, news and more"), + query = "", + onQueryChange = {}, + isActive = false, + onActiveChange = {}, + ), + ) +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index ecb96f61cf..9b687679f5 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -5,23 +5,18 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* -import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Icon import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.remember 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.graphics.lerp import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider -import androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -48,6 +43,7 @@ fun TangemTopBar( title: TextReference? = null, subtitle: TextReference? = null, startAction: TangemTopBarActionUM? = null, + type: TangemTopBarType = TangemTopBarType.Default, endActions: ImmutableList = persistentListOf(), @DrawableRes titleIconRes: Int? = null, ) { @@ -55,17 +51,21 @@ fun TangemTopBar( title = title, subtitle = subtitle, titleIconRes = titleIconRes, + type = type, modifier = modifier, startContent = if (startAction != null) { - { TangemTopBarActionContent(startAction) } + { TangemTopBarActionContent(actionUM = startAction, type = type) } } else { null }, endContent = if (endActions.isNotEmpty()) { { - Row(horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x5)) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x5), + ) { endActions.forEach { action -> - TangemTopBarActionContent(action) + TangemTopBarActionContent(actionUM = action, type = type) } } } @@ -88,54 +88,88 @@ fun TangemTopBar( @Composable fun TangemTopBar( modifier: Modifier = Modifier, + type: TangemTopBarType = TangemTopBarType.Default, title: TextReference? = null, subtitle: TextReference? = null, @DrawableRes titleIconRes: Int? = null, startContent: @Composable (() -> Unit)? = null, endContent: @Composable (() -> Unit)? = null, ) { - Box( + TangemTopBar( + modifier = modifier, + type = type, + startContent = startContent, + endContent = endContent, + content = { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), + ) { + TangemTopBarTitle(title = title, titleIconRes = titleIconRes) + AnimatedVisibility( + visible = subtitle != null, + label = "Subtitle Visibility", + ) { + val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } + Text( + text = wrappedSubtitle.resolveAnnotatedReference(), + color = TangemTheme.colors2.text.neutral.secondary, + style = TangemTheme.typography2.bodyRegular15, + textAlign = TextAlign.Center, + maxLines = 1, + ) + } + } + }, + ) +} + +/** + * A top bar composable that displays a title and optional start and end icons. + * [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev) + * + * @param modifier Modifier to be applied to the top bar. + * @param type Type of the top bar, which determines its size and padding. + * @param content Composable content to be displayed in the center of the top bar + * @param startContent Optional composable content to be displayed at the start (left) of the top bar. + * @param endContent Optional composable content to be displayed at the end (right) of the top bar. + */ +@Composable +fun TangemTopBar( + modifier: Modifier = Modifier, + type: TangemTopBarType = TangemTopBarType.Default, + content: @Composable () -> Unit, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, modifier = modifier .fillMaxWidth() - .height(TangemTheme.dimens2.x16) - .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3), + .heightIn(min = type.getSize()) + .padding(type.getPadding()), ) { - AnimatedVisibility( - visible = startContent != null, - modifier = Modifier.align(Alignment.CenterStart), + AnimatedContent( + targetState = startContent != null, + modifier = Modifier.size(TangemTheme.dimens2.x11), label = "Start Content Visibility", - ) { - startContent?.invoke() - } - - Column( - modifier = Modifier - .align(Alignment.Center) - .fillMaxWidth(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), - ) { - TangemTopBarTitle(title = title, titleIconRes = titleIconRes) - AnimatedVisibility( - visible = subtitle != null, - label = "Subtitle Visibility", - ) { - val wrappedSubtitle = remember(this) { requireNotNull(subtitle) } - Text( - text = wrappedSubtitle.resolveAnnotatedReference(), - color = TangemTheme.colors2.text.neutral.secondary, - style = TangemTheme.typography2.bodyRegular15, - textAlign = TextAlign.Center, - maxLines = 1, - ) + ) { isVisible -> + if (isVisible) { + startContent?.invoke() } } - AnimatedVisibility( - visible = endContent != null, - modifier = Modifier.align(Alignment.CenterEnd), + + content() + + AnimatedContent( + targetState = endContent != null, + modifier = Modifier.size(TangemTheme.dimens2.x11), label = "End Content Visibility", - ) { - endContent?.invoke() + ) { isVisible -> + if (isVisible) { + endContent?.invoke() + } } } } @@ -151,7 +185,6 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: val wrappedTitle = remember(this) { requireNotNull(title) } Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy( space = TangemTheme.dimens2.x1, alignment = Alignment.CenterHorizontally, @@ -184,33 +217,6 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: } } -@Composable -fun TangemTopBarActionContent( - actionUM: TangemTopBarActionUM, - modifier: Modifier = Modifier, - iconSize: Dp = TangemTheme.dimens2.x8, -) { - val background = lerp( - start = Color.Transparent, - stop = TangemTheme.colors2.button.backgroundSecondary, - fraction = actionUM.ghostModeProgress, - ) - val padding = (TangemTheme.dimens2.x10 - iconSize) / 2 - Icon( - imageVector = ImageVector.vectorResource(id = actionUM.iconRes), - contentDescription = null, - tint = TangemTheme.colors2.graphic.neutral.primary, - modifier = modifier - .size(TangemTheme.dimens2.x10) - .clip(CircleShape) - .conditional(actionUM.isActionable) { background(background) } - .conditionalCompose(actionUM.isActionable && actionUM.onClick != null) { - clickableSingle(onClick = requireNotNull(actionUM.onClick)) - } - .padding(padding), - ) -} - // region Preview @Composable @Preview(showBackground = true, widthDp = 375) @@ -221,6 +227,7 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param title = params.title, subtitle = params.subtitle, titleIconRes = params.titleIconRes, + type = TangemTopBarType.BottomSheet, modifier = Modifier.background(TangemTheme.colors2.surface.level1), startContent = params.startActionUM?.let { { TangemTopBarActionContent(it) } }, endContent = if (params.endActions.isNotEmpty()) { @@ -256,11 +263,13 @@ private class PreviewProvider : PreviewParameterProvider TangemTheme.dimens2.x16 + BottomSheet -> TangemTheme.dimens2.x19 + } + } + + @ReadOnlyComposable + @Composable + fun getSideContentSize(): Dp { + return when (this) { + Default -> TangemTheme.dimens2.x8 + BottomSheet -> TangemTheme.dimens2.x7 + } + } + + @ReadOnlyComposable + @Composable + fun getPadding(): PaddingValues { + return when (this) { + Default -> PaddingValues(horizontal = TangemTheme.dimens2.x4, vertical = TangemTheme.dimens2.x3) + BottomSheet -> PaddingValues(TangemTheme.dimens2.x4) + } + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index a410eeb471..51aacf9640 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -3,6 +3,8 @@ package com.tangem.core.ui.res import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.text.selection.LocalTextSelectionColors +import androidx.compose.foundation.text.selection.TextSelectionColors import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.* import androidx.compose.ui.Modifier @@ -31,15 +33,27 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) { LocalTangemTypography2 provides TangemTypography2(InterFamily), LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) }, ) { - ProvideHaze { - Box(Modifier.hazeSourceTangem(zIndex = 1f)) { - content() + CompositionLocalProvider( + LocalTextSelectionColors provides TangemTextSelectionColors2, + ) { + ProvideHaze { + Box(Modifier.hazeSourceTangem(zIndex = 1f)) { + content() + } } } } } } +private val TangemTextSelectionColors2: TextSelectionColors + @Composable + @ReadOnlyComposable + get() = TextSelectionColors( + handleColor = TangemTheme.colors2.text.neutral.primary, + backgroundColor = TangemTheme.colors2.text.neutral.primary.copy(alpha = 0.3f), + ) + @Composable @ReadOnlyComposable private fun lightThemeColors2(): TangemColors2 { @@ -48,7 +62,7 @@ private fun lightThemeColors2(): TangemColors2 { primary = TangemColorPalette.Dark6, primaryInverted = TangemColorPalette.White, secondary = TangemColorPalette.Dark2, - tertiary = TangemColorPalette.Dark3, + tertiary = TangemColorPalette.Dark1, primaryInvertedConstant = TangemColorPalette.White, ), status = TangemColors2.Text.Status( @@ -67,7 +81,7 @@ private fun lightThemeColors2(): TangemColors2 { tertiary = TangemColorPalette.Dark3, quaternary = TangemColorPalette.Light4, primaryInvertedConstant = TangemColorPalette.White, - tertiaryConstant = TangemColorPalette.Dark3, + tertiaryConstant = TangemColorPalette.Dark1, ), status = TangemColors2.Graphic.Status( accent = TangemColorPalette.Azure, @@ -243,7 +257,7 @@ private fun darkThemeColors2(): TangemColors2 { secondary = TangemColorPalette.Light5, tertiary = TangemColorPalette.Dark3, quaternary = TangemColorPalette.Dark3, - tertiaryConstant = TangemColorPalette.Dark3, + tertiaryConstant = TangemColorPalette.Dark1, primaryInvertedConstant = TangemColorPalette.White, ), status = TangemColors2.Graphic.Status( diff --git a/core/ui/src/main/res/drawable/ic_search_24.xml b/core/ui/src/main/res/drawable/ic_search_24.xml index 0e1e91bcd9..2fa4918529 100644 --- a/core/ui/src/main/res/drawable/ic_search_24.xml +++ b/core/ui/src/main/res/drawable/ic_search_24.xml @@ -1,10 +1,9 @@ - + diff --git a/core/ui/src/main/res/drawable/ic_search_default_24.xml b/core/ui/src/main/res/drawable/ic_search_default_24.xml new file mode 100644 index 0000000000..510a7cb21a --- /dev/null +++ b/core/ui/src/main/res/drawable/ic_search_default_24.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt index 66cb344ef0..1efc678bcb 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -1,6 +1,7 @@ package com.tangem.feature.tester.presentation.storybook.entity import com.tangem.core.ui.ds.badge.TangemBadgeColor +import com.tangem.core.ui.ds.field.search.TangemFieldShape import com.tangem.core.ui.ds.message.TangemMessageEffect internal sealed interface StoryBookPage @@ -53,4 +54,9 @@ internal data class TangemContextMenuStory( internal data class TangemHeaderRowStory( val isBalanceHidden: Boolean, val onBalanceHiddenToggle: () -> Unit, +) : StoryBookPage + +internal data class TangemSearchFieldStory( + val selectedShape: TangemFieldShape, + val onShapeChange: (TangemFieldShape) -> Unit, ) : StoryBookPage \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/searchfield/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/searchfield/Build.kt new file mode 100644 index 0000000000..591019540e --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/searchfield/Build.kt @@ -0,0 +1,18 @@ +package com.tangem.feature.tester.presentation.storybook.page.searchfield + +import com.tangem.core.ui.ds.field.search.TangemFieldShape +import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchFieldStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemSearchFieldStory { + return TangemSearchFieldStory( + selectedShape = TangemFieldShape.RoundedCorners, + onShapeChange = { shape -> + updateStory { it.copy(selectedShape = shape) } + }, + ) +} + +internal val tangemSearchFieldStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/searchfield/TangemSearchFieldStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/searchfield/TangemSearchFieldStory.kt new file mode 100644 index 0000000000..bf3fa6b999 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/searchfield/TangemSearchFieldStory.kt @@ -0,0 +1,151 @@ +@file:Suppress("MagicNumber", "LongMethod") + +package com.tangem.feature.tester.presentation.storybook.page.searchfield + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.R +import com.tangem.core.ui.components.fields.entity.SearchBarUM +import com.tangem.core.ui.ds.field.search.TangemFieldShape +import com.tangem.core.ui.ds.field.search.TangemSearchField +import com.tangem.core.ui.extensions.resourceReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchFieldStory + +@Composable +internal fun TangemSearchFieldStory(state: TangemSearchFieldStory, modifier: Modifier = Modifier) { + var query by remember { mutableStateOf("") } + var isActive by remember { mutableStateOf(false) } + + LazyColumn( + contentPadding = PaddingValues(bottom = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = modifier + .fillMaxSize() + .statusBarsPadding() + .background(TangemTheme.colors2.surface.level1), + ) { + stickyHeader("shape_toggle") { + ShapeToggle( + selected = state.selectedShape, + onSelect = state.onShapeChange, + modifier = Modifier + .fillMaxWidth() + .background(TangemTheme.colors2.surface.level1) + .padding(horizontal = 16.dp, vertical = 8.dp), + ) + } + + item("empty") { + VariantSection(label = "Empty") { + TangemSearchField( + state = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = query, + onQueryChange = { query = it }, + isActive = isActive, + onActiveChange = { isActive = it }, + onClearClick = { query = "" }, + ), + shape = state.selectedShape, + ) + } + } + + item("prefilled") { + VariantSection(label = "Pre-filled") { + var prefilledQuery by remember { mutableStateOf("Bitcoin") } + var isPrefilledActive by remember { mutableStateOf(false) } + TangemSearchField( + state = SearchBarUM( + placeholderText = resourceReference(R.string.manage_tokens_search_placeholder), + query = prefilledQuery, + onQueryChange = { prefilledQuery = it }, + isActive = isPrefilledActive, + onActiveChange = { isPrefilledActive = it }, + onClearClick = { prefilledQuery = "" }, + ), + shape = state.selectedShape, + ) + } + } + } +} + +@Composable +private fun ShapeToggle( + selected: TangemFieldShape, + onSelect: (TangemFieldShape) -> Unit, + modifier: Modifier = Modifier, +) { + val shape = RoundedCornerShape(50) + Row( + modifier = modifier + .clip(shape) + .background(TangemTheme.colors2.surface.level2) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.secondary, + shape = shape, + ) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + TangemFieldShape.entries.forEach { fieldShape -> + ShapeChip( + label = fieldShape.name, + selected = fieldShape == selected, + onClick = { onSelect(fieldShape) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun ShapeChip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background( + if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2, + ) + .clickable(onClick = onClick) + .padding(vertical = 8.dp, horizontal = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + } +} + +@Composable +private fun VariantSection(label: String, content: @Composable ColumnScope.() -> Unit) { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp), + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.body2, + color = TangemTheme.colors.text.secondary, + ) + content() + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt index 6fc4111d73..b413ec4bce 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -23,6 +23,7 @@ import com.tangem.feature.tester.presentation.storybook.page.contextmenu.tangemC import com.tangem.feature.tester.presentation.storybook.page.headerrow.tangemHeaderRowStoryFactory import com.tangem.feature.tester.presentation.storybook.page.message.tangemMessageStoryFactory import com.tangem.feature.tester.presentation.storybook.page.opportunities.opportunitiesBGStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.searchfield.tangemSearchFieldStoryFactory import com.tangem.feature.tester.presentation.storybook.page.tabs.tangemSegmentedPickerStoryFactory import com.tangem.feature.tester.presentation.storybook.page.tokenrow.tangemTokenRowStoryFactory @@ -39,6 +40,7 @@ private fun buildStories() = listOf( StoryItem(title = "🪙 Token Row", factory = tangemTokenRowStoryFactory), StoryItem(title = "📑 Header Row", factory = tangemHeaderRowStoryFactory), StoryItem(title = "📋 Context Menu", factory = tangemContextMenuStoryFactory), + StoryItem(title = "🔍 Search Field", factory = tangemSearchFieldStoryFactory), ) @Composable diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt index f3e58f0000..14e0c65291 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -14,6 +14,7 @@ import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxSto import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchFieldStory import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory @@ -26,6 +27,7 @@ import com.tangem.feature.tester.presentation.storybook.page.tabs.TangemSegmente import com.tangem.feature.tester.presentation.storybook.page.tokenrow.TangemTokenRowStory import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory import com.tangem.feature.tester.presentation.storybook.page.contextmenu.TangemContextMenuStory +import com.tangem.feature.tester.presentation.storybook.page.searchfield.TangemSearchFieldStory @Composable internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { @@ -48,6 +50,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) is TangemTokenRowStory -> TangemTokenRowStory(state = storyState) is TangemHeaderRowStory -> TangemHeaderRowStory(state = storyState) is TangemContextMenuStory -> TangemContextMenuStory(state = storyState) + is TangemSearchFieldStory -> TangemSearchFieldStory(state = storyState) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt index 120fd7a0b8..71cc0d0a57 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/child/organizetokens/ui/OrganizeTokensContent.kt @@ -38,6 +38,7 @@ import com.tangem.core.ui.ds.row.token.TangemTokenRow import com.tangem.core.ui.ds.topbar.TangemTopBar import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM +import com.tangem.core.ui.ds.topbar.TangemTopBarType import com.tangem.core.ui.extensions.resourceReference import com.tangem.core.ui.reordarable.ReorderableItem import com.tangem.core.ui.res.TangemTheme @@ -77,6 +78,7 @@ internal fun OrganizeTokensContent( title = { TangemTopBar( title = resourceReference(R.string.organize_tokens_title), + type = TangemTopBarType.BottomSheet, endContent = { TangemTopBarActionContent( actionUM = TangemTopBarActionUM( @@ -85,7 +87,7 @@ internal fun OrganizeTokensContent( onClick = { isShowDropdownMenu = true }, ghostModeProgress = 0f, ), - iconSize = TangemTheme.dimens2.x7, + type = TangemTopBarType.BottomSheet, ) OrganizeDropDownMenu( organizeMenuUM = organizeTokensUM.organizeMenuUM,