Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-12 19:12:25 +03:00
parent d5554a7f7d
commit e5475e26e1
13 changed files with 1297 additions and 12 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.button.TangemButton
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
import com.tangem.core.ui.ds.message.TangemMessageEffect
import com.tangem.core.ui.ds.topbar.TangemTopBarType
@ -100,4 +101,25 @@ internal data class DsComponentsListStory(
internal data class TangemLoaderStory(
val selectedSize: TangemLoaderSize,
val onSizeChange: (TangemLoaderSize) -> Unit,
) : DsStoryBookPage
internal data class TangemButtonStory(
val variant: TangemButton.Variant,
val size: TangemButton.Size,
val isLoading: Boolean,
val isEnabled: Boolean,
val hasIconStart: Boolean,
val hasIconEnd: Boolean,
val hasText: Boolean,
val isBlurEnabled: Boolean,
val textScale: Float,
val onVariantChange: (TangemButton.Variant) -> Unit,
val onSizeChange: (TangemButton.Size) -> Unit,
val onLoadingToggle: () -> Unit,
val onEnabledToggle: () -> Unit,
val onIconStartToggle: () -> Unit,
val onIconEndToggle: () -> Unit,
val onTextToggle: () -> Unit,
val onBlurToggle: () -> Unit,
val onTextScaleChange: (Float) -> Unit,
) : DsStoryBookPage

View file

@ -15,12 +15,14 @@ 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.button.tangemButtonStoryFactory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
private fun buildDsStories() = listOf(
DsStoryItem(title = "⏳ TangemLoader", factory = tangemLoaderStoryFactory),
DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory),
)
@Composable

View file

@ -0,0 +1,50 @@
package com.tangem.feature.tester.presentation.storybook.page.ds.button
import com.tangem.core.ui.ds2.button.TangemButton
import com.tangem.feature.tester.presentation.storybook.entity.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
internal fun StateUpdater<TangemButtonStory>.build(): TangemButtonStory {
return TangemButtonStory(
variant = TangemButton.Variant.Primary,
size = TangemButton.Size.X10,
isLoading = false,
isEnabled = true,
hasIconStart = false,
hasIconEnd = false,
hasText = true,
isBlurEnabled = true,
textScale = 1f,
onVariantChange = { variant ->
updateStory { it.copy(variant = variant) }
},
onSizeChange = { size ->
updateStory { it.copy(size = size) }
},
onLoadingToggle = {
updateStory { it.copy(isLoading = !it.isLoading) }
},
onEnabledToggle = {
updateStory { it.copy(isEnabled = !it.isEnabled) }
},
onIconStartToggle = {
updateStory { it.copy(hasIconStart = !it.hasIconStart) }
},
onIconEndToggle = {
updateStory { it.copy(hasIconEnd = !it.hasIconEnd) }
},
onTextToggle = {
updateStory { it.copy(hasText = !it.hasText) }
},
onBlurToggle = {
updateStory { it.copy(isBlurEnabled = !it.isBlurEnabled) }
},
onTextScaleChange = { scale ->
updateStory { it.copy(textScale = scale) }
},
)
}
internal val tangemButtonStoryFactory
get() = storyPageFactory(StateUpdater<TangemButtonStory>::build)

View file

@ -0,0 +1,305 @@
@file:Suppress("MagicNumber")
package com.tangem.feature.tester.presentation.storybook.page.ds.button
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.tween
import androidx.compose.animation.core.animateFloat
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
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.SideEffect
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.button.TangemButton
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
@Composable
internal fun TangemButtonStory(state: TangemButtonStory, modifier: Modifier = Modifier) {
val hazeState = LocalHazeState.current
SideEffect { hazeState.blurEnabled = state.isBlurEnabled }
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)
SizeSelector(selected = state.size, onSelect = state.onSizeChange)
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
)
}
// Hard-edged stripes — sharp seams make the blur visually obvious.
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 = "blur-bg")
val offset by transition.animateFloat(
initialValue = 0f,
targetValue = tilePx,
animationSpec = infiniteRepeatable(
animation = tween(durationMillis = 4_000, easing = LinearEasing),
repeatMode = RepeatMode.Restart,
),
label = "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 ComponentPreview(state: TangemButtonStory) {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
.clip(RoundedCornerShape(16.dp)),
) {
BlurTestBackground(
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) {
TangemButton(
variant = state.variant,
size = state.size,
isLoading = state.isLoading,
isEnabled = state.isEnabled,
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
},
text = if (state.hasText) stringReference("Button") else null,
onClick = {},
)
}
}
}
}
@Composable
private fun VariantSelector(selected: TangemButton.Variant, onSelect: (TangemButton.Variant) -> Unit) {
Section(label = "Variant") {
ChipGrid(
items = TangemButton.Variant.entries,
label = { it.name },
isSelected = { it == selected },
onSelect = onSelect,
)
}
}
@Composable
private fun SizeSelector(selected: TangemButton.Size, onSelect: (TangemButton.Size) -> Unit) {
Section(label = "Size") {
ChipGrid(
items = TangemButton.Size.entries,
label = { it.name },
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: TangemButtonStory) {
Section(label = "Flags") {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
ToggleRow(label = "isLoading", checked = state.isLoading, onToggle = state.onLoadingToggle)
ToggleRow(label = "isEnabled", checked = state.isEnabled, onToggle = state.onEnabledToggle)
ToggleRow(label = "iconStart", checked = state.hasIconStart, onToggle = state.onIconStartToggle)
ToggleRow(label = "iconEnd", checked = state.hasIconEnd, onToggle = state.onIconEndToggle)
ToggleRow(label = "text", checked = state.hasText, onToggle = state.onTextToggle)
ToggleRow(label = "blur", checked = state.isBlurEnabled, onToggle = state.onBlurToggle)
}
}
}
@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

@ -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.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM
import com.tangem.feature.tester.presentation.storybook.entity.StoryList
import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory
@ -31,6 +32,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.button.TangemButtonStory
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
import com.tangem.feature.tester.presentation.storybook.page.opportunities.OpportunitiesBGStory
import com.tangem.feature.tester.presentation.storybook.page.checkbox.TangemCheckboxStory
@ -79,6 +81,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
DeviceIconStory -> DeviceIconStory()
is DsComponentsListStory -> DsComponentsListStory(state = storyState)
is TangemLoaderStory -> TangemLoaderStory(state = storyState)
is TangemButtonStory -> TangemButtonStory(state = storyState)
}
}
}