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

@ -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)
}
}
}