Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-14 13:24:56 +03:00
parent a9b5490cc4
commit f58a422765
10 changed files with 888 additions and 6 deletions

View file

@ -24,6 +24,20 @@ internal fun ProvideHaze(content: @Composable () -> Unit) {
}
}
/**
* Returns whether the haze blur effect would actually render for the given [state], taking both
* the global [HazeState.blurEnabled] flag and the device's power-saving mode into account.
*
* Callers that pass a fully-transparent fallback to [hazeEffectTangem] should use this to decide
* whether they need to render an opaque fallback layer themselves otherwise the surface can
* become invisible whenever blur is disabled (e.g. while power-saving mode is on).
*/
@Composable
fun isHazeBlurEffectivelyEnabled(state: HazeState = LocalHazeState.current): Boolean {
val isPowerSavingEnabled by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
return state.blurEnabled && !isPowerSavingEnabled
}
/**
* Applies a haze effect to the [Modifier] with consideration of global haze settings and power saving mode.
*
@ -36,8 +50,7 @@ fun Modifier.hazeEffectTangem(
style: HazeStyle = HazeStyle.Unspecified,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val isGlobalBlurEnabled = state.blurEnabled && !powerSavingEnabled.value
val isGlobalBlurEnabled = isHazeBlurEffectivelyEnabled(state)
val rootBackground by LocalRootBackgroundColor.current
return hazeEffect(state, style) {

View file

@ -0,0 +1,405 @@
package com.tangem.core.ui.ds2.badge
import androidx.compose.foundation.BorderStroke
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.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
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.TextStyle
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.surface.TangemSurface
import com.tangem.core.ui.extensions.ColorReference2
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
/**
* Design-system v2 badge: a compact pill displaying a short label with optional leading / trailing
* icons.
*
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=2002-213)
*
* Behavior notes:
* - Shape is always a fully-rounded pill (`borderRadius.full`).
* - Icon tints are driven by [variant] + [status]; any tint set on a supplied [TangemIconUM.Icon]
* is overridden. Other [TangemIconUM] subtypes (e.g. currency / image / url) pass through with
* their own colors intact.
* - The badge is non-interactive by default. Pass [onClick] to make it clickable.
*
* @param text Badge label.
* @param modifier Modifier applied to the badge container.
* @param variant Visual style. See [TangemBadge.Variant].
* @param status Status color scheme (Neutral / Info / Error / Success / Warning).
* @param size Token-driven size preset controlling height, padding, icon size and text style.
* See [TangemBadge.Size].
* @param iconStart Optional leading icon.
* @param iconEnd Optional trailing icon.
* @param contentDescription Accessibility label announced by TalkBack. When non-null it overrides
* the label text for screen readers.
* @param onClick Optional click handler. `null` makes the badge non-interactive.
*/
@Suppress("LongParameterList")
@Composable
fun TangemBadge(
text: TextReference,
modifier: Modifier = Modifier,
variant: TangemBadge.Variant = TangemBadge.Variant.Tinted,
status: TangemBadge.Status = TangemBadge.Status.Neutral,
size: TangemBadge.Size = TangemBadge.Size.X9,
iconStart: TangemIconUM? = null,
iconEnd: TangemIconUM? = null,
contentDescription: String? = null,
onClick: (() -> Unit)? = null,
) {
val colorTokens = resolveColorTokens(variant = variant, status = status)
val sizeTokens = size.tokens()
TangemSurface(
modifier = modifier
.semantics(mergeDescendants = true) {
if (onClick != null) role = Role.Button
contentDescription?.let { this.contentDescription = it }
}
.heightIn(min = sizeTokens.minHeight),
onClick = onClick,
color = colorTokens.backgroundColor,
border = colorTokens.borderColor?.let { BorderStroke(TangemTheme.dimens3.borderWidth.sm, it) },
shape = RoundedCornerShape(TangemTheme.dimens3.borderRadius.full),
) {
BadgeContent(
iconStart = iconStart,
iconEnd = iconEnd,
text = text,
colorTokens = colorTokens,
sizeTokens = sizeTokens,
)
}
}
@Composable
private fun BadgeContent(
iconStart: TangemIconUM?,
iconEnd: TangemIconUM?,
text: TextReference,
colorTokens: BadgeColorTokens,
sizeTokens: BadgeSizeTokens,
) {
Row(
modifier = Modifier
.heightIn(min = sizeTokens.minHeight)
.padding(
horizontal = sizeTokens.containerHorizontalPadding,
vertical = sizeTokens.containerVerticalPadding,
),
verticalAlignment = Alignment.CenterVertically,
) {
iconStart?.let { icon ->
TangemIcon(
modifier = Modifier.size(sizeTokens.iconSize),
tangemIconUM = icon.applyTint(colorTokens.iconTint),
)
}
Text(
modifier = Modifier.padding(horizontal = sizeTokens.labelPadding),
text = text.resolveReference(),
color = colorTokens.textColor,
style = sizeTokens.textStyle,
maxLines = 1,
softWrap = false,
overflow = TextOverflow.Ellipsis,
)
iconEnd?.let { icon ->
TangemIcon(
modifier = Modifier.size(sizeTokens.iconSize),
tangemIconUM = icon.applyTint(colorTokens.iconTint),
)
}
}
}
/**
* Forces [tint] onto [TangemIconUM.Icon] so the badge's variant always drives icon color. Other
* icon types (currency, image, url) pass through unchanged so they keep their own visuals.
*/
private fun TangemIconUM.applyTint(tint: Color): TangemIconUM = when (this) {
is TangemIconUM.Icon -> copy(tint = ColorReference2 { tint })
else -> this
}
object TangemBadge {
/**
* Visual style of the badge.
*
* - [Tinted] soft tinted fill (subtle status color or opaque neutral), no border.
* - [Outline] tinted fill with a matching border.
* - [Solid] saturated status color fill with static-dark content.
*/
enum class Variant {
Tinted,
Outline,
Solid,
}
/** Status color scheme of the badge. */
enum class Status {
Neutral,
Info,
Error,
Success,
Warning,
}
/**
* Size preset. Names follow the design-system size scale: X9 is the largest (min height 36dp),
* X4 is the smallest (min height 16dp).
*/
enum class Size {
X9,
X6,
X4,
}
}
/** Resolved colors for a (variant, status) pair. */
private data class BadgeColorTokens(
val backgroundColor: Color,
val textColor: Color,
val iconTint: Color,
val borderColor: Color? = null,
)
/** Resolved per-size dimensions used by [TangemBadge]. */
private data class BadgeSizeTokens(
val minHeight: Dp,
val containerHorizontalPadding: Dp,
val containerVerticalPadding: Dp,
val labelPadding: Dp,
val iconSize: Dp,
val textStyle: TextStyle,
)
@Composable
@ReadOnlyComposable
private fun TangemBadge.Size.tokens(): BadgeSizeTokens {
val dimens = TangemTheme.dimens3
val typography = TangemTheme.typography3
return when (this) {
TangemBadge.Size.X9 -> BadgeSizeTokens(
minHeight = dimens.size.s450,
containerHorizontalPadding = dimens.spacing.s100,
containerVerticalPadding = dimens.spacing.s100,
labelPadding = dimens.spacing.s050,
iconSize = 20.dp,
textStyle = typography.subheading.medium,
)
TangemBadge.Size.X6 -> BadgeSizeTokens(
minHeight = dimens.size.s300,
containerHorizontalPadding = dimens.spacing.s050,
containerVerticalPadding = dimens.spacing.s050,
labelPadding = dimens.spacing.s050,
iconSize = 16.dp,
textStyle = typography.caption.medium,
)
TangemBadge.Size.X4 -> BadgeSizeTokens(
minHeight = dimens.size.s200,
containerHorizontalPadding = dimens.spacing.s025,
containerVerticalPadding = dimens.spacing.none,
labelPadding = dimens.spacing.s025,
iconSize = 12.dp,
textStyle = typography.caption.medium,
)
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable
@ReadOnlyComposable
private fun resolveColorTokens(variant: TangemBadge.Variant, status: TangemBadge.Status): BadgeColorTokens {
val colors = TangemTheme.colors3
return when (variant) {
TangemBadge.Variant.Tinted -> when (status) {
TangemBadge.Status.Neutral -> BadgeColorTokens(
backgroundColor = colors.bg.opaque.primary,
textColor = colors.text.secondary,
iconTint = colors.icon.secondary,
)
TangemBadge.Status.Info -> BadgeColorTokens(
backgroundColor = colors.bg.status.infoSubtle,
textColor = colors.text.status.info,
iconTint = colors.icon.status.info,
)
TangemBadge.Status.Error -> BadgeColorTokens(
backgroundColor = colors.bg.status.errorSubtle,
textColor = colors.text.status.error,
iconTint = colors.icon.status.error,
)
TangemBadge.Status.Success -> BadgeColorTokens(
backgroundColor = colors.bg.status.successSubtle,
textColor = colors.text.status.success,
iconTint = colors.icon.status.success,
)
TangemBadge.Status.Warning -> BadgeColorTokens(
backgroundColor = colors.bg.status.warningSubtle,
textColor = colors.text.status.warning,
iconTint = colors.icon.status.warning,
)
}
TangemBadge.Variant.Outline -> when (status) {
TangemBadge.Status.Neutral -> BadgeColorTokens(
backgroundColor = colors.bg.opaque.primary,
textColor = colors.text.secondary,
iconTint = colors.icon.secondary,
borderColor = colors.border.primary,
)
TangemBadge.Status.Info -> BadgeColorTokens(
backgroundColor = colors.bg.status.infoSubtle,
textColor = colors.text.status.info,
iconTint = colors.icon.status.info,
borderColor = colors.border.status.infoSubtle,
)
TangemBadge.Status.Error -> BadgeColorTokens(
backgroundColor = colors.bg.status.errorSubtle,
textColor = colors.text.status.error,
iconTint = colors.icon.status.error,
borderColor = colors.border.status.errorSubtle,
)
TangemBadge.Status.Success -> BadgeColorTokens(
backgroundColor = colors.bg.status.successSubtle,
textColor = colors.text.status.success,
iconTint = colors.icon.status.success,
borderColor = colors.border.status.successSubtle,
)
TangemBadge.Status.Warning -> BadgeColorTokens(
backgroundColor = colors.bg.status.warningSubtle,
textColor = colors.text.status.warning,
iconTint = colors.icon.status.warning,
borderColor = colors.border.status.warningSubtle,
)
}
TangemBadge.Variant.Solid -> when (status) {
TangemBadge.Status.Neutral -> BadgeColorTokens(
backgroundColor = colors.bg.opaque.primary,
textColor = colors.text.primary,
iconTint = colors.icon.primary,
)
TangemBadge.Status.Info -> BadgeColorTokens(
backgroundColor = colors.bg.status.info,
textColor = colors.text.staticDark.primary,
iconTint = colors.icon.staticDark,
)
TangemBadge.Status.Error -> BadgeColorTokens(
backgroundColor = colors.bg.status.error,
textColor = colors.text.staticDark.primary,
iconTint = colors.icon.staticDark,
)
TangemBadge.Status.Success -> BadgeColorTokens(
backgroundColor = colors.bg.status.success,
textColor = colors.text.staticDark.primary,
iconTint = colors.icon.staticDark,
)
TangemBadge.Status.Warning -> BadgeColorTokens(
backgroundColor = colors.bg.status.warning,
textColor = colors.text.staticDark.primary,
iconTint = colors.icon.staticDark,
)
}
}
}
// region Previews
@Preview(name = "Light", showBackground = true)
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
@Composable
private fun TangemBadgePreview() {
TangemThemePreviewRedesign {
Column(
modifier = Modifier
.background(TangemTheme.colors3.bg.primary)
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
TangemBadge.Variant.entries.forEach { variant ->
PreviewVariantBlock(variant = variant)
}
PreviewSizesBlock()
}
}
}
@Composable
private fun PreviewVariantBlock(variant: TangemBadge.Variant) {
val icon = remember { TangemIconUM.Icon(iconRes = R.drawable.ic_information_24) }
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = variant.name,
color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.body.medium,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemBadge.Status.entries.forEach { status ->
TangemBadge(
text = stringReference(status.name),
variant = variant,
status = status,
iconStart = icon,
)
}
}
}
}
@Composable
private fun PreviewSizesBlock() {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
modifier = Modifier.widthIn(min = 72.dp),
text = "Sizes (Tinted / Info)",
color = TangemTheme.colors3.text.secondary,
style = TangemTheme.typography3.body.medium,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
TangemBadge.Size.entries.forEach { size ->
TangemBadge(
text = stringReference(size.name),
variant = TangemBadge.Variant.Tinted,
status = TangemBadge.Status.Info,
size = size,
)
}
}
}
}
// endregion

View file

@ -25,9 +25,9 @@ import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.isHazeBlurEffectivelyEnabled
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.TangemTheme
import dev.chrisbanes.haze.HazeStyle
import dev.chrisbanes.haze.HazeTint
@ -133,9 +133,15 @@ private fun Modifier.materialBorder(shape: Shape): Modifier = border(
*
* When the haze state is enabled, paints a haze-blurred backdrop. When disabled, layers two
* solid colors so the result still reads as "tinted fill" instead of going transparent.
*
* Uses [isHazeBlurEffectivelyEnabled] (rather than [LocalHazeState]'s `blurEnabled` directly) so
* the solid fallback is also applied when blur is suppressed for reasons other than the haze flag
* most notably when the device is in power-saving mode. Otherwise the haze modifier's
* `fallbackTint = HazeTint(Color.Transparent)` would leave the surface fully transparent.
*/
@Composable
private fun Modifier.materialFill(): Modifier {
val isBlurEnabled = isHazeBlurEffectivelyEnabled()
val hazed = hazeEffectTangem(
style = HazeStyle(
backgroundColor = TangemTheme.colors3.material.fill.blur,
@ -145,7 +151,7 @@ private fun Modifier.materialFill(): Modifier {
) {
fallbackTint = HazeTint(Color.Transparent)
}
return hazed.conditionalCompose(!LocalHazeState.current.blurEnabled) {
return hazed.conditionalCompose(!isBlurEnabled) {
// Paint the opaque fill first, then layer the translucent tint on top so both are visible.
background(TangemTheme.colors3.material.fill.solid)
.background(TangemTheme.colors3.material.tint.solid)

View file

@ -2,6 +2,7 @@ package com.tangem.feature.tester.presentation.storybook.entity
import com.tangem.core.ui.ds.badge.TangemBadgeColor
import com.tangem.core.ui.ds.field.search.TangemFieldShape
import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
import com.tangem.core.ui.ds.message.TangemMessageEffect
@ -106,6 +107,7 @@ internal data class TangemLoaderStory(
internal data class TangemButtonStory(
val variant: TangemButton.Variant,
val size: TangemButton.Size,
val background: Background,
val isLoading: Boolean,
val isEnabled: Boolean,
val hasIconStart: Boolean,
@ -115,6 +117,7 @@ internal data class TangemButtonStory(
val textScale: Float,
val onVariantChange: (TangemButton.Variant) -> Unit,
val onSizeChange: (TangemButton.Size) -> Unit,
val onBackgroundChange: (Background) -> Unit,
val onLoadingToggle: () -> Unit,
val onEnabledToggle: () -> Unit,
val onIconStartToggle: () -> Unit,
@ -122,4 +125,41 @@ internal data class TangemButtonStory(
val onTextToggle: () -> Unit,
val onBlurToggle: () -> Unit,
val onTextScaleChange: (Float) -> Unit,
) : DsStoryBookPage
) : DsStoryBookPage {
/** Backdrop the button preview is rendered on top of. */
enum class Background(val label: String) {
Rainbow("rainbow"),
BgPrimary("bg.primary"),
BgSecondary("bg.secondary"),
BgBrand("bg.brand"),
BgInverse("bg.inverse"),
}
}
internal data class TangemBadgeV2Story(
val variant: TangemBadge.Variant,
val status: TangemBadge.Status,
val size: TangemBadge.Size,
val background: Background,
val hasIconStart: Boolean,
val hasIconEnd: Boolean,
val textScale: Float,
val onVariantChange: (TangemBadge.Variant) -> Unit,
val onStatusChange: (TangemBadge.Status) -> Unit,
val onSizeChange: (TangemBadge.Size) -> Unit,
val onBackgroundChange: (Background) -> Unit,
val onIconStartToggle: () -> Unit,
val onIconEndToggle: () -> Unit,
val onTextScaleChange: (Float) -> Unit,
) : DsStoryBookPage {
/** Backdrop the badge preview is rendered on top of. */
enum class Background(val label: String) {
Rainbow("rainbow"),
BgPrimary("bg.primary"),
BgSecondary("bg.secondary"),
BgBrand("bg.brand"),
BgInverse("bg.inverse"),
}
}

View file

@ -15,6 +15,7 @@ import com.tangem.core.ui.components.PrimaryButton
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.DsComponentsListStory
import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.badge.tangemBadgeV2StoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemButtonStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
@ -23,6 +24,7 @@ private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
private fun buildDsStories() = listOf(
DsStoryItem(title = "⏳ TangemLoader", factory = tangemLoaderStoryFactory),
DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory),
DsStoryItem(title = "🏷️ TangemBadge", factory = tangemBadgeV2StoryFactory),
)
@Composable

View file

@ -0,0 +1,42 @@
package com.tangem.feature.tester.presentation.storybook.page.ds.badge
import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeV2Story
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<TangemBadgeV2Story>.build(): TangemBadgeV2Story {
return TangemBadgeV2Story(
variant = TangemBadge.Variant.Tinted,
status = TangemBadge.Status.Info,
size = TangemBadge.Size.X9,
background = TangemBadgeV2Story.Background.BgPrimary,
hasIconStart = false,
hasIconEnd = false,
textScale = 1f,
onVariantChange = { variant ->
updateStory { it.copy(variant = variant) }
},
onStatusChange = { status ->
updateStory { it.copy(status = status) }
},
onSizeChange = { size ->
updateStory { it.copy(size = size) }
},
onBackgroundChange = { background ->
updateStory { it.copy(background = background) }
},
onIconStartToggle = {
updateStory { it.copy(hasIconStart = !it.hasIconStart) }
},
onIconEndToggle = {
updateStory { it.copy(hasIconEnd = !it.hasIconEnd) }
},
onTextScaleChange = { scale ->
updateStory { it.copy(textScale = scale) }
},
)
}
internal val tangemBadgeV2StoryFactory
get() = storyPageFactory(StateUpdater<TangemBadgeV2Story>::build)

View file

@ -0,0 +1,341 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.ds.badge
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
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.shape.RoundedCornerShape
import androidx.compose.material3.Slider
import androidx.compose.material3.SliderDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.TileMode
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.image.TangemIconUM
import com.tangem.core.ui.ds2.badge.TangemBadge
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeV2Story
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeV2Story.Background
@Composable
internal fun TangemBadgeV2Story(state: TangemBadgeV2Story, modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.background(TangemTheme.colors.background.primary)
.padding(vertical = 16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
ComponentPreview(state = state)
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
StatusSelector(selected = state.status, onSelect = state.onStatusChange)
SizeSelector(selected = state.size, onSelect = state.onSizeChange)
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
TextScaleSlider(value = state.textScale, onChange = state.onTextScaleChange)
Toggles(state = state)
}
}
@Composable
private fun BlurTestBackground(modifier: Modifier = Modifier) {
val bands = remember {
listOf(
Color(0xFFFF1744), // red
Color(0xFFFF9100), // orange
Color(0xFFFFEA00), // yellow
Color(0xFF00E676), // green
Color(0xFF00B8D4), // cyan
Color(0xFF2962FF), // blue
Color(0xFFD500F9), // magenta
)
}
val stops = remember(bands) {
buildList {
bands.forEachIndexed { index, color ->
val start = index.toFloat() / bands.size
val end = (index + 1).toFloat() / bands.size
add(start to color)
add(end to color)
}
}.toTypedArray()
}
val tilePx = with(LocalDensity.current) { 320.dp.toPx() }
val transition = rememberInfiniteTransition(label = "badge-blur-bg")
val offset by transition.animateFloat(
initialValue = 0f,
targetValue = tilePx,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4_000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "badge-blur-bg-offset",
)
Box(
modifier = modifier.background(
brush = Brush.linearGradient(
colorStops = stops,
start = Offset(offset, 0f),
end = Offset(offset + tilePx, 0f),
tileMode = TileMode.Repeated,
),
),
)
}
@Composable
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
when (background) {
Background.Rainbow -> BlurTestBackground(modifier = modifier)
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
Background.BgBrand -> Box(modifier.background(TangemTheme.colors3.bg.brand))
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
}
}
@Composable
private fun ComponentPreview(state: TangemBadgeV2Story) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(16.dp)),
) {
PreviewBackground(
background = state.background,
modifier = Modifier
.matchParentSize()
.hazeSourceTangem(zIndex = 0f),
)
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 32.dp),
) {
val baseDensity = LocalDensity.current
val scaledDensity = remember(baseDensity, state.textScale) {
Density(density = baseDensity.density, fontScale = state.textScale)
}
CompositionLocalProvider(LocalDensity provides scaledDensity) {
TangemBadge(
text = stringReference("Label"),
variant = state.variant,
status = state.status,
size = state.size,
iconStart = if (state.hasIconStart) {
TangemIconUM.Icon(iconRes = R.drawable.ic_information_24)
} else {
null
},
iconEnd = if (state.hasIconEnd) {
TangemIconUM.Icon(iconRes = R.drawable.ic_information_24)
} else {
null
},
)
}
}
}
}
@Composable
private fun VariantSelector(selected: TangemBadge.Variant, onSelect: (TangemBadge.Variant) -> Unit) {
Section(label = "Variant") {
ChipGrid(
items = TangemBadge.Variant.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun StatusSelector(selected: TangemBadge.Status, onSelect: (TangemBadge.Status) -> Unit) {
Section(label = "Status") {
ChipGrid(
items = TangemBadge.Status.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun SizeSelector(selected: TangemBadge.Size, onSelect: (TangemBadge.Size) -> Unit) {
Section(label = "Size") {
ChipGrid(
items = TangemBadge.Size.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@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 TextScaleSlider(value: Float, onChange: (Float) -> Unit) {
Section(label = "Text scale: ${"%.2f".format(value)}x") {
Slider(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
value = value,
onValueChange = onChange,
valueRange = 0.5f..2f,
steps = 14,
colors = SliderDefaults.colors(
thumbColor = TangemTheme.colors.text.accent,
activeTrackColor = TangemTheme.colors.text.accent,
activeTickColor = TangemTheme.colors2.surface.level3,
inactiveTrackColor = TangemTheme.colors2.surface.level3,
inactiveTickColor = TangemTheme.colors.text.accent,
),
)
}
}
@Composable
private fun Toggles(state: TangemBadgeV2Story) {
Section(label = "Flags") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ToggleRow(label = "iconStart", checked = state.hasIconStart, onToggle = state.onIconStartToggle)
ToggleRow(label = "iconEnd", checked = state.hasIconEnd, onToggle = state.onIconEndToggle)
}
}
}
@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()
.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,
)
}
}

View file

@ -9,6 +9,7 @@ internal fun StateUpdater<TangemButtonStory>.build(): TangemButtonStory {
return TangemButtonStory(
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X10,
background = TangemButtonStory.Background.Rainbow,
isLoading = false,
isEnabled = true,
hasIconStart = false,
@ -22,6 +23,9 @@ internal fun StateUpdater<TangemButtonStory>.build(): TangemButtonStory {
onSizeChange = { size ->
updateStory { it.copy(size = size) }
},
onBackgroundChange = { background ->
updateStory { it.copy(background = background) }
},
onLoadingToggle = {
updateStory { it.copy(isLoading = !it.isLoading) }
},

View file

@ -37,6 +37,7 @@ import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.TangemTheme
import com.tangem.feature.tester.presentation.storybook.entity.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemButtonStory.Background
@Composable
internal fun TangemButtonStory(state: TangemButtonStory, modifier: Modifier = Modifier) {
@ -52,6 +53,7 @@ internal fun TangemButtonStory(state: TangemButtonStory, modifier: Modifier = Mo
ComponentPreview(state = state)
VariantSelector(selected = state.variant, onSelect = state.onVariantChange)
SizeSelector(selected = state.size, onSelect = state.onSizeChange)
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
TextScaleSlider(value = state.textScale, onChange = state.onTextScaleChange)
Toggles(state = state)
}
@ -104,6 +106,17 @@ private fun BlurTestBackground(modifier: Modifier = Modifier) {
)
}
@Composable
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
when (background) {
Background.Rainbow -> BlurTestBackground(modifier = modifier)
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
Background.BgBrand -> Box(modifier.background(TangemTheme.colors3.bg.brand))
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
}
}
@Composable
private fun ComponentPreview(state: TangemButtonStory) {
Box(
@ -113,7 +126,8 @@ private fun ComponentPreview(state: TangemButtonStory) {
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(16.dp)),
) {
BlurTestBackground(
PreviewBackground(
background = state.background,
modifier = Modifier
.matchParentSize()
.hazeSourceTangem(zIndex = 0f),
@ -176,6 +190,18 @@ private fun SizeSelector(selected: TangemButton.Size, onSelect: (TangemButton.Si
}
}
@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 TextScaleSlider(value: Float, onChange: (Float) -> Unit) {
Section(label = "Text scale: ${"%.2f".format(value)}x") {

View file

@ -12,6 +12,7 @@ import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGSt
import com.tangem.feature.tester.presentation.storybook.entity.PlaceholderStory
import com.tangem.feature.tester.presentation.storybook.entity.ProgressIndicatorStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory
import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeV2Story
import com.tangem.feature.tester.presentation.storybook.entity.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM
import com.tangem.feature.tester.presentation.storybook.entity.StoryList
@ -32,6 +33,7 @@ import com.tangem.feature.tester.presentation.storybook.page.badge.TangemBadgeSt
import com.tangem.feature.tester.presentation.storybook.page.buttons.ButtonsStory
import com.tangem.feature.tester.presentation.storybook.page.deviceicon.DeviceIconStory
import com.tangem.feature.tester.presentation.storybook.page.ds.DsComponentsListStory
import com.tangem.feature.tester.presentation.storybook.page.ds.badge.TangemBadgeV2Story
import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory
@ -82,6 +84,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
is DsComponentsListStory -> DsComponentsListStory(state = storyState)
is TangemLoaderStory -> TangemLoaderStory(state = storyState)
is TangemButtonStory -> TangemButtonStory(state = storyState)
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
}
}
}