Updated on 2026-08-14
This commit is contained in:
parent
e018eee87b
commit
f0f5fe96fc
9 changed files with 1019 additions and 43 deletions
468
core/ui/src/main/java/com/tangem/core/ui/ds2/row/TangemRow.kt
Normal file
468
core/ui/src/main/java/com/tangem/core/ui/ds2/row/TangemRow.kt
Normal file
|
|
@ -0,0 +1,468 @@
|
|||
package com.tangem.core.ui.ds2.row
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.foundation.LocalIndication
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.interaction.collectIsFocusedAsState
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.ripple.RippleAlpha
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.LocalRippleConfiguration
|
||||
import androidx.compose.material3.RippleConfiguration
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.ReadOnlyComposable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.drawWithContent
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.Layout
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
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 androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.components.SpacerH
|
||||
import com.tangem.core.ui.extensions.conditionalCompose
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
/** Controls how the title and value sides share horizontal space in a [TangemRow]. */
|
||||
@Immutable
|
||||
enum class TangemRowContentLead { Equal, Start, End }
|
||||
|
||||
/** Vertical alignment between the start slot, content labels, and end slot in a [TangemRow]. */
|
||||
@Immutable
|
||||
enum class TangemRowVerticalAlignment { Top, Center }
|
||||
|
||||
/**
|
||||
* Design-system v2 row: a list-item container with start / end slots, title+subtitle on the
|
||||
* leading side, value+subvalue on the trailing side, and an optional slot below the row.
|
||||
*
|
||||
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=2344-1406)
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* TangemRow(
|
||||
* titleSlot = { TangemRowText("Network fee", TangemRowTextRole.Title) },
|
||||
* valueSlot = { TangemRowText("0.0001 ETH", TangemRowTextRole.Value) },
|
||||
* contentLead = TangemRowContentLead.Start,
|
||||
* onClick = { /* ... */ },
|
||||
* )
|
||||
* ```
|
||||
*
|
||||
* @param modifier Modifier applied to the row container.
|
||||
* @param divider Draws an inset bottom divider.
|
||||
* @param includeInnerPaddings Applies the default row padding. Disable when wrapped in a container
|
||||
* that already handles padding.
|
||||
* @param contentLead Strategy for sharing space between the title and value sides.
|
||||
* See [TangemRowContentLead].
|
||||
* @param verticalAlignment Vertical alignment of the slots. See [TangemRowVerticalAlignment].
|
||||
* @param titleSlot Leading primary label.
|
||||
* @param subtitleSlot Leading secondary label rendered under [titleSlot].
|
||||
* @param valueSlot Trailing primary label.
|
||||
* @param subvalueSlot Trailing secondary label rendered under [valueSlot].
|
||||
* @param startSlot Leading icon / control slot.
|
||||
* @param endSlot Trailing icon / control slot.
|
||||
* @param extraBottomSlot Full-width content rendered below the row.
|
||||
* @param interactionSource Interaction source used when [onClick] is non-null.
|
||||
* @param onClick Click handler. `null` makes the row non-interactive.
|
||||
*/
|
||||
@Suppress("LongParameterList")
|
||||
@Composable
|
||||
fun TangemRow(
|
||||
modifier: Modifier = Modifier,
|
||||
divider: Boolean = false,
|
||||
includeInnerPaddings: Boolean = true,
|
||||
contentLead: TangemRowContentLead = TangemRowContentLead.Equal,
|
||||
verticalAlignment: TangemRowVerticalAlignment = TangemRowVerticalAlignment.Top,
|
||||
titleSlot: (@Composable RowScope.() -> Unit)? = null,
|
||||
subtitleSlot: (@Composable RowScope.() -> Unit)? = null,
|
||||
valueSlot: (@Composable RowScope.() -> Unit)? = null,
|
||||
subvalueSlot: (@Composable RowScope.() -> Unit)? = null,
|
||||
startSlot: (@Composable BoxScope.() -> Unit)? = null,
|
||||
endSlot: (@Composable BoxScope.() -> Unit)? = null,
|
||||
extraBottomSlot: (@Composable () -> Unit)? = null,
|
||||
interactionSource: MutableInteractionSource? = null,
|
||||
onClick: (() -> Unit)? = null,
|
||||
) {
|
||||
val resolvedInteractionSource: MutableInteractionSource? = if (onClick != null) {
|
||||
interactionSource ?: remember { MutableInteractionSource() }
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val isFocused = if (resolvedInteractionSource != null) {
|
||||
val isFocusedByState by resolvedInteractionSource.collectIsFocusedAsState()
|
||||
isFocusedByState
|
||||
} else {
|
||||
false
|
||||
}
|
||||
|
||||
WithRowRipple(enabled = onClick != null) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.conditionalCompose(isFocused) { focusBorder() }
|
||||
.then(
|
||||
if (onClick != null) {
|
||||
Modifier.clickable(
|
||||
interactionSource = resolvedInteractionSource,
|
||||
indication = LocalIndication.current,
|
||||
onClick = onClick,
|
||||
)
|
||||
} else {
|
||||
Modifier
|
||||
},
|
||||
)
|
||||
.conditionalCompose(divider) {
|
||||
bottomDivider(
|
||||
color = TangemTheme.colors3.border.secondary,
|
||||
width = TangemTheme.dimens3.borderWidth.sm,
|
||||
horizontalInset = TangemTheme.dimens3.spacing.s200,
|
||||
)
|
||||
}
|
||||
.conditionalCompose(includeInnerPaddings) {
|
||||
padding(TangemTheme.dimens3.spacing.s200)
|
||||
},
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = verticalAlignment.toCompose(),
|
||||
) {
|
||||
SideSlot(slot = startSlot, position = SideSlotPosition.Start)
|
||||
ContentLabels(
|
||||
modifier = Modifier.weight(1f),
|
||||
contentLead = contentLead,
|
||||
verticalAlignment = verticalAlignment,
|
||||
titleSlot = titleSlot,
|
||||
subtitleSlot = subtitleSlot,
|
||||
valueSlot = valueSlot,
|
||||
subvalueSlot = subvalueSlot,
|
||||
)
|
||||
SideSlot(slot = endSlot, position = SideSlotPosition.End)
|
||||
}
|
||||
if (extraBottomSlot != null) {
|
||||
SpacerH(TangemTheme.dimens3.spacing.s100)
|
||||
extraBottomSlot()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WithRowRipple(enabled: Boolean, content: @Composable () -> Unit) {
|
||||
if (enabled) {
|
||||
CompositionLocalProvider(LocalRippleConfiguration provides tangemRowRipple(), content = content)
|
||||
} else {
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
private fun tangemRowRipple(): RippleConfiguration = RippleConfiguration(
|
||||
color = TangemTheme.colors3.interaction.press.default,
|
||||
rippleAlpha = RippleAlpha(
|
||||
draggedAlpha = 0f,
|
||||
focusedAlpha = 0f,
|
||||
hoveredAlpha = 0.05f,
|
||||
pressedAlpha = 0.1f,
|
||||
),
|
||||
)
|
||||
|
||||
private fun TangemRowVerticalAlignment.toCompose(): Alignment.Vertical = when (this) {
|
||||
TangemRowVerticalAlignment.Top -> Alignment.Top
|
||||
TangemRowVerticalAlignment.Center -> Alignment.CenterVertically
|
||||
}
|
||||
|
||||
private enum class SideSlotPosition { Start, End }
|
||||
|
||||
@Composable
|
||||
private fun SideSlot(slot: (@Composable BoxScope.() -> Unit)?, position: SideSlotPosition) {
|
||||
if (slot == null) return
|
||||
val spacing = TangemTheme.dimens3.spacing.s150
|
||||
val padding = when (position) {
|
||||
SideSlotPosition.Start -> Modifier.padding(end = spacing)
|
||||
SideSlotPosition.End -> Modifier.padding(start = spacing)
|
||||
}
|
||||
Box(modifier = padding, content = slot)
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays out the title/value columns side-by-side with [contentLead]-aware width sharing.
|
||||
*/
|
||||
@Suppress("UnnecessaryParentheses", "LongParameterList")
|
||||
@Composable
|
||||
private fun ContentLabels(
|
||||
contentLead: TangemRowContentLead,
|
||||
verticalAlignment: TangemRowVerticalAlignment,
|
||||
titleSlot: (@Composable RowScope.() -> Unit)?,
|
||||
subtitleSlot: (@Composable RowScope.() -> Unit)?,
|
||||
valueSlot: (@Composable RowScope.() -> Unit)?,
|
||||
subvalueSlot: (@Composable RowScope.() -> Unit)?,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val hasLeft = titleSlot != null || subtitleSlot != null
|
||||
val hasRight = valueSlot != null || subvalueSlot != null
|
||||
if (!hasLeft && !hasRight) return
|
||||
|
||||
Layout(
|
||||
modifier = modifier,
|
||||
content = {
|
||||
// Always emit two roots so `measurables` indices are stable in the measure block.
|
||||
LabelColumnContent(primary = titleSlot, secondary = subtitleSlot, alignment = Alignment.Start)
|
||||
LabelColumnContent(primary = valueSlot, secondary = subvalueSlot, alignment = Alignment.End)
|
||||
},
|
||||
) { measurables, constraints ->
|
||||
val titleMeasurable = measurables[0]
|
||||
val valueMeasurable = measurables[1]
|
||||
// Fall back to summed intrinsics when parent provides unbounded width — `layout()` cannot
|
||||
// report an infinite size.
|
||||
val rowWidth = if (constraints.hasBoundedWidth) {
|
||||
constraints.maxWidth
|
||||
} else {
|
||||
titleMeasurable.maxIntrinsicWidth(Int.MAX_VALUE) + valueMeasurable.maxIntrinsicWidth(Int.MAX_VALUE)
|
||||
}
|
||||
|
||||
val (titleSlotWidth, valueSlotWidth) = when {
|
||||
!hasLeft -> 0 to rowWidth
|
||||
!hasRight -> rowWidth to 0
|
||||
else -> when (contentLead) {
|
||||
TangemRowContentLead.Equal -> (rowWidth / 2) to (rowWidth - rowWidth / 2)
|
||||
TangemRowContentLead.Start -> {
|
||||
// Value hugs (intrinsic), title fills the rest. Cap the hugging side at half
|
||||
// the row so the filling label never collapses to zero on overflow — degrades
|
||||
// to an equal split when the hugging side wants more than half.
|
||||
val v = valueMeasurable.maxIntrinsicWidth(Int.MAX_VALUE).coerceAtMost(rowWidth / 2)
|
||||
(rowWidth - v) to v
|
||||
}
|
||||
TangemRowContentLead.End -> {
|
||||
// Title hugs (intrinsic), value fills the rest. Same half-row cap as above.
|
||||
val t = titleMeasurable.maxIntrinsicWidth(Int.MAX_VALUE).coerceAtMost(rowWidth / 2)
|
||||
t to (rowWidth - t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val titlePlaceable = titleMeasurable.measure(
|
||||
constraints.copy(minWidth = 0, maxWidth = titleSlotWidth),
|
||||
)
|
||||
val valuePlaceable = valueMeasurable.measure(
|
||||
constraints.copy(minWidth = 0, maxWidth = valueSlotWidth),
|
||||
)
|
||||
|
||||
val rowHeight = maxOf(titlePlaceable.height, valuePlaceable.height)
|
||||
val yFor: (Int) -> Int = when (verticalAlignment) {
|
||||
TangemRowVerticalAlignment.Top -> { _ -> 0 }
|
||||
TangemRowVerticalAlignment.Center -> { h -> (rowHeight - h) / 2 }
|
||||
}
|
||||
|
||||
layout(rowWidth, rowHeight) {
|
||||
titlePlaceable.placeRelative(x = 0, y = yFor(titlePlaceable.height))
|
||||
// Filling side is pinned to the row's trailing edge.
|
||||
valuePlaceable.placeRelative(
|
||||
x = rowWidth - valuePlaceable.width,
|
||||
y = yFor(valuePlaceable.height),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelColumnContent(
|
||||
primary: (@Composable RowScope.() -> Unit)?,
|
||||
secondary: (@Composable RowScope.() -> Unit)?,
|
||||
alignment: Alignment.Horizontal,
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens3.spacing.s025),
|
||||
horizontalAlignment = alignment,
|
||||
) {
|
||||
if (primary != null) LabelRow(alignment = alignment, content = primary)
|
||||
if (secondary != null) LabelRow(alignment = alignment, content = secondary)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun LabelRow(alignment: Alignment.Horizontal, content: @Composable RowScope.() -> Unit) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(
|
||||
space = TangemTheme.dimens3.spacing.s050,
|
||||
alignment = alignment,
|
||||
),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
/** Semantic role of a label inside a [TangemRow], driving its typography, color and alignment. */
|
||||
@Immutable
|
||||
enum class TangemRowTextRole { Title, Subtitle, Value, Subvalue }
|
||||
|
||||
/**
|
||||
* Default text styling for [TangemRow] label slots.
|
||||
*
|
||||
* Usage:
|
||||
* ```
|
||||
* TangemRowText(text = "Network fee", role = TangemRowTextRole.Title)
|
||||
* ```
|
||||
*
|
||||
* @param text Label text.
|
||||
* @param role Semantic role. See [TangemRowTextRole].
|
||||
* @param modifier Modifier applied to the underlying [Text].
|
||||
* @param maxLines Maximum number of visible lines before truncation.
|
||||
* @param overflow Overflow behavior. Defaults to ellipsis.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemRowText(
|
||||
text: String,
|
||||
role: TangemRowTextRole,
|
||||
modifier: Modifier = Modifier,
|
||||
maxLines: Int = 1,
|
||||
overflow: TextOverflow = TextOverflow.Ellipsis,
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
modifier = modifier,
|
||||
color = rowTextColor(role),
|
||||
style = rowTextStyle(role),
|
||||
textAlign = rowTextAlign(role),
|
||||
maxLines = maxLines,
|
||||
overflow = overflow,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rowTextStyle(role: TangemRowTextRole): TextStyle = when (role) {
|
||||
TangemRowTextRole.Title, TangemRowTextRole.Value -> TangemTheme.typography3.body.medium
|
||||
TangemRowTextRole.Subtitle, TangemRowTextRole.Subvalue -> TangemTheme.typography3.caption.medium
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun rowTextColor(role: TangemRowTextRole): Color = when (role) {
|
||||
TangemRowTextRole.Title, TangemRowTextRole.Value -> TangemTheme.colors3.text.primary
|
||||
TangemRowTextRole.Subtitle, TangemRowTextRole.Subvalue -> TangemTheme.colors3.text.secondary
|
||||
}
|
||||
|
||||
private fun rowTextAlign(role: TangemRowTextRole): TextAlign = when (role) {
|
||||
TangemRowTextRole.Title, TangemRowTextRole.Subtitle -> TextAlign.Start
|
||||
TangemRowTextRole.Value, TangemRowTextRole.Subvalue -> TextAlign.End
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Modifier.focusBorder(): Modifier {
|
||||
val radius = TangemTheme.dimens3.borderRadius.b200
|
||||
val shape = remember(radius) { RoundedCornerShape(radius) }
|
||||
return border(
|
||||
width = TangemTheme.dimens3.borderWidth.md,
|
||||
color = TangemTheme.colors3.interaction.focusRing.default,
|
||||
shape = shape,
|
||||
)
|
||||
}
|
||||
|
||||
private fun Modifier.bottomDivider(color: Color, width: Dp, horizontalInset: Dp): Modifier = drawWithContent {
|
||||
drawContent()
|
||||
val stroke = width.toPx()
|
||||
val inset = horizontalInset.toPx()
|
||||
val y = size.height - stroke / 2f
|
||||
drawLine(
|
||||
color = color,
|
||||
start = Offset(x = inset, y = y),
|
||||
end = Offset(x = size.width - inset, y = y),
|
||||
strokeWidth = stroke,
|
||||
)
|
||||
}
|
||||
|
||||
// region Previews
|
||||
|
||||
@Preview(name = "Light", showBackground = true)
|
||||
@Preview(name = "Dark", uiMode = Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||
@Composable
|
||||
private fun TangemRowPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(vertical = 16.dp),
|
||||
) {
|
||||
// Equal lead, text only, with divider
|
||||
TangemRow(
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.Equal,
|
||||
titleSlot = { TangemRowText(text = "Title", role = TangemRowTextRole.Title) },
|
||||
subtitleSlot = { TangemRowText(text = "Subtitle", role = TangemRowTextRole.Subtitle) },
|
||||
valueSlot = { TangemRowText(text = "Value", role = TangemRowTextRole.Value) },
|
||||
subvalueSlot = { TangemRowText(text = "Subvalue", role = TangemRowTextRole.Subvalue) },
|
||||
)
|
||||
// Start lead — title hugs, value side fills
|
||||
TangemRow(
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.Start,
|
||||
titleSlot = { TangemRowText(text = "Network fee", role = TangemRowTextRole.Title) },
|
||||
valueSlot = { TangemRowText(text = "0.0001 ETH", role = TangemRowTextRole.Value) },
|
||||
subvalueSlot = { TangemRowText(text = "≈ $0.32", role = TangemRowTextRole.Subvalue) },
|
||||
)
|
||||
// End lead — value hugs, title side fills
|
||||
TangemRow(
|
||||
divider = true,
|
||||
contentLead = TangemRowContentLead.End,
|
||||
titleSlot = {
|
||||
TangemRowText(
|
||||
text = "Recipient with a very long address that should ellipsize",
|
||||
role = TangemRowTextRole.Title,
|
||||
)
|
||||
},
|
||||
valueSlot = { TangemRowText(text = "0x1234…abcd", role = TangemRowTextRole.Value) },
|
||||
)
|
||||
// Side slots + clickable + centered alignment
|
||||
TangemRow(
|
||||
verticalAlignment = TangemRowVerticalAlignment.Center,
|
||||
startSlot = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.primary,
|
||||
)
|
||||
},
|
||||
titleSlot = { TangemRowText(text = "Settings", role = TangemRowTextRole.Title) },
|
||||
subtitleSlot = {
|
||||
TangemRowText(text = "Tap to configure", role = TangemRowTextRole.Subtitle)
|
||||
},
|
||||
endSlot = {
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.secondary,
|
||||
)
|
||||
},
|
||||
onClick = {},
|
||||
)
|
||||
// Title only — exercises nullable slots / single LabelRow path
|
||||
TangemRow(
|
||||
titleSlot = { TangemRowText(text = "Single-line row", role = TangemRowTextRole.Title) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -9,6 +9,8 @@ import com.tangem.core.ui.ds2.badge.TangemBadge
|
|||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.ds2.fade.TangemFade
|
||||
import com.tangem.core.ui.ds2.loader.TangemLoaderSize
|
||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||
import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle
|
||||
|
||||
internal sealed interface StoryBookPage
|
||||
|
|
@ -177,6 +179,46 @@ internal data class TangemButtonStory(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
internal data class TangemRowStory(
|
||||
val contentLead: TangemRowContentLead,
|
||||
val verticalAlignment: TangemRowVerticalAlignment,
|
||||
val background: Background,
|
||||
val divider: Boolean,
|
||||
val includeInnerPaddings: Boolean,
|
||||
val isClickable: Boolean,
|
||||
val hasStartSlot: Boolean,
|
||||
val hasEndSlot: Boolean,
|
||||
val hasSubtitle: Boolean,
|
||||
val hasValue: Boolean,
|
||||
val hasSubvalue: Boolean,
|
||||
val hasExtraBottom: Boolean,
|
||||
val longTitle: Boolean,
|
||||
val textScale: Float,
|
||||
val onContentLeadChange: (TangemRowContentLead) -> Unit,
|
||||
val onVerticalAlignmentChange: (TangemRowVerticalAlignment) -> Unit,
|
||||
val onBackgroundChange: (Background) -> Unit,
|
||||
val onDividerToggle: () -> Unit,
|
||||
val onInnerPaddingsToggle: () -> Unit,
|
||||
val onClickableToggle: () -> Unit,
|
||||
val onStartSlotToggle: () -> Unit,
|
||||
val onEndSlotToggle: () -> Unit,
|
||||
val onSubtitleToggle: () -> Unit,
|
||||
val onValueToggle: () -> Unit,
|
||||
val onSubvalueToggle: () -> Unit,
|
||||
val onExtraBottomToggle: () -> Unit,
|
||||
val onLongTitleToggle: () -> Unit,
|
||||
val onTextScaleChange: (Float) -> Unit,
|
||||
) : DsStoryBookPage {
|
||||
|
||||
/** Backdrop the row preview is rendered on top of. */
|
||||
enum class Background(val label: String) {
|
||||
BgPrimary("bg.primary"),
|
||||
BgSecondary("bg.secondary"),
|
||||
BgBrand("bg.brand"),
|
||||
BgInverse("bg.inverse"),
|
||||
}
|
||||
}
|
||||
internal data class TangemFadeStory(
|
||||
val variant: TangemFade.Variant,
|
||||
val position: TangemFade.Position,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.badge.tangemBadg
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemButtonStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
|
||||
|
||||
private data class DsStoryItem(val title: String, val factory: StoryPageFactory)
|
||||
|
|
@ -27,6 +28,7 @@ private fun buildDsStories() = listOf(
|
|||
DsStoryItem(title = "⏳ TangemLoader", factory = tangemLoaderStoryFactory),
|
||||
DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory),
|
||||
DsStoryItem(title = "🏷️ TangemBadge", factory = tangemBadgeV2StoryFactory),
|
||||
DsStoryItem(title = "📋 TangemRow", factory = tangemRowStoryFactory),
|
||||
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
||||
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -18,7 +18,9 @@ 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.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.SliderDefaults
|
||||
import androidx.compose.material3.Text
|
||||
|
|
@ -54,13 +56,20 @@ internal fun TangemBadgeV2Story(state: TangemBadgeV2Story, modifier: Modifier =
|
|||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Preview stays pinned at the top.
|
||||
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)
|
||||
// Only the controls scroll.
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,12 +50,19 @@ internal fun TangemButtonStory(state: TangemButtonStory, modifier: Modifier = Mo
|
|||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Preview stays pinned at the top.
|
||||
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)
|
||||
// Only the controls scroll.
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,71 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.page.ds.row
|
||||
|
||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||
|
||||
internal fun StateUpdater<TangemRowStory>.build(): TangemRowStory {
|
||||
return TangemRowStory(
|
||||
contentLead = TangemRowContentLead.Equal,
|
||||
verticalAlignment = TangemRowVerticalAlignment.Top,
|
||||
background = TangemRowStory.Background.BgPrimary,
|
||||
divider = false,
|
||||
includeInnerPaddings = true,
|
||||
isClickable = false,
|
||||
hasStartSlot = false,
|
||||
hasEndSlot = false,
|
||||
hasSubtitle = true,
|
||||
hasValue = true,
|
||||
hasSubvalue = true,
|
||||
hasExtraBottom = false,
|
||||
longTitle = false,
|
||||
textScale = 1f,
|
||||
onContentLeadChange = { contentLead ->
|
||||
updateStory { it.copy(contentLead = contentLead) }
|
||||
},
|
||||
onVerticalAlignmentChange = { alignment ->
|
||||
updateStory { it.copy(verticalAlignment = alignment) }
|
||||
},
|
||||
onBackgroundChange = { background ->
|
||||
updateStory { it.copy(background = background) }
|
||||
},
|
||||
onDividerToggle = {
|
||||
updateStory { it.copy(divider = !it.divider) }
|
||||
},
|
||||
onInnerPaddingsToggle = {
|
||||
updateStory { it.copy(includeInnerPaddings = !it.includeInnerPaddings) }
|
||||
},
|
||||
onClickableToggle = {
|
||||
updateStory { it.copy(isClickable = !it.isClickable) }
|
||||
},
|
||||
onStartSlotToggle = {
|
||||
updateStory { it.copy(hasStartSlot = !it.hasStartSlot) }
|
||||
},
|
||||
onEndSlotToggle = {
|
||||
updateStory { it.copy(hasEndSlot = !it.hasEndSlot) }
|
||||
},
|
||||
onSubtitleToggle = {
|
||||
updateStory { it.copy(hasSubtitle = !it.hasSubtitle) }
|
||||
},
|
||||
onValueToggle = {
|
||||
updateStory { it.copy(hasValue = !it.hasValue) }
|
||||
},
|
||||
onSubvalueToggle = {
|
||||
updateStory { it.copy(hasSubvalue = !it.hasSubvalue) }
|
||||
},
|
||||
onExtraBottomToggle = {
|
||||
updateStory { it.copy(hasExtraBottom = !it.hasExtraBottom) }
|
||||
},
|
||||
onLongTitleToggle = {
|
||||
updateStory { it.copy(longTitle = !it.longTitle) }
|
||||
},
|
||||
onTextScaleChange = { scale ->
|
||||
updateStory { it.copy(textScale = scale) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal val tangemRowStoryFactory
|
||||
get() = storyPageFactory(StateUpdater<TangemRowStory>::build)
|
||||
|
|
@ -0,0 +1,369 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.ds.row
|
||||
|
||||
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.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.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.Density
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.R
|
||||
import com.tangem.core.ui.ds2.row.TangemRow
|
||||
import com.tangem.core.ui.ds2.row.TangemRowContentLead
|
||||
import com.tangem.core.ui.ds2.row.TangemRowText
|
||||
import com.tangem.core.ui.ds2.row.TangemRowTextRole
|
||||
import com.tangem.core.ui.ds2.row.TangemRowVerticalAlignment
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemRowStory.Background
|
||||
|
||||
private const val SHORT_TITLE = "Title"
|
||||
private const val LONG_TITLE = "Recipient address that should ellipsize when constrained by width"
|
||||
private const val SUBTITLE = "Subtitle"
|
||||
private const val VALUE = "0.0421 ETH"
|
||||
private const val SUBVALUE = "≈ $124.80"
|
||||
|
||||
@Composable
|
||||
internal fun TangemRowStory(state: TangemRowStory, 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),
|
||||
) {
|
||||
ContentLeadSelector(selected = state.contentLead, onSelect = state.onContentLeadChange)
|
||||
VerticalAlignmentSelector(
|
||||
selected = state.verticalAlignment,
|
||||
onSelect = state.onVerticalAlignmentChange,
|
||||
)
|
||||
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
|
||||
TextScaleSlider(value = state.textScale, onChange = state.onTextScaleChange)
|
||||
Toggles(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(state: TangemRowStory) {
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp)),
|
||||
) {
|
||||
PreviewBackground(background = state.background, modifier = Modifier.matchParentSize())
|
||||
val baseDensity = LocalDensity.current
|
||||
val scaledDensity = remember(baseDensity, state.textScale) {
|
||||
Density(density = baseDensity.density, fontScale = state.textScale)
|
||||
}
|
||||
CompositionLocalProvider(LocalDensity provides scaledDensity) {
|
||||
Box(modifier = Modifier.padding(vertical = 24.dp)) {
|
||||
StorybookRow(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun StorybookRow(state: TangemRowStory) {
|
||||
val titleText = if (state.longTitle) LONG_TITLE else SHORT_TITLE
|
||||
TangemRow(
|
||||
contentLead = state.contentLead,
|
||||
verticalAlignment = state.verticalAlignment,
|
||||
divider = state.divider,
|
||||
includeInnerPaddings = state.includeInnerPaddings,
|
||||
titleSlot = { TangemRowText(text = titleText, role = TangemRowTextRole.Title) },
|
||||
subtitleSlot = if (state.hasSubtitle) {
|
||||
{ TangemRowText(text = SUBTITLE, role = TangemRowTextRole.Subtitle) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
valueSlot = if (state.hasValue) {
|
||||
{ TangemRowText(text = VALUE, role = TangemRowTextRole.Value) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
subvalueSlot = if (state.hasSubvalue) {
|
||||
{ TangemRowText(text = SUBVALUE, role = TangemRowTextRole.Subvalue) }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
startSlot = if (state.hasStartSlot) {
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_information_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.primary,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
endSlot = if (state.hasEndSlot) {
|
||||
{
|
||||
Icon(
|
||||
modifier = Modifier.size(24.dp),
|
||||
painter = painterResource(id = R.drawable.ic_chevron_24),
|
||||
contentDescription = null,
|
||||
tint = TangemTheme.colors3.icon.secondary,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
extraBottomSlot = if (state.hasExtraBottom) {
|
||||
{
|
||||
Text(
|
||||
text = "Extra bottom slot — multi-line description content.",
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
style = TangemTheme.typography3.caption.medium,
|
||||
)
|
||||
}
|
||||
} else {
|
||||
null
|
||||
},
|
||||
onClick = if (state.isClickable) {
|
||||
{ /* no-op — ripple + focus demo */ }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@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.BgBrand -> Box(modifier.background(TangemTheme.colors3.bg.brand))
|
||||
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ContentLeadSelector(selected: TangemRowContentLead, onSelect: (TangemRowContentLead) -> Unit) {
|
||||
Section(label = "Content lead") {
|
||||
ChipGrid(
|
||||
items = TangemRowContentLead.entries,
|
||||
label = { it.name },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun VerticalAlignmentSelector(
|
||||
selected: TangemRowVerticalAlignment,
|
||||
onSelect: (TangemRowVerticalAlignment) -> Unit,
|
||||
) {
|
||||
Section(label = "Vertical alignment") {
|
||||
ChipGrid(
|
||||
items = TangemRowVerticalAlignment.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: TangemRowStory) {
|
||||
Section(label = "Flags") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ToggleRow(label = "divider", checked = state.divider, onToggle = state.onDividerToggle)
|
||||
ToggleRow(
|
||||
label = "includeInnerPaddings",
|
||||
checked = state.includeInnerPaddings,
|
||||
onToggle = state.onInnerPaddingsToggle,
|
||||
)
|
||||
ToggleRow(
|
||||
label = "clickable (ripple + focus)",
|
||||
checked = state.isClickable,
|
||||
onToggle = state.onClickableToggle,
|
||||
)
|
||||
ToggleRow(
|
||||
label = "startSlot (leading icon)",
|
||||
checked = state.hasStartSlot,
|
||||
onToggle = state.onStartSlotToggle,
|
||||
)
|
||||
ToggleRow(
|
||||
label = "endSlot (trailing icon)",
|
||||
checked = state.hasEndSlot,
|
||||
onToggle = state.onEndSlotToggle,
|
||||
)
|
||||
ToggleRow(label = "subtitle", checked = state.hasSubtitle, onToggle = state.onSubtitleToggle)
|
||||
ToggleRow(label = "value", checked = state.hasValue, onToggle = state.onValueToggle)
|
||||
ToggleRow(label = "subvalue", checked = state.hasSubvalue, onToggle = state.onSubvalueToggle)
|
||||
ToggleRow(
|
||||
label = "extraBottomSlot",
|
||||
checked = state.hasExtraBottom,
|
||||
onToggle = state.onExtraBottomToggle,
|
||||
)
|
||||
ToggleRow(
|
||||
label = "long title (ellipsis test)",
|
||||
checked = state.longTitle,
|
||||
onToggle = state.onLongTitleToggle,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -26,42 +26,48 @@ internal fun TangemShimmerStory(state: TangemShimmerStory, modifier: Modifier =
|
|||
.statusBarsPadding()
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.verticalScroll(rememberScrollState())
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
// Preview stays pinned at the top.
|
||||
ComponentPreview(state = state)
|
||||
ChipSection(label = "Text style") {
|
||||
ChipGrid(
|
||||
items = TextShimmerStyle.entries,
|
||||
label = { it.chipLabel() },
|
||||
isSelected = { it == state.textStyle },
|
||||
onSelect = state.onTextStyleChange,
|
||||
)
|
||||
}
|
||||
ChipSection(label = "Radius") {
|
||||
ChipGrid(
|
||||
items = RadiusOption.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == state.radius },
|
||||
onSelect = state.onRadiusChange,
|
||||
)
|
||||
}
|
||||
ChipSection(label = "Rectangle width") {
|
||||
ChipGrid(
|
||||
items = RectangleWidthOption.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == state.rectangleWidth },
|
||||
onSelect = state.onRectangleWidthChange,
|
||||
)
|
||||
}
|
||||
ChipSection(label = "Rectangle height") {
|
||||
ChipGrid(
|
||||
items = RectangleHeightOption.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == state.rectangleHeight },
|
||||
onSelect = state.onRectangleHeightChange,
|
||||
)
|
||||
// Only the controls scroll.
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
ChipSection(label = "Text style") {
|
||||
ChipGrid(
|
||||
items = TextShimmerStyle.entries,
|
||||
label = { it.chipLabel() },
|
||||
isSelected = { it == state.textStyle },
|
||||
onSelect = state.onTextStyleChange,
|
||||
)
|
||||
}
|
||||
ChipSection(label = "Radius") {
|
||||
ChipGrid(
|
||||
items = RadiusOption.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == state.radius },
|
||||
onSelect = state.onRadiusChange,
|
||||
)
|
||||
}
|
||||
ChipSection(label = "Rectangle width") {
|
||||
ChipGrid(
|
||||
items = RectangleWidthOption.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == state.rectangleWidth },
|
||||
onSelect = state.onRectangleWidthChange,
|
||||
)
|
||||
}
|
||||
ChipSection(label = "Rectangle height") {
|
||||
ChipGrid(
|
||||
items = RectangleHeightOption.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == state.rectangleHeight },
|
||||
onSelect = state.onRectangleHeightChange,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.badge.TangemBadg
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemButtonStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessageStory
|
||||
|
|
@ -89,6 +90,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
|||
is TangemLoaderStory -> TangemLoaderStory(state = storyState)
|
||||
is TangemButtonStory -> TangemButtonStory(state = storyState)
|
||||
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
|
||||
is TangemRowStory -> TangemRowStory(state = storyState)
|
||||
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
|
||||
is TangemFadeStory -> TangemFadeStory(state = storyState)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue