Updated on 2026-08-14

This commit is contained in:
Tangem 2026-03-16 17:21:27 +05:00
commit e1d578efb9
15 changed files with 213 additions and 26 deletions

View file

@ -137,6 +137,7 @@ private fun StartIcon(
is TangemIconUM.Ident,
is TangemIconUM.Image,
is TangemIconUM.Url,
TangemIconUM.Empty,
-> wrappedIconRes
is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
},
@ -164,6 +165,7 @@ private fun EndIcon(
is TangemIconUM.Ident,
is TangemIconUM.Image,
is TangemIconUM.Url,
TangemIconUM.Empty,
-> wrappedIconRes
is TangemIconUM.Icon -> wrappedIconRes.copy(tintReference = ColorReference2 { iconColor })
},

View file

@ -2,6 +2,9 @@ package com.tangem.core.ui.ds.image
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
@ -25,6 +28,9 @@ import com.tangem.core.ui.res.TangemTheme
@Immutable
sealed interface TangemIconUM {
/** Empty icon */
data object Empty : TangemIconUM
/** Icon representing a currency. */
data class Currency(
val currencyIconState: CurrencyIconState,
@ -100,5 +106,9 @@ fun TangemIcon(tangemIconUM: TangemIconUM, modifier: Modifier = Modifier) {
},
contentDescription = null,
)
TangemIconUM.Empty -> Box(
modifier = modifier
.background(TangemTheme.colors2.skeleton.backgroundPrimary, shape = CircleShape),
)
}
}

View file

@ -128,6 +128,7 @@ private fun BoxScope.BackgroundLayer(icon: TangemIconUM, blurRadius: Dp = 26.dp)
ResBackground(icon.fallbackRes, blurRadius)
}
}
TangemIconUM.Empty -> SolidColorBackground(TangemTheme.colors2.skeleton.backgroundPrimary, blurRadius)
}
}

View file

@ -0,0 +1,86 @@
package com.tangem.core.ui.ds.placeholder
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Placeholder composable to display a skeleton loading state.
*
* @param modifier Modifier to be applied to the placeholder.
* @param size Size of the placeholder. Default is 100x20 dp.
* @param radius Corner radius of the placeholder. Default is 25 dp.
*/
@Composable
fun Placeholder(
modifier: Modifier = Modifier,
size: DpSize = DpSize(TangemTheme.dimens2.x10, TangemTheme.dimens2.x2),
radius: Dp = TangemTheme.dimens2.x25,
) {
Box(
modifier = modifier
.size(size)
.background(
color = TangemTheme.colors2.skeleton.backgroundPrimary,
shape = RoundedCornerShape(radius),
),
)
}
/**
* TextPlaceholder composable to display a skeleton loading state for text elements.
*
* @param textStyle TextStyle to determine the line height of the placeholder.
* @param modifier Modifier to be applied to the placeholder.
* @param width Width of the placeholder. Default is 200 dp.
* @param radius Corner radius of the placeholder. Default is 25 dp.
*/
@Composable
fun TextPlaceholder(
textStyle: TextStyle,
modifier: Modifier = Modifier,
width: Dp = 200.dp,
radius: Dp = TangemTheme.dimens2.x25,
) {
val lineHeight = with(LocalDensity.current) { textStyle.lineHeight.toDp() }
Box(
modifier = modifier
.size(width = width, height = lineHeight)
.background(
color = TangemTheme.colors2.skeleton.backgroundPrimary,
shape = RoundedCornerShape(radius),
),
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun Placeholder_Preview() {
TangemThemePreviewRedesign {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
) {
Placeholder()
TextPlaceholder(
textStyle = TangemTheme.typography2.titleRegular44,
)
}
}
}
// endregion

View file

@ -87,7 +87,7 @@ fun TangemRowContainer(
val startTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.START_TOP,
constraints = constraints.copy(
minWidth = startTopMinWidth,
minWidth = 0,
maxWidth = max(
a = startTopMinWidth,
b = availableWidthForBody - endTopPlaceable.widthOrZero(),
@ -99,7 +99,7 @@ fun TangemRowContainer(
val startBottomPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.START_BOTTOM,
constraints = constraints.copy(
minWidth = startBottomMinWidth,
minWidth = 0,
maxWidth = max(
a = startBottomMinWidth,
b = availableWidthForBody - endBottomPlaceable.widthOrZero(),

View file

@ -6,6 +6,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
@ -50,6 +51,7 @@ fun TangemTokenRow(
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x9)
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
)
@ -74,6 +76,7 @@ fun TangemTokenRow(
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.bodySemibold16,
textColor = TangemTheme.colors2.text.neutral.primary,
placeholderWidth = TangemTheme.dimens2.x20,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
@ -84,6 +87,7 @@ fun TangemTokenRow(
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.captionSemibold12,
textColor = TangemTheme.colors2.text.neutral.secondary,
placeholderWidth = TangemTheme.dimens2.x11,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
@ -169,6 +173,7 @@ fun TangemTokenRow(
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.bodySemibold16,
textColor = TangemTheme.colors2.text.neutral.primary,
placeholderWidth = TangemTheme.dimens2.x20,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
@ -179,6 +184,7 @@ fun TangemTokenRow(
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.captionSemibold12,
textColor = TangemTheme.colors2.text.neutral.secondary,
placeholderWidth = TangemTheme.dimens2.x11,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
@ -227,7 +233,7 @@ private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = co
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTokenRow_Preview(
@PreviewParameter(TangemTokenRow_PreviewProvider::class) tokenRowUM: TangemTokenRowUM,
@PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM,
) {
TangemThemePreviewRedesign {
TangemTokenRow(
@ -239,8 +245,7 @@ private fun TangemTokenRow_Preview(
}
}
@Suppress("ClassNaming")
class TangemTokenRow_PreviewProvider : CollectionPreviewParameterProvider<TangemTokenRowUM>(
private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider<TangemTokenRowUM>(
collection = listOf(
TangemTokenRowPreviewData.defaultState,
TangemTokenRowPreviewData.defaultEllipsisState,
@ -249,6 +254,7 @@ class TangemTokenRow_PreviewProvider : CollectionPreviewParameterProvider<Tangem
TangemTokenRowPreviewData.draggableState,
TangemTokenRowPreviewData.draggableStateV2,
TangemTokenRowPreviewData.loadingState,
TangemTokenRowPreviewData.emptyState,
TangemTokenRowPreviewData.unreachableState,
TangemTokenRowPreviewData.accountState,
TangemTokenRowPreviewData.accountLetterState,

View file

@ -19,7 +19,7 @@ sealed class TangemTokenRowUM : TangemRowUM {
abstract override val id: String
/** Token icon state */
abstract val headIconUM: TangemIconUM.Currency
abstract val headIconUM: TangemIconUM
/** Token title UM (in one row with [topEndContentUM]) */
abstract val titleUM: TitleUM
@ -78,6 +78,23 @@ sealed class TangemTokenRowUM : TangemRowUM {
override val onItemLongClick: (() -> Unit)? = null
}
/**
* Loading state of [TangemTokenRowUM]
*/
data class Empty(
override val id: String,
) : TangemTokenRowUM() {
override val headIconUM: TangemIconUM = TangemIconUM.Empty
override val subtitleUM: SubtitleUM = SubtitleUM.Placeholder
override val titleUM: TitleUM = TitleUM.Placeholder
override val topEndContentUM: EndContentUM = EndContentUM.Placeholder
override val bottomEndContentUM: EndContentUM = EndContentUM.Placeholder
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
override val tailUM: TangemRowTailUM = TangemRowTailUM.Empty
override val onItemClick: (() -> Unit)? = null
override val onItemLongClick: (() -> Unit)? = null
}
/**
* Actionable state of [TangemTokenRowUM]
*/
@ -108,6 +125,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
data object Loading : TitleUM()
data object Placeholder : TitleUM()
data object Empty : TitleUM()
}
@ -125,6 +144,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
data object Loading : SubtitleUM()
data object Placeholder : SubtitleUM()
data object Empty : SubtitleUM()
}
@ -142,6 +163,8 @@ sealed class TangemTokenRowUM : TangemRowUM {
data object Loading : EndContentUM()
data object Placeholder : EndContentUM()
data object Empty : EndContentUM()
}

View file

@ -19,7 +19,7 @@ import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
internal object TangemTokenRowPreviewData {
object TangemTokenRowPreviewData {
private val priceChangeState: PriceChangeState.Content
get() = PriceChangeState.Content(
@ -218,6 +218,9 @@ internal object TangemTokenRowPreviewData {
subtitleUM = TangemTokenRowUM.SubtitleUM.Loading,
)
val emptyState: TangemTokenRowUM.Empty
get() = TangemTokenRowUM.Empty(id = UUID.randomUUID().toString())
val unreachableState: TangemTokenRowUM
get() = defaultState.copy(
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(

View file

@ -17,10 +17,12 @@ 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.unit.Dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.placeholder.TextPlaceholder
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
@ -33,6 +35,7 @@ internal fun TokenRowEndContent(
isBalanceHidden: Boolean,
textStyle: TextStyle,
textColor: Color,
placeholderWidth: Dp,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
@ -43,12 +46,17 @@ internal fun TokenRowEndContent(
textStyle = textStyle,
textColor = textColor,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = textStyle,
modifier = modifier.width(TangemTheme.dimens2.x10),
modifier = modifier.width(placeholderWidth),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.EndContentUM.Placeholder -> TextPlaceholder(
modifier = modifier,
textStyle = textStyle,
width = placeholderWidth,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
}
}
@ -141,6 +149,7 @@ private fun TokenRowEndContent_Preview(
isBalanceHidden = false,
textColor = TangemTheme.colors2.text.neutral.primary,
textStyle = TangemTheme.typography2.captionSemibold12,
placeholderWidth = TangemTheme.dimens2.x11,
)
}
}
@ -149,6 +158,8 @@ private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<Tange
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.bottomEndContentUM,
TangemTokenRowUM.EndContentUM.Loading,
TangemTokenRowUM.EndContentUM.Empty,
)
}
// endregion

View file

@ -17,6 +17,7 @@ import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.placeholder.TextPlaceholder
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
@ -31,9 +32,14 @@ internal fun TokenRowSubtitle(subtitleUM: TangemTokenRowUM.SubtitleUM, modifier:
)
TangemTokenRowUM.SubtitleUM.Loading -> TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
modifier = modifier.width(TangemTheme.dimens2.x8),
modifier = modifier.width(TangemTheme.dimens2.x11),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.SubtitleUM.Placeholder -> TextPlaceholder(
modifier = modifier,
textStyle = TangemTheme.typography2.captionSemibold12,
width = TangemTheme.dimens2.x11,
)
TangemTokenRowUM.SubtitleUM.Empty -> Unit
}
}
@ -95,6 +101,7 @@ private class TokenRowSubtitlePreviewProvider : PreviewParameterProvider<TangemT
get() = sequenceOf(
TangemTokenRowPreviewData.subtitleUM,
TangemTokenRowUM.SubtitleUM.Loading,
TangemTokenRowUM.SubtitleUM.Empty,
)
}
// endregion

View file

@ -16,9 +16,12 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
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 com.tangem.core.ui.R
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.placeholder.TextPlaceholder
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveAnnotatedReference
@ -34,6 +37,11 @@ fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifi
modifier = modifier.width(TangemTheme.dimens2.x18),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.TitleUM.Placeholder -> TextPlaceholder(
modifier = modifier,
textStyle = TangemTheme.typography2.bodySemibold16,
width = TangemTheme.dimens2.x22,
)
TangemTokenRowUM.TitleUM.Empty -> Unit
}
}
@ -89,13 +97,23 @@ private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Mo
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowTitle_Preview() {
private fun TokenRowTitle_Preview(
@PreviewParameter(TokenRowTitlePreviewProvider::class) params: TangemTokenRowUM.TitleUM,
) {
TangemThemePreviewRedesign {
TokenRowTitle(
titleUM = TangemTokenRowPreviewData.titleUM,
titleUM = params,
modifier = Modifier.fillMaxWidth(),
)
}
}
private class TokenRowTitlePreviewProvider : PreviewParameterProvider<TangemTokenRowUM.TitleUM> {
override val values: Sequence<TangemTokenRowUM.TitleUM>
get() = sequenceOf(
TangemTokenRowPreviewData.titleUM,
TangemTokenRowUM.TitleUM.Loading,
TangemTokenRowUM.TitleUM.Empty,
)
}
// endregion

View file

@ -147,7 +147,7 @@ private fun lightThemeColors2(): TangemColors2 {
borderInvalid = border.status.warning,
)
val skeleton = TangemColors2.Skeleton(
backgroundPrimary = TangemColorPalette.Light1V2,
backgroundPrimary = TangemColorPalette.Dark_10,
)
val markers = TangemColors2.Markers(
backgroundSolidGray = TangemColorPalette.Light3,
@ -324,7 +324,7 @@ private fun darkThemeColors2(): TangemColors2 {
borderInvalid = border.status.warning,
)
val skeleton = TangemColors2.Skeleton(
backgroundPrimary = TangemColorPalette.Dark5,
backgroundPrimary = TangemColorPalette.Light_10,
)
val markers = TangemColors2.Markers(
backgroundSolidGray = TangemColorPalette.Dark5,

View file

@ -15,13 +15,29 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds.row.token.TangemTokenRow
import com.tangem.core.ui.ds.row.token.TangemTokenRow_PreviewProvider
import com.tangem.core.ui.ds.row.token.internal.TangemTokenRowPreviewData
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory
@Composable
internal fun TangemTokenRowStory(state: TangemTokenRowStory, modifier: Modifier = Modifier) {
val rows = remember { TangemTokenRow_PreviewProvider().values.toList() }
val rows = remember {
listOf(
TangemTokenRowPreviewData.defaultState,
TangemTokenRowPreviewData.defaultEllipsisState,
TangemTokenRowPreviewData.tokenState,
TangemTokenRowPreviewData.customTokenState,
TangemTokenRowPreviewData.draggableState,
TangemTokenRowPreviewData.draggableStateV2,
TangemTokenRowPreviewData.loadingState,
TangemTokenRowPreviewData.emptyState,
TangemTokenRowPreviewData.unreachableState,
TangemTokenRowPreviewData.accountState,
TangemTokenRowPreviewData.accountLetterState,
TangemTokenRowPreviewData.accountEllipsisState,
TangemTokenRowPreviewData.promoBannerState,
)
}
LazyColumn(
contentPadding = PaddingValues(bottom = 16.dp),

View file

@ -4,7 +4,6 @@ import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfi
import com.tangem.domain.models.wallet.UserWalletId
import com.tangem.feature.wallet.presentation.wallet.state.model.*
import com.tangem.feature.wallet.presentation.wallet.state.utils.enableButtons
import com.tangem.feature.wallet.presentation.wallet.state.model.WalletUM
import kotlinx.collections.immutable.PersistentList
import kotlinx.collections.immutable.mutate

View file

@ -31,6 +31,7 @@ import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
import com.tangem.core.ui.ds.row.header.TangemHeaderRowUM
import com.tangem.core.ui.ds.row.internal.TangemRowTailUM
@ -321,17 +322,21 @@ internal fun PortfolioRowItem(
SharedTokenRowComposables(
icon = { modifier ->
val size = if (isExpandedWrapped) AccountIconSize.ExtraSmall else AccountIconSize.Default
val currencyIconState =
when (val currencyIconState = item.tokenRowUM.headIconUM.currencyIconState) {
is CurrencyIconState.CryptoPortfolio.Icon ->
currencyIconState.copy(size = size)
is CurrencyIconState.CryptoPortfolio.Letter ->
currencyIconState.copy(size = size)
else -> currencyIconState
}
val headIcon = item.tokenRowUM.headIconUM
val sizedHeadIcon = if (headIcon is TangemIconUM.Currency) {
headIcon.copy(
currencyIconState = when (val currencyIconState = headIcon.currencyIconState) {
is CurrencyIconState.CryptoPortfolio.Icon -> currencyIconState.copy(size = size)
is CurrencyIconState.CryptoPortfolio.Letter -> currencyIconState.copy(size = size)
else -> currencyIconState
},
)
} else {
headIcon
}
TangemIcon(
tangemIconUM = item.tokenRowUM.headIconUM.copy(currencyIconState = currencyIconState),
tangemIconUM = sizedHeadIcon,
modifier = modifier.sharedBounds(
sharedContentState = iconSharedContentState,
animatedVisibilityScope = animatedContentScope,