Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-21 18:24:20 +05:00
commit 01663f0230
16 changed files with 547 additions and 52 deletions

View file

@ -0,0 +1,223 @@
package com.tangem.core.ui.ds2.shimmers
import android.content.res.Configuration
import androidx.compose.animation.core.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import kotlin.math.cos
import kotlin.math.sin
/**
* Design-system rectangle shimmer placeholder.
*
* A rounded rectangle painted with `bg.opaque.secondary`. A tilted band sweeps across it where
* the base color's alpha is gradually dimmed toward the center of the band and restored at the
* edges, producing a soft "blade" highlight passing through the placeholder. The alpha profile
* matches [com.tangem.core.ui.components.text.BladeAnimation].
*
* Cycle: 1.5s hold 0.8s linear sweep restart.
*
* Version 1.0
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=3398-625&p=f&m=dev)
*
* Sizing is the caller's responsibility set width and height via [modifier].
*
* @param modifier Modifier applied to the shimmer's root.
* @param radius Corner radius of the rectangle.
*/
@Composable
fun RectangleShimmer(modifier: Modifier = Modifier, radius: Dp = 6.dp) {
val baseColor = TangemTheme.colors3.bg.opaque.secondary
val progress = LocalTangemShimmerProgress.current ?: rememberShimmerProgressInstance()
val colorStops = remember(baseColor) { buildColorStops(baseColor) }
Box(
modifier = modifier
.clip(RoundedCornerShape(radius))
.drawWithCache {
// Stable per layout — recomputed only when size or density changes.
val shimmerWidthPx = SHIMMER_WIDTH.toPx()
val coverage = size.width * SHIMMER_DX + size.height * SHIMMER_DY
val travel = coverage + shimmerWidthPx
val halfWidth = shimmerWidthPx / 2f
onDrawBehind {
val center = -halfWidth + progress.value * travel
drawRect(
brush = Brush.linearGradient(
colorStops = colorStops,
start = Offset(
x = (center - halfWidth) * SHIMMER_DX,
y = (center - halfWidth) * SHIMMER_DY,
),
end = Offset(
x = (center + halfWidth) * SHIMMER_DX,
y = (center + halfWidth) * SHIMMER_DY,
),
),
)
}
},
)
}
/**
* Text-sized shimmer placeholder. Sizes itself to the bounding box of the [text] measured in the
* typography preset selected by [style], plus the preset's vertical padding (top + bottom).
*
* @param text Text used to determine the shimmer's size. Not drawn.
* @param style Typography preset drives both the measurement style and the vertical padding.
* @param radius Corner radius of the rectangle.
* @param modifier Modifier applied to the shimmer's root.
*/
@Composable
fun TextShimmer(text: String, style: TextShimmerStyle, radius: Dp, modifier: Modifier = Modifier) {
val textStyle = style.toTextStyle()
val measurer = rememberTextMeasurer()
val density = LocalDensity.current
val (widthDp, heightDp) = remember(text, textStyle, measurer, density) {
val measured = measurer.measure(text = text, style = textStyle)
with(density) { measured.size.width.toDp() to measured.size.height.toDp() }
}
RectangleShimmer(
modifier = modifier.size(
width = widthDp,
height = heightDp + style.verticalPadding * 2,
),
radius = radius,
)
}
/**
* Typography preset for [TextShimmer]. Each preset maps to a [TangemTheme.typography3] style
* and contributes additional [verticalPadding] applied to both top and bottom the shimmer
* block ends up `2 * verticalPadding` taller than the raw measured text.
*/
enum class TextShimmerStyle(val verticalPadding: Dp) {
DISPLAY(verticalPadding = 4.dp),
HEADING_MEDIUM(verticalPadding = 2.dp),
HEADING_SMALL(verticalPadding = 2.dp),
BODY(verticalPadding = 2.dp),
SUBHEADING(verticalPadding = 2.dp),
CAPTION(verticalPadding = 2.dp),
}
@Composable
@ReadOnlyComposable
private fun TextShimmerStyle.toTextStyle(): TextStyle = when (this) {
TextShimmerStyle.DISPLAY -> TangemTheme.typography3.display.medium
TextShimmerStyle.HEADING_MEDIUM -> TangemTheme.typography3.heading.medium
TextShimmerStyle.HEADING_SMALL -> TangemTheme.typography3.heading.small
TextShimmerStyle.BODY -> TangemTheme.typography3.body.medium
TextShimmerStyle.SUBHEADING -> TangemTheme.typography3.subheading.medium
TextShimmerStyle.CAPTION -> TangemTheme.typography3.caption.medium
}
/**
* Wraps [content] so every [RectangleShimmer] / [TextShimmer] inside reuses a single shimmer
* animation driver. Without this provider each shimmer creates its own
* [rememberInfiniteTransition] that scales poorly in lists and lets sweeps drift out of phase.
* Safe to nest; safe to omit (each shimmer falls back to its own driver).
*/
@Composable
fun ProvideTangemShimmer(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalTangemShimmerProgress provides rememberShimmerProgressInstance(),
content = content,
)
}
private val LocalTangemShimmerProgress = compositionLocalOf<State<Float>?> { null }
@Composable
private fun rememberShimmerProgressInstance(): State<Float> {
val transition = rememberInfiniteTransition(label = "TangemShimmer")
return transition.animateFloat(
initialValue = 0f,
targetValue = 1f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = SHIMMER_DURATION_MS,
delayMillis = SHIMMER_DELAY_MS,
easing = LinearEasing,
),
repeatMode = RepeatMode.Restart,
),
label = "TangemShimmerProgress",
)
}
private fun buildColorStops(baseColor: Color): Array<Pair<Float, Color>> = SHIMMER_ALPHA_STOPS
.map { (position, factor) -> position to baseColor.copy(alpha = baseColor.alpha * factor) }
.toTypedArray()
private val SHIMMER_WIDTH: Dp = 400.dp
private const val SHIMMER_DURATION_MS = 800
private const val SHIMMER_DELAY_MS = 1_500
private const val SHIMMER_ROTATION_DEG = 15.0
private val SHIMMER_DX = cos(Math.toRadians(SHIMMER_ROTATION_DEG)).toFloat()
private val SHIMMER_DY = sin(Math.toRadians(SHIMMER_ROTATION_DEG)).toFloat()
/** Alpha profile borrowed from BladeAnimation — a wide, gradual dim through the band's center. */
private val SHIMMER_ALPHA_STOPS: List<Pair<Float, Float>> = listOf(
0f to 1f,
0.15f to 0.75f,
0.35f to 0.45f,
0.5f to 0.3f,
0.65f to 0.45f,
0.85f to 0.75f,
1f to 1f,
)
// region Previews
@Preview(name = "Light", showBackground = true)
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
private fun TangemShimmerPreview() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
RectangleShimmer(
modifier = Modifier.size(width = 200.dp, height = 24.dp),
radius = 6.dp,
)
RectangleShimmer(
modifier = Modifier.size(width = 120.dp, height = 16.dp),
radius = 4.dp,
)
TextShimmer(
text = "Account balance",
style = TextShimmerStyle.BODY,
radius = 4.dp,
)
TextShimmer(
text = "$12,345.67",
style = TextShimmerStyle.HEADING_MEDIUM,
radius = 6.dp,
)
}
}
}
// endregion

View file

@ -29,7 +29,6 @@ import com.tangem.domain.pay.repository.OnboardingRepository
import com.tangem.domain.tangempay.TangemPayAnalyticsEvents
import com.tangem.domain.visa.error.VisaApiError
import com.tangem.utils.coroutines.CoroutineDispatcherProvider
import com.tangem.utils.extensions.orZero
import kotlinx.coroutines.withContext
import java.util.concurrent.ConcurrentHashMap
import javax.inject.Inject

View file

@ -15,6 +15,7 @@ import com.tangem.domain.pay.model.CustomerInfo.ProductInstance
import com.tangem.domain.pay.model.CustomerInfo.ProductInstance.Status
import com.tangem.domain.visa.model.TangemPayCardFrozenState
import com.tangem.utils.converter.Converter
import com.tangem.utils.extensions.orZero
internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, CustomerInfo> {
@Suppress("ComplexCondition")
@ -33,6 +34,7 @@ internal object CustomerInfoConverter : Converter<CustomerMeResponse.Result, Cus
isPinSet = value.card?.isPinSet == true,
fiatBalance = fiatBalance.toDomain(),
cryptoBalance = cryptoBalance.toDomain(),
availableForWithdrawal = value.balance?.availableForWithdrawal?.amount.orZero(),
)
} else {
null

View file

@ -12,13 +12,16 @@ import com.tangem.common.ui.components.currency.icon.converter.CryptoCurrencyToI
import com.tangem.common.ui.notifications.NotificationUM
import com.tangem.common.ui.userwallet.ext.walletInterationIcon
import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfig
import com.tangem.domain.express.models.ProviderFilterType
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.*
import com.tangem.core.ui.format.bigdecimal.crypto
import com.tangem.core.ui.format.bigdecimal.fiat
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.utils.parseBigDecimalOrNull
import com.tangem.domain.appcurrency.model.AppCurrency
import com.tangem.domain.express.models.ExpressError
import com.tangem.domain.express.models.ProviderFilterType
import com.tangem.domain.models.account.Account
import com.tangem.domain.models.currency.CryptoCurrency
import com.tangem.domain.models.currency.CryptoCurrencyStatus
@ -28,20 +31,16 @@ import com.tangem.domain.transaction.usecase.gasless.IsGaslessFeeSupportedForNet
import com.tangem.feature.swap.converters.SwapProviderStateBuilder
import com.tangem.feature.swap.domain.models.ExpressDataError
import com.tangem.feature.swap.domain.models.SwapAmount
import com.tangem.feature.swap.domain.models.domain.ExchangeProviderType
import com.tangem.feature.swap.domain.models.domain.IncludeFeeInAmount
import com.tangem.feature.swap.domain.models.domain.RateType
import com.tangem.feature.swap.domain.models.domain.SwapProvider
import com.tangem.feature.swap.domain.models.domain.SwapUIMode
import com.tangem.feature.swap.domain.models.domain.*
import com.tangem.feature.swap.domain.models.ui.*
import com.tangem.feature.swap.model.SwapNotificationsFactory
import com.tangem.feature.swap.model.SwapProcessDataState
import com.tangem.feature.swap.models.*
import com.tangem.feature.swap.models.SwapButton.Mode
import com.tangem.feature.swap.models.states.*
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.feature.swap.presentation.R
import com.tangem.feature.swap.utils.formatToUIRepresentation
import com.tangem.features.swap.SwapFeatureToggles
import com.tangem.utils.Provider
import com.tangem.utils.StringsSigns
import com.tangem.utils.StringsSigns.DASH_SIGN
@ -55,7 +54,7 @@ import java.math.BigDecimal
/**
* State builder creates a specific states for SwapScreen
*/
@Suppress("LargeClass", "TooManyFunctions")
@Suppress("LargeClass", "TooManyFunctions", "LongParameterList")
internal class StateBuilder(
private val actions: UiActions,
private val isBalanceHiddenProvider: Provider<Boolean>,

View file

@ -65,7 +65,7 @@ internal class DefaultTangemPayDetailsContainerComponent @AssistedInject constru
tokenReceiveComponentFactory = tokenReceiveComponentFactory,
expressTransactionsComponentProvider = expressTransactionsComponentProvider,
)
is TangemPayAccountDetailsInnerRoute.CardDetails -> tangemPayCardPageFactory.create(
TangemPayAccountDetailsInnerRoute.CardDetails -> tangemPayCardPageFactory.create(
context = childByContext(componentContext = componentContext, router = innerRouter),
params = TangemPayCardPageComponent.Params(initialStatus = params.initialStatus),
)

View file

@ -102,7 +102,7 @@ internal class TangemPayDetailsModel @Inject constructor(
stateFactory.getInitialState(
isTangemPayDeactivated = isTangemPayDeactivated,
cardNumberEnd = firstCard?.lastDigits.orEmpty(),
isReissuing = params.config.isReissuing,
isReissuing = firstCard?.isReissuing ?: false,
),
)
@ -122,7 +122,7 @@ internal class TangemPayDetailsModel @Inject constructor(
subscribeToCardFrozenState(firstCard.id)
fetchAddToWalletBanner()
paymentAccountStatusSupplier.invoke(params.userWalletId)
paymentAccountStatusSupplier.invoke(userWalletId)
.map { it.value }
.filterIsInstance<PaymentAccountStatusValue.Loaded>()
.filter { it.source == StatusSource.ACTUAL }
@ -131,7 +131,7 @@ internal class TangemPayDetailsModel @Inject constructor(
uiState.update(
TangemPayCardDataTransformer(
card = card,
onCardClick = { onCardClick(params.config.copy(cardId = card.id)) },
onCardClick = { onCardClick() },
),
)
}
@ -365,7 +365,7 @@ internal class TangemPayDetailsModel @Inject constructor(
override fun onCardClick() {
analytics.send(TangemPayAnalyticsEvents.CardIconClicked())
router.push(TangemPayAccountDetailsInnerRoute.CardDetails(config))
router.push(TangemPayAccountDetailsInnerRoute.CardDetails)
}
override fun onAddCardClick() {

View file

@ -17,7 +17,7 @@ internal class TangemPayCardDataTransformer(
onClick = onCardClick,
isReissuing = card.isReissuing,
)
val cardsBlockState = prevState.balanceBlockState.cardsBlockState.copy(
val cardsBlockState = prevState.balanceBlockState.cardsBlockState?.copy(
cards = persistentListOf(updatedCard),
)
val newBalanceBlockState = when (val bs = prevState.balanceBlockState) {

View file

@ -1,7 +1,6 @@
package com.tangem.features.tangempay.navigation
import com.tangem.core.decompose.navigation.Route
import com.tangem.domain.pay.TangemPayDetailsConfig
import kotlinx.serialization.Serializable
@Serializable
@ -10,7 +9,7 @@ internal sealed class TangemPayAccountDetailsInnerRoute : Route {
data object AccountDetails : TangemPayAccountDetailsInnerRoute()
@Serializable
data class CardDetails(val config: TangemPayDetailsConfig) : TangemPayAccountDetailsInnerRoute()
data object CardDetails : TangemPayAccountDetailsInnerRoute()
@Serializable
data object AddToWallet : TangemPayAccountDetailsInnerRoute()

View file

@ -116,7 +116,7 @@ internal fun TangemPayDetailsScreen(
},
)
if (state.balanceBlockState.cardsBlockState.cards.fastAny { it.isReissuing }) {
if (state.balanceBlockState.cardsBlockState?.cards?.fastAny { it.isReissuing } == true) {
item(
key = "REISSUE_MESSAGE",
content = {

View file

@ -1,7 +1,6 @@
package com.tangem.features.tangempay.utils
import com.tangem.core.ui.components.containers.pullToRefresh.PullToRefreshConfig.ShowRefreshState
import com.tangem.domain.pay.TangemPayDetailsConfig
internal interface TangemPayDetailIntents {
fun onContactSupportClicked()
@ -9,6 +8,6 @@ internal interface TangemPayDetailIntents {
fun onClickAddFunds()
fun onClickWithdraw()
fun onClickTermsAndLimits()
fun onCardClick(config: TangemPayDetailsConfig)
fun onCardClick()
fun onAddCardClick()
}

View file

@ -48,6 +48,7 @@ internal class TangemPayCardLimitSetupModelTest {
isFrozen = false,
lastDigits = "1234",
limit = null,
isReissuing = false,
)
private val initialStatus: AccountStatus.Payment = AccountStatus.Payment(

View file

@ -1,12 +1,14 @@
package com.tangem.feature.tester.presentation.storybook.entity
import androidx.compose.runtime.Immutable
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
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.topbar.TangemTopBarType
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
internal sealed interface StoryBookPage
@ -104,6 +106,43 @@ internal data class TangemLoaderStory(
val onSizeChange: (TangemLoaderSize) -> Unit,
) : DsStoryBookPage
@Immutable
internal data class TangemShimmerStory(
val textStyle: TextShimmerStyle,
val radius: RadiusOption,
val rectangleWidth: RectangleWidthOption,
val rectangleHeight: RectangleHeightOption,
val onTextStyleChange: (TextShimmerStyle) -> Unit,
val onRadiusChange: (RadiusOption) -> Unit,
val onRectangleWidthChange: (RectangleWidthOption) -> Unit,
val onRectangleHeightChange: (RectangleHeightOption) -> Unit,
) : DsStoryBookPage {
/** Selectable corner radius (matches `borderRadius` tokens). */
enum class RadiusOption(val label: String) {
R4("4dp"),
R8("8dp"),
R16("16dp"),
R24("24dp"),
R32("32dp"),
FULL("full"),
}
enum class RectangleWidthOption(val label: String) {
W80("80dp"),
W160("160dp"),
W240("240dp"),
FILL("fill"),
}
enum class RectangleHeightOption(val label: String) {
H16("16dp"),
H24("24dp"),
H40("40dp"),
H64("64dp"),
}
}
internal data class TangemButtonStory(
val variant: TangemButton.Variant,
val size: TangemButton.Size,

View file

@ -18,6 +18,7 @@ import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.badge.tangemBadgeV2StoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemButtonStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
@ -25,6 +26,7 @@ private fun buildDsStories() = listOf(
DsStoryItem(title = "⏳ TangemLoader", factory = tangemLoaderStoryFactory),
DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory),
DsStoryItem(title = "🏷️ TangemBadge", factory = tangemBadgeV2StoryFactory),
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
)
@Composable

View file

@ -0,0 +1,30 @@
package com.tangem.feature.tester.presentation.storybook.page.ds.shimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.feature.tester.presentation.storybook.entity.TangemShimmerStory
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<TangemShimmerStory>.build(): TangemShimmerStory {
return TangemShimmerStory(
textStyle = TextShimmerStyle.BODY,
radius = TangemShimmerStory.RadiusOption.R24,
rectangleWidth = TangemShimmerStory.RectangleWidthOption.W240,
rectangleHeight = TangemShimmerStory.RectangleHeightOption.H24,
onTextStyleChange = { textStyle ->
updateStory { it.copy(textStyle = textStyle) }
},
onRadiusChange = { radius ->
updateStory { it.copy(radius = radius) }
},
onRectangleWidthChange = { width ->
updateStory { it.copy(rectangleWidth = width) }
},
onRectangleHeightChange = { height ->
updateStory { it.copy(rectangleHeight = height) }
},
)
}
internal val tangemShimmerStoryFactory
get() = storyPageFactory(StateUpdater<TangemShimmerStory>::build)

View file

@ -0,0 +1,223 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.ds.shimmer
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.ds2.shimmers.RectangleShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmer
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.TangemShimmerStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemShimmerStory.*
@Composable
internal fun TangemShimmerStory(state: TangemShimmerStory, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.statusBarsPadding()
.fillMaxSize()
.background(TangemTheme.colors3.bg.primary)
.verticalScroll(rememberScrollState())
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
ComponentPreview(state = state)
ChipSection(label = "Text style") {
ChipGrid(
items = TextShimmerStyle.entries,
label = { it.chipLabel() },
isSelected = { it == state.textStyle },
onSelect = state.onTextStyleChange,
)
}
ChipSection(label = "Radius") {
ChipGrid(
items = RadiusOption.entries,
label = { it.label },
isSelected = { it == state.radius },
onSelect = state.onRadiusChange,
)
}
ChipSection(label = "Rectangle width") {
ChipGrid(
items = RectangleWidthOption.entries,
label = { it.label },
isSelected = { it == state.rectangleWidth },
onSelect = state.onRectangleWidthChange,
)
}
ChipSection(label = "Rectangle height") {
ChipGrid(
items = RectangleHeightOption.entries,
label = { it.label },
isSelected = { it == state.rectangleHeight },
onSelect = state.onRectangleHeightChange,
)
}
}
}
@Composable
private fun ComponentPreview(state: TangemShimmerStory) {
val radius = state.radius.value()
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(TangemTheme.dimens3.borderRadius.b200))
.background(TangemTheme.colors3.bg.secondary)
.padding(vertical = 24.dp, horizontal = 16.dp),
) {
Column(
verticalArrangement = Arrangement.spacedBy(20.dp),
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
PreviewLabel(text = "RectangleShimmer")
RectangleShimmerPreview(
width = state.rectangleWidth,
height = state.rectangleHeight,
radius = radius,
)
PreviewLabel(text = "TextShimmer · ${state.textStyle.chipLabel()}")
TextShimmer(
text = SAMPLE_TEXT,
style = state.textStyle,
radius = radius,
)
}
}
}
@Composable
private fun RectangleShimmerPreview(width: RectangleWidthOption, height: RectangleHeightOption, radius: Dp) {
val sizeModifier = when (width) {
RectangleWidthOption.FILL -> Modifier.fillMaxWidth()
else -> Modifier.width(width.value())
}.height(height.value())
RectangleShimmer(
modifier = sizeModifier,
radius = radius,
)
}
@Composable
private fun PreviewLabel(text: String) {
Text(
text = text,
style = TangemTheme.typography3.caption.medium,
color = TangemTheme.colors3.text.secondary,
)
}
// region Chip selector — uses ds2 surfaces/typography so the controls match the redesign.
@Composable
private fun ChipSection(label: String, content: @Composable () -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
modifier = Modifier.padding(horizontal = 16.dp),
text = label,
style = TangemTheme.typography3.subheading.medium,
color = TangemTheme.colors3.text.primary,
)
content()
}
}
@Composable
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
val shape = RoundedCornerShape(TangemTheme.dimens3.borderRadius.full)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(shape)
.background(TangemTheme.colors3.bg.opaque.primary)
.border(
width = TangemTheme.dimens3.borderWidth.sm,
color = TangemTheme.colors3.border.primary,
shape = shape,
)
.padding(4.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
items.forEach { item ->
Chip(
label = label(item),
selected = isSelected(item),
onClick = { onSelect(item) },
modifier = Modifier.weight(1f),
)
}
}
}
@Composable
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
val chipShape = RoundedCornerShape(TangemTheme.dimens3.borderRadius.full)
Box(
contentAlignment = Alignment.Center,
modifier = modifier
.clip(chipShape)
.background(
if (selected) TangemTheme.colors3.bg.opaque.secondary else TangemTheme.colors3.bg.opaque.primary,
)
.clickable(onClick = onClick)
.padding(vertical = 8.dp, horizontal = 4.dp),
) {
Text(
text = label,
style = TangemTheme.typography3.caption.medium,
color = if (selected) TangemTheme.colors3.text.primary else TangemTheme.colors3.text.secondary,
)
}
}
// endregion
private fun TextShimmerStyle.chipLabel(): String = when (this) {
TextShimmerStyle.DISPLAY -> "Display"
TextShimmerStyle.HEADING_MEDIUM -> "Head.M"
TextShimmerStyle.HEADING_SMALL -> "Head.S"
TextShimmerStyle.BODY -> "Body"
TextShimmerStyle.SUBHEADING -> "Sub.H"
TextShimmerStyle.CAPTION -> "Caption"
}
private fun RadiusOption.value(): Dp = when (this) {
RadiusOption.R4 -> 4.dp
RadiusOption.R8 -> 8.dp
RadiusOption.R16 -> 16.dp
RadiusOption.R24 -> 24.dp
RadiusOption.R32 -> 32.dp
RadiusOption.FULL -> 1000.dp
}
private fun RectangleWidthOption.value(): Dp = when (this) {
RectangleWidthOption.W80 -> 80.dp
RectangleWidthOption.W160 -> 160.dp
RectangleWidthOption.W240 -> 240.dp
RectangleWidthOption.FILL -> 0.dp // unused — handled separately
}
private fun RectangleHeightOption.value(): Dp = when (this) {
RectangleHeightOption.H16 -> 16.dp
RectangleHeightOption.H24 -> 24.dp
RectangleHeightOption.H40 -> 40.dp
RectangleHeightOption.H64 -> 64.dp
}
private const val SAMPLE_TEXT = "Sample shimmer text"

View file

@ -4,51 +4,29 @@ import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedContent
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory
import com.tangem.feature.tester.presentation.storybook.entity.DeviceIconStory
import com.tangem.feature.tester.presentation.storybook.entity.DsComponentsListStory
import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory
import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGStory
import com.tangem.feature.tester.presentation.storybook.entity.PlaceholderStory
import com.tangem.feature.tester.presentation.storybook.entity.ProgressIndicatorStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeV2Story
import com.tangem.feature.tester.presentation.storybook.entity.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM
import com.tangem.feature.tester.presentation.storybook.entity.StoryList
import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemLoaderStory
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.TangemPagerIndicatorStory
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.TangemTabStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemTopBarStory
import com.tangem.feature.tester.presentation.storybook.entity.TypographyStory
import com.tangem.feature.tester.presentation.storybook.entity.*
import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory
import com.tangem.feature.tester.presentation.storybook.page.badge.TangemBadgeStory
import com.tangem.feature.tester.presentation.storybook.page.buttons.ButtonsStory
import com.tangem.feature.tester.presentation.storybook.page.checkbox.TangemCheckboxStory
import com.tangem.feature.tester.presentation.storybook.page.contextmenu.TangemContextMenuStory
import com.tangem.feature.tester.presentation.storybook.page.deviceicon.DeviceIconStory
import com.tangem.feature.tester.presentation.storybook.page.ds.DsComponentsListStory
import com.tangem.feature.tester.presentation.storybook.page.ds.badge.TangemBadgeV2Story
import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory
import com.tangem.feature.tester.presentation.storybook.page.checkbox.TangemCheckboxStory
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory
import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessageStory
import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory
import com.tangem.feature.tester.presentation.storybook.page.pagerindicator.TangemPagerIndicatorStory
import com.tangem.feature.tester.presentation.storybook.page.placeholder.PlaceholderStory
import com.tangem.feature.tester.presentation.storybook.page.progress.ProgressIndicatorStory
import com.tangem.feature.tester.presentation.storybook.page.searchfield.TangemSearchFieldStory
import com.tangem.feature.tester.presentation.storybook.page.tab.TangemTabStory
import com.tangem.feature.tester.presentation.storybook.page.tabs.TangemSegmentedPickerStory
import com.tangem.feature.tester.presentation.storybook.page.tokenrow.TangemTokenRowStory
import com.tangem.feature.tester.presentation.storybook.page.topbar.TangemTopBarStory
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
import com.tangem.feature.tester.presentation.storybook.page.typography.TypographyStory
@Suppress("CyclomaticComplexMethod")
@ -85,6 +63,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
is TangemLoaderStory -> TangemLoaderStory(state = storyState)
is TangemButtonStory -> TangemButtonStory(state = storyState)
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
}
}
}