Updated on 2026-08-14

This commit is contained in:
Tangem 2026-05-07 12:08:37 +02:00
parent a354c649b2
commit 840e9d0766
14 changed files with 185 additions and 44 deletions

View file

@ -225,7 +225,6 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
val contentModifier = when (type) {
Default -> Modifier
.padding(bottom = bottomBarHeight)
.clip(
RoundedCornerShape(
topStart = TangemTheme.dimens2.x8,

View file

@ -63,7 +63,7 @@ fun ActionButtons(buttons: ImmutableList<TangemButtonUM>, modifier: Modifier = M
)
Text(
text = button.text.orEmpty().resolveReference(),
style = TangemTheme.typography2.calloutSemibold15,
style = TangemTheme.typography2.subheadlineMedium14,
color = textColor,
maxLines = 1,
)

View file

@ -23,7 +23,7 @@ import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun TangemRowTail(
fun TangemRowTail(
tangemRowTailUM: TangemRowTailUM,
modifier: Modifier = Modifier,
reorderableState: ReorderableLazyListState? = null,

View file

@ -46,8 +46,8 @@ fun TangemTokenRow(
tangemIconUM = tokenRowUM.headIconUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x9)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10)
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
)

View file

@ -30,7 +30,7 @@ import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndContent(
fun TokenRowEndContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
textStyle: TextStyle,

View file

@ -5,6 +5,7 @@ import androidx.annotation.DrawableRes
import androidx.compose.animation.*
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
@ -12,6 +13,9 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.MeasurePolicy
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -95,14 +99,39 @@ fun TangemTopBar(
startContent: @Composable (() -> Unit)? = null,
endContent: @Composable (() -> Unit)? = null,
) {
TangemTopBar(
modifier = modifier,
type = type,
startContent = startContent,
endContent = endContent,
Layout(
modifier = modifier
.fillMaxWidth()
.heightIn(min = type.getSize())
.padding(type.getPadding()),
measurePolicy = TopBarMeasurePolicy,
content = {
Box(modifier = Modifier.layoutId(SLOT_START)) {
AnimatedContent(
targetState = startContent != null,
modifier = Modifier.size(TangemTheme.dimens2.x11),
label = "Start Content Visibility",
) { isVisible ->
if (isVisible) {
startContent?.invoke()
}
}
}
Box(modifier = Modifier.layoutId(SLOT_END)) {
AnimatedContent(
targetState = endContent != null,
modifier = Modifier
.height(TangemTheme.dimens2.x11)
.widthIn(min = TangemTheme.dimens2.x11),
label = "End Content Visibility",
) { isVisible ->
if (isVisible) {
endContent?.invoke()
}
}
}
Column(
modifier = Modifier.weight(1f),
modifier = Modifier.layoutId(SLOT_TITLE),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
) {
@ -125,6 +154,48 @@ fun TangemTopBar(
)
}
private const val SLOT_START = "start"
private const val SLOT_END = "end"
private const val SLOT_TITLE = "title"
/**
* Measure policy for [TangemTopBar].
*
* Title is centered relative to the full bar width. To avoid overlap with side slots,
* the larger of the two slot widths is reserved on both sides symmetrically.
*/
private val TopBarMeasurePolicy = MeasurePolicy { measurables, constraints ->
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
val startPlaceable = measurables.first { it.layoutId == SLOT_START }.measure(looseConstraints)
val endPlaceable = measurables.first { it.layoutId == SLOT_END }.measure(looseConstraints)
val totalWidth = constraints.maxWidth
val sideReserve = maxOf(startPlaceable.width, endPlaceable.width)
val titleMaxWidth = (totalWidth - sideReserve * 2).coerceAtLeast(0)
val titlePlaceable = measurables.first { it.layoutId == SLOT_TITLE }
.measure(looseConstraints.copy(maxWidth = titleMaxWidth))
val height = maxOf(startPlaceable.height, endPlaceable.height, titlePlaceable.height)
.coerceAtLeast(constraints.minHeight)
layout(totalWidth, height) {
startPlaceable.placeRelative(
x = 0,
y = (height - startPlaceable.height) / 2,
)
endPlaceable.placeRelative(
x = totalWidth - endPlaceable.width,
y = (height - endPlaceable.height) / 2,
)
titlePlaceable.placeRelative(
x = (totalWidth - titlePlaceable.width) / 2,
y = (height - titlePlaceable.height) / 2,
)
}
}
/**
* A top bar composable that displays a title and optional start and end icons.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
@ -222,6 +293,9 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes:
style = TangemTheme.typography2.headingSemibold17,
textAlign = TextAlign.Center,
maxLines = 1,
autoSize = TextAutoSize.StepBased(
minFontSize = TangemTheme.typography2.captionRegular12.fontSize,
),
)
}
}

View file

@ -28,10 +28,7 @@ enum class TangemTopBarType {
@ReadOnlyComposable
@Composable
fun getSideContentSize(): Dp {
return when (this) {
Default -> TangemTheme.dimens2.x8
BottomSheet -> TangemTheme.dimens2.x7
}
return TangemTheme.dimens2.x7
}
@ReadOnlyComposable

View file

@ -7,6 +7,8 @@ import com.tangem.core.analytics.models.AnalyticsParam
import com.tangem.core.decompose.di.ModelScoped
import com.tangem.core.decompose.model.Model
import com.tangem.core.decompose.model.ParamsContainer
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.button.TangemButtonType
import com.tangem.core.ui.ds.button.TangemButtonUM
import com.tangem.core.ui.event.consumedEvent
@ -268,11 +270,15 @@ internal class OrganizeTokensModel @Inject constructor(
text = resourceReference(R.string.common_cancel),
onClick = ::onCancelClick,
type = TangemButtonType.Secondary,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X12,
),
applyButton = TangemButtonUM(
text = resourceReference(R.string.common_apply),
onClick = ::onApplyClick,
type = TangemButtonType.Primary,
shape = TangemButtonShape.Rounded,
size = TangemButtonSize.X12,
),
scrollListToTop = consumedEvent(),
isBalanceHidden = true,

View file

@ -35,6 +35,7 @@ internal class OrganizeTokensListConverter(
return value.accountStatuses
.asSequence()
.filterCryptoPortfolio()
.filter { it.tokenList !is TokenList.Empty }
.flatMap { accountStatus ->
buildList {
addIf(

View file

@ -18,6 +18,7 @@ import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
@ -33,8 +34,14 @@ import com.tangem.core.ui.components.bottomsheets.TangemBottomSheetConfigContent
import com.tangem.core.ui.components.haze.hazeEffectTangem
import com.tangem.core.ui.components.haze.hazeSourceTangem
import com.tangem.core.ui.ds.button.TangemButton
import com.tangem.core.ui.ds.image.TangemIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds.row.header.TangemHeaderRow
import com.tangem.core.ui.ds.row.token.TangemTokenRow
import com.tangem.core.ui.ds.row.internal.TangemRowTail
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.ds.row.token.internal.TokenRowEndContent
import com.tangem.core.ui.ds.row.token.internal.TokenRowTitle
import com.tangem.core.ui.ds.topbar.TangemTopBar
import com.tangem.core.ui.ds.topbar.TangemTopBarActionContent
import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM
@ -44,6 +51,7 @@ import com.tangem.core.ui.reordarable.ReorderableItem
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import com.tangem.core.ui.test.TokenElementsTestTags
import com.tangem.core.ui.utils.lazyListItemPosition
import com.tangem.feature.wallet.child.organizetokens.entity.DraggableItem
import com.tangem.feature.wallet.child.organizetokens.entity.OrganizeRowItemUM
@ -219,7 +227,7 @@ private fun LazyItemScope.DraggableItem(
headerRowUM = item.headerRowUM,
isBalanceHidden = isBalanceHidden,
)
is OrganizeRowItemUM.Token -> TangemTokenRow(
is OrganizeRowItemUM.Token -> OrganizeTokenRow(
modifier = modifierWithBackground,
tokenRowUM = item.tokenRowUM,
reorderableState = reorderableState,
@ -291,6 +299,55 @@ private fun Modifier.applyShapeAndShadow(roundingMode: RoundingModeUM, showShado
}
}
@Composable
private fun OrganizeTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
content = {
TangemIcon(
tangemIconUM = tokenRowUM.headIconUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x3)
.size(TangemTheme.dimens2.x10)
.testTag(tag = TokenElementsTestTags.TOKEN_ICON),
)
TokenRowTitle(
titleUM = tokenRowUM.titleUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2)
.testTag(tag = TokenElementsTestTags.TOKEN_TITLE),
)
TokenRowEndContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
textStyle = TangemTheme.typography2.captionSemibold12,
textColor = TangemTheme.colors2.text.neutral.secondary,
placeholderWidth = TangemTheme.dimens2.x11,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
TangemRowTail(
tangemRowTailUM = tokenRowUM.tailUM,
reorderableState = reorderableState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(tag = TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
},
modifier = modifier,
)
}
@Composable
@ReadOnlyComposable
private fun getItemGap(roundingMode: RoundingModeUM): PaddingValues {

View file

@ -84,15 +84,11 @@ internal class WalletTokensListUMConverter(
.asSequence()
.flatMap { accountStatus ->
if (isAccountsModeEnabled) {
val currencies = accountStatus.tokenList.flattenCurrencies()
val isCollapsable = currencies.isNotEmpty()
val isExpanded =
currencies.isEmpty() || expandedAccounts.contains(accountStatus.account.accountId)
sequenceOf(
TokensListItemUM2.Portfolio(
tokenRowUM = accountRowConverter.convert(accountStatus),
isExpanded = isExpanded,
isCollapsable = isCollapsable,
isExpanded = expandedAccounts.contains(accountStatus.account.accountId),
isCollapsable = true,
onEmptyClick = { clickIntents.onManageTokensClick(accountStatus.account.accountId) },
tokenList = getTokenListItems(
accountStatus,

View file

@ -3,6 +3,7 @@ package com.tangem.feature.wallet.presentation.wallet.ui
import android.content.res.Configuration
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.spring
@ -42,6 +43,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.arkivanov.decompose.ExperimentalDecomposeApi
import com.tangem.core.ui.components.BottomFade
@ -57,6 +59,7 @@ import com.tangem.core.ui.components.sheetscaffold.*
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior
import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingTopBar
import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior
import com.tangem.core.ui.extensions.softLayerShadow
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.*
import com.tangem.core.ui.utils.TangemSharedTransitionLayout
@ -362,13 +365,23 @@ private inline fun BaseScaffoldWithMarkets(
val peekHeight = bottomSheetHeaderHeightProvider() + TangemTheme.dimens2.x3 + bottomBarHeight
val coroutineScope = rememberCoroutineScope()
val background = TangemTheme.colors2.surface.level2
val bottomSheetState = rememberTangemStandardBottomSheetState()
val scaffoldState = rememberTangemBottomSheetScaffoldState(bottomSheetState = bottomSheetState)
val expandedBackground = TangemTheme.colors2.surface.level2
val collapsedBackground = TangemTheme.colors2.surface.level3
val background by animateColorAsState(
targetValue = if (bottomSheetState.targetValue == TangemSheetValue.Expanded) {
expandedBackground
} else {
collapsedBackground
},
label = "bottomSheetBackground",
)
CompositionLocalProvider(
LocalMainBottomSheetColor provides remember(background) { mutableStateOf(background) },
LocalMainBottomSheetColor provides remember { mutableStateOf(background) }.apply { value = background },
) {
val backgroundColor by LocalMainBottomSheetColor.current
var isSearchFieldFocused by remember { mutableStateOf(false) }
@ -498,6 +511,13 @@ private fun BottomSheet(
Box(
modifier = Modifier
.fillMaxWidth()
.softLayerShadow(
radius = 16.dp,
color = Color.Black.copy(alpha = if (LocalIsInDarkTheme.current) .24f else .12f),
shape = shape,
offset = DpOffset(x = 0.dp, y = (-6).dp),
isAlphaContentClip = true,
)
.clip(shape)
.background(backgroundColor)
.onFocusChanged(onFocusChange),

View file

@ -3,7 +3,6 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common
import android.content.res.Configuration
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.statusBarsPadding
import androidx.compose.foundation.shape.CircleShape
@ -80,7 +79,6 @@ internal fun WalletTopBar(
},
endContent = {
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x5),
modifier = Modifier
.clip(CircleShape)
.background(

View file

@ -40,7 +40,7 @@ import com.tangem.core.ui.components.tokenlist.NON_CONTENT_TOKENS_LIST_KEY
import com.tangem.core.ui.components.tokenlist.TokenListItem
import com.tangem.core.ui.components.tokenlist.state.TokensListItemUM
import com.tangem.core.ui.decorations.roundedShapeItemDecoration
import com.tangem.core.ui.ds.button.PrimaryInverseTangemButton
import com.tangem.core.ui.ds.button.SecondaryTangemButton
import com.tangem.core.ui.ds.button.TangemButtonShape
import com.tangem.core.ui.ds.button.TangemButtonSize
import com.tangem.core.ui.ds.image.TangemIcon
@ -206,8 +206,6 @@ private fun LazyListScope.portfolioItem(
if (listItem.tokenList.isEmpty()) {
nonContentAccountItem(
listItem = listItem,
index = index,
lastIndex = lastIndex,
modifier = modifier,
)
} else {
@ -222,7 +220,7 @@ private fun LazyListScope.portfolioItem(
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
radius = if (listItem.isExpanded) TangemTheme.dimens2.x6 else TangemTheme.dimens2.x5,
currentIndex = tokenIndex + 1,
addDefaultPadding = false,
lastIndex = lastIndex,
@ -294,7 +292,7 @@ private fun LazyListScope.accountItem(
.semantics { lazyListItemPosition = index }
.roundedShapeItemDecoration(
currentIndex = 0,
radius = 18.dp,
radius = if (listItem.isExpanded) TangemTheme.dimens2.x6 else TangemTheme.dimens2.x5,
addDefaultPadding = false,
lastIndex = effectiveLastIndex,
backgroundColor = TangemTheme.colors2.surface.level3,
@ -492,23 +490,18 @@ private fun LazyListScope.nonContentItem2(onEmptyClick: () -> Unit, modifier: Mo
}
}
private fun LazyListScope.nonContentAccountItem(
listItem: TokensListItemUM2.Portfolio,
index: Int,
lastIndex: Int,
modifier: Modifier = Modifier,
) {
private fun LazyListScope.nonContentAccountItem(listItem: TokensListItemUM2.Portfolio, modifier: Modifier = Modifier) {
item(
key = "$NON_CONTENT_TOKENS_LIST_KEY account-${listItem.tokenRowUM.id}",
contentType = "$NON_CONTENT_TOKENS_LIST_KEY account-${listItem.tokenRowUM.id}",
) {
SlideInItemVisibility(
currentIndex = index + 1,
lastIndex = lastIndex,
currentIndex = 1,
lastIndex = 1,
modifier = modifier
.animateItem(fadeInSpec = null, placementSpec = null, fadeOutSpec = null)
.roundedShapeItemDecoration(
radius = 18.dp,
radius = if (listItem.isExpanded) TangemTheme.dimens2.x6 else TangemTheme.dimens2.x5,
addDefaultPadding = false,
currentIndex = 1,
lastIndex = 1,
@ -545,8 +538,8 @@ internal fun NonContentItemContentV2(textColor: Color, modifier: Modifier = Modi
textAlign = TextAlign.Center,
style = TangemTheme.typography2.bodyRegular14,
)
SpacerH(TangemTheme.dimens2.x2)
PrimaryInverseTangemButton(
SpacerH(TangemTheme.dimens2.x4)
SecondaryTangemButton(
text = resourceReference(id = R.string.common_add_tokens),
onClick = onClick,
size = TangemButtonSize.X8,