Updated on 2026-08-14

This commit is contained in:
Tangem 2026-07-22 15:08:50 +03:00
parent 55c1c597b4
commit 1853aed903
4 changed files with 78 additions and 13 deletions

View file

@ -12,9 +12,13 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
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.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Alignment
@ -35,6 +39,7 @@ import com.tangem.core.ui.ds2.glowring.TangemGlowRing
import com.tangem.core.ui.ds2.surface.TangemSurface
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
@ -46,11 +51,15 @@ import com.tangem.core.ui.res.generated.icons.ic_cross_circle_20_filled
* Design-system v2 (DS3) **Message Banner** low-level slot API: a [content] block above an
* optional action-button row. For the common title/description layout, prefer the `title` overload.
*
* Version: 1.2
*
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5475-7680&m=dev)
*
* @param variant Visual appearance background color + glow ring.
* @param showGlowRing Whether the glow ring is drawn around the banner. `false` shows only the
* background.
* @param onClick Makes the whole banner clickable. Honored only when no action buttons are present ignored while
* [secondaryButton] or [primaryButton] is set.
* @param secondaryButton Start action. `null` hides it.
* @param primaryButton End action. `null` hides it.
* @param content The banner body above the buttons.
@ -60,26 +69,33 @@ fun TangemMessageBanner(
modifier: Modifier = Modifier,
variant: TangemMessageBanner.Variant = TangemMessageBanner.Variant.Default,
showGlowRing: Boolean = true,
onClick: (() -> Unit)? = null,
secondaryButton: TangemMessageBanner.Button? = null,
primaryButton: TangemMessageBanner.Button? = null,
content: @Composable ColumnScope.() -> Unit,
) {
val tokens = variant.tokens()
val isClickable = onClick != null && secondaryButton == null && primaryButton == null
Box(modifier = modifier) {
TangemSurface(
modifier = Modifier.fillMaxWidth(),
color = tokens.background,
shape = RoundedCornerShape(28.dp),
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(24.dp),
WithMessageBannerRipple(enabled = isClickable) {
TangemSurface(
modifier = Modifier.fillMaxWidth(),
color = tokens.background,
shape = RoundedCornerShape(28.dp),
) {
content()
MessageBannerButtons(secondaryButton = secondaryButton, primaryButton = primaryButton)
Column(
modifier = Modifier
.fillMaxWidth()
.conditionalCompose(isClickable) {
clickableSingle(role = Role.Button) { onClick?.invoke() }
}
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
content()
MessageBannerButtons(secondaryButton = secondaryButton, primaryButton = primaryButton)
}
}
}
if (showGlowRing) {
@ -96,6 +112,8 @@ fun TangemMessageBanner(
* Design-system v2 (DS3) **Message Banner** title/description header with optional slots and an
* action-button row.
*
* Version: 1.2
*
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=5475-7680&m=dev)
*
* @param title Banner headline.
@ -103,6 +121,8 @@ fun TangemMessageBanner(
* @param contentAlign Horizontal alignment of the text block.
* @param showGlowRing Whether the glow ring is drawn around the banner. `false` shows only the
* background.
* @param onClick Makes the whole banner clickable. Honored only when no action buttons are present ignored while
* [secondaryButton] or [primaryButton] is set.
* @param description Secondary line under the [title]. `null` hides it.
* @param secondaryButton Start action. `null` hides it.
* @param primaryButton End action. `null` hides it.
@ -118,6 +138,7 @@ fun TangemMessageBanner(
variant: TangemMessageBanner.Variant = TangemMessageBanner.Variant.Default,
contentAlign: TangemMessageBanner.ContentAlign = TangemMessageBanner.ContentAlign.Start,
showGlowRing: Boolean = true,
onClick: (() -> Unit)? = null,
description: TextReference? = null,
secondaryButton: TangemMessageBanner.Button? = null,
primaryButton: TangemMessageBanner.Button? = null,
@ -129,6 +150,7 @@ fun TangemMessageBanner(
modifier = modifier,
variant = variant,
showGlowRing = showGlowRing,
onClick = onClick,
secondaryButton = secondaryButton,
primaryButton = primaryButton,
) {
@ -211,7 +233,7 @@ private fun MessageBannerTextWrapper(
)
}
extraBottomSlot?.let { slot ->
Column(modifier = Modifier.padding(top = 12.dp)) { slot() }
Column(modifier = Modifier.padding(top = 8.dp)) { slot() }
}
}
}
@ -321,6 +343,29 @@ fun TangemMessageBanner.CloseButton(
)
}
/** Overrides the ripple for a clickable banner; pass-through when [enabled] is `false`. */
@Composable
private fun WithMessageBannerRipple(enabled: Boolean, content: @Composable () -> Unit) {
if (enabled) {
CompositionLocalProvider(LocalRippleConfiguration provides messageBannerRipple(), content = content)
} else {
content()
}
}
/** Press ripple of a clickable banner — the `color/interaction/press/static-light` token. */
@Composable
@ReadOnlyComposable
private fun messageBannerRipple(): RippleConfiguration = RippleConfiguration(
color = TangemTheme.colors3.interaction.press.staticLight,
rippleAlpha = RippleAlpha(
draggedAlpha = 0f,
focusedAlpha = 0f,
hoveredAlpha = 0.05f,
pressedAlpha = 0.1f,
),
)
/** Resolved appearance tokens for a [TangemMessageBanner.Variant]. */
private data class MessageBannerTokens(val background: Color, val glowRing: TangemGlowRing.Variant)
@ -383,6 +428,12 @@ private fun TangemMessageBannerPreview() {
},
primaryButton = TangemMessageBanner.Button(text = stringReference("Invite friends"), onClick = {}),
)
TangemMessageBanner(
modifier = Modifier.fillMaxWidth(),
title = stringReference("Clickable banner"),
description = stringReference("Whole banner is tappable when no buttons are set."),
onClick = {},
)
}
}

View file

@ -384,6 +384,7 @@ internal data class TangemMessageBannerStory(
val hasSlotStart: Boolean,
val hasSlotEnd: Boolean,
val hasExtraContent: Boolean,
val isClickable: Boolean,
val background: Background,
val onVariantChange: (TangemMessageBanner.Variant) -> Unit,
val onContentAlignChange: (TangemMessageBanner.ContentAlign) -> Unit,
@ -395,6 +396,7 @@ internal data class TangemMessageBannerStory(
val onSlotStartToggle: () -> Unit,
val onSlotEndToggle: () -> Unit,
val onExtraContentToggle: () -> Unit,
val onClickableToggle: () -> Unit,
val onBackgroundChange: (Background) -> Unit,
) : DsStoryBookPage {

View file

@ -18,6 +18,7 @@ internal fun StateUpdater<TangemMessageBannerStory>.build(): TangemMessageBanner
hasSlotStart = true,
hasSlotEnd = true,
hasExtraContent = true,
isClickable = false,
background = Background.BgSecondary,
onVariantChange = { variant -> updateStory { it.copy(variant = variant) } },
onContentAlignChange = { align -> updateStory { it.copy(contentAlign = align) } },
@ -29,6 +30,7 @@ internal fun StateUpdater<TangemMessageBannerStory>.build(): TangemMessageBanner
onSlotStartToggle = { updateStory { it.copy(hasSlotStart = !it.hasSlotStart) } },
onSlotEndToggle = { updateStory { it.copy(hasSlotEnd = !it.hasSlotEnd) } },
onExtraContentToggle = { updateStory { it.copy(hasExtraContent = !it.hasExtraContent) } },
onClickableToggle = { updateStory { it.copy(isClickable = !it.isClickable) } },
onBackgroundChange = { background -> updateStory { it.copy(background = background) } },
)
}

View file

@ -95,6 +95,11 @@ private fun PreviewBanner(state: TangemMessageBannerStory) {
variant = state.variant,
contentAlign = state.contentAlign,
showGlowRing = state.hasGlowRing,
onClick = if (state.isClickable) {
{}
} else {
null
},
title = stringReference("Would you predict?"),
description = if (state.hasDescription) {
stringReference("France will win FIFA 2026")
@ -233,6 +238,11 @@ private fun Toggles(state: TangemMessageBannerStory) {
ToggleRow(label = "slotStart", checked = state.hasSlotStart, onToggle = state.onSlotStartToggle)
ToggleRow(label = "slotEnd", checked = state.hasSlotEnd, onToggle = state.onSlotEndToggle)
ToggleRow(label = "extraContent", checked = state.hasExtraContent, onToggle = state.onExtraContentToggle)
ToggleRow(
label = "clickable (no buttons only)",
checked = state.isClickable,
onToggle = state.onClickableToggle,
)
}
}
}