Updated on 2026-08-14
This commit is contained in:
commit
01663f0230
16 changed files with 547 additions and 52 deletions
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
@ -48,6 +48,7 @@ internal class TangemPayCardLimitSetupModelTest {
|
|||
isFrozen = false,
|
||||
lastDigits = "1234",
|
||||
limit = null,
|
||||
isReissuing = false,
|
||||
)
|
||||
|
||||
private val initialStatus: AccountStatus.Payment = AccountStatus.Payment(
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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"
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue