Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-03 15:33:29 +05:00
parent 31a8ee5dd4
commit 7c3b952ee8
7 changed files with 170 additions and 76 deletions

View file

@ -287,6 +287,10 @@
<string name="common_approve">Approve</string>
<string name="common_approved">Approved</string>
<string name="common_approving">Approving</string>
<plurals name="common_assets">
<item quantity="one">%d asset</item>
<item quantity="other">%d assets</item>
</plurals>
<string name="common_attention">Attention</string>
<string name="common_available_networks">Available networks</string>
<string name="common_backup">Backup</string>

View file

@ -3,14 +3,7 @@
package com.tangem.core.ui.ds2.messagebanner
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
@ -29,6 +22,7 @@ import androidx.compose.ui.text.style.TextAlign
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.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
@ -143,6 +137,49 @@ fun TangemMessageBanner(
}
}
/**
* Design-system v2 (DS3) **Message Banner** title/description header with optional slots and an
* action-button row.
*
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5475-7680&m=dev)
*
* @param state component state model
* @param modifier component Modifier
* @param extraBottomSlot Slot under the description, inside the text column.
*/
@Suppress("LongParameterList")
@Composable
fun TangemMessageBanner(
state: TangemMessageBanner.State,
modifier: Modifier = Modifier,
extraBottomSlot: (@Composable ColumnScope.() -> Unit)? = null,
) {
TangemMessageBanner(
modifier = modifier,
variant = state.variant,
showGlowRing = state.isShowGlowRing,
secondaryButton = state.secondaryButton,
primaryButton = state.primaryButton,
) {
MessageBannerContentRow(
title = state.title,
description = state.description,
contentAlign = state.contentAlign,
slotStart = if (state.iconStart != null) {
{ TangemIcon(state.iconStart) }
} else {
null
},
slotEnd = if (state.iconEnd != null) {
{ TangemIcon(state.iconEnd) }
} else {
null
},
extraBottomSlot = extraBottomSlot,
)
}
}
@Suppress("LongParameterList")
@Composable
private fun MessageBannerContentRow(
@ -168,7 +205,11 @@ private fun MessageBannerContentRow(
horizontalArrangement = Arrangement.spacedBy(12.dp),
) {
slotStart?.let { slot -> Box(modifier = Modifier.align(Alignment.Top)) { slot() } }
textWrapper(Modifier.weight(1f).align(Alignment.Top))
textWrapper(
Modifier
.weight(1f)
.align(Alignment.Top),
)
slotEnd?.let { slot -> Box(modifier = Modifier.align(Alignment.Top)) { slot() } }
}
TangemMessageBanner.ContentAlign.Center -> Box(modifier = Modifier.fillMaxWidth()) {
@ -293,6 +334,32 @@ object TangemMessageBanner {
val isEnabled: Boolean = true,
val isLoading: Boolean = false,
)
/**
* A model holding configuration data for TangemMessageBanner
*
* @param title Banner headline.
* @param variant Visual appearance background color + glow ring.
* @param contentAlign Horizontal alignment of the text block.
* @param isShowGlowRing Whether the glow ring is drawn around the banner. `false` shows only the
* background.
* @param description Secondary line under the [title]. `null` hides it.
* @param secondaryButton Start action. `null` hides it.
* @param primaryButton End action. `null` hides it.
* @param iconStart Leading icon before the title. `null` hides it.
* @param iconEnd Trailing icon after the title (e.g. the [CloseButton] preset). `null` hides it.
*/
data class State(
val title: TextReference,
val variant: Variant = Variant.Default,
val contentAlign: ContentAlign = ContentAlign.Start,
val isShowGlowRing: Boolean = true,
val description: TextReference? = null,
val secondaryButton: Button? = null,
val primaryButton: Button? = null,
val iconStart: TangemIconUM? = null,
val iconEnd: TangemIconUM? = null,
)
}
/**

View file

@ -8,18 +8,9 @@ import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberUpdatedState
import androidx.compose.runtime.setValue
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawBehind
@ -43,7 +34,10 @@ import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.DonutSegmentColor
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.math.BigDecimal
import kotlin.math.atan2
import kotlin.math.hypot
@ -88,7 +82,7 @@ import kotlin.math.min
@Suppress("MagicNumber", "LongParameterList", "LongMethod", "NamedArguments")
@Composable
internal fun DonutChart(
segments: List<DonutSegmentUM>,
segments: ImmutableList<DonutSegmentUM>,
modifier: Modifier = Modifier,
selectedIndex: Int? = null,
onSegmentClick: ((index: Int?) -> Unit)? = null,
@ -100,6 +94,11 @@ internal fun DonutChart(
val strokePx = with(LocalDensity.current) { strokeWidth.toPx() }
val dimOverlayColor = TangemTheme.colors3.border.inverse.tertiary
// Resolve each slice's themed colour once, here in composition (the palette reads TangemTheme, which
// isn't available inside the drawBehind DrawScope). Order is preserved 1:1 with [segments] so slice i
// keeps the colour its producer assigned by rank — the draw pass below indexes by i, not by paint order.
val segmentColors = segments.map { it.color.getColor() }
// Fade the dim in/out in step with the segment tooltip's pop-in (same spring as DonutSegmentTooltip).
val dimProgress by animateFloatAsState(
targetValue = if (selectedIndex != null) 1f else 0f,
@ -175,7 +174,7 @@ internal fun DonutChart(
for (i in segments.indices.reversed()) {
if (sweeps[i] <= 0f) continue
drawArc(
color = segments[i].color,
color = segmentColors[i],
startAngle = starts[i],
sweepAngle = sweeps[i],
useCenter = false,
@ -352,28 +351,28 @@ private fun PreviewDonutChart() {
modifier = Modifier.size(260.dp),
selectedIndex = selectedIndex,
onSegmentClick = { index -> selectedIndex = index.takeIf { it != selectedIndex } },
segments = listOf(
segments = persistentListOf(
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
color = DonutSegmentColor.Brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.07),
color = TangemTheme.colors3.border.accent.violet,
color = DonutSegmentColor.Violet,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
DonutSegmentUM(
weight = BigDecimal(0.06),
color = TangemTheme.colors3.border.accent.red,
color = DonutSegmentColor.Red,
title = stringReference("Polkadot"),
fiatValue = stringReference("$624.26"),
),
DonutSegmentUM(
weight = BigDecimal(0.05),
color = TangemTheme.colors3.border.accent.green,
color = DonutSegmentColor.Green,
title = stringReference("Tether"),
fiatValue = stringReference("$520.18"),
),
@ -409,7 +408,7 @@ private fun PreviewDonutChartEmpty() {
) {
DonutChart(
modifier = Modifier.size(260.dp),
segments = emptyList(),
segments = persistentListOf(),
) {
Text(
text = stringResourceSafe(R.string.market_chart_bubble_no_data),

View file

@ -8,27 +8,10 @@ import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.awaitEachGesture
import androidx.compose.foundation.gestures.awaitFirstDown
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.runtime.withFrameNanos
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
@ -51,21 +34,14 @@ import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds2.surface.TangemSurface
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.pluralStringResourceSafe
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.stringResourceSafe
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.format.bigdecimal.format
import com.tangem.core.ui.format.bigdecimal.percent
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.features.foryou.impl.R
import com.tangem.features.foryou.impl.components.state.AiInsightUM
import com.tangem.features.foryou.impl.components.state.DonutChartUM
import com.tangem.features.foryou.impl.components.state.DonutSegmentUM
import com.tangem.features.foryou.impl.components.state.MarketChartUM
import com.tangem.features.foryou.impl.components.state.*
import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.math.BigDecimal
@ -80,7 +56,7 @@ internal fun MarketChart(marketChart: MarketChartUM, modifier: Modifier = Modifi
.onGloballyPositioned { cardBoundsInWindow = it.boundsInWindow() },
color = TangemTheme.colors3.bg.secondary,
) {
Column {
Column(modifier = Modifier.fillMaxWidth()) {
DonutChartBlock(marketChart.donutChart, cardBoundsInWindow)
Spacer(modifier = Modifier.height(16.dp))
if (marketChart is MarketChartUM.Loaded) {
@ -353,10 +329,7 @@ private fun MarketChart_Preview(
}
}
/**
* Maps a [scenario] to the state shown. Built inside a `@Composable` (not the [PreviewParameterProvider])
* because the segment colors come from [TangemTheme.colors3], which can only be read in composition.
*/
/** Maps a [scenario] to the state shown in the preview. */
@Suppress("MagicNumber")
@Composable
private fun previewMarketChartState(scenario: MarketChartPreviewScenario): MarketChartUM = when (scenario) {
@ -378,16 +351,16 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
aiInsight = AiInsightUM.Hide,
donutChart = DonutChartUM.Loaded(
totalAmount = "$10,12345678912.1333",
donutSegmentList = listOf(
donutSegmentList = persistentListOf(
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
color = DonutSegmentColor.Brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.45),
color = TangemTheme.colors3.border.accent.green,
color = DonutSegmentColor.Green,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
@ -401,28 +374,28 @@ private fun previewMarketChartState(scenario: MarketChartPreviewScenario): Marke
@Composable
private fun previewLoadedDonut(): DonutChartUM.Loaded = DonutChartUM.Loaded(
totalAmount = "$10,123456.1333",
donutSegmentList = listOf(
donutSegmentList = persistentListOf(
DonutSegmentUM(
weight = BigDecimal(0.55),
color = TangemTheme.colors3.border.brand,
color = DonutSegmentColor.Brand,
title = stringReference("Ethereum"),
fiatValue = stringReference("$5,720.22"),
),
DonutSegmentUM(
weight = BigDecimal(0.077),
color = TangemTheme.colors3.border.accent.violet,
color = DonutSegmentColor.Violet,
title = stringReference("Solana"),
fiatValue = stringReference("$728.30"),
),
DonutSegmentUM(
weight = BigDecimal(0.0666),
color = TangemTheme.colors3.border.accent.red,
color = DonutSegmentColor.Red,
title = stringReference("Polkadot"),
fiatValue = stringReference("$624.26"),
),
DonutSegmentUM(
weight = BigDecimal(0.05),
color = TangemTheme.colors3.border.accent.green,
color = DonutSegmentColor.Green,
title = stringReference("Tether"),
fiatValue = stringReference("$520.18"),
),

View file

@ -1,8 +1,11 @@
package com.tangem.features.foryou.impl.components.state
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.graphics.Color
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import java.math.BigDecimal
/**
@ -12,7 +15,8 @@ import java.math.BigDecimal
* contiguously; whatever is left after `sum(weight)` shows through as the track.
* For a portfolio where slices sum to 1f the ring fills completely and no track is visible.
* Also doubles as the slice's share for the selection tooltip (rendered as `weight * 100%`).
* @param color Solid fill of the slice.
* @param color Palette entry for the slice's solid fill. Assigned by the producer in segment order, so the
* slice's colour follows its rank; [DonutChart] resolves it to a themed [Color] in composition.
* @param title Human-readable name of the asset this slice represents (e.g. `"Ethereum"`). Shown in the
* selection tooltip. Empty by default for slices that don't need a label.
* @param fiatValue Pre-formatted fiat value of the slice (e.g. `"$5,720.22"`). Shown in the selection
@ -21,8 +25,27 @@ import java.math.BigDecimal
@Immutable
internal data class DonutSegmentUM(
val color: Color,
val color: DonutSegmentColor,
val weight: BigDecimal,
val title: TextReference,
val fiatValue: TextReference,
)
)
internal enum class DonutSegmentColor {
Brand,
Violet,
Red,
Green,
;
@ReadOnlyComposable
@Composable
fun getColor(): Color {
return when (this) {
Brand -> TangemTheme.colors3.border.brand
Violet -> TangemTheme.colors3.border.accent.violet
Red -> TangemTheme.colors3.border.accent.red
Green -> TangemTheme.colors3.border.accent.green
}
}
}

View file

@ -2,6 +2,8 @@ package com.tangem.features.foryou.impl.components.state
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
internal sealed class MarketChartUM(
@ -27,14 +29,14 @@ internal sealed class MarketChartUM(
@Immutable
internal sealed class DonutChartUM(
open val donutSegmentList: List<DonutSegmentUM>,
open val donutSegmentList: ImmutableList<DonutSegmentUM>,
) {
data class Loaded(
val totalAmount: String,
override val donutSegmentList: List<DonutSegmentUM>,
override val donutSegmentList: ImmutableList<DonutSegmentUM>,
) : DonutChartUM(donutSegmentList = donutSegmentList)
data object NoData : DonutChartUM(donutSegmentList = emptyList())
data object NoData : DonutChartUM(donutSegmentList = persistentListOf())
}
@Immutable

View file

@ -0,0 +1,26 @@
package com.tangem.features.foryou.impl.model
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
import com.tangem.core.ui.extensions.resourceReference
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.features.foryou.impl.R
@Immutable
internal sealed class ForYouNotification(val state: TangemMessageBanner.State) {
data object UsedOutdatedData : ForYouNotification(
state = TangemMessageBanner.State(
title = resourceReference(R.string.warning_some_token_balances_not_updated),
iconEnd = TangemIconUM.Icon(
imageVector = Icons.ic_cloud_exclamation_20,
tintReference = { TangemTheme.colors3.icon.primary },
),
variant = TangemMessageBanner.Variant.Warning,
isShowGlowRing = false,
),
)
}