Updated on 2026-08-14
This commit is contained in:
parent
ff958f6bba
commit
a8c70f3925
7 changed files with 933 additions and 0 deletions
|
|
@ -0,0 +1,96 @@
|
|||
package com.tangem.core.ui.components.notifications
|
||||
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.tangem.core.ui.components.notifications.NotificationConfig.ButtonsState
|
||||
import com.tangem.core.ui.ds.image.TangemIcon
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds2.messagebanner.CloseButton
|
||||
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
|
||||
import com.tangem.core.ui.extensions.ColorReference2
|
||||
import com.tangem.core.ui.extensions.clickableSingle
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
|
||||
/**
|
||||
* Driver that renders a DS3 [TangemMessageBanner] from a legacy [NotificationConfig], so existing
|
||||
* notification call sites can adopt the new banner without rebuilding their models.
|
||||
*
|
||||
* @param config Legacy notification model.
|
||||
* @param variant Banner appearance. See [TangemMessageBanner.Variant].
|
||||
* @param contentAlign Text alignment. See [TangemMessageBanner.ContentAlign].
|
||||
*/
|
||||
@Deprecated("Use as migration solution, not production one")
|
||||
@Composable
|
||||
fun TangemMessageBanner(
|
||||
config: NotificationConfig,
|
||||
modifier: Modifier = Modifier,
|
||||
variant: TangemMessageBanner.Variant = TangemMessageBanner.Variant.Default,
|
||||
contentAlign: TangemMessageBanner.ContentAlign = TangemMessageBanner.ContentAlign.Start,
|
||||
) {
|
||||
val onClick = config.onClick
|
||||
val (secondaryButton, primaryButton) = config.buttonsState.toBannerButtons()
|
||||
val icon = config.toBannerIcon()
|
||||
|
||||
TangemMessageBanner(
|
||||
modifier = if (onClick != null) modifier.clickableSingle(onClick = onClick) else modifier,
|
||||
variant = variant,
|
||||
contentAlign = contentAlign,
|
||||
title = config.title ?: config.subtitle,
|
||||
description = config.title?.let { config.subtitle },
|
||||
secondaryButton = secondaryButton,
|
||||
primaryButton = primaryButton,
|
||||
slotStart = icon?.let { iconUM ->
|
||||
{ TangemIcon(tangemIconUM = iconUM, modifier = Modifier.size(config.iconSize)) }
|
||||
},
|
||||
slotEnd = config.onCloseClick?.let { onClose ->
|
||||
{ TangemMessageBanner.CloseButton(onClick = onClose) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the leading icon, honouring the remote-url, untinted, and tinted cases of [NotificationConfig].
|
||||
* Returns `null` when the config carries no icon (no url and an unset [NotificationConfig.iconResId]),
|
||||
* so the banner hides the leading slot instead of rendering a broken one.
|
||||
*/
|
||||
private fun NotificationConfig.toBannerIcon(): TangemIconUM? = when {
|
||||
iconUrl != null -> TangemIconUM.Url(url = iconUrl, fallbackRes = iconResId)
|
||||
iconResId == 0 -> null
|
||||
iconTint == NotificationConfig.IconTint.Unspecified -> TangemIconUM.Image(imageRes = iconResId)
|
||||
else -> TangemIconUM.Icon(iconRes = iconResId, tintReference = ColorReference2 { iconTint.toColor() })
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun NotificationConfig.IconTint.toColor() = when (this) {
|
||||
NotificationConfig.IconTint.Unspecified -> TangemTheme.colors3.icon.primary
|
||||
NotificationConfig.IconTint.Accent -> TangemTheme.colors3.icon.status.info
|
||||
NotificationConfig.IconTint.Attention -> TangemTheme.colors3.icon.status.warning
|
||||
NotificationConfig.IconTint.Warning -> TangemTheme.colors3.icon.status.warning
|
||||
}
|
||||
|
||||
/** Maps the legacy button configuration onto the banner's (secondary = start, primary = end) pair. */
|
||||
private fun ButtonsState?.toBannerButtons(): Pair<TangemMessageBanner.Button?, TangemMessageBanner.Button?> =
|
||||
when (this) {
|
||||
is ButtonsState.PrimaryButtonConfig -> null to TangemMessageBanner.Button(
|
||||
text = text,
|
||||
onClick = onClick,
|
||||
iconEnd = iconResId?.let { TangemIconUM.Icon(iconRes = it) },
|
||||
isLoading = shouldShowProgress,
|
||||
)
|
||||
is ButtonsState.SecondaryButtonConfig -> TangemMessageBanner.Button(
|
||||
text = text,
|
||||
onClick = onClick,
|
||||
iconEnd = iconResId?.let { TangemIconUM.Icon(iconRes = it) },
|
||||
isLoading = shouldShowProgress,
|
||||
) to null
|
||||
is ButtonsState.PairButtonsConfig -> TangemMessageBanner.Button(
|
||||
text = secondaryText,
|
||||
onClick = onSecondaryClick,
|
||||
) to TangemMessageBanner.Button(text = primaryText, onClick = onPrimaryClick)
|
||||
is ButtonsState.SecondaryPairButtonsConfig -> TangemMessageBanner.Button(
|
||||
text = leftText,
|
||||
onClick = onLeftClick,
|
||||
) to TangemMessageBanner.Button(text = rightText, onClick = onRightClick)
|
||||
null -> null to null
|
||||
}
|
||||
|
|
@ -0,0 +1,436 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
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.graphics.Color
|
||||
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.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.TangemIconUM
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
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.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_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.
|
||||
*
|
||||
* [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 secondaryButton Start action. `null` hides it.
|
||||
* @param primaryButton End action. `null` hides it.
|
||||
* @param content The banner body above the buttons.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemMessageBanner(
|
||||
modifier: Modifier = Modifier,
|
||||
variant: TangemMessageBanner.Variant = TangemMessageBanner.Variant.Default,
|
||||
showGlowRing: Boolean = true,
|
||||
secondaryButton: TangemMessageBanner.Button? = null,
|
||||
primaryButton: TangemMessageBanner.Button? = null,
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
val tokens = variant.tokens()
|
||||
|
||||
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),
|
||||
) {
|
||||
content()
|
||||
MessageBannerButtons(secondaryButton = secondaryButton, primaryButton = primaryButton)
|
||||
}
|
||||
}
|
||||
if (showGlowRing) {
|
||||
TangemGlowRing(
|
||||
modifier = Modifier.matchParentSize(),
|
||||
variant = tokens.glowRing,
|
||||
cornerRadius = 28.dp,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 title Banner headline.
|
||||
* @param variant Visual appearance — background color + glow ring.
|
||||
* @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 description Secondary line under the [title]. `null` hides it.
|
||||
* @param secondaryButton Start action. `null` hides it.
|
||||
* @param primaryButton End action. `null` hides it.
|
||||
* @param slotStart Leading slot before the title. `null` hides it.
|
||||
* @param slotEnd Trailing slot after the title (e.g. the [CloseButton] preset). `null` hides it.
|
||||
* @param extraBottomSlot Slot under the description, inside the text column.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun TangemMessageBanner(
|
||||
title: TextReference,
|
||||
modifier: Modifier = Modifier,
|
||||
variant: TangemMessageBanner.Variant = TangemMessageBanner.Variant.Default,
|
||||
contentAlign: TangemMessageBanner.ContentAlign = TangemMessageBanner.ContentAlign.Start,
|
||||
showGlowRing: Boolean = true,
|
||||
description: TextReference? = null,
|
||||
secondaryButton: TangemMessageBanner.Button? = null,
|
||||
primaryButton: TangemMessageBanner.Button? = null,
|
||||
slotStart: (@Composable () -> Unit)? = null,
|
||||
slotEnd: (@Composable () -> Unit)? = null,
|
||||
extraBottomSlot: (@Composable ColumnScope.() -> Unit)? = null,
|
||||
) {
|
||||
TangemMessageBanner(
|
||||
modifier = modifier,
|
||||
variant = variant,
|
||||
showGlowRing = showGlowRing,
|
||||
secondaryButton = secondaryButton,
|
||||
primaryButton = primaryButton,
|
||||
) {
|
||||
MessageBannerContentRow(
|
||||
title = title,
|
||||
description = description,
|
||||
contentAlign = contentAlign,
|
||||
slotStart = slotStart,
|
||||
slotEnd = slotEnd,
|
||||
extraBottomSlot = extraBottomSlot,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
private fun MessageBannerContentRow(
|
||||
title: TextReference,
|
||||
description: TextReference?,
|
||||
contentAlign: TangemMessageBanner.ContentAlign,
|
||||
slotStart: (@Composable () -> Unit)?,
|
||||
slotEnd: (@Composable () -> Unit)?,
|
||||
extraBottomSlot: (@Composable ColumnScope.() -> Unit)?,
|
||||
) {
|
||||
val textWrapper: @Composable (Modifier) -> Unit = { textModifier ->
|
||||
MessageBannerTextWrapper(
|
||||
modifier = textModifier,
|
||||
title = title,
|
||||
description = description,
|
||||
contentAlign = contentAlign,
|
||||
extraBottomSlot = extraBottomSlot,
|
||||
)
|
||||
}
|
||||
when (contentAlign) {
|
||||
TangemMessageBanner.ContentAlign.Start -> Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||
) {
|
||||
slotStart?.let { slot -> Box(modifier = Modifier.align(Alignment.Top)) { slot() } }
|
||||
textWrapper(Modifier.weight(1f).align(Alignment.Top))
|
||||
slotEnd?.let { slot -> Box(modifier = Modifier.align(Alignment.Top)) { slot() } }
|
||||
}
|
||||
TangemMessageBanner.ContentAlign.Center -> Box(modifier = Modifier.fillMaxWidth()) {
|
||||
textWrapper(Modifier.fillMaxWidth())
|
||||
slotStart?.let { slot -> Box(modifier = Modifier.align(Alignment.TopStart)) { slot() } }
|
||||
slotEnd?.let { slot -> Box(modifier = Modifier.align(Alignment.TopEnd)) { slot() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBannerTextWrapper(
|
||||
title: TextReference,
|
||||
description: TextReference?,
|
||||
contentAlign: TangemMessageBanner.ContentAlign,
|
||||
extraBottomSlot: (@Composable ColumnScope.() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val isCenter = contentAlign == TangemMessageBanner.ContentAlign.Center
|
||||
val textAlign = if (isCenter) TextAlign.Center else TextAlign.Start
|
||||
Column(
|
||||
modifier = if (isCenter) modifier.padding(horizontal = 32.dp) else modifier,
|
||||
horizontalAlignment = if (isCenter) Alignment.CenterHorizontally else Alignment.Start,
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = title.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
textAlign = textAlign,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
if (description != null) {
|
||||
Text(
|
||||
text = description.resolveAnnotatedReference(),
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
textAlign = textAlign,
|
||||
)
|
||||
}
|
||||
extraBottomSlot?.let { slot ->
|
||||
Column(modifier = Modifier.padding(top = 12.dp)) { slot() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MessageBannerButtons(
|
||||
secondaryButton: TangemMessageBanner.Button?,
|
||||
primaryButton: TangemMessageBanner.Button?,
|
||||
) {
|
||||
if (secondaryButton == null && primaryButton == null) return
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
secondaryButton?.let { button ->
|
||||
TangemButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
variant = TangemButton.Variant.Secondary,
|
||||
text = button.text,
|
||||
iconStart = button.iconStart,
|
||||
iconEnd = button.iconEnd,
|
||||
isEnabled = button.isEnabled,
|
||||
isLoading = button.isLoading,
|
||||
onClick = button.onClick,
|
||||
)
|
||||
}
|
||||
primaryButton?.let { button ->
|
||||
TangemButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
variant = TangemButton.Variant.Primary,
|
||||
text = button.text,
|
||||
iconStart = button.iconStart,
|
||||
iconEnd = button.iconEnd,
|
||||
isEnabled = button.isEnabled,
|
||||
isLoading = button.isLoading,
|
||||
onClick = button.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Public API surface of [TangemMessageBanner]. */
|
||||
object TangemMessageBanner {
|
||||
|
||||
/** Visual appearance — background color + glow ring color. */
|
||||
enum class Variant {
|
||||
/** Neutral opaque background with a multi-color "magic" glow ring. */
|
||||
Default,
|
||||
|
||||
/** Neutral tertiary (filled) background with a multi-color "magic" glow ring. */
|
||||
Solid,
|
||||
|
||||
/** Subtle success-green background and matching glow ring. */
|
||||
Success,
|
||||
|
||||
/** Subtle error-red background and matching glow ring. */
|
||||
Error,
|
||||
|
||||
/** Subtle warning-yellow background and matching glow ring. */
|
||||
Warning,
|
||||
|
||||
/** Subtle info-blue background and matching glow ring. */
|
||||
Info,
|
||||
}
|
||||
|
||||
/** Horizontal alignment of the text block. */
|
||||
enum class ContentAlign {
|
||||
Start,
|
||||
Center,
|
||||
}
|
||||
|
||||
/** An action button shown in the banner's button row. */
|
||||
@Immutable
|
||||
data class Button(
|
||||
val text: TextReference,
|
||||
val onClick: () -> Unit,
|
||||
val iconStart: TangemIconUM? = null,
|
||||
val iconEnd: TangemIconUM? = null,
|
||||
val isEnabled: Boolean = true,
|
||||
val isLoading: Boolean = false,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismiss-button preset for [TangemMessageBanner] — a filled cross-circle to pass as `slotEnd`.
|
||||
*
|
||||
* @param contentDescription Accessibility label announced by TalkBack (e.g. `"Dismiss"`).
|
||||
*/
|
||||
@Composable
|
||||
fun TangemMessageBanner.CloseButton(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.ic_cross_circle_20_filled,
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.secondary,
|
||||
modifier = modifier
|
||||
.size(20.dp)
|
||||
.clip(RoundedCornerShape(percent = 50))
|
||||
.clickableSingle(onClick = onClick)
|
||||
.semantics {
|
||||
role = Role.Button
|
||||
contentDescription?.let { this.contentDescription = it }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
/** Resolved appearance tokens for a [TangemMessageBanner.Variant]. */
|
||||
private data class MessageBannerTokens(val background: Color, val glowRing: TangemGlowRing.Variant)
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun TangemMessageBanner.Variant.tokens(): MessageBannerTokens {
|
||||
val colors = TangemTheme.colors3
|
||||
return when (this) {
|
||||
TangemMessageBanner.Variant.Default -> MessageBannerTokens(
|
||||
background = colors.bg.opaque.primary,
|
||||
glowRing = TangemGlowRing.Variant.Magic,
|
||||
)
|
||||
TangemMessageBanner.Variant.Solid -> MessageBannerTokens(
|
||||
background = colors.bg.tertiary,
|
||||
glowRing = TangemGlowRing.Variant.Magic,
|
||||
)
|
||||
TangemMessageBanner.Variant.Success -> MessageBannerTokens(
|
||||
background = colors.bg.status.successSubtle,
|
||||
glowRing = TangemGlowRing.Variant.Success,
|
||||
)
|
||||
TangemMessageBanner.Variant.Error -> MessageBannerTokens(
|
||||
background = colors.bg.status.errorSubtle,
|
||||
glowRing = TangemGlowRing.Variant.Error,
|
||||
)
|
||||
TangemMessageBanner.Variant.Warning -> MessageBannerTokens(
|
||||
background = colors.bg.status.warningSubtle,
|
||||
glowRing = TangemGlowRing.Variant.Warning,
|
||||
)
|
||||
TangemMessageBanner.Variant.Info -> MessageBannerTokens(
|
||||
background = colors.bg.status.infoSubtle,
|
||||
glowRing = TangemGlowRing.Variant.Info,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Light", showBackground = true)
|
||||
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||
@Composable
|
||||
private fun TangemMessageBannerPreview() {
|
||||
PreviewContainer {
|
||||
TangemMessageBanner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
title = stringReference("Title"),
|
||||
description = stringReference("Description"),
|
||||
slotEnd = { TangemMessageBanner.CloseButton(onClick = {}, contentDescription = "Dismiss") },
|
||||
secondaryButton = TangemMessageBanner.Button(text = stringReference("Label"), onClick = {}),
|
||||
primaryButton = TangemMessageBanner.Button(text = stringReference("Label"), onClick = {}),
|
||||
)
|
||||
TangemMessageBanner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
title = stringReference("Invite friends. Earn 10 USDT."),
|
||||
description = stringReference("Share Tangem, give 10% OFF, and earn 10 USDT."),
|
||||
slotStart = {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(percent = 50))
|
||||
.background(TangemTheme.colors3.bg.tertiary),
|
||||
)
|
||||
},
|
||||
primaryButton = TangemMessageBanner.Button(text = stringReference("Invite friends"), onClick = {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Variants Light", showBackground = true)
|
||||
@Preview(name = "Variants Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||
@Composable
|
||||
private fun TangemMessageBannerVariantsPreview() {
|
||||
PreviewContainer {
|
||||
TangemMessageBanner.Variant.entries.forEach { variant ->
|
||||
TangemMessageBanner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
variant = variant,
|
||||
title = stringReference(variant.name),
|
||||
description = stringReference("Description"),
|
||||
secondaryButton = TangemMessageBanner.Button(text = stringReference("Label"), onClick = {}),
|
||||
primaryButton = TangemMessageBanner.Button(text = stringReference("Label"), onClick = {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Preview(name = "Align Light", showBackground = true)
|
||||
@Preview(name = "Align Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||
@Composable
|
||||
private fun TangemMessageBannerContentAlignPreview() {
|
||||
PreviewContainer {
|
||||
TangemMessageBanner.ContentAlign.entries.forEach { align ->
|
||||
TangemMessageBanner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
contentAlign = align,
|
||||
title = stringReference("Content align ${align.name}"),
|
||||
description = stringReference("Share Tangem, give 10% OFF, and earn 10 USDT."),
|
||||
secondaryButton = TangemMessageBanner.Button(text = stringReference("Later"), onClick = {}),
|
||||
primaryButton = TangemMessageBanner.Button(text = stringReference("Invite"), onClick = {}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviewContainer(content: @Composable ColumnScope.() -> Unit) {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import com.tangem.core.ui.ds2.button.TangemButton
|
|||
import com.tangem.core.ui.ds2.fade.TangemFade
|
||||
import com.tangem.core.ui.ds2.glowring.TangemGlowRing
|
||||
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
|
||||
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
|
||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
|
||||
|
|
@ -353,6 +354,40 @@ internal data class TangemGlowRingStory(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
internal data class TangemMessageBannerStory(
|
||||
val variant: TangemMessageBanner.Variant,
|
||||
val contentAlign: TangemMessageBanner.ContentAlign,
|
||||
val hasGlowRing: Boolean,
|
||||
val hasDescription: Boolean,
|
||||
val hasSecondaryButton: Boolean,
|
||||
val hasPrimaryButton: Boolean,
|
||||
val hasCloseButton: Boolean,
|
||||
val hasSlotStart: Boolean,
|
||||
val hasSlotEnd: Boolean,
|
||||
val hasExtraContent: Boolean,
|
||||
val background: Background,
|
||||
val onVariantChange: (TangemMessageBanner.Variant) -> Unit,
|
||||
val onContentAlignChange: (TangemMessageBanner.ContentAlign) -> Unit,
|
||||
val onGlowRingToggle: () -> Unit,
|
||||
val onDescriptionToggle: () -> Unit,
|
||||
val onSecondaryButtonToggle: () -> Unit,
|
||||
val onPrimaryButtonToggle: () -> Unit,
|
||||
val onCloseButtonToggle: () -> Unit,
|
||||
val onSlotStartToggle: () -> Unit,
|
||||
val onSlotEndToggle: () -> Unit,
|
||||
val onExtraContentToggle: () -> Unit,
|
||||
val onBackgroundChange: (Background) -> Unit,
|
||||
) : DsStoryBookPage {
|
||||
|
||||
/** Backdrop the banner preview is rendered on top of. */
|
||||
enum class Background(val label: String) {
|
||||
BgPrimary("bg.primary"),
|
||||
BgSecondary("bg.secondary"),
|
||||
BgInverse("bg.inverse"),
|
||||
}
|
||||
}
|
||||
|
||||
internal data class TangemBadgeV2Story(
|
||||
val variant: TangemBadge.Variant,
|
||||
val status: TangemBadge.Status,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.tangem
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.glowring.tangemGlowRingStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner.tangemMessageBannerStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSearchStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
|
||||
|
|
@ -41,6 +42,7 @@ private fun buildDsStories() = listOf(
|
|||
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
||||
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
|
||||
DsStoryItem(title = "💫 TangemGlowRing", factory = tangemGlowRingStoryFactory),
|
||||
DsStoryItem(title = "📢 TangemMessageBanner", factory = tangemMessageBannerStoryFactory),
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
|
|||
|
|
@ -0,0 +1,37 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner
|
||||
|
||||
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory.Background
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||
|
||||
internal fun StateUpdater<TangemMessageBannerStory>.build(): TangemMessageBannerStory {
|
||||
return TangemMessageBannerStory(
|
||||
variant = TangemMessageBanner.Variant.Default,
|
||||
contentAlign = TangemMessageBanner.ContentAlign.Start,
|
||||
hasGlowRing = true,
|
||||
hasDescription = true,
|
||||
hasSecondaryButton = true,
|
||||
hasPrimaryButton = true,
|
||||
hasCloseButton = true,
|
||||
hasSlotStart = true,
|
||||
hasSlotEnd = true,
|
||||
hasExtraContent = true,
|
||||
background = Background.BgSecondary,
|
||||
onVariantChange = { variant -> updateStory { it.copy(variant = variant) } },
|
||||
onContentAlignChange = { align -> updateStory { it.copy(contentAlign = align) } },
|
||||
onGlowRingToggle = { updateStory { it.copy(hasGlowRing = !it.hasGlowRing) } },
|
||||
onDescriptionToggle = { updateStory { it.copy(hasDescription = !it.hasDescription) } },
|
||||
onSecondaryButtonToggle = { updateStory { it.copy(hasSecondaryButton = !it.hasSecondaryButton) } },
|
||||
onPrimaryButtonToggle = { updateStory { it.copy(hasPrimaryButton = !it.hasPrimaryButton) } },
|
||||
onCloseButtonToggle = { updateStory { it.copy(hasCloseButton = !it.hasCloseButton) } },
|
||||
onSlotStartToggle = { updateStory { it.copy(hasSlotStart = !it.hasSlotStart) } },
|
||||
onSlotEndToggle = { updateStory { it.copy(hasSlotEnd = !it.hasSlotEnd) } },
|
||||
onExtraContentToggle = { updateStory { it.copy(hasExtraContent = !it.hasExtraContent) } },
|
||||
onBackgroundChange = { background -> updateStory { it.copy(background = background) } },
|
||||
)
|
||||
}
|
||||
|
||||
internal val tangemMessageBannerStoryFactory
|
||||
get() = storyPageFactory(StateUpdater<TangemMessageBannerStory>::build)
|
||||
|
|
@ -0,0 +1,325 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
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.Icon
|
||||
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.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds2.messagebanner.CloseButton
|
||||
import com.tangem.core.ui.ds2.messagebanner.TangemMessageBanner
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageBannerStory.Background
|
||||
|
||||
@Composable
|
||||
internal fun TangemMessageBannerStory(state: TangemMessageBannerStory, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Preview stays pinned at the top.
|
||||
ComponentPreview(state = state)
|
||||
// Only the controls scroll.
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
|
||||
ContentAlignSelector(selected = state.contentAlign, onSelect = state.onContentAlignChange)
|
||||
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
|
||||
Toggles(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
|
||||
when (background) {
|
||||
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
|
||||
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
|
||||
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(state: TangemMessageBannerStory) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp)),
|
||||
) {
|
||||
PreviewBackground(background = state.background, modifier = Modifier.matchParentSize())
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
) {
|
||||
PreviewBanner(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviewBanner(state: TangemMessageBannerStory) {
|
||||
TangemMessageBanner(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
variant = state.variant,
|
||||
contentAlign = state.contentAlign,
|
||||
showGlowRing = state.hasGlowRing,
|
||||
title = stringReference("Would you predict?"),
|
||||
description = if (state.hasDescription) {
|
||||
stringReference("France will win FIFA 2026")
|
||||
} else {
|
||||
null
|
||||
},
|
||||
secondaryButton = if (state.hasSecondaryButton) {
|
||||
TangemMessageBanner.Button(text = stringReference("Yes"), onClick = {})
|
||||
} else {
|
||||
null
|
||||
},
|
||||
primaryButton = if (state.hasPrimaryButton) {
|
||||
TangemMessageBanner.Button(text = stringReference("Oh, yes"), onClick = {})
|
||||
} else {
|
||||
null
|
||||
},
|
||||
slotStart = if (state.hasSlotStart) {
|
||||
{ BannerLeadingIcon() }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
slotEnd = when {
|
||||
state.hasCloseButton -> {
|
||||
{ TangemMessageBanner.CloseButton(onClick = {}, contentDescription = "Dismiss") }
|
||||
}
|
||||
state.hasSlotEnd -> {
|
||||
{ CirclePlaceholder(size = 24.dp) }
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
extraBottomSlot = if (state.hasExtraContent) {
|
||||
{ ProtectedByRow() }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BannerLeadingIcon() {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.img_solana_22),
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.clip(RoundedCornerShape(percent = 50)),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ProtectedByRow() {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
Text(
|
||||
text = "Protected by Tangem Security",
|
||||
style = TangemTheme.typography3.body.medium,
|
||||
color = TangemTheme.colors3.text.primary,
|
||||
)
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_shield_check_16),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.status.info,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CirclePlaceholder(size: Dp) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(size)
|
||||
.clip(RoundedCornerShape(percent = 50))
|
||||
.background(TangemTheme.colors3.bg.tertiary),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackgroundSelector(selected: Background, onSelect: (Background) -> Unit) {
|
||||
Section(label = "Background") {
|
||||
ChipGrid(
|
||||
items = Background.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VariantSelector(selected: TangemMessageBanner.Variant, onSelect: (TangemMessageBanner.Variant) -> Unit) {
|
||||
Section(label = "Variant") {
|
||||
ChipGrid(
|
||||
items = TangemMessageBanner.Variant.entries,
|
||||
label = { it.name },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentAlignSelector(
|
||||
selected: TangemMessageBanner.ContentAlign,
|
||||
onSelect: (TangemMessageBanner.ContentAlign) -> Unit,
|
||||
) {
|
||||
Section(label = "Content align") {
|
||||
ChipGrid(
|
||||
items = TangemMessageBanner.ContentAlign.entries,
|
||||
label = { it.name },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Toggles(state: TangemMessageBannerStory) {
|
||||
Section(label = "Flags") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ToggleRow(label = "glowRing", checked = state.hasGlowRing, onToggle = state.onGlowRingToggle)
|
||||
ToggleRow(label = "description", checked = state.hasDescription, onToggle = state.onDescriptionToggle)
|
||||
ToggleRow(
|
||||
label = "secondaryButton",
|
||||
checked = state.hasSecondaryButton,
|
||||
onToggle = state.onSecondaryButtonToggle,
|
||||
)
|
||||
ToggleRow(label = "primaryButton", checked = state.hasPrimaryButton, onToggle = state.onPrimaryButtonToggle)
|
||||
ToggleRow(label = "closeButton", checked = state.hasCloseButton, onToggle = state.onCloseButtonToggle)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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 Color.Transparent,
|
||||
)
|
||||
.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()
|
||||
.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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.checkmark.Tangem
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.glowring.TangemGlowRingStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.messagebanner.TangemMessageBannerStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSearchStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
|
||||
|
|
@ -103,6 +104,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
|||
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
|
||||
is TangemFadeStory -> TangemFadeStory(state = storyState)
|
||||
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
|
||||
is TangemMessageBannerStory -> TangemMessageBannerStory(state = storyState)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue