Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-12 17:17:24 +05:00
commit bc8bb4da19
10 changed files with 209 additions and 122 deletions

View file

@ -1,7 +1,6 @@
package com.tangem.core.ui.components.bottomsheets
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@ -212,14 +211,6 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
Modal -> windowSize.height * MODAL_SHEET_MAX_HEIGHT
}
val buttonHeight by animateDpAsState(
if (footer != null) {
80.dp
} else {
0.dp
},
)
val contentModifier = when (type) {
Default -> Modifier.clip(
RoundedCornerShape(
@ -260,7 +251,6 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
Box(
modifier = Modifier
.fillMaxWidth()
.height(buttonHeight)
.align(Alignment.BottomCenter),
) {
if (footer != null) {

View file

@ -8,6 +8,7 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.tooling.preview.Preview
@ -27,14 +28,15 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign
* @param modifier Modifier to be applied to the button.
*/
@Composable
fun AccentTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
AccentTangemButton(
fun StatusTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
StatusTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
type = buttonUM.type,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
@ -57,35 +59,39 @@ fun AccentTangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier)
[REDACTED_AUTHOR]
*/
@Composable
fun AccentTangemButton(
fun StatusTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
type: TangemButtonType = TangemButtonType.Accent,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
shape: TangemButtonShape = TangemButtonShape.Default,
) {
val statusColor = type.getStatusColor()
val contentColor = when (state) {
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
else -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
}
val backgroundModifier = when (state) {
TangemButtonState.Disabled -> Modifier.background(TangemTheme.colors2.button.backgroundDisabled)
TangemButtonState.Default -> Modifier.background(statusColor)
TangemButtonState.Loading,
TangemButtonState.Pressed,
-> Modifier
.background(statusColor)
.background(TangemTheme.colors2.overlay.overlaySecondary)
}
TangemButtonInternal(
onClick = onClick,
modifier = modifier
.clip(shape.toShape(size))
.then(
when (state) {
TangemButtonState.Disabled,
TangemButtonState.Default,
-> Modifier.background(TangemTheme.colors2.button.backgroundPositive)
TangemButtonState.Loading,
TangemButtonState.Pressed,
-> Modifier
.background(TangemTheme.colors2.button.backgroundPositive)
.background(TangemTheme.colors2.overlay.overlaySecondary)
},
),
.then(backgroundModifier),
text = text,
contentColor = TangemTheme.colors2.text.neutral.primaryInvertedConstant,
contentColor = contentColor,
iconRes = iconRes,
enabled = enabled,
size = size,
@ -94,11 +100,19 @@ fun AccentTangemButton(
)
}
@ReadOnlyComposable
@Composable
private fun TangemButtonType.getStatusColor() = when (this) {
TangemButtonType.Accent -> TangemTheme.colors2.button.backgroundAccent
TangemButtonType.Positive -> TangemTheme.colors2.button.backgroundPositive
else -> TangemTheme.colors2.button.backgroundPrimary
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun AccentTangemButton_Preview(
private fun StatusTangemButton_Preview(
@PreviewParameter(AccentTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
@ -118,13 +132,14 @@ private fun AccentTangemButton_Preview(
} else {
TangemButtonIconPosition.End
}
AccentTangemButton(
StatusTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
shape = shape,
iconPosition = iconPosition,
iconRes = R.drawable.ic_tangem_24,
type = TangemButtonType.Accent,
state = params,
)
}

View file

@ -39,13 +39,26 @@ fun TangemButton(buttonUM: TangemButtonUM, modifier: Modifier = Modifier) {
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.Accent -> AccentTangemButton(
TangemButtonType.Accent -> StatusTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
type = TangemButtonType.Positive,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,
)
TangemButtonType.Positive -> StatusTangemButton(
onClick = buttonUM.onClick,
modifier = modifier,
text = buttonUM.text,
iconRes = buttonUM.iconRes,
iconPosition = buttonUM.iconPosition,
enabled = buttonUM.isEnabled,
type = TangemButtonType.Positive,
size = buttonUM.size,
state = buttonUM.state,
shape = buttonUM.shape,

View file

@ -1,9 +1,11 @@
package com.tangem.core.ui.ds.button
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
@ -15,6 +17,7 @@ import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.testTag
@ -25,13 +28,13 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.components.SpacerW
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.BaseButtonTestTags
private const val LOADING_ANIMATION_DURATION = 150
/**
* A customizable button component that supports text, icons, and different states.
*
@ -61,7 +64,7 @@ internal fun TangemButtonInternal(
state: TangemButtonState = TangemButtonState.Default,
) {
ProvideButtonRippleConfiguration {
Row(
Box(
modifier = modifier
.testTag(BaseButtonTestTags.BUTTON)
.clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button)
@ -70,63 +73,107 @@ internal fun TangemButtonInternal(
width(size.toHeightDp())
}
.conditionalCompose(text != null) {
padding(horizontal = size.toPaddingDp())
padding(size.toPaddingDp())
}
.animateContentSize(),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
modifier = Modifier.size(size = size.toContentSize()),
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.align(Alignment.Center)
.conditional(state == TangemButtonState.Loading) {
alpha(0f)
},
) {
val wrappedIconRes = remember(this, iconRes) { requireNotNull(iconRes) }
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
}
val isStartIcon = iconRes != null && iconPosition == TangemButtonIconPosition.Start
TangemButtonIcon(
iconRes = iconRes,
iconColor = contentColor,
isVisible = isStartIcon,
size = size,
)
if (isStartIcon && (text != null || descriptionText != null)) {
SpacerW(TangemTheme.dimens2.x1)
}
AnimatedVisibility(text != null && state != TangemButtonState.Loading) {
val wrappedText = remember(this) { requireNotNull(text) }
val textStyle = size.toTextStyle()
Text(
text = wrappedText.resolveReference(),
style = textStyle,
ButtonContent(
text = text,
descriptionText = descriptionText,
contentColor = contentColor,
size = size,
)
val isEndIcon = iconRes != null && iconPosition == TangemButtonIconPosition.End
if (isEndIcon && (text != null || descriptionText != null)) {
SpacerW(TangemTheme.dimens2.x1)
}
TangemButtonIcon(
iconRes = iconRes,
iconColor = contentColor,
isVisible = isEndIcon,
size = size,
)
}
AnimatedVisibility(
modifier = Modifier
.align(Alignment.Center)
.size(size.toContentSize()),
visible = state == TangemButtonState.Loading,
exit = fadeOut(animationSpec = tween(LOADING_ANIMATION_DURATION)),
enter = fadeIn(animationSpec = tween(LOADING_ANIMATION_DURATION)),
) {
CircularProgressIndicator(
color = contentColor,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
modifier = Modifier.size(size.toContentSize()),
)
}
}
}
}
AnimatedVisibility(descriptionText != null && state != TangemButtonState.Loading) {
val wrappedText = remember(this) { requireNotNull(descriptionText) }
val textStyle = TangemTheme.typography2.captionSemibold12
Text(
text = wrappedText.resolveReference(),
style = textStyle,
color = TangemTheme.colors2.text.status.disabled,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
)
}
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
modifier = Modifier.size(size = size.toContentSize()),
) {
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
}
@Composable
private fun ButtonContent(
text: TextReference?,
descriptionText: TextReference?,
contentColor: Color,
size: TangemButtonSize,
) {
Column {
AnimatedVisibility(text != null) {
val wrappedText = remember(this) { text.orEmpty() }
val textStyle = size.toTextStyle()
Text(
text = wrappedText.resolveReference(),
style = textStyle,
color = contentColor,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
)
}
AnimatedVisibility(descriptionText != null) {
val wrappedText = remember(this) { descriptionText.orEmpty() }
val textStyle = TangemTheme.typography2.captionSemibold12
Text(
text = wrappedText.resolveReference(),
style = textStyle,
color = TangemTheme.colors2.text.status.disabled,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
)
}
}
}
@ -150,36 +197,21 @@ private inline fun ProvideButtonRippleConfiguration(crossinline content: @Compos
@Composable
private fun TangemButtonIcon(
@DrawableRes iconRes: Int,
@DrawableRes iconRes: Int?,
iconColor: Color,
state: TangemButtonState,
isVisible: Boolean,
size: TangemButtonSize,
) {
AnimatedContent(state) { targetState ->
when (targetState) {
TangemButtonState.Loading -> CircularProgressIndicator(
color = iconColor,
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
modifier = Modifier.padding(
when (size) {
TangemButtonSize.X7,
TangemButtonSize.X8,
TangemButtonSize.X9,
TangemButtonSize.X10,
-> 0.5.dp
TangemButtonSize.X12,
TangemButtonSize.X15,
-> 4.5.dp
},
),
)
else -> Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconColor,
)
}
AnimatedVisibility(
visible = isVisible,
modifier = Modifier.size(size = size.toContentSize()),
) {
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
Icon(
painter = painterResource(id = wrappedIconRes),
contentDescription = null,
tint = iconColor,
)
}
}
@ -227,14 +259,30 @@ enum class TangemButtonSize {
@ReadOnlyComposable
@Composable
internal fun toPaddingDp() = when (this) {
X7 -> TangemTheme.dimens2.x2
X8,
X9,
X10,
-> TangemTheme.dimens2.x3
X12,
X15,
-> TangemTheme.dimens2.x6
X7 -> PaddingValues(
horizontal = TangemTheme.dimens2.x2,
vertical = TangemTheme.dimens2.x0_5,
)
X8 -> PaddingValues(
horizontal = TangemTheme.dimens2.x3,
vertical = TangemTheme.dimens2.x1_5,
)
X9 -> PaddingValues(
horizontal = TangemTheme.dimens2.x3,
vertical = TangemTheme.dimens2.x2,
)
X10 -> PaddingValues(
horizontal = TangemTheme.dimens2.x3,
vertical = TangemTheme.dimens2.x2_5,
)
X12 -> PaddingValues(
horizontal = TangemTheme.dimens2.x6,
vertical = TangemTheme.dimens2.x2_5,
)
X15 -> PaddingValues(
horizontal = TangemTheme.dimens2.x6,
vertical = TangemTheme.dimens2.x4,
)
}
@ReadOnlyComposable

View file

@ -39,6 +39,7 @@ enum class TangemButtonType {
Primary,
Secondary,
Accent,
Positive,
Outline,
PrimaryInverse,
Ghost,

View file

@ -165,6 +165,7 @@ class TangemColors2 internal constructor(
backgroundPrimary: Color,
backgroundSecondary: Color,
backgroundDisabled: Color,
backgroundAccent: Color,
backgroundPositive: Color,
backgroundPrimaryInverse: Color,
textPrimary: Color,
@ -183,6 +184,8 @@ class TangemColors2 internal constructor(
private set
var backgroundPositive by mutableStateOf(backgroundPositive)
private set
var backgroundAccent by mutableStateOf(backgroundAccent)
private set
var backgroundPrimaryInverse by mutableStateOf(backgroundPrimaryInverse)
private set
var textPrimary by mutableStateOf(textPrimary)
@ -205,6 +208,7 @@ class TangemColors2 internal constructor(
backgroundSecondary = other.backgroundSecondary
backgroundDisabled = other.backgroundDisabled
backgroundPositive = other.backgroundPositive
backgroundAccent = other.backgroundAccent
backgroundPrimaryInverse = other.backgroundPrimaryInverse
textPrimary = other.textPrimary
textSecondary = other.textSecondary

View file

@ -11,6 +11,7 @@ data class TangemDimens2 internal constructor(
val x0: Dp = 0.dp,
val x0_5: Dp = 2.dp,
val x1: Dp = 4.dp,
val x1_5: Dp = 6.dp,
val x2: Dp = 8.dp,
val x2_5: Dp = 10.dp,
val x3: Dp = 12.dp,

View file

@ -112,7 +112,8 @@ private fun lightThemeColors2(): TangemColors2 {
backgroundPrimary = TangemColorPalette.Dark6,
backgroundSecondary = TangemColorPalette.Dark_10,
backgroundDisabled = TangemColorPalette.Light3,
backgroundPositive = TangemColorPalette.Azure,
backgroundAccent = TangemColorPalette.Azure,
backgroundPositive = TangemColorPalette.Eucalyptus,
backgroundPrimaryInverse = TangemColorPalette.White,
textSecondary = TangemColorPalette.Dark6,
textPrimary = TangemColorPalette.Light2,
@ -288,7 +289,8 @@ private fun darkThemeColors2(): TangemColors2 {
backgroundPrimary = TangemColorPalette.Light1V2,
backgroundSecondary = TangemColorPalette.Light_10,
backgroundDisabled = TangemColorPalette.Dark5,
backgroundPositive = TangemColorPalette.Azure,
backgroundAccent = TangemColorPalette.Azure,
backgroundPositive = TangemColorPalette.Eucalyptus,
backgroundPrimaryInverse = TangemColorPalette.Light_10,
textSecondary = TangemColorPalette.Light4,
textPrimary = TangemColorPalette.Dark4,

View file

@ -82,7 +82,7 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) {
}
item("accent") {
ButtonSection(title = "Accent") { state, text, shape ->
AccentTangemButton(
StatusTangemButton(
onClick = {},
text = if (text) stringReference("Continue") else null,
iconRes = R.drawable.ic_tangem_24,
@ -92,6 +92,19 @@ internal fun ButtonsStory(modifier: Modifier = Modifier) {
)
}
}
item("positive") {
ButtonSection(title = "Positive") { state, text, shape ->
StatusTangemButton(
onClick = {},
text = if (text) stringReference("Continue") else null,
iconRes = R.drawable.ic_tangem_24,
size = TangemButtonSize.X10,
type = TangemButtonType.Positive,
state = state,
shape = shape,
)
}
}
item("ghost") {
ButtonSection(title = "Ghost") { state, text, shape ->
GhostTangemButton(

View file

@ -26,7 +26,7 @@ internal fun LazyListScope.organizeTokens2(state: WalletUM, itemModifier: Modifi
contentType = "OrganizeTokensButton",
) {
TangemButton(
organizeButton,
buttonUM = organizeButton,
modifier = itemModifier,
)
}