Updated on 2026-08-14
This commit is contained in:
parent
ddba5fe3cf
commit
5ce84d1500
14 changed files with 2832 additions and 0 deletions
|
|
@ -0,0 +1,121 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.common.ui.markets
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.common.ui.charts.MarketChartMini
|
||||||
|
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||||
|
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||||
|
import com.tangem.common.ui.markets.models.MarketsListItemUM
|
||||||
|
import com.tangem.common.ui.markets.preview.MarketChartListItemPreviewDataProvider
|
||||||
|
import com.tangem.core.ui.components.marketprice.PriceChangeType
|
||||||
|
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||||
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.TangemTokenRowMarket
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.LocalWindowSize
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.windowsize.WindowSizeType
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DS3 markets-list item — [TangemTokenRowMarket] bound to the legacy [MarketsListItemUM], for a
|
||||||
|
* drop-in migration from [MarketsListItemV2].
|
||||||
|
*
|
||||||
|
* Behavior notes (kept from [MarketsListItemV2]):
|
||||||
|
* - The chart is hidden on small window widths.
|
||||||
|
* - While [MarketsListItemUM.chartData] is `null`, the graph slot shows a shimmer bar.
|
||||||
|
* - On a live price update the price text flashes in the update-direction color
|
||||||
|
* ([MarketsListItemUM.Price.changeType]), like the legacy `TokenPriceText`.
|
||||||
|
*
|
||||||
|
* @param model Legacy markets list item model.
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param onClick Row click handler. `null` makes the row non-interactive.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun MarketsListItemV3(model: MarketsListItemUM, modifier: Modifier = Modifier, onClick: (() -> Unit)? = null) {
|
||||||
|
val isChartVisible = LocalWindowSize.current.widthAtLeast(WindowSizeType.Small)
|
||||||
|
TangemTokenRowMarket(
|
||||||
|
icon = TangemTokenIcon.UiState.Token(TangemTokenIcon.State(url = model.iconUrl)),
|
||||||
|
title = stringReference(model.name),
|
||||||
|
modifier = modifier,
|
||||||
|
ticker = stringReference(model.currencySymbol),
|
||||||
|
position = model.ratingPosition?.let(::stringReference),
|
||||||
|
capitalization = model.marketCap?.let(::stringReference),
|
||||||
|
price = stringReference(model.price.text),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference(model.trendPercentText),
|
||||||
|
direction = model.trendType.toPriceChangeDirection(),
|
||||||
|
),
|
||||||
|
priceUpdateDirection = model.price.changeType?.toPriceChangeDirection(),
|
||||||
|
chart = if (isChartVisible) {
|
||||||
|
{ MarketRowChart(chartType = model.chartType, chartRawData = model.chartData) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun PriceChangeType.toPriceChangeDirection(): TangemPriceChange.Direction = when (this) {
|
||||||
|
PriceChangeType.UP -> TangemPriceChange.Direction.Up
|
||||||
|
PriceChangeType.DOWN -> TangemPriceChange.Direction.Down
|
||||||
|
PriceChangeType.NEUTRAL -> TangemPriceChange.Direction.Neutral
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Mini-graph for the [TangemTokenRowMarket] chart slot — 24x32 collapsed, shimmer while loading. */
|
||||||
|
@Composable
|
||||||
|
private fun MarketRowChart(chartType: MarketChartLook.Type, chartRawData: MarketChartRawData?) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.size(width = 24.dp, height = 32.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
if (chartRawData != null) {
|
||||||
|
MarketChartMini(
|
||||||
|
rawData = chartRawData,
|
||||||
|
type = chartType,
|
||||||
|
neutralColor = TangemTheme.colors3.border.accent.neutral,
|
||||||
|
growingColor = TangemTheme.colors3.border.accent.blue,
|
||||||
|
fallingColor = TangemTheme.colors3.border.accent.red,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
TangemShimmer(
|
||||||
|
radius = 4.dp,
|
||||||
|
modifier = Modifier.size(width = 24.dp, height = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Preview
|
||||||
|
|
||||||
|
@Preview(name = "Light", showBackground = true, widthDp = 360)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark",
|
||||||
|
showBackground = true,
|
||||||
|
widthDp = 360,
|
||||||
|
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
private fun MarketsListItemV3Preview(
|
||||||
|
@PreviewParameter(MarketChartListItemPreviewDataProvider::class) state: MarketsListItemUM,
|
||||||
|
) {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
|
||||||
|
MarketsListItemV3(model = state, onClick = {})
|
||||||
|
MarketsListItemV3(model = state.copy(chartData = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
@ -0,0 +1,280 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.messagebubble
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.RoundRect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathOperation
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
|
import androidx.compose.ui.semantics.role
|
||||||
|
import androidx.compose.ui.semantics.semantics
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
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.resolveAnnotatedReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_cross_circle_16_filled
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_info_16
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Message Bubble** — a compact caption pill with an optional leading icon,
|
||||||
|
* an optional close button and an optional "tip" tail on top pointing at the anchored element.
|
||||||
|
* DS3 replacement for the legacy `TokenRowPromoBanner`.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=6327-22326)
|
||||||
|
*
|
||||||
|
* @param text Bubble message, rendered in caption/medium typography.
|
||||||
|
* @param modifier Modifier applied to the bubble. The bubble hugs its content in both dimensions.
|
||||||
|
* @param variant Visual appearance — background, text and tip colors.
|
||||||
|
* @param showTip Whether the tail on top of the bubble is drawn. `false` shows only the pill.
|
||||||
|
* @param icon Leading 16dp icon, tinted with the [variant] content color. `null` hides it.
|
||||||
|
* @param onClick Invoked when the bubble body is tapped. `null` makes the bubble non-interactive.
|
||||||
|
* @param onClose Invoked when the close button is tapped. `null` hides the button.
|
||||||
|
* @param closeContentDescription Accessibility label for the close button announced by TalkBack
|
||||||
|
* (e.g. `"Dismiss"`). Supply it whenever [onClose] is set.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun TangemMessageBubble(
|
||||||
|
text: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
variant: TangemMessageBubble.Variant = TangemMessageBubble.Variant.Neutral,
|
||||||
|
showTip: Boolean = true,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onClose: (() -> Unit)? = null,
|
||||||
|
closeContentDescription: String? = null,
|
||||||
|
) {
|
||||||
|
val tokens = variant.tokens()
|
||||||
|
val shape = if (showTip) MessageBubbleShape else RoundedCornerShape(12.dp)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(shape)
|
||||||
|
.background(tokens.background)
|
||||||
|
.conditionalCompose(onClick != null) {
|
||||||
|
clickableSingle(role = Role.Button, onClick = requireNotNull(onClick))
|
||||||
|
}
|
||||||
|
.padding(
|
||||||
|
start = 8.dp,
|
||||||
|
top = if (showTip) 12.dp else 4.dp,
|
||||||
|
end = if (onClose != null) 4.dp else 8.dp,
|
||||||
|
bottom = 4.dp,
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = tokens.content,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = text.resolveAnnotatedReference(),
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
color = tokens.content,
|
||||||
|
)
|
||||||
|
if (onClose != null) {
|
||||||
|
CloseButton(
|
||||||
|
onClick = onClose,
|
||||||
|
tint = tokens.closeIcon,
|
||||||
|
contentDescription = closeContentDescription,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public API surface of [TangemMessageBubble]. */
|
||||||
|
object TangemMessageBubble {
|
||||||
|
|
||||||
|
/** Visual appearance — background, text and tip colors. */
|
||||||
|
enum class Variant {
|
||||||
|
/** Neutral tertiary background with secondary text. */
|
||||||
|
Neutral,
|
||||||
|
|
||||||
|
/** Subtle success-green background with success text. */
|
||||||
|
Success,
|
||||||
|
|
||||||
|
/** Subtle info-blue background with info text. */
|
||||||
|
Info,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun CloseButton(onClick: () -> Unit, tint: Color, contentDescription: String?, modifier: Modifier = Modifier) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(start = 4.dp)
|
||||||
|
.size(16.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredSize(28.dp)
|
||||||
|
.clip(RoundedCornerShape(percent = 50))
|
||||||
|
.clickableSingle(onClick = onClick)
|
||||||
|
.semantics {
|
||||||
|
role = Role.Button
|
||||||
|
contentDescription?.let { this.contentDescription = it }
|
||||||
|
},
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.ic_cross_circle_16_filled,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = tint,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pill with the tip tail as a single-path [Shape]: an 8x8 swoosh (apex at the top-start corner,
|
||||||
|
* concave curve down to the bottom-end — the geometry of the legacy `shape_triangular` drawable)
|
||||||
|
* sitting on a 12dp-rounded rect. One shape means no anti-aliasing seam between tail and pill, no
|
||||||
|
* double-painting of translucent background tokens, and a ripple clipped to the full silhouette.
|
||||||
|
*/
|
||||||
|
private object MessageBubbleShape : Shape {
|
||||||
|
|
||||||
|
override fun createOutline(size: Size, layoutDirection: LayoutDirection, density: Density): Outline {
|
||||||
|
val path = Path.combine(
|
||||||
|
operation = PathOperation.Union,
|
||||||
|
path1 = density.pillPath(size),
|
||||||
|
path2 = density.tipPath(size, layoutDirection),
|
||||||
|
)
|
||||||
|
return Outline.Generic(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Density.pillPath(size: Size): Path = Path().apply {
|
||||||
|
addRoundRect(
|
||||||
|
RoundRect(
|
||||||
|
left = 0f,
|
||||||
|
top = 8.dp.toPx(),
|
||||||
|
right = size.width,
|
||||||
|
bottom = size.height,
|
||||||
|
cornerRadius = CornerRadius(12.dp.toPx()),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Density.tipPath(size: Size, layoutDirection: LayoutDirection): Path {
|
||||||
|
val tipWidth = 8.dp.toPx()
|
||||||
|
val tipHeight = 8.dp.toPx()
|
||||||
|
// Extends below the tip band, into the pill, to guarantee the union shapes overlap.
|
||||||
|
val skirtHeight = 2.dp.toPx()
|
||||||
|
val startX = when (layoutDirection) {
|
||||||
|
LayoutDirection.Ltr -> 16.dp.toPx()
|
||||||
|
LayoutDirection.Rtl -> size.width - 16.dp.toPx() - tipWidth
|
||||||
|
}
|
||||||
|
// Maps a 0..1 fraction of the tip width to an x coordinate, mirrored in RTL.
|
||||||
|
fun x(fraction: Float): Float = when (layoutDirection) {
|
||||||
|
LayoutDirection.Ltr -> startX + fraction * tipWidth
|
||||||
|
LayoutDirection.Rtl -> startX + (1 - fraction) * tipWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vector drawable path "M8,8 L0,8 L0,0 C0,0 2,6 8,8 Z" in an 8x8 viewport.
|
||||||
|
return Path().apply {
|
||||||
|
moveTo(x(1f), tipHeight + skirtHeight)
|
||||||
|
lineTo(x(0f), tipHeight + skirtHeight)
|
||||||
|
lineTo(x(0f), 0f)
|
||||||
|
cubicTo(
|
||||||
|
x1 = x(0f),
|
||||||
|
y1 = 0f,
|
||||||
|
x2 = x(0.25f),
|
||||||
|
y2 = tipHeight * 0.75f,
|
||||||
|
x3 = x(1f),
|
||||||
|
y3 = tipHeight,
|
||||||
|
)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Resolved appearance tokens for a [TangemMessageBubble.Variant]. */
|
||||||
|
private data class MessageBubbleTokens(val background: Color, val content: Color, val closeIcon: Color)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun TangemMessageBubble.Variant.tokens(): MessageBubbleTokens {
|
||||||
|
val colors = TangemTheme.colors3
|
||||||
|
return when (this) {
|
||||||
|
TangemMessageBubble.Variant.Neutral -> MessageBubbleTokens(
|
||||||
|
background = colors.bg.tertiary,
|
||||||
|
content = colors.text.secondary,
|
||||||
|
closeIcon = colors.icon.secondary,
|
||||||
|
)
|
||||||
|
TangemMessageBubble.Variant.Success -> MessageBubbleTokens(
|
||||||
|
background = colors.bg.status.successSubtle,
|
||||||
|
content = colors.text.status.success,
|
||||||
|
closeIcon = colors.icon.status.success,
|
||||||
|
)
|
||||||
|
TangemMessageBubble.Variant.Info -> MessageBubbleTokens(
|
||||||
|
background = colors.bg.status.infoSubtle,
|
||||||
|
content = colors.text.status.info,
|
||||||
|
closeIcon = colors.icon.status.info,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light", showBackground = true)
|
||||||
|
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun TangemMessageBubblePreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(TangemTheme.colors3.bg.primary)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
TangemMessageBubble.Variant.entries.forEach { variant ->
|
||||||
|
TangemMessageBubble(
|
||||||
|
text = stringReference("Description"),
|
||||||
|
variant = variant,
|
||||||
|
icon = Icons.ic_info_16,
|
||||||
|
onClick = {},
|
||||||
|
onClose = {},
|
||||||
|
closeContentDescription = "Dismiss",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TangemMessageBubble(
|
||||||
|
text = stringReference("Text only"),
|
||||||
|
showTip = false,
|
||||||
|
)
|
||||||
|
TangemMessageBubble(
|
||||||
|
text = stringReference("No icon"),
|
||||||
|
variant = TangemMessageBubble.Variant.Info,
|
||||||
|
onClose = {},
|
||||||
|
closeContentDescription = "Dismiss",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,719 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.tokenrow
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.ds2.badge.TangemBadge
|
||||||
|
import com.tangem.core.ui.ds2.messagebubble.TangemMessageBubble
|
||||||
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_chart_bar_vertical_16
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_sign_equal_24
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row** — a portfolio list item: token icon, title with an optional
|
||||||
|
* badge, quote + price change, fiat/crypto balances and an optional message-bubble promo line
|
||||||
|
* underneath. DS3 replacement for the legacy `ds/row/token` `TangemTokenRow`.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5723-2473)
|
||||||
|
*
|
||||||
|
* @param icon Token icon state, rendered at 40dp. See [TangemTokenIcon.UiState].
|
||||||
|
* @param title Token name. Single line, ellipsized.
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param badge Optional [TangemTokenRow.Badge] after the title (e.g. an `"APY 5.47%"` chip; use a
|
||||||
|
* [TangemBadge.Variant.Solid] badge for the filled look). `null` hides it.
|
||||||
|
* @param hasPending Shows a small spinner after the title while a transaction is pending.
|
||||||
|
* @param quote Fiat quote for one token (e.g. `"$1.00"`). `null` hides it.
|
||||||
|
* @param priceChange Price change indicator next to the quote. `null` hides it.
|
||||||
|
* @param fiatBalance Primary balance at the end (e.g. `"$583.00"`). `null` hides the line.
|
||||||
|
* @param cryptoBalance Secondary balance under [fiatBalance] (e.g. `"0,000015 BTC"`). `null` hides it.
|
||||||
|
* @param showContractWarning Shows the orange contract-error warning before [fiatBalance].
|
||||||
|
* @param showUpdateWarning Shows the cloud update-error icon before [fiatBalance].
|
||||||
|
* @param isBalanceHidden When `true`, [fiatBalance] and [cryptoBalance] are masked with stars.
|
||||||
|
* @param isQuoteFlickering Runs the blade animation over [quote] and [priceChange] while the price
|
||||||
|
* is being refreshed.
|
||||||
|
* @param isBalanceFlickering Runs the blade animation over the balances while they are being
|
||||||
|
* refreshed.
|
||||||
|
* @param messageBubble Optional slot below the row content — pass a [TangemMessageBubble].
|
||||||
|
* @param onClick Row click handler. `null` with no [onLongClick] makes the row non-interactive.
|
||||||
|
* @param onLongClick Row long-press handler. `null` disables long-press.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRow(
|
||||||
|
icon: TangemTokenIcon.UiState,
|
||||||
|
title: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
badge: TangemTokenRow.Badge? = null,
|
||||||
|
hasPending: Boolean = false,
|
||||||
|
quote: TextReference? = null,
|
||||||
|
priceChange: TangemPriceChange.State? = null,
|
||||||
|
fiatBalance: TextReference? = null,
|
||||||
|
cryptoBalance: TextReference? = null,
|
||||||
|
showContractWarning: Boolean = false,
|
||||||
|
showUpdateWarning: Boolean = false,
|
||||||
|
isBalanceHidden: Boolean = false,
|
||||||
|
isQuoteFlickering: Boolean = false,
|
||||||
|
isBalanceFlickering: Boolean = false,
|
||||||
|
messageBubble: (@Composable () -> Unit)? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
TokenRowContainer(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
) {
|
||||||
|
TokenRowHeadIcon(icon = icon)
|
||||||
|
TokenRowTitleContent(
|
||||||
|
title = title,
|
||||||
|
badge = badge,
|
||||||
|
hasPending = hasPending,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_TOP)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
if (quote != null || priceChange != null) {
|
||||||
|
TokenRowSubtitleContent(
|
||||||
|
quote = quote,
|
||||||
|
priceChange = priceChange,
|
||||||
|
isFlickering = isQuoteFlickering,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (fiatBalance != null) {
|
||||||
|
TokenRowBalanceContent(
|
||||||
|
fiatBalance = fiatBalance,
|
||||||
|
showContractWarning = showContractWarning,
|
||||||
|
showUpdateWarning = showUpdateWarning,
|
||||||
|
isFlickering = isBalanceFlickering,
|
||||||
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.END_TOP),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (cryptoBalance != null) {
|
||||||
|
TokenRowCaptionText(
|
||||||
|
text = cryptoBalance,
|
||||||
|
isFlickering = isBalanceFlickering,
|
||||||
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.END_BOTTOM),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (messageBubble != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.EXTRA_BOTTOM)
|
||||||
|
.padding(start = 40.dp, bottom = 8.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
messageBubble()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public API surface of [TangemTokenRow]. */
|
||||||
|
object TangemTokenRow {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Badge shown after the title (e.g. an `"APY 5.47%"` chip). Wraps [TangemBadge] so callers can
|
||||||
|
* pick a tinted or a filled ([TangemBadge.Variant.Solid]) look with a status color, as in
|
||||||
|
* production usage.
|
||||||
|
*
|
||||||
|
* @param text Badge label.
|
||||||
|
* @param variant Badge appearance — [TangemBadge.Variant.Tinted] (default) or
|
||||||
|
* [TangemBadge.Variant.Solid] for the filled look. See [TangemBadge.Variant].
|
||||||
|
* @param status Status color scheme. See [TangemBadge.Status].
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class Badge(
|
||||||
|
val text: TextReference,
|
||||||
|
val variant: TangemBadge.Variant = TangemBadge.Variant.Tinted,
|
||||||
|
val status: TangemBadge.Status = TangemBadge.Status.Neutral,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Model of the message bubble rendered under the row content by the [State.Content] overload.
|
||||||
|
* Mirrors the [TangemMessageBubble] parameters.
|
||||||
|
*
|
||||||
|
* @param text Bubble message.
|
||||||
|
* @param variant Bubble appearance. See [TangemMessageBubble.Variant].
|
||||||
|
* @param shouldShowTip Whether the tail on top of the bubble is drawn.
|
||||||
|
* @param icon Leading 16dp bubble icon. `null` hides it.
|
||||||
|
* @param onClick Invoked when the bubble body is tapped. `null` makes it non-interactive.
|
||||||
|
* @param onClose Invoked when the bubble close button is tapped. `null` hides the button.
|
||||||
|
* @param closeContentDescription Accessibility label for the bubble close button.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class MessageBubble(
|
||||||
|
val text: TextReference,
|
||||||
|
val variant: TangemMessageBubble.Variant = TangemMessageBubble.Variant.Neutral,
|
||||||
|
val shouldShowTip: Boolean = true,
|
||||||
|
val icon: ImageVector? = null,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
val onClose: (() -> Unit)? = null,
|
||||||
|
val closeContentDescription: String? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State model of [TangemTokenRow] — one subtype per Figma variant. Render it with the
|
||||||
|
* `TangemTokenRow(state = …)` overload.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
sealed class State {
|
||||||
|
|
||||||
|
/** Unique id, e.g. for `LazyColumn` item keys. */
|
||||||
|
abstract val id: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default variant — balances, optional badge and an optional message bubble.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
* @param icon Token icon state.
|
||||||
|
* @param title Token name.
|
||||||
|
* @param badge Badge after the title. `null` hides it. See [Badge].
|
||||||
|
* @param hasPending Shows a small spinner after the title while a transaction is pending.
|
||||||
|
* @param quote Fiat quote for one token. `null` hides it.
|
||||||
|
* @param priceChange Price change indicator next to the quote. `null` hides it.
|
||||||
|
* @param fiatBalance Primary balance at the end. `null` hides the line.
|
||||||
|
* @param cryptoBalance Secondary balance under [fiatBalance]. `null` hides it.
|
||||||
|
* @param shouldShowContractWarning Shows the contract-error warning before [fiatBalance].
|
||||||
|
* @param shouldShowUpdateWarning Shows the update-error icon before [fiatBalance].
|
||||||
|
* @param isQuoteFlickering Runs the blade animation over the quote and price change while
|
||||||
|
* the price is being refreshed.
|
||||||
|
* @param isBalanceFlickering Runs the blade animation over the balances while they are
|
||||||
|
* being refreshed.
|
||||||
|
* @param messageBubble Message bubble under the row content. `null` hides it.
|
||||||
|
* @param onClick Row click handler. `null` with no [onLongClick] makes the row
|
||||||
|
* non-interactive.
|
||||||
|
* @param onLongClick Row long-press handler. `null` disables long-press.
|
||||||
|
*/
|
||||||
|
data class Content(
|
||||||
|
override val id: String,
|
||||||
|
val icon: TangemTokenIcon.UiState,
|
||||||
|
val title: TextReference,
|
||||||
|
val badge: Badge? = null,
|
||||||
|
val hasPending: Boolean = false,
|
||||||
|
val quote: TextReference? = null,
|
||||||
|
val priceChange: TangemPriceChange.State? = null,
|
||||||
|
val fiatBalance: TextReference? = null,
|
||||||
|
val cryptoBalance: TextReference? = null,
|
||||||
|
val shouldShowContractWarning: Boolean = false,
|
||||||
|
val shouldShowUpdateWarning: Boolean = false,
|
||||||
|
val isQuoteFlickering: Boolean = false,
|
||||||
|
val isBalanceFlickering: Boolean = false,
|
||||||
|
val messageBubble: MessageBubble? = null,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
val onLongClick: (() -> Unit)? = null,
|
||||||
|
) : State()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Organize (reorder) variant — ticker after the title, fiat balance underneath, drag
|
||||||
|
* handle at the end. Pass the reorder modifier via `dragHandleModifier` of the overload.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
* @param icon Token icon state.
|
||||||
|
* @param title Token name.
|
||||||
|
* @param ticker Currency ticker after the title. `null` hides it.
|
||||||
|
* @param fiatBalance Balance line under the title. `null` hides it.
|
||||||
|
*/
|
||||||
|
data class Organize(
|
||||||
|
override val id: String,
|
||||||
|
val icon: TangemTokenIcon.UiState,
|
||||||
|
val title: TextReference,
|
||||||
|
val ticker: TextReference? = null,
|
||||||
|
val fiatBalance: TextReference? = null,
|
||||||
|
) : State()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unreachable variant — dimmed leading texts and a warning badge instead of balances.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
* @param icon Token icon state.
|
||||||
|
* @param title Token name, dimmed.
|
||||||
|
* @param badge Localized badge label (e.g. `"Unreachable"`).
|
||||||
|
* @param quote Fiat quote, dimmed. `null` hides it.
|
||||||
|
* @param priceChange Price change indicator, dimmed. `null` hides it.
|
||||||
|
* @param onClick Row click handler. `null` with no [onLongClick] makes the row
|
||||||
|
* non-interactive.
|
||||||
|
* @param onLongClick Row long-press handler. `null` disables long-press.
|
||||||
|
*/
|
||||||
|
data class Unreachable(
|
||||||
|
override val id: String,
|
||||||
|
val icon: TangemTokenIcon.UiState,
|
||||||
|
val title: TextReference,
|
||||||
|
val badge: TextReference,
|
||||||
|
val quote: TextReference? = null,
|
||||||
|
val priceChange: TangemPriceChange.State? = null,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
val onLongClick: (() -> Unit)? = null,
|
||||||
|
) : State()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* No-address variant — a tertiary message instead of balances.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
* @param icon Token icon state.
|
||||||
|
* @param title Token name.
|
||||||
|
* @param message Localized end message (e.g. `"No address"`).
|
||||||
|
* @param quote Fiat quote. `null` hides it.
|
||||||
|
* @param priceChange Price change indicator. `null` hides it.
|
||||||
|
* @param onClick Row click handler. `null` with no [onLongClick] makes the row
|
||||||
|
* non-interactive.
|
||||||
|
* @param onLongClick Row long-press handler. `null` disables long-press.
|
||||||
|
*/
|
||||||
|
data class NoAddress(
|
||||||
|
override val id: String,
|
||||||
|
val icon: TangemTokenIcon.UiState,
|
||||||
|
val title: TextReference,
|
||||||
|
val message: TextReference,
|
||||||
|
val quote: TextReference? = null,
|
||||||
|
val priceChange: TangemPriceChange.State? = null,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
val onLongClick: (() -> Unit)? = null,
|
||||||
|
) : State()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading variant — icon and text-line shimmers.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
*/
|
||||||
|
data class Shimmer(
|
||||||
|
override val id: String,
|
||||||
|
) : State()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row** — state-driven overload: renders the variant described by
|
||||||
|
* [TangemTokenRow.State].
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5723-2473)
|
||||||
|
*
|
||||||
|
* @param state Row state model. See [TangemTokenRow.State].
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param isBalanceHidden When `true`, balances are masked with stars. Kept outside [state] because
|
||||||
|
* it is an app-wide setting, like in the legacy DS2 row.
|
||||||
|
* @param dragHandleModifier Modifier applied to the drag-handle icon of the
|
||||||
|
* [TangemTokenRow.State.Organize] variant (e.g. a reorderable drag-handle modifier); ignored by
|
||||||
|
* other variants.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRow(
|
||||||
|
state: TangemTokenRow.State,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isBalanceHidden: Boolean = false,
|
||||||
|
dragHandleModifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is TangemTokenRow.State.Content -> TangemTokenRow(
|
||||||
|
icon = state.icon,
|
||||||
|
title = state.title,
|
||||||
|
modifier = modifier,
|
||||||
|
badge = state.badge,
|
||||||
|
hasPending = state.hasPending,
|
||||||
|
quote = state.quote,
|
||||||
|
priceChange = state.priceChange,
|
||||||
|
fiatBalance = state.fiatBalance,
|
||||||
|
cryptoBalance = state.cryptoBalance,
|
||||||
|
showContractWarning = state.shouldShowContractWarning,
|
||||||
|
showUpdateWarning = state.shouldShowUpdateWarning,
|
||||||
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
isQuoteFlickering = state.isQuoteFlickering,
|
||||||
|
isBalanceFlickering = state.isBalanceFlickering,
|
||||||
|
messageBubble = state.messageBubble?.let { bubble -> { TokenRowMessageBubble(bubble = bubble) } },
|
||||||
|
onClick = state.onClick,
|
||||||
|
onLongClick = state.onLongClick,
|
||||||
|
)
|
||||||
|
is TangemTokenRow.State.Organize -> TangemTokenRow.Organize(
|
||||||
|
icon = state.icon,
|
||||||
|
title = state.title,
|
||||||
|
modifier = modifier,
|
||||||
|
ticker = state.ticker,
|
||||||
|
fiatBalance = state.fiatBalance,
|
||||||
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
dragHandleModifier = dragHandleModifier,
|
||||||
|
)
|
||||||
|
is TangemTokenRow.State.Unreachable -> TangemTokenRow.Unreachable(
|
||||||
|
icon = state.icon,
|
||||||
|
title = state.title,
|
||||||
|
badge = state.badge,
|
||||||
|
modifier = modifier,
|
||||||
|
quote = state.quote,
|
||||||
|
priceChange = state.priceChange,
|
||||||
|
onClick = state.onClick,
|
||||||
|
onLongClick = state.onLongClick,
|
||||||
|
)
|
||||||
|
is TangemTokenRow.State.NoAddress -> TangemTokenRow.NoAddress(
|
||||||
|
icon = state.icon,
|
||||||
|
title = state.title,
|
||||||
|
message = state.message,
|
||||||
|
modifier = modifier,
|
||||||
|
quote = state.quote,
|
||||||
|
priceChange = state.priceChange,
|
||||||
|
onClick = state.onClick,
|
||||||
|
onLongClick = state.onLongClick,
|
||||||
|
)
|
||||||
|
is TangemTokenRow.State.Shimmer -> TangemTokenRow.Shimmer(modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Renders the [TangemTokenRow.MessageBubble] model as a [TangemMessageBubble]. */
|
||||||
|
@Composable
|
||||||
|
private fun TokenRowMessageBubble(bubble: TangemTokenRow.MessageBubble) {
|
||||||
|
TangemMessageBubble(
|
||||||
|
text = bubble.text,
|
||||||
|
variant = bubble.variant,
|
||||||
|
showTip = bubble.shouldShowTip,
|
||||||
|
icon = bubble.icon,
|
||||||
|
onClick = bubble.onClick,
|
||||||
|
onClose = bubble.onClose,
|
||||||
|
closeContentDescription = bubble.closeContentDescription,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row / Organize** — reorder mode: title with a ticker, fiat balance
|
||||||
|
* underneath and a drag handle at the end.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5723-2473)
|
||||||
|
*
|
||||||
|
* @param icon Token icon state, rendered at 40dp.
|
||||||
|
* @param title Token name. Single line, ellipsized.
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param ticker Currency ticker after the title (e.g. `"BTC"`), baseline-aligned. `null` hides it.
|
||||||
|
* @param fiatBalance Balance line under the title (e.g. `"$583.00"`). `null` hides it.
|
||||||
|
* @param isBalanceHidden When `true`, [fiatBalance] is masked with stars.
|
||||||
|
* @param dragHandleModifier Modifier applied to the drag-handle icon — pass the reorderable
|
||||||
|
* drag-handle modifier here to make the row draggable.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRow.Organize(
|
||||||
|
icon: TangemTokenIcon.UiState,
|
||||||
|
title: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
ticker: TextReference? = null,
|
||||||
|
fiatBalance: TextReference? = null,
|
||||||
|
isBalanceHidden: Boolean = false,
|
||||||
|
dragHandleModifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TokenRowContainer(modifier = modifier) {
|
||||||
|
TokenRowHeadIcon(icon = icon)
|
||||||
|
TokenRowTitleContent(
|
||||||
|
title = title,
|
||||||
|
ticker = ticker,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_TOP)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
if (fiatBalance != null) {
|
||||||
|
TokenRowCaptionText(
|
||||||
|
text = fiatBalance,
|
||||||
|
isBalanceHidden = isBalanceHidden,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.TAIL)
|
||||||
|
.padding(start = 8.dp)
|
||||||
|
.size(24.dp)
|
||||||
|
.then(dragHandleModifier),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.ic_sign_equal_24,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = TangemTheme.colors3.icon.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row / Unreachable** — network-error state: leading texts are
|
||||||
|
* dimmed to the disabled opacity and a warning badge replaces the balances.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5723-2473)
|
||||||
|
*
|
||||||
|
* @param icon Token icon state, rendered at 40dp.
|
||||||
|
* @param title Token name, dimmed. Single line, ellipsized.
|
||||||
|
* @param badge Localized badge label (e.g. `"Unreachable"`), shown as a warning-tinted badge.
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param quote Fiat quote, dimmed. `null` hides it.
|
||||||
|
* @param priceChange Price change indicator, dimmed. `null` hides it.
|
||||||
|
* @param onClick Row click handler. `null` with no [onLongClick] makes the row non-interactive.
|
||||||
|
* @param onLongClick Row long-press handler. `null` disables long-press.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRow.Unreachable(
|
||||||
|
icon: TangemTokenIcon.UiState,
|
||||||
|
title: TextReference,
|
||||||
|
badge: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
quote: TextReference? = null,
|
||||||
|
priceChange: TangemPriceChange.State? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
TokenRowContainer(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
) {
|
||||||
|
TokenRowHeadIcon(icon = icon)
|
||||||
|
TokenRowTitleContent(
|
||||||
|
title = title,
|
||||||
|
isDimmed = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_TOP)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
if (quote != null || priceChange != null) {
|
||||||
|
TokenRowSubtitleContent(
|
||||||
|
quote = quote,
|
||||||
|
priceChange = priceChange,
|
||||||
|
isDimmed = true,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// A single end child has no bottom counterpart, so the container centers it vertically.
|
||||||
|
TangemBadge(
|
||||||
|
text = badge,
|
||||||
|
variant = TangemBadge.Variant.Tinted,
|
||||||
|
status = TangemBadge.Status.Warning,
|
||||||
|
size = TangemBadge.Size.X6,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.END_TOP),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row / No Address** — missing-derivation state: a tertiary message
|
||||||
|
* replaces the balances.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5723-2473)
|
||||||
|
*
|
||||||
|
* @param icon Token icon state, rendered at 40dp.
|
||||||
|
* @param title Token name. Single line, ellipsized.
|
||||||
|
* @param message Localized end message (e.g. `"No address"`), body typography in tertiary color.
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param quote Fiat quote. `null` hides it.
|
||||||
|
* @param priceChange Price change indicator. `null` hides it.
|
||||||
|
* @param onClick Row click handler. `null` with no [onLongClick] makes the row non-interactive.
|
||||||
|
* @param onLongClick Row long-press handler. `null` disables long-press.
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRow.NoAddress(
|
||||||
|
icon: TangemTokenIcon.UiState,
|
||||||
|
title: TextReference,
|
||||||
|
message: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
quote: TextReference? = null,
|
||||||
|
priceChange: TangemPriceChange.State? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
TokenRowContainer(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
) {
|
||||||
|
TokenRowHeadIcon(icon = icon)
|
||||||
|
TokenRowTitleContent(
|
||||||
|
title = title,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_TOP)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
if (quote != null || priceChange != null) {
|
||||||
|
TokenRowSubtitleContent(
|
||||||
|
quote = quote,
|
||||||
|
priceChange = priceChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM)
|
||||||
|
.padding(end = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// A single end child has no bottom counterpart, so the container centers it vertically.
|
||||||
|
Text(
|
||||||
|
text = message.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
color = TangemTheme.colors3.text.tertiary,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.END_TOP),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row / Shimmer** — loading placeholder: circular icon shimmer plus
|
||||||
|
* text-line bars on both sides.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5723-2473)
|
||||||
|
*
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRow.Shimmer(modifier: Modifier = Modifier) {
|
||||||
|
TokenRowContainer(modifier = modifier) {
|
||||||
|
TokenRowHeadIcon(icon = TangemTokenIcon.UiState.Shimmer)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
width = 72.dp,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.START_TOP),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
width = 44.dp,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
width = 72.dp,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.END_TOP),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
width = 44.dp,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.END_BOTTOM),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Previews
|
||||||
|
|
||||||
|
@Preview(name = "Light", showBackground = true, widthDp = 360)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark",
|
||||||
|
showBackground = true,
|
||||||
|
widthDp = 360,
|
||||||
|
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
private fun TangemTokenRowPreview() {
|
||||||
|
PreviewContainer { icon ->
|
||||||
|
TangemTokenRow(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
badge = TangemTokenRow.Badge(
|
||||||
|
text = stringReference("APY 5.47%"),
|
||||||
|
variant = TangemBadge.Variant.Solid,
|
||||||
|
status = TangemBadge.Status.Info,
|
||||||
|
),
|
||||||
|
quote = stringReference("$1.00"),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference("2.08%"),
|
||||||
|
direction = TangemPriceChange.Direction.Up,
|
||||||
|
),
|
||||||
|
fiatBalance = stringReference("$583.00"),
|
||||||
|
cryptoBalance = stringReference("0,000015 BTC"),
|
||||||
|
showContractWarning = true,
|
||||||
|
showUpdateWarning = true,
|
||||||
|
messageBubble = {
|
||||||
|
TangemMessageBubble(
|
||||||
|
text = stringReference("Enable 5.47% APY on your balance"),
|
||||||
|
variant = TangemMessageBubble.Variant.Success,
|
||||||
|
icon = Icons.ic_chart_bar_vertical_16,
|
||||||
|
onClick = {},
|
||||||
|
onClose = {},
|
||||||
|
closeContentDescription = "Dismiss",
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
TangemTokenRow(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
quote = stringReference("$1.00"),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference("0.4%"),
|
||||||
|
direction = TangemPriceChange.Direction.Down,
|
||||||
|
),
|
||||||
|
fiatBalance = stringReference("$583.00"),
|
||||||
|
cryptoBalance = stringReference("0,000015 BTC"),
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
TangemTokenRow.Organize(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
ticker = stringReference("BTC"),
|
||||||
|
fiatBalance = stringReference("$583.00"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "States Light", showBackground = true, widthDp = 360)
|
||||||
|
@Preview(
|
||||||
|
name = "States Dark",
|
||||||
|
showBackground = true,
|
||||||
|
widthDp = 360,
|
||||||
|
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
private fun TangemTokenRowStatesPreview() {
|
||||||
|
PreviewContainer { icon ->
|
||||||
|
TangemTokenRow.Unreachable(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
badge = stringReference("Unreachable"),
|
||||||
|
quote = stringReference("$1.00"),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference("2.08%"),
|
||||||
|
direction = TangemPriceChange.Direction.Up,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
TangemTokenRow.NoAddress(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
message = stringReference("No address"),
|
||||||
|
quote = stringReference("$1.00"),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference("2.08%"),
|
||||||
|
direction = TangemPriceChange.Direction.Neutral,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
TangemTokenRow.Shimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreviewContainer(content: @Composable (icon: TangemTokenIcon.UiState) -> Unit) {
|
||||||
|
val icon = TangemTokenIcon.UiState.Token(TangemTokenIcon.State(url = null))
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
|
||||||
|
content(icon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
@ -0,0 +1,538 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.tokenrow
|
||||||
|
|
||||||
|
import androidx.compose.animation.Animatable
|
||||||
|
import androidx.compose.animation.core.snap
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.LocalIndication
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.ripple.RippleAlpha
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalRippleConfiguration
|
||||||
|
import androidx.compose.material3.RippleConfiguration
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.ReadOnlyComposable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.Measurable
|
||||||
|
import androidx.compose.ui.layout.Placeable
|
||||||
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||||
|
import com.tangem.core.ui.ds2.badge.TangemBadge
|
||||||
|
import com.tangem.core.ui.ds2.loader.TangemLoader
|
||||||
|
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
|
||||||
|
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||||
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.conditionalCompose
|
||||||
|
import com.tangem.core.ui.extensions.orMaskWithStars
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_cloud_exclamation_20
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_warning_20
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/** Content dimming applied to the leading texts of unavailable rows (Figma `opacity/disabled`). */
|
||||||
|
internal const val TOKEN_ROW_DISABLED_ALPHA = 0.4f
|
||||||
|
|
||||||
|
// region Container layout
|
||||||
|
|
||||||
|
/** Slot ids of the [TokenRowContainer] custom layout. */
|
||||||
|
internal enum class TokenRowLayoutId {
|
||||||
|
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_BOTTOM
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Token-row container
|
||||||
|
*
|
||||||
|
* Policy:
|
||||||
|
* - HEAD and TAIL are measured first and take their intrinsic width.
|
||||||
|
* - END slots take the free space left after guaranteeing the START side its minimum width
|
||||||
|
* (30% of the row for the top line, 32% for the bottom line).
|
||||||
|
* - START slots fill the remaining width, never shrinking below that minimum.
|
||||||
|
* - A line with no counterpart on the other axis is vertically centered in the main area.
|
||||||
|
* - EXTRA_BOTTOM is placed full-width below the main content with an 8dp gap.
|
||||||
|
*/
|
||||||
|
@Suppress("LongMethod")
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowContainer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
onLongClick: (() -> Unit)? = null,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
|
val isInteractive = onClick != null || onLongClick != null
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val verticalPadding = with(density) { 4.dp.roundToPx() }
|
||||||
|
val extraContentPadding = with(density) { 8.dp.roundToPx() }
|
||||||
|
val contentPadding = with(density) { 12.dp.roundToPx() }
|
||||||
|
|
||||||
|
val rowModifier = modifier
|
||||||
|
.conditionalCompose(isFocused) {
|
||||||
|
border(
|
||||||
|
width = 2.dp,
|
||||||
|
color = TangemTheme.colors3.interaction.focusRing.brand,
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.conditionalCompose(isInteractive) {
|
||||||
|
combinedClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = LocalIndication.current,
|
||||||
|
role = Role.Button,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onClick = onClick ?: {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
WithTokenRowRipple(enabled = isInteractive) {
|
||||||
|
Layout(
|
||||||
|
content = content,
|
||||||
|
modifier = rowModifier,
|
||||||
|
) { measurables, constraints ->
|
||||||
|
val layoutWidth = max(0, constraints.maxWidth - contentPadding * 2)
|
||||||
|
|
||||||
|
val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
|
||||||
|
val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
|
||||||
|
|
||||||
|
val headPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.HEAD,
|
||||||
|
constraints = constraints.copy(minWidth = 0),
|
||||||
|
)
|
||||||
|
val tailPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.TAIL,
|
||||||
|
constraints = constraints.copy(minWidth = 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
val availableWidthForBody = layoutWidth - headPlaceable.widthOrZero() - tailPlaceable.widthOrZero()
|
||||||
|
|
||||||
|
// End slots take the whole free space but always leave the start side its minimum width.
|
||||||
|
val endTopPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.END_TOP,
|
||||||
|
constraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
maxWidth = max(0, availableWidthForBody - startTopMinWidth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val endBottomPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.END_BOTTOM,
|
||||||
|
constraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
maxWidth = max(0, availableWidthForBody - startBottomMinWidth),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Start slots fill the remaining width but never less than their minimum.
|
||||||
|
val startTopPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.START_TOP,
|
||||||
|
constraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
maxWidth = max(
|
||||||
|
a = startTopMinWidth,
|
||||||
|
b = availableWidthForBody - endTopPlaceable.widthOrZero(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
val startBottomPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.START_BOTTOM,
|
||||||
|
constraints = constraints.copy(
|
||||||
|
minWidth = 0,
|
||||||
|
maxWidth = max(
|
||||||
|
a = startBottomMinWidth,
|
||||||
|
b = availableWidthForBody - endBottomPlaceable.widthOrZero(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val extraBottomPlaceable = measurables.measure(
|
||||||
|
layoutId = TokenRowLayoutId.EXTRA_BOTTOM,
|
||||||
|
constraints = constraints,
|
||||||
|
)
|
||||||
|
|
||||||
|
val mainLayoutHeight = maxOf(
|
||||||
|
headPlaceable.heightOrZero(),
|
||||||
|
tailPlaceable.heightOrZero(),
|
||||||
|
startTopPlaceable.heightOrZero() + startBottomPlaceable.heightOrZero() + verticalPadding,
|
||||||
|
endTopPlaceable.heightOrZero() + endBottomPlaceable.heightOrZero() + verticalPadding,
|
||||||
|
)
|
||||||
|
|
||||||
|
val mainContentBottomPadding = if (extraBottomPlaceable != null) {
|
||||||
|
extraBottomPlaceable.heightOrZero() + contentPadding
|
||||||
|
} else {
|
||||||
|
contentPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
val layoutHeight = mainLayoutHeight + contentPadding + mainContentBottomPadding
|
||||||
|
|
||||||
|
layout(width = constraints.maxWidth, height = layoutHeight) {
|
||||||
|
headPlaceable?.placeRelative(
|
||||||
|
x = contentPadding,
|
||||||
|
y = contentPadding + (mainLayoutHeight - headPlaceable.height).div(other = 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
startTopPlaceable?.placeRelative(
|
||||||
|
x = contentPadding + headPlaceable.widthOrZero(),
|
||||||
|
y = contentPadding + if (startBottomPlaceable == null) {
|
||||||
|
(mainLayoutHeight - startTopPlaceable.height).div(2)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
startBottomPlaceable?.placeRelative(
|
||||||
|
x = contentPadding + headPlaceable.widthOrZero(),
|
||||||
|
y = contentPadding + if (startTopPlaceable == null) {
|
||||||
|
(mainLayoutHeight - startBottomPlaceable.height).div(2)
|
||||||
|
} else {
|
||||||
|
startTopPlaceable.heightOrZero() + verticalPadding
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
endTopPlaceable?.placeRelative(
|
||||||
|
x = layoutWidth - endTopPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentPadding,
|
||||||
|
y = contentPadding + if (endBottomPlaceable == null) {
|
||||||
|
(mainLayoutHeight - endTopPlaceable.height).div(2)
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
endBottomPlaceable?.placeRelative(
|
||||||
|
x = layoutWidth - endBottomPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentPadding,
|
||||||
|
y = contentPadding + if (endTopPlaceable == null) {
|
||||||
|
(mainLayoutHeight - endBottomPlaceable.height).div(2)
|
||||||
|
} else {
|
||||||
|
endTopPlaceable.heightOrZero() + verticalPadding
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
tailPlaceable?.placeRelative(
|
||||||
|
x = layoutWidth - tailPlaceable.width + contentPadding,
|
||||||
|
y = contentPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
|
||||||
|
)
|
||||||
|
|
||||||
|
extraBottomPlaceable?.placeRelative(
|
||||||
|
x = 0,
|
||||||
|
y = contentPadding + mainLayoutHeight + extraContentPadding,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
|
||||||
|
private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32
|
||||||
|
|
||||||
|
private fun List<Measurable>.measure(layoutId: TokenRowLayoutId, constraints: Constraints): Placeable? {
|
||||||
|
return firstOrNull { it.layoutId == layoutId }?.measure(constraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Placeable?.widthOrZero(): Int = this?.width ?: 0
|
||||||
|
|
||||||
|
private fun Placeable?.heightOrZero(): Int = this?.height ?: 0
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun WithTokenRowRipple(enabled: Boolean, content: @Composable () -> Unit) {
|
||||||
|
if (enabled) {
|
||||||
|
CompositionLocalProvider(LocalRippleConfiguration provides tokenRowRipple(), content = content)
|
||||||
|
} else {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
private fun tokenRowRipple(): RippleConfiguration = RippleConfiguration(
|
||||||
|
color = TangemTheme.colors3.interaction.press.default,
|
||||||
|
rippleAlpha = RippleAlpha(
|
||||||
|
draggedAlpha = 0f,
|
||||||
|
focusedAlpha = 0f,
|
||||||
|
hoveredAlpha = 0.05f,
|
||||||
|
pressedAlpha = 0.1f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
// region Slot contents
|
||||||
|
|
||||||
|
/** Head slot: 40dp token icon with the 12dp gap to the content, like the DS2 row. */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowHeadIcon(icon: TangemTokenIcon.UiState) {
|
||||||
|
TangemTokenIcon(
|
||||||
|
state = icon,
|
||||||
|
size = TangemTokenIcon.Size.X40,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.HEAD)
|
||||||
|
.padding(end = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Market subtitle line: rank badge (e.g. `2`) and capitalization (e.g. `1.196T`). */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowMarketSubtitleContent(
|
||||||
|
position: TextReference?,
|
||||||
|
capitalization: TextReference?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (position != null) {
|
||||||
|
TangemBadge(
|
||||||
|
text = position,
|
||||||
|
variant = TangemBadge.Variant.Tinted,
|
||||||
|
status = TangemBadge.Status.Neutral,
|
||||||
|
size = TangemBadge.Size.X4,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (capitalization != null) {
|
||||||
|
Text(
|
||||||
|
text = capitalization.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
color = TangemTheme.colors3.text.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Market price line. When [updateDirection] is set, the text flashes in the direction color and
|
||||||
|
* fades back to primary every time [price] changes — ports the live-update blink of the legacy
|
||||||
|
* `TokenPriceText`. The first composition never blinks.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowMarketPriceContent(
|
||||||
|
price: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
updateDirection: TangemPriceChange.Direction? = null,
|
||||||
|
) {
|
||||||
|
val generalColor = TangemTheme.colors3.text.primary
|
||||||
|
val growColor = TangemTheme.colors3.text.accent.blue
|
||||||
|
val fallColor = TangemTheme.colors3.text.status.error
|
||||||
|
|
||||||
|
val color = remember(generalColor) { Animatable(generalColor) }
|
||||||
|
var isFirstEmissionSkipped by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
LaunchedEffect(price) {
|
||||||
|
if (!isFirstEmissionSkipped) {
|
||||||
|
isFirstEmissionSkipped = true
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val blinkColor = when (updateDirection) {
|
||||||
|
TangemPriceChange.Direction.Up -> growColor
|
||||||
|
TangemPriceChange.Direction.Down -> fallColor
|
||||||
|
TangemPriceChange.Direction.Neutral, null -> return@LaunchedEffect
|
||||||
|
}
|
||||||
|
color.animateTo(blinkColor, snap())
|
||||||
|
color.animateTo(generalColor, tween(durationMillis = PRICE_BLINK_FADE_DURATION_MILLIS))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = price.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
color = color.value,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val PRICE_BLINK_FADE_DURATION_MILLIS = 500
|
||||||
|
|
||||||
|
/** Title line: token name, optional pending-transaction loader, ticker (baseline-aligned), badge. */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowTitleContent(
|
||||||
|
title: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
ticker: TextReference? = null,
|
||||||
|
badge: TangemTokenRow.Badge? = null,
|
||||||
|
hasPending: Boolean = false,
|
||||||
|
isDimmed: Boolean = false,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.alpha(if (isDimmed) TOKEN_ROW_DISABLED_ALPHA else 1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
color = TangemTheme.colors3.text.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
.alignByBaseline(),
|
||||||
|
)
|
||||||
|
if (hasPending) {
|
||||||
|
TangemLoader(
|
||||||
|
size = TangemLoaderSize.X16,
|
||||||
|
color = TangemTheme.colors3.icon.tertiary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (ticker != null) {
|
||||||
|
Text(
|
||||||
|
text = ticker.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
color = TangemTheme.colors3.text.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.alignByBaseline(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (badge != null) {
|
||||||
|
TangemBadge(
|
||||||
|
text = badge.text,
|
||||||
|
variant = badge.variant,
|
||||||
|
status = badge.status,
|
||||||
|
size = TangemBadge.Size.X4,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Subtitle line: quote (e.g. `$1.00`) and the [TangemPriceChange] indicator. */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowSubtitleContent(
|
||||||
|
quote: TextReference?,
|
||||||
|
priceChange: TangemPriceChange.State?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isDimmed: Boolean = false,
|
||||||
|
isFlickering: Boolean = false,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.alpha(if (isDimmed) TOKEN_ROW_DISABLED_ALPHA else 1f),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (quote != null) {
|
||||||
|
Text(
|
||||||
|
text = quote.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.caption.medium.applyBladeBrush(
|
||||||
|
isEnabled = isFlickering,
|
||||||
|
textColor = TangemTheme.colors3.text.secondary,
|
||||||
|
),
|
||||||
|
color = TangemTheme.colors3.text.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (priceChange != null) {
|
||||||
|
TangemPriceChange(state = priceChange, isFlickering = isFlickering)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fiat balance with optional contract-error and update-error icons in front of it. */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowBalanceContent(
|
||||||
|
fiatBalance: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
showContractWarning: Boolean = false,
|
||||||
|
showUpdateWarning: Boolean = false,
|
||||||
|
isFlickering: Boolean = false,
|
||||||
|
isBalanceHidden: Boolean = false,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
if (showContractWarning) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.ic_warning_20,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = TangemTheme.colors3.icon.status.warning,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (showUpdateWarning) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.ic_cloud_exclamation_20,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = TangemTheme.colors3.icon.primary,
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = fiatBalance.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||||
|
style = TangemTheme.typography3.body.medium.applyBladeBrush(
|
||||||
|
isEnabled = isFlickering,
|
||||||
|
textColor = TangemTheme.colors3.text.primary,
|
||||||
|
),
|
||||||
|
color = TangemTheme.colors3.text.primary,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Plain caption line used for secondary amounts (crypto balance, Organize fiat balance). */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowCaptionText(
|
||||||
|
text: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isFlickering: Boolean = false,
|
||||||
|
isBalanceHidden: Boolean = false,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text.orMaskWithStars(isBalanceHidden).resolveReference(),
|
||||||
|
style = TangemTheme.typography3.caption.medium.applyBladeBrush(
|
||||||
|
isEnabled = isFlickering,
|
||||||
|
textColor = TangemTheme.colors3.text.secondary,
|
||||||
|
),
|
||||||
|
color = TangemTheme.colors3.text.secondary,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fixed-width shimmer bar sized after a typography line, like the [TangemShimmer] text overload. */
|
||||||
|
@Composable
|
||||||
|
internal fun TokenRowShimmerLine(style: TextStyle, width: Dp, modifier: Modifier = Modifier) {
|
||||||
|
val lineHeight = with(LocalDensity.current) { style.lineHeight.toDp() }
|
||||||
|
TangemShimmer(
|
||||||
|
radius = 16.dp,
|
||||||
|
modifier = modifier
|
||||||
|
.width(width)
|
||||||
|
.height(lineHeight)
|
||||||
|
.padding(vertical = 2.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
@ -0,0 +1,315 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.tokenrow
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.layoutId
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.ds2.shimmers.TangemShimmer
|
||||||
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row Market** — a markets-list item: token icon, title with a
|
||||||
|
* ticker, rank badge + capitalization, price with a [TangemPriceChange] indicator and a trailing
|
||||||
|
* mini-graph slot.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5724-3510&m=dev)
|
||||||
|
*
|
||||||
|
* @param icon Token icon state, rendered at 40dp. See [TangemTokenIcon.UiState].
|
||||||
|
* @param title Token name. Single line, ellipsized.
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param ticker Currency ticker after the title (e.g. `"BTC"`), baseline-aligned. `null` hides it.
|
||||||
|
* @param position Market-rank badge label (e.g. `"2"`). `null` hides the badge (Figma `Position`).
|
||||||
|
* @param capitalization Market capitalization text (e.g. `"1.196T"`). `null` hides it.
|
||||||
|
* @param price Current price at the end (e.g. `"$59,723.24"`). `null` hides the line.
|
||||||
|
* @param priceChange Price change indicator under the [price]. `null` hides it.
|
||||||
|
* @param priceUpdateDirection Direction of the latest live price update. When set, the [price]
|
||||||
|
* text flashes in the direction color and fades back each time [price] changes. `null` disables
|
||||||
|
* the flash.
|
||||||
|
* @param chart Trailing mini-graph slot, vertically centered. `null` hides it.
|
||||||
|
* @param onClick Row click handler. `null` makes the row non-interactive (no ripple/focus).
|
||||||
|
*/
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRowMarket(
|
||||||
|
icon: TangemTokenIcon.UiState,
|
||||||
|
title: TextReference,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
ticker: TextReference? = null,
|
||||||
|
position: TextReference? = null,
|
||||||
|
capitalization: TextReference? = null,
|
||||||
|
price: TextReference? = null,
|
||||||
|
priceChange: TangemPriceChange.State? = null,
|
||||||
|
priceUpdateDirection: TangemPriceChange.Direction? = null,
|
||||||
|
chart: (@Composable () -> Unit)? = null,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
TokenRowContainer(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
TangemTokenIcon(
|
||||||
|
state = icon,
|
||||||
|
size = TangemTokenIcon.Size.X40,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.HEAD),
|
||||||
|
)
|
||||||
|
TokenRowTitleContent(
|
||||||
|
title = title,
|
||||||
|
ticker = ticker,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_TOP)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
if (position != null || capitalization != null) {
|
||||||
|
TokenRowMarketSubtitleContent(
|
||||||
|
position = position,
|
||||||
|
capitalization = capitalization,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (price != null) {
|
||||||
|
TokenRowMarketPriceContent(
|
||||||
|
price = price,
|
||||||
|
updateDirection = priceUpdateDirection,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.END_TOP)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (priceChange != null) {
|
||||||
|
TangemPriceChange(
|
||||||
|
state = priceChange,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.END_BOTTOM)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (chart != null) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.TAIL)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
) {
|
||||||
|
chart()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row Market** — state-driven overload: renders the variant
|
||||||
|
* described by [TangemTokenRowMarket.State].
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5724-3510&m=dev)
|
||||||
|
*
|
||||||
|
* @param state Row state model. See [TangemTokenRowMarket.State].
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
* @param chart Trailing mini-graph slot; only used by [TangemTokenRowMarket.State.Content].
|
||||||
|
* Stays a slot (not a state field) because the chart component lives in `:common:ui-charts`.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRowMarket(
|
||||||
|
state: TangemTokenRowMarket.State,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
chart: (@Composable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
when (state) {
|
||||||
|
is TangemTokenRowMarket.State.Content -> TangemTokenRowMarket(
|
||||||
|
icon = state.icon,
|
||||||
|
title = state.title,
|
||||||
|
modifier = modifier,
|
||||||
|
ticker = state.ticker,
|
||||||
|
position = state.position,
|
||||||
|
capitalization = state.capitalization,
|
||||||
|
price = state.price,
|
||||||
|
priceChange = state.priceChange,
|
||||||
|
priceUpdateDirection = state.priceUpdateDirection,
|
||||||
|
chart = chart,
|
||||||
|
onClick = state.onClick,
|
||||||
|
)
|
||||||
|
is TangemTokenRowMarket.State.Shimmer -> TangemTokenRowMarket.Shimmer(modifier = modifier)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public API surface of [TangemTokenRowMarket]. */
|
||||||
|
object TangemTokenRowMarket {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* State model of [TangemTokenRowMarket]. Render it with the `TangemTokenRowMarket(state = …)`
|
||||||
|
* overload.
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
sealed class State {
|
||||||
|
|
||||||
|
/** Unique id, e.g. for `LazyColumn` item keys. */
|
||||||
|
abstract val id: String
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loaded market row.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
* @param icon Token icon state.
|
||||||
|
* @param title Token name.
|
||||||
|
* @param ticker Currency ticker after the title. `null` hides it.
|
||||||
|
* @param position Market-rank badge label. `null` hides the badge.
|
||||||
|
* @param capitalization Market capitalization text. `null` hides it.
|
||||||
|
* @param price Current price at the end. `null` hides the line.
|
||||||
|
* @param priceChange Price change indicator under the price. `null` hides it.
|
||||||
|
* @param priceUpdateDirection Direction of the latest live price update — the price text
|
||||||
|
* flashes in this color each time [price] changes. `null` disables the flash.
|
||||||
|
* @param onClick Row click handler. `null` makes the row non-interactive.
|
||||||
|
*/
|
||||||
|
data class Content(
|
||||||
|
override val id: String,
|
||||||
|
val icon: TangemTokenIcon.UiState,
|
||||||
|
val title: TextReference,
|
||||||
|
val ticker: TextReference? = null,
|
||||||
|
val position: TextReference? = null,
|
||||||
|
val capitalization: TextReference? = null,
|
||||||
|
val price: TextReference? = null,
|
||||||
|
val priceChange: TangemPriceChange.State? = null,
|
||||||
|
val priceUpdateDirection: TangemPriceChange.Direction? = null,
|
||||||
|
val onClick: (() -> Unit)? = null,
|
||||||
|
) : State()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loading variant — icon and text-line shimmers.
|
||||||
|
*
|
||||||
|
* @param id Unique id.
|
||||||
|
*/
|
||||||
|
data class Shimmer(
|
||||||
|
override val id: String,
|
||||||
|
) : State()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Token Row Market / Shimmer** — loading placeholder: circular icon
|
||||||
|
* shimmer, text-line bars on both sides and a graph-sized bar at the end.
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5724-3510&m=dev)
|
||||||
|
*
|
||||||
|
* @param modifier Modifier applied to the row container.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemTokenRowMarket.Shimmer(modifier: Modifier = Modifier) {
|
||||||
|
TokenRowContainer(modifier = modifier) {
|
||||||
|
TangemTokenIcon(
|
||||||
|
state = TangemTokenIcon.UiState.Shimmer,
|
||||||
|
size = TangemTokenIcon.Size.X40,
|
||||||
|
modifier = Modifier.layoutId(layoutId = TokenRowLayoutId.HEAD),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
width = 72.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_TOP)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
width = 44.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.START_BOTTOM)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.body.medium,
|
||||||
|
width = 72.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.END_TOP)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
TokenRowShimmerLine(
|
||||||
|
style = TangemTheme.typography3.caption.medium,
|
||||||
|
width = 44.dp,
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.END_BOTTOM)
|
||||||
|
.padding(start = 12.dp),
|
||||||
|
)
|
||||||
|
// Graph placeholder — a small bar centered in the 24x32 graph slot.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.layoutId(layoutId = TokenRowLayoutId.TAIL)
|
||||||
|
.padding(start = 12.dp)
|
||||||
|
.size(width = 24.dp, height = 32.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
TangemShimmer(
|
||||||
|
radius = 4.dp,
|
||||||
|
modifier = Modifier.size(width = 24.dp, height = 12.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// region Previews
|
||||||
|
|
||||||
|
@Preview(name = "Light", showBackground = true, widthDp = 360)
|
||||||
|
@Preview(
|
||||||
|
name = "Dark",
|
||||||
|
showBackground = true,
|
||||||
|
widthDp = 360,
|
||||||
|
uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES,
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
private fun TangemTokenRowMarketPreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(modifier = Modifier.background(TangemTheme.colors3.bg.primary)) {
|
||||||
|
TangemTokenRowMarket(
|
||||||
|
icon = TangemTokenIcon.UiState.Token(TangemTokenIcon.State(url = null)),
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
ticker = stringReference("BTC"),
|
||||||
|
position = stringReference("2"),
|
||||||
|
capitalization = stringReference("1.196T"),
|
||||||
|
price = stringReference("$59,723.24"),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference("2.08%"),
|
||||||
|
direction = TangemPriceChange.Direction.Up,
|
||||||
|
),
|
||||||
|
chart = { PreviewChartPlaceholder() },
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
TangemTokenRowMarket(
|
||||||
|
icon = TangemTokenIcon.UiState.Token(TangemTokenIcon.State(url = null)),
|
||||||
|
title = stringReference("Very Long Token Name Coin"),
|
||||||
|
ticker = stringReference("VLTNC"),
|
||||||
|
capitalization = stringReference("796.9B"),
|
||||||
|
price = stringReference("$2,591.65"),
|
||||||
|
priceChange = TangemPriceChange.State(
|
||||||
|
value = stringReference("0.42%"),
|
||||||
|
direction = TangemPriceChange.Direction.Down,
|
||||||
|
),
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
TangemTokenRowMarket.Shimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PreviewChartPlaceholder() {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 24.dp, height = 32.dp)
|
||||||
|
.background(TangemTheme.colors3.bg.tertiary),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
@ -0,0 +1,166 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.core.ui.ds2.util
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.NonRestartableComposable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.tangem.core.ui.components.text.applyBladeBrush
|
||||||
|
import com.tangem.core.ui.extensions.TextReference
|
||||||
|
import com.tangem.core.ui.extensions.resolveReference
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||||
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_dot_12_filled
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_triangle_down_12
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_triangle_up_12
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Util / Price Change** — a compact directional indicator: an up/down
|
||||||
|
* triangle (or a dot for [TangemPriceChange.Direction.Neutral]) and a percent label colored by
|
||||||
|
* [direction]. A "util" building block, not a complete component — embed it next to quotes and
|
||||||
|
* balances (e.g. inside the token row).
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5724-3922&m=dev)
|
||||||
|
*
|
||||||
|
* @param value Formatted percent text (e.g. `"2.08%"`).
|
||||||
|
* @param direction Change direction driving the arrow and its color. See
|
||||||
|
* [TangemPriceChange.Direction].
|
||||||
|
* @param modifier Modifier applied to the indicator root.
|
||||||
|
* @param isFlickering Runs the blade animation over the label while the value is being refreshed.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun TangemPriceChange(
|
||||||
|
value: TextReference,
|
||||||
|
direction: TangemPriceChange.Direction,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isFlickering: Boolean = false,
|
||||||
|
) {
|
||||||
|
val textColor = when (direction) {
|
||||||
|
TangemPriceChange.Direction.Up -> TangemTheme.colors3.text.accent.blue
|
||||||
|
TangemPriceChange.Direction.Down -> TangemTheme.colors3.text.accent.red
|
||||||
|
TangemPriceChange.Direction.Neutral -> TangemTheme.colors3.text.tertiary
|
||||||
|
}
|
||||||
|
val iconTint = when (direction) {
|
||||||
|
TangemPriceChange.Direction.Up -> TangemTheme.colors3.icon.accent.blue
|
||||||
|
TangemPriceChange.Direction.Down -> TangemTheme.colors3.icon.accent.red
|
||||||
|
TangemPriceChange.Direction.Neutral -> TangemTheme.colors3.icon.tertiary
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(2.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
val arrow = when (direction) {
|
||||||
|
TangemPriceChange.Direction.Up -> Icons.ic_triangle_up_12
|
||||||
|
TangemPriceChange.Direction.Down -> Icons.ic_triangle_down_12
|
||||||
|
TangemPriceChange.Direction.Neutral -> Icons.ic_dot_12_filled
|
||||||
|
}
|
||||||
|
Icon(
|
||||||
|
imageVector = arrow,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = iconTint,
|
||||||
|
modifier = Modifier.size(12.dp),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = value.resolveReference(),
|
||||||
|
style = TangemTheme.typography3.caption.medium.applyBladeBrush(
|
||||||
|
isEnabled = isFlickering,
|
||||||
|
textColor = textColor,
|
||||||
|
),
|
||||||
|
color = textColor,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Design-system v2 (DS3) **Util / Price Change** — state-driven overload of [TangemPriceChange].
|
||||||
|
*
|
||||||
|
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5724-3922&m=dev)
|
||||||
|
*
|
||||||
|
* @param state Indicator state model. See [TangemPriceChange.State].
|
||||||
|
* @param modifier Modifier applied to the indicator root.
|
||||||
|
* @param isFlickering Runs the blade animation over the label while the value is being refreshed.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
@NonRestartableComposable
|
||||||
|
fun TangemPriceChange(state: TangemPriceChange.State, modifier: Modifier = Modifier, isFlickering: Boolean = false) {
|
||||||
|
TangemPriceChange(
|
||||||
|
value = state.value,
|
||||||
|
direction = state.direction,
|
||||||
|
modifier = modifier,
|
||||||
|
isFlickering = isFlickering,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Public API surface of [TangemPriceChange]. */
|
||||||
|
object TangemPriceChange {
|
||||||
|
|
||||||
|
/** Direction of the price change, driving the leading icon and its color. */
|
||||||
|
enum class Direction {
|
||||||
|
/** Price went up — upward arrow, accent color. */
|
||||||
|
Up,
|
||||||
|
|
||||||
|
/** Price went down — downward arrow, error color. */
|
||||||
|
Down,
|
||||||
|
|
||||||
|
/** No significant change — dot icon, tertiary color. */
|
||||||
|
Neutral,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Price change state model.
|
||||||
|
*
|
||||||
|
* @param value Formatted percent text (e.g. `"2.08%"`).
|
||||||
|
* @param direction Change direction. See [Direction].
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
data class State(
|
||||||
|
val value: TextReference,
|
||||||
|
val direction: Direction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Light", showBackground = true)
|
||||||
|
@Preview(name = "Dark", showBackground = true, uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun TangemPriceChangePreview() {
|
||||||
|
TangemThemePreviewRedesign {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(TangemTheme.colors3.bg.primary)
|
||||||
|
.padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
TangemPriceChange.Direction.entries.forEach { direction ->
|
||||||
|
TangemPriceChange(
|
||||||
|
value = stringReference("2.08%"),
|
||||||
|
direction = direction,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
TangemPriceChange(
|
||||||
|
state = TangemPriceChange.State(
|
||||||
|
value = stringReference("2.08%"),
|
||||||
|
direction = TangemPriceChange.Direction.Up,
|
||||||
|
),
|
||||||
|
isFlickering = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -84,6 +84,7 @@ dependencies {
|
||||||
api(projects.libs.blockchainSdk)
|
api(projects.libs.blockchainSdk)
|
||||||
implementation(projects.common)
|
implementation(projects.common)
|
||||||
implementation(projects.common.ui)
|
implementation(projects.common.ui)
|
||||||
|
implementation(projects.common.uiCharts)
|
||||||
implementation(projects.libs.auth)
|
implementation(projects.libs.auth)
|
||||||
implementation(projects.libs.crypto)
|
implementation(projects.libs.crypto)
|
||||||
implementation(projects.libs.tangemSdkApi)
|
implementation(projects.libs.tangemSdkApi)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||||
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
|
import com.tangem.core.ui.ds2.topnavigation.TangemTopNavigation
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
|
||||||
internal sealed interface StoryBookPage
|
internal sealed interface StoryBookPage
|
||||||
|
|
||||||
|
|
@ -446,6 +447,64 @@ internal data class TextStyleStory(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Suppress("BooleanPropertyNaming")
|
||||||
|
internal data class TangemTokenRowV2Story(
|
||||||
|
val variant: Variant,
|
||||||
|
val direction: TangemPriceChange.Direction,
|
||||||
|
val hasBadge: Boolean,
|
||||||
|
val isBadgeFilled: Boolean,
|
||||||
|
val hasPending: Boolean,
|
||||||
|
val hasQuote: Boolean,
|
||||||
|
val hasPriceChange: Boolean,
|
||||||
|
val hasCryptoBalance: Boolean,
|
||||||
|
val hasContractWarning: Boolean,
|
||||||
|
val hasUpdateWarning: Boolean,
|
||||||
|
val hasMessageBubble: Boolean,
|
||||||
|
val isBalanceHidden: Boolean,
|
||||||
|
val isQuoteFlickering: Boolean,
|
||||||
|
val isBalanceFlickering: Boolean,
|
||||||
|
val onVariantChange: (Variant) -> Unit,
|
||||||
|
val onDirectionChange: (TangemPriceChange.Direction) -> Unit,
|
||||||
|
val onBadgeToggle: () -> Unit,
|
||||||
|
val onBadgeFilledToggle: () -> Unit,
|
||||||
|
val onPendingToggle: () -> Unit,
|
||||||
|
val onQuoteToggle: () -> Unit,
|
||||||
|
val onPriceChangeToggle: () -> Unit,
|
||||||
|
val onCryptoBalanceToggle: () -> Unit,
|
||||||
|
val onContractWarningToggle: () -> Unit,
|
||||||
|
val onUpdateWarningToggle: () -> Unit,
|
||||||
|
val onMessageBubbleToggle: () -> Unit,
|
||||||
|
val onBalanceHiddenToggle: () -> Unit,
|
||||||
|
val onQuoteFlickeringToggle: () -> Unit,
|
||||||
|
val onBalanceFlickeringToggle: () -> Unit,
|
||||||
|
) : DsStoryBookPage {
|
||||||
|
|
||||||
|
/** Which [com.tangem.core.ui.ds2.tokenrow.TangemTokenRow] variant/preset is rendered. */
|
||||||
|
enum class Variant { Default, Organize, Unreachable, NoAddress, Shimmer }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BooleanPropertyNaming")
|
||||||
|
internal data class TangemTokenRowMarketStory(
|
||||||
|
val direction: TangemPriceChange.Direction,
|
||||||
|
val isShimmer: Boolean,
|
||||||
|
val hasTicker: Boolean,
|
||||||
|
val hasPosition: Boolean,
|
||||||
|
val hasCapitalization: Boolean,
|
||||||
|
val hasPrice: Boolean,
|
||||||
|
val hasPriceChange: Boolean,
|
||||||
|
val hasChart: Boolean,
|
||||||
|
val longTitle: Boolean,
|
||||||
|
val onDirectionChange: (TangemPriceChange.Direction) -> Unit,
|
||||||
|
val onShimmerToggle: () -> Unit,
|
||||||
|
val onTickerToggle: () -> Unit,
|
||||||
|
val onPositionToggle: () -> Unit,
|
||||||
|
val onCapitalizationToggle: () -> Unit,
|
||||||
|
val onPriceToggle: () -> Unit,
|
||||||
|
val onPriceChangeToggle: () -> Unit,
|
||||||
|
val onChartToggle: () -> Unit,
|
||||||
|
val onLongTitleToggle: () -> Unit,
|
||||||
|
) : DsStoryBookPage
|
||||||
|
|
||||||
internal data class TangemBadgeV2Story(
|
internal data class TangemBadgeV2Story(
|
||||||
val variant: TangemBadge.Variant,
|
val variant: TangemBadge.Variant,
|
||||||
val status: TangemBadge.Status,
|
val status: TangemBadge.Status,
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,8 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSea
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.textstyle.textStyleStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.textstyle.textStyleStoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenicon.tangemTokenIconStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenicon.tangemTokenIconStoryFactory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowmarket.tangemTokenRowMarketStoryFactory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowv2.tangemTokenRowV2StoryFactory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.tangemTopNavigationStoryFactory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.tangemTopNavigationStoryFactory
|
||||||
|
|
||||||
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
|
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
|
||||||
|
|
@ -40,6 +42,8 @@ private fun buildDsStories() = listOf(
|
||||||
DsStoryItem(title = "⭕ TangemCheckmark", factory = tangemCheckmarkStoryFactory),
|
DsStoryItem(title = "⭕ TangemCheckmark", factory = tangemCheckmarkStoryFactory),
|
||||||
DsStoryItem(title = "🎨 TangemTokenIcon", factory = tangemTokenIconStoryFactory),
|
DsStoryItem(title = "🎨 TangemTokenIcon", factory = tangemTokenIconStoryFactory),
|
||||||
DsStoryItem(title = "📋 TangemRow", factory = tangemRowStoryFactory),
|
DsStoryItem(title = "📋 TangemRow", factory = tangemRowStoryFactory),
|
||||||
|
DsStoryItem(title = "🪙 TangemTokenRow", factory = tangemTokenRowV2StoryFactory),
|
||||||
|
DsStoryItem(title = "📈 TangemTokenRowMarket", factory = tangemTokenRowMarketStoryFactory),
|
||||||
DsStoryItem(title = "🔎 TangemSearch", factory = tangemSearchStoryFactory),
|
DsStoryItem(title = "🔎 TangemSearch", factory = tangemSearchStoryFactory),
|
||||||
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
||||||
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
package com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowmarket
|
||||||
|
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowMarketStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||||
|
|
||||||
|
internal fun StateUpdater<TangemTokenRowMarketStory>.build(): TangemTokenRowMarketStory {
|
||||||
|
return TangemTokenRowMarketStory(
|
||||||
|
direction = TangemPriceChange.Direction.Up,
|
||||||
|
isShimmer = false,
|
||||||
|
hasTicker = true,
|
||||||
|
hasPosition = true,
|
||||||
|
hasCapitalization = true,
|
||||||
|
hasPrice = true,
|
||||||
|
hasPriceChange = true,
|
||||||
|
hasChart = true,
|
||||||
|
longTitle = false,
|
||||||
|
onDirectionChange = { direction -> updateStory { it.copy(direction = direction) } },
|
||||||
|
onShimmerToggle = { updateStory { it.copy(isShimmer = !it.isShimmer) } },
|
||||||
|
onTickerToggle = { updateStory { it.copy(hasTicker = !it.hasTicker) } },
|
||||||
|
onPositionToggle = { updateStory { it.copy(hasPosition = !it.hasPosition) } },
|
||||||
|
onCapitalizationToggle = { updateStory { it.copy(hasCapitalization = !it.hasCapitalization) } },
|
||||||
|
onPriceToggle = { updateStory { it.copy(hasPrice = !it.hasPrice) } },
|
||||||
|
onPriceChangeToggle = { updateStory { it.copy(hasPriceChange = !it.hasPriceChange) } },
|
||||||
|
onChartToggle = { updateStory { it.copy(hasChart = !it.hasChart) } },
|
||||||
|
onLongTitleToggle = { updateStory { it.copy(longTitle = !it.longTitle) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val tangemTokenRowMarketStoryFactory
|
||||||
|
get() = storyPageFactory(StateUpdater<TangemTokenRowMarketStory>::build)
|
||||||
|
|
@ -0,0 +1,243 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowmarket
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
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 com.tangem.common.ui.charts.MarketChartMini
|
||||||
|
import com.tangem.common.ui.charts.state.MarketChartLook
|
||||||
|
import com.tangem.common.ui.charts.state.MarketChartRawData
|
||||||
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.Shimmer
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.TangemTokenRowMarket
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowMarketStory
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
private const val SAMPLE_ICON_URL = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TangemTokenRowMarketStory(state: TangemTokenRowMarketStory, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
ComponentPreview(state = state)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
DirectionSelector(selected = state.direction, onSelect = state.onDirectionChange)
|
||||||
|
Toggles(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ComponentPreview(state: TangemTokenRowMarketStory) {
|
||||||
|
val icon = TangemTokenIcon.UiState.Token(TangemTokenIcon.State(url = SAMPLE_ICON_URL))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(TangemTheme.colors3.bg.primary)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
) {
|
||||||
|
if (state.isShimmer) {
|
||||||
|
TangemTokenRowMarket.Shimmer()
|
||||||
|
} else {
|
||||||
|
TangemTokenRowMarket(
|
||||||
|
icon = icon,
|
||||||
|
title = if (state.longTitle) {
|
||||||
|
stringReference("Very Long Token Name Coin")
|
||||||
|
} else {
|
||||||
|
stringReference("Bitcoin")
|
||||||
|
},
|
||||||
|
ticker = if (state.hasTicker) stringReference("BTC") else null,
|
||||||
|
position = if (state.hasPosition) stringReference("2") else null,
|
||||||
|
capitalization = if (state.hasCapitalization) stringReference("1.196T") else null,
|
||||||
|
price = if (state.hasPrice) stringReference("$59,723.24") else null,
|
||||||
|
priceChange = if (state.hasPriceChange) {
|
||||||
|
TangemPriceChange.State(value = stringReference("2.08%"), direction = state.direction)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
chart = if (state.hasChart) {
|
||||||
|
{ MarketChart(direction = state.direction) }
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The graph slot is filled with the real MarketChartMini from :common:ui-charts, sized 24x32
|
||||||
|
// (collapsed) per the Figma spec, showing how a feature wires the chart into the DS slot.
|
||||||
|
@Composable
|
||||||
|
private fun MarketChart(direction: TangemPriceChange.Direction) {
|
||||||
|
val type = when (direction) {
|
||||||
|
TangemPriceChange.Direction.Up -> MarketChartLook.Type.Growing
|
||||||
|
TangemPriceChange.Direction.Down -> MarketChartLook.Type.Falling
|
||||||
|
TangemPriceChange.Direction.Neutral -> MarketChartLook.Type.Neutral
|
||||||
|
}
|
||||||
|
MarketChartMini(
|
||||||
|
rawData = PreviewChartData,
|
||||||
|
type = type,
|
||||||
|
modifier = Modifier.size(width = 24.dp, height = 32.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private val PreviewChartData = MarketChartRawData(
|
||||||
|
y = persistentListOf(0.4, 0.2, 0.4, 0.1, 0.4, 2.0, 5.0, 0.1, 2.0, 2.0, 3.0),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DirectionSelector(selected: TangemPriceChange.Direction, onSelect: (TangemPriceChange.Direction) -> Unit) {
|
||||||
|
Section(label = "Price change") {
|
||||||
|
ChipGrid(
|
||||||
|
items = TangemPriceChange.Direction.entries,
|
||||||
|
label = { it.name },
|
||||||
|
isSelected = { it == selected },
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Toggles(state: TangemTokenRowMarketStory) {
|
||||||
|
Section(label = "Flags") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ToggleRow(label = "shimmer", checked = state.isShimmer, onToggle = state.onShimmerToggle)
|
||||||
|
ToggleRow(label = "ticker", checked = state.hasTicker, onToggle = state.onTickerToggle)
|
||||||
|
ToggleRow(label = "position", checked = state.hasPosition, onToggle = state.onPositionToggle)
|
||||||
|
ToggleRow(
|
||||||
|
label = "capitalization",
|
||||||
|
checked = state.hasCapitalization,
|
||||||
|
onToggle = state.onCapitalizationToggle,
|
||||||
|
)
|
||||||
|
ToggleRow(label = "price", checked = state.hasPrice, onToggle = state.onPriceToggle)
|
||||||
|
ToggleRow(label = "priceChange", checked = state.hasPriceChange, onToggle = state.onPriceChangeToggle)
|
||||||
|
ToggleRow(label = "chart", checked = state.hasChart, onToggle = state.onChartToggle)
|
||||||
|
ToggleRow(label = "longTitle", checked = state.longTitle, onToggle = state.onLongTitleToggle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Section(label: String, content: @Composable () -> Unit) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.subtitle1,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
|
||||||
|
val shape = RoundedCornerShape(50)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(shape)
|
||||||
|
.background(TangemTheme.colors2.surface.level2)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = TangemTheme.colors2.border.neutral.secondary,
|
||||||
|
shape = shape,
|
||||||
|
)
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
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(50)
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.clip(chipShape)
|
||||||
|
.background(
|
||||||
|
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(TangemTheme.colors2.surface.level2)
|
||||||
|
.clickable(onClick = onToggle)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.body2,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (checked) "ON" else "OFF",
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
package com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowv2
|
||||||
|
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowV2Story
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||||
|
|
||||||
|
internal fun StateUpdater<TangemTokenRowV2Story>.build(): TangemTokenRowV2Story {
|
||||||
|
return TangemTokenRowV2Story(
|
||||||
|
variant = TangemTokenRowV2Story.Variant.Default,
|
||||||
|
direction = TangemPriceChange.Direction.Up,
|
||||||
|
hasBadge = true,
|
||||||
|
isBadgeFilled = false,
|
||||||
|
hasPending = false,
|
||||||
|
hasQuote = true,
|
||||||
|
hasPriceChange = true,
|
||||||
|
hasCryptoBalance = true,
|
||||||
|
hasContractWarning = false,
|
||||||
|
hasUpdateWarning = false,
|
||||||
|
hasMessageBubble = true,
|
||||||
|
isBalanceHidden = false,
|
||||||
|
isQuoteFlickering = false,
|
||||||
|
isBalanceFlickering = false,
|
||||||
|
onVariantChange = { variant -> updateStory { it.copy(variant = variant) } },
|
||||||
|
onDirectionChange = { direction -> updateStory { it.copy(direction = direction) } },
|
||||||
|
onBadgeToggle = { updateStory { it.copy(hasBadge = !it.hasBadge) } },
|
||||||
|
onBadgeFilledToggle = { updateStory { it.copy(isBadgeFilled = !it.isBadgeFilled) } },
|
||||||
|
onPendingToggle = { updateStory { it.copy(hasPending = !it.hasPending) } },
|
||||||
|
onQuoteToggle = { updateStory { it.copy(hasQuote = !it.hasQuote) } },
|
||||||
|
onPriceChangeToggle = { updateStory { it.copy(hasPriceChange = !it.hasPriceChange) } },
|
||||||
|
onCryptoBalanceToggle = { updateStory { it.copy(hasCryptoBalance = !it.hasCryptoBalance) } },
|
||||||
|
onContractWarningToggle = { updateStory { it.copy(hasContractWarning = !it.hasContractWarning) } },
|
||||||
|
onUpdateWarningToggle = { updateStory { it.copy(hasUpdateWarning = !it.hasUpdateWarning) } },
|
||||||
|
onMessageBubbleToggle = { updateStory { it.copy(hasMessageBubble = !it.hasMessageBubble) } },
|
||||||
|
onBalanceHiddenToggle = { updateStory { it.copy(isBalanceHidden = !it.isBalanceHidden) } },
|
||||||
|
onQuoteFlickeringToggle = { updateStory { it.copy(isQuoteFlickering = !it.isQuoteFlickering) } },
|
||||||
|
onBalanceFlickeringToggle = { updateStory { it.copy(isBalanceFlickering = !it.isBalanceFlickering) } },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal val tangemTokenRowV2StoryFactory
|
||||||
|
get() = storyPageFactory(StateUpdater<TangemTokenRowV2Story>::build)
|
||||||
|
|
@ -0,0 +1,308 @@
|
||||||
|
@file:Suppress("MagicNumber")
|
||||||
|
|
||||||
|
package com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowv2
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
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 com.tangem.core.ui.ds2.badge.TangemBadge
|
||||||
|
import com.tangem.core.ui.ds2.messagebubble.TangemMessageBubble
|
||||||
|
import com.tangem.core.ui.ds2.tokenicon.TangemTokenIcon
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.NoAddress
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.Organize
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.Shimmer
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.TangemTokenRow
|
||||||
|
import com.tangem.core.ui.ds2.tokenrow.Unreachable
|
||||||
|
import com.tangem.core.ui.ds2.util.TangemPriceChange
|
||||||
|
import com.tangem.core.ui.extensions.stringReference
|
||||||
|
import com.tangem.core.ui.res.TangemTheme
|
||||||
|
import com.tangem.core.ui.res.generated.icons.Icons
|
||||||
|
import com.tangem.core.ui.res.generated.icons.ic_chart_bar_vertical_16
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowV2Story
|
||||||
|
|
||||||
|
private const val SAMPLE_ICON_URL = "https://s3.eu-central-1.amazonaws.com/tangem.api/coins/large/bitcoin.png"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun TangemTokenRowV2Story(state: TangemTokenRowV2Story, modifier: Modifier = Modifier) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(TangemTheme.colors.background.primary)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
ComponentPreview(state = state)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
) {
|
||||||
|
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
|
||||||
|
DirectionSelector(selected = state.direction, onSelect = state.onDirectionChange)
|
||||||
|
Toggles(state = state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ComponentPreview(state: TangemTokenRowV2Story) {
|
||||||
|
val icon = TangemTokenIcon.UiState.Token(TangemTokenIcon.State(url = SAMPLE_ICON_URL))
|
||||||
|
val priceChange = if (state.hasPriceChange) {
|
||||||
|
TangemPriceChange.State(value = stringReference("2.08%"), direction = state.direction)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(TangemTheme.colors3.bg.primary)
|
||||||
|
.padding(vertical = 16.dp),
|
||||||
|
) {
|
||||||
|
when (state.variant) {
|
||||||
|
TangemTokenRowV2Story.Variant.Default -> DefaultRowPreview(
|
||||||
|
state = state,
|
||||||
|
icon = icon,
|
||||||
|
priceChange = priceChange,
|
||||||
|
)
|
||||||
|
TangemTokenRowV2Story.Variant.Organize -> TangemTokenRow.Organize(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
ticker = stringReference("BTC"),
|
||||||
|
fiatBalance = stringReference("$583.00"),
|
||||||
|
isBalanceHidden = state.isBalanceHidden,
|
||||||
|
)
|
||||||
|
TangemTokenRowV2Story.Variant.Unreachable -> TangemTokenRow.Unreachable(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
badge = stringReference("Unreachable"),
|
||||||
|
quote = if (state.hasQuote) stringReference("$1.00") else null,
|
||||||
|
priceChange = priceChange,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
TangemTokenRowV2Story.Variant.NoAddress -> TangemTokenRow.NoAddress(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
message = stringReference("No address"),
|
||||||
|
quote = if (state.hasQuote) stringReference("$1.00") else null,
|
||||||
|
priceChange = priceChange,
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
TangemTokenRowV2Story.Variant.Shimmer -> TangemTokenRow.Shimmer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DefaultRowPreview(
|
||||||
|
state: TangemTokenRowV2Story,
|
||||||
|
icon: TangemTokenIcon.UiState,
|
||||||
|
priceChange: TangemPriceChange.State?,
|
||||||
|
) {
|
||||||
|
TangemTokenRow(
|
||||||
|
icon = icon,
|
||||||
|
title = stringReference("Bitcoin"),
|
||||||
|
badge = if (state.hasBadge) {
|
||||||
|
TangemTokenRow.Badge(
|
||||||
|
text = stringReference("APY 5.47%"),
|
||||||
|
variant = if (state.isBadgeFilled) TangemBadge.Variant.Solid else TangemBadge.Variant.Tinted,
|
||||||
|
status = if (state.isBadgeFilled) TangemBadge.Status.Success else TangemBadge.Status.Neutral,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
hasPending = state.hasPending,
|
||||||
|
quote = if (state.hasQuote) stringReference("$1.00") else null,
|
||||||
|
priceChange = priceChange,
|
||||||
|
fiatBalance = stringReference("$583.00"),
|
||||||
|
cryptoBalance = if (state.hasCryptoBalance) stringReference("0,000015 BTC") else null,
|
||||||
|
showContractWarning = state.hasContractWarning,
|
||||||
|
showUpdateWarning = state.hasUpdateWarning,
|
||||||
|
isBalanceHidden = state.isBalanceHidden,
|
||||||
|
isQuoteFlickering = state.isQuoteFlickering,
|
||||||
|
isBalanceFlickering = state.isBalanceFlickering,
|
||||||
|
messageBubble = if (state.hasMessageBubble) {
|
||||||
|
{
|
||||||
|
TangemMessageBubble(
|
||||||
|
text = stringReference("Enable 5.47% APY on your balance"),
|
||||||
|
variant = TangemMessageBubble.Variant.Success,
|
||||||
|
icon = Icons.ic_chart_bar_vertical_16,
|
||||||
|
onClick = {},
|
||||||
|
onClose = {},
|
||||||
|
closeContentDescription = "Dismiss",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
onClick = {},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun VariantSelector(
|
||||||
|
selected: TangemTokenRowV2Story.Variant,
|
||||||
|
onSelect: (TangemTokenRowV2Story.Variant) -> Unit,
|
||||||
|
) {
|
||||||
|
Section(label = "Variant") {
|
||||||
|
ChipGrid(
|
||||||
|
items = TangemTokenRowV2Story.Variant.entries,
|
||||||
|
label = { it.name },
|
||||||
|
isSelected = { it == selected },
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DirectionSelector(selected: TangemPriceChange.Direction, onSelect: (TangemPriceChange.Direction) -> Unit) {
|
||||||
|
Section(label = "Price change") {
|
||||||
|
ChipGrid(
|
||||||
|
items = TangemPriceChange.Direction.entries,
|
||||||
|
label = { it.name },
|
||||||
|
isSelected = { it == selected },
|
||||||
|
onSelect = onSelect,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Toggles(state: TangemTokenRowV2Story) {
|
||||||
|
Section(label = "Flags") {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
ToggleRow(label = "badge", checked = state.hasBadge, onToggle = state.onBadgeToggle)
|
||||||
|
ToggleRow(label = "badgeFilled", checked = state.isBadgeFilled, onToggle = state.onBadgeFilledToggle)
|
||||||
|
ToggleRow(label = "pending", checked = state.hasPending, onToggle = state.onPendingToggle)
|
||||||
|
ToggleRow(label = "quote", checked = state.hasQuote, onToggle = state.onQuoteToggle)
|
||||||
|
ToggleRow(label = "priceChange", checked = state.hasPriceChange, onToggle = state.onPriceChangeToggle)
|
||||||
|
ToggleRow(label = "cryptoBalance", checked = state.hasCryptoBalance, onToggle = state.onCryptoBalanceToggle)
|
||||||
|
ToggleRow(
|
||||||
|
label = "contractWarning",
|
||||||
|
checked = state.hasContractWarning,
|
||||||
|
onToggle = state.onContractWarningToggle,
|
||||||
|
)
|
||||||
|
ToggleRow(label = "updateWarning", checked = state.hasUpdateWarning, onToggle = state.onUpdateWarningToggle)
|
||||||
|
ToggleRow(label = "messageBubble", checked = state.hasMessageBubble, onToggle = state.onMessageBubbleToggle)
|
||||||
|
ToggleRow(label = "balanceHidden", checked = state.isBalanceHidden, onToggle = state.onBalanceHiddenToggle)
|
||||||
|
ToggleRow(
|
||||||
|
label = "quoteFlickering",
|
||||||
|
checked = state.isQuoteFlickering,
|
||||||
|
onToggle = state.onQuoteFlickeringToggle,
|
||||||
|
)
|
||||||
|
ToggleRow(
|
||||||
|
label = "balanceFlickering",
|
||||||
|
checked = state.isBalanceFlickering,
|
||||||
|
onToggle = state.onBalanceFlickeringToggle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Section(label: String, content: @Composable () -> Unit) {
|
||||||
|
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.subtitle1,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
|
||||||
|
val shape = RoundedCornerShape(50)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(shape)
|
||||||
|
.background(TangemTheme.colors2.surface.level2)
|
||||||
|
.border(
|
||||||
|
width = 1.dp,
|
||||||
|
color = TangemTheme.colors2.border.neutral.secondary,
|
||||||
|
shape = shape,
|
||||||
|
)
|
||||||
|
.padding(4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
) {
|
||||||
|
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(50)
|
||||||
|
Box(
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
modifier = modifier
|
||||||
|
.clip(chipShape)
|
||||||
|
.background(
|
||||||
|
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
|
||||||
|
)
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(TangemTheme.colors2.surface.level2)
|
||||||
|
.clickable(onClick = onToggle)
|
||||||
|
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = label,
|
||||||
|
style = TangemTheme.typography.body2,
|
||||||
|
color = TangemTheme.colors.text.primary1,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (checked) "ON" else "OFF",
|
||||||
|
style = TangemTheme.typography.caption2,
|
||||||
|
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -50,6 +50,8 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSea
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.textstyle.TextStyleStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.textstyle.TextStyleStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenicon.TangemTokenIconStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenicon.TangemTokenIconStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowmarket.TangemTokenRowMarketStory
|
||||||
|
import com.tangem.feature.tester.presentation.storybook.page.ds.tokenrowv2.TangemTokenRowV2Story
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.TangemTopNavigationStory
|
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.TangemTopNavigationStory
|
||||||
import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory
|
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.message.TangemMessageStory
|
||||||
|
|
@ -108,6 +110,8 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
||||||
is TangemFadeStory -> TangemFadeStory(state = storyState)
|
is TangemFadeStory -> TangemFadeStory(state = storyState)
|
||||||
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
|
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
|
||||||
is TangemMessageBannerStory -> TangemMessageBannerStory(state = storyState)
|
is TangemMessageBannerStory -> TangemMessageBannerStory(state = storyState)
|
||||||
|
is TangemTokenRowV2Story -> TangemTokenRowV2Story(state = storyState)
|
||||||
|
is TangemTokenRowMarketStory -> TangemTokenRowMarketStory(state = storyState)
|
||||||
is TextStyleStory -> TextStyleStory(state = storyState)
|
is TextStyleStory -> TextStyleStory(state = storyState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue