Updated on 2026-08-14

This commit is contained in:
Tangem 2025-12-30 11:46:31 +03:00
commit 14a7ac4f5e
431 changed files with 23077 additions and 3701 deletions

View file

@ -1,184 +0,0 @@
package com.tangem.core.ui.components
import androidx.annotation.FloatRange
import androidx.compose.foundation.layout.wrapContentHeight
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.sp
private const val COEFFICIENT = 0.8f
@Suppress("MagicNumber")
@Composable
fun ResizableText(
text: String,
fontSizeRange: FontSizeRange,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
style: TextStyle = LocalTextStyle.current,
) {
val fontSizeValue = remember { mutableFloatStateOf(fontSizeRange.max.value) }
val readyToDraw = remember { mutableStateOf(false) }
val textState = remember { mutableStateOf(text) }
if (textState.value != text) {
readyToDraw.value = false
fontSizeValue.floatValue = fontSizeRange.max.value
textState.value = text
}
Text(
text = text,
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
color = color,
fontSize = fontSizeValue.floatValue.sp,
overflow = overflow,
softWrap = false,
maxLines = maxLines,
onTextLayout = { textLayoutResult ->
if (textLayoutResult.hasVisualOverflow) {
val nextFontSizeValue = fontSizeValue.floatValue - fontSizeRange.step.value
if (nextFontSizeValue <= fontSizeRange.min.value) {
fontSizeValue.floatValue = fontSizeRange.min.value
readyToDraw.value = true
} else {
fontSizeValue.floatValue = nextFontSizeValue * COEFFICIENT
}
} else {
readyToDraw.value = true
}
},
style = style,
)
}
/**
* A Composable function that displays text which can be resized based on its content's overflow.
*
* This function draws text on the screen and checks if it overflows. If the text overflows,
* its font size is reduced recursively until it either fits the available space or reaches a
* specified minimum font size.
*/
@Composable
fun ResizableText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
textAlign: TextAlign? = null,
overflow: TextOverflow = TextOverflow.Clip,
softWrap: Boolean = true,
maxLines: Int = Int.MAX_VALUE,
style: TextStyle = LocalTextStyle.current,
minFontSize: TextUnit = TextUnit.Unspecified,
@FloatRange(from = 0.0, to = 1.0, fromInclusive = false, toInclusive = false)
reduceFactor: Double = 0.9,
) {
var fontSize by remember { mutableStateOf(style.fontSize) }
var isReadyToDraw by remember { mutableStateOf(value = false) }
Text(
modifier = modifier
.drawWithContent {
if (isReadyToDraw) drawContent()
}
.wrapContentHeight(),
text = text,
color = color,
fontSize = fontSize,
textAlign = textAlign,
overflow = overflow,
softWrap = softWrap,
maxLines = maxLines,
style = style,
onTextLayout = { result ->
fun reduceFontSize() {
val reducedFontSize = fontSize * reduceFactor
if (minFontSize != TextUnit.Unspecified && reducedFontSize <= minFontSize) {
fontSize = minFontSize
isReadyToDraw = true
} else {
fontSize = reducedFontSize
}
}
if (result.hasVisualOverflow) {
reduceFontSize()
} else {
isReadyToDraw = true
}
},
)
}
@Composable
fun ResizableText(
text: String,
fontSizeValue: TextUnit,
fontSizeRange: FontSizeRange,
onFontSizeChange: (Float) -> Unit,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
overflow: TextOverflow = TextOverflow.Clip,
maxLines: Int = Int.MAX_VALUE,
style: TextStyle = LocalTextStyle.current,
) {
val readyToDraw = remember { mutableStateOf(false) }
val textState = remember { mutableStateOf(text) }
if (textState.value != text) {
readyToDraw.value = false
onFontSizeChange(fontSizeRange.max.value)
textState.value = text
}
Text(
text = text,
modifier = modifier.drawWithContent { if (readyToDraw.value) drawContent() },
color = color,
fontSize = fontSizeValue.value.sp,
overflow = overflow,
softWrap = false,
maxLines = maxLines,
onTextLayout = { result ->
if (result.hasVisualOverflow) {
val nextFontSizeValue = fontSizeValue.value - fontSizeRange.step.value
if (nextFontSizeValue <= fontSizeRange.min.value) {
onFontSizeChange(fontSizeRange.min.value)
readyToDraw.value = true
} else {
val newSizeValue = nextFontSizeValue * COEFFICIENT
onFontSizeChange(newSizeValue)
}
} else {
readyToDraw.value = true
}
},
style = style,
)
}
data class FontSizeRange(
val min: TextUnit,
val max: TextUnit,
val step: TextUnit = DEFAULT_TEXT_STEP,
) {
init {
require(min < max) { "min should be less than max, $this" }
require(step.value > 0) { "step should be greater than 0, $this" }
}
companion object {
private val DEFAULT_TEXT_STEP = 1.sp
}
}

View file

@ -15,6 +15,8 @@ import com.tangem.core.ui.res.TangemThemePreview
fun AppBarWithBackButtonAndIcon(
onBackClick: () -> Unit,
modifier: Modifier = Modifier,
backButtonEnabled: Boolean = true,
endButtonEnabled: Boolean = true,
text: String? = null,
subtitle: String? = null,
@DrawableRes backIconRes: Int? = null,
@ -30,11 +32,13 @@ fun AppBarWithBackButtonAndIcon(
startButton = TopAppBarButtonUM.Icon(
iconRes = backIconRes ?: R.drawable.ic_back_24,
onClicked = onBackClick,
isEnabled = backButtonEnabled,
),
endButton = if (iconRes != null && onIconClick != null) {
TopAppBarButtonUM.Icon(
iconRes = iconRes,
onClicked = onIconClick,
isEnabled = endButtonEnabled,
)
} else {
null

View file

@ -16,7 +16,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -146,7 +145,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheet(
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
),
onBack = null,
bsContent = {

View file

@ -18,7 +18,6 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
@ -141,7 +140,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewModalBottomSheetW
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
),
onBack = null,
containerColor = containerColor,

View file

@ -134,7 +134,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
),
onBack = null,
containerColor = containerColor,

View file

@ -6,6 +6,7 @@ import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
@ -23,7 +24,6 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.components.ResizableText
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.BaseButtonTestTags
import com.tangem.core.ui.utils.MultipleClickPreventer
@ -75,7 +75,7 @@ fun TangemButton(
)
},
text = {
ResizableText(
Text(
modifier = Modifier
.weight(1f, fill = false)
.heightIn(MinButtonContentSize, maxContentSize)
@ -86,7 +86,10 @@ fun TangemButton(
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
minFontSize = 12.sp,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
)
},
icon = { iconResId ->

View file

@ -0,0 +1,122 @@
package com.tangem.core.ui.components.chip
import android.content.res.Configuration
import androidx.compose.animation.animateColorAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
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.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.components.chip.entity.ChipUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@Composable
fun Chip(state: ChipUM, modifier: Modifier = Modifier) {
val backgroundColor by animateColorAsState(
targetValue = if (state.isSelected) {
TangemTheme.colors.button.primary
} else {
TangemTheme.colors.button.secondary
},
)
val textColor by animateColorAsState(
targetValue = if (state.isSelected) {
TangemTheme.colors.text.primary2
} else {
TangemTheme.colors.text.primary1
},
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.Center,
modifier = modifier
.clip(RoundedCornerShape(12.dp))
.background(color = backgroundColor)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
onClick = state.onClick,
)
.padding(PaddingValues(horizontal = 24.dp, vertical = 8.dp)),
) {
Text(
text = state.text.resolveReference(),
style = TangemTheme.typography.button,
color = textColor,
)
}
}
@OptIn(ExperimentalLayoutApi::class)
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun ChipPreview() {
TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(16.dp),
) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Chip(
state = ChipUM(
id = 0,
text = TextReference.Str("All News"),
isSelected = true,
onClick = {},
),
)
Chip(
state = ChipUM(
id = 1,
text = TextReference.Str("Regulation"),
isSelected = false,
onClick = {},
),
)
Chip(
state = ChipUM(
id = 2,
text = TextReference.Str("ETFs"),
isSelected = false,
onClick = {},
),
)
Chip(
state = ChipUM(
id = 3,
text = TextReference.Str("Institutions"),
isSelected = false,
onClick = {},
),
)
}
}
}
}

View file

@ -0,0 +1,10 @@
package com.tangem.core.ui.components.chip.entity
import com.tangem.core.ui.extensions.TextReference
data class ChipUM(
val id: Int,
val text: TextReference,
val isSelected: Boolean = false,
val onClick: () -> Unit,
)

View file

@ -10,9 +10,8 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.remember
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusManager
import androidx.compose.ui.focus.FocusRequester
@ -51,16 +50,22 @@ fun SearchBar(
val keyboardController = LocalSoftwareKeyboardController.current
val focusManager = LocalFocusManager.current
val interactionSource = remember { MutableInteractionSource() }
var isInitialComposition by rememberSaveable { mutableStateOf(true) }
LaunchedEffect(Unit) {
isInitialComposition = false
}
BasicTextField(
modifier = modifier
.fillMaxWidth()
.heightIn(min = TangemTheme.dimens.size48)
.onFocusChanged { focusState ->
if (focusState.isFocused) {
state.onActiveChange(true)
} else {
state.onActiveChange(false)
if (!isInitialComposition) {
if (focusState.isFocused) {
state.onActiveChange(true)
} else {
state.onActiveChange(false)
}
}
}
.focusRequester(focusRequester)
@ -163,6 +168,7 @@ private fun ClearButton(
focusManager.clearFocus()
keyboardController?.hide()
state.onActiveChange(false)
state.onClearClick()
},
) {
Icon(

View file

@ -8,4 +8,5 @@ data class SearchBarUM(
val onQueryChange: (String) -> Unit,
val isActive: Boolean,
val onActiveChange: (Boolean) -> Unit,
val onClearClick: () -> Unit = {},
)

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
@ -31,6 +32,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
/**
* [InputRowEnter](https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-799&mode=design&t=IQ5lBJEkFGU4WSvi-4)
@ -92,7 +94,7 @@ fun InputRowEnter(
modifier = Modifier
.size(16.dp)
.clip(CircleShape),
text = description.resolveReference(),
text = description,
content = { contentModifier ->
Icon(
modifier = contentModifier.size(16.dp),
@ -113,7 +115,8 @@ fun InputRowEnter(
visualTransformation = visualTransformation,
keyboardOptions = keyboardOptions,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8),
.padding(top = TangemTheme.dimens.spacing8)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.NONCE_INPUT_TEXT_FIELD),
)
}
iconRes?.let { iconRes ->

View file

@ -11,6 +11,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
@ -21,6 +22,7 @@ import com.tangem.core.ui.components.tooltip.TangemTooltip
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.SendSelectNetworkFeeBottomSheetTestTags
import com.tangem.core.ui.utils.rememberDecimalFormat
/**
@ -112,6 +114,7 @@ fun InputRowEnterInfoAmount(
}
}
@Suppress("LongMethod")
@Composable
fun InputRowEnterInfoAmountV2(
title: TextReference,
@ -139,20 +142,23 @@ fun InputRowEnterInfoAmountV2(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(16.dp)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM),
) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp)) {
Text(
text = title.resolveReference(),
style = TangemTheme.typography.subtitle2,
color = titleColor,
modifier = Modifier.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TITLE),
)
if (description != null) {
TangemTooltip(
modifier = Modifier
.size(16.dp)
.clip(CircleShape),
text = description.resolveReference(),
.clip(CircleShape)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_TOOLTIP_ICON),
text = description,
content = { contentModifier ->
Icon(
modifier = contentModifier.size(16.dp),
@ -183,7 +189,8 @@ fun InputRowEnterInfoAmountV2(
backgroundColor = Color.Transparent,
modifier = Modifier
.padding(top = TangemTheme.dimens.spacing8)
.weight(1f),
.weight(1f)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD),
)
info?.let { info ->
Text(
@ -192,7 +199,8 @@ fun InputRowEnterInfoAmountV2(
color = infoColor,
modifier = Modifier
.padding(start = TangemTheme.dimens.spacing8)
.align(Alignment.Bottom),
.align(Alignment.Bottom)
.testTag(SendSelectNetworkFeeBottomSheetTestTags.CUSTOM_INPUT_ITEM_FIAT_AMOUNT),
)
}
}

View file

@ -168,7 +168,9 @@ private fun LabelPreview() {
TangemThemePreview {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(16.dp),
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(16.dp),
) {
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),

View file

@ -1,5 +1,6 @@
package com.tangem.core.ui.components.pager
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
@ -8,18 +9,24 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.pager.PagerState
import androidx.compose.foundation.pager.rememberPagerState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.*
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlin.math.abs
import kotlin.math.min
// six - cause the central indicator has width multiplied twice
private const val TOTAL_MAX_INDICATORS = 6
private const val SPACER_COUNT_BETWEEN_INDICATORS = 4
/**
* Horizontal pager indicator
@ -29,82 +36,163 @@ import com.tangem.core.ui.res.TangemThemePreview
*/
@Composable
fun PagerIndicator(pagerState: PagerState, modifier: Modifier = Modifier, indicatorCount: Int = 5) {
if (pagerState.pageCount == 0) return
val listState = rememberLazyListState()
val indicatorColor = TangemTheme.colors.control.key
val overlayColor = TangemTheme.colors.overlay.secondary
val indicatorSize = 8.dp
val inactiveIndicatorColor = remember(indicatorColor) {
indicatorColor.copy(alpha = 0.5f)
}
val baseIndicatorSize = 8.dp
val spacing = 4.dp
val totalWidth: Dp = indicatorSize * indicatorCount + spacing * (indicatorCount - 1)
val widthInPx = LocalDensity.current.run { indicatorSize.toPx() }
val currentItem by remember {
val indicatorState by remember(pagerState, indicatorCount) {
derivedStateOf {
pagerState.currentPage
val count = pagerState.pageCount
val current = pagerState.currentPage
val winSize = min(indicatorCount, count)
val centerPosition = winSize / 2
val start = when {
count <= winSize -> 0
current <= centerPosition -> 0
current >= count - centerPosition - 1 -> count - winSize
else -> current - centerPosition
}
Triple(count, winSize, start)
}
}
val itemCount = pagerState.pageCount
val (itemCount, windowSize, windowStart) = indicatorState
val currentItem by remember { derivedStateOf { pagerState.currentPage } }
LaunchedEffect(key1 = currentItem) {
val viewportSize = listState.layoutInfo.viewportSize
listState.animateScrollToItem(
currentItem,
(widthInPx / 2 - viewportSize.width / 2).toInt(),
)
LaunchedEffect(currentItem, windowStart) {
if (itemCount > windowSize) {
listState.animateScrollToItem(windowStart.coerceIn(0, itemCount - 1))
}
}
val maxContainerWidth = remember(baseIndicatorSize, spacing) {
baseIndicatorSize * TOTAL_MAX_INDICATORS + spacing * SPACER_COUNT_BETWEEN_INDICATORS
}
Box(
modifier = modifier
.height(32.dp)
.width(maxContainerWidth + 32.dp)
.background(
color = overlayColor,
shape = CircleShape,
)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = 16.dp, vertical = 12.dp)
.clip(CircleShape),
contentAlignment = Alignment.Center,
) {
LazyRow(
modifier = Modifier
.width(totalWidth),
modifier = Modifier.wrapContentWidth(),
state = listState,
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
horizontalArrangement = Arrangement.spacedBy(spacing),
userScrollEnabled = false,
) {
indicatorItems(
itemCount = itemCount,
currentItem = currentItem,
indicatorShape = CircleShape,
activeColor = indicatorColor,
inActiveColor = indicatorColor.copy(alpha = 0.5f),
indicatorSize = indicatorSize,
inActiveColor = inactiveIndicatorColor,
baseSize = baseIndicatorSize,
windowSize = windowSize,
windowStart = windowStart,
)
}
}
}
@Suppress("MagicNumber", "CyclomaticComplexMethod")
private fun calculateIndicatorHeight(position: Int, currentPosition: Int, baseSize: Dp, windowSize: Int): Dp {
val distance = abs(position - currentPosition)
val mediumSize = 6.dp
val smallSize = 4.dp
if (windowSize < 5) {
return when {
distance <= 1 -> baseSize
distance == 2 -> mediumSize
else -> smallSize
}
}
val isEdgeFocus = currentPosition == 0 || currentPosition == windowSize - 1
val isNearEdgeFocus = currentPosition == 1 || currentPosition == windowSize - 2
return when {
isEdgeFocus -> when {
distance <= 2 -> baseSize
distance == 3 -> mediumSize
else -> smallSize
}
isNearEdgeFocus -> when {
distance <= 1 -> baseSize
distance == 2 -> mediumSize
else -> smallSize
}
else -> when {
distance <= 1 -> baseSize
else -> mediumSize
}
}
}
@Suppress("LongParameterList")
private fun LazyListScope.indicatorItems(
itemCount: Int,
currentItem: Int,
indicatorShape: Shape,
activeColor: Color,
inActiveColor: Color,
indicatorSize: Dp,
baseSize: Dp,
windowSize: Int,
windowStart: Int,
) {
items(itemCount) { index ->
val safeWindowSize = min(windowSize, itemCount)
if (safeWindowSize <= 0) return
val isSelected = index == currentItem
val windowEnd = windowStart + safeWindowSize
val currentPosInWindow = (currentItem - windowStart).coerceIn(0, safeWindowSize - 1)
items(itemCount) { pageIndex ->
val isInWindow = pageIndex in windowStart until windowEnd
val positionInWindow = (pageIndex - windowStart).coerceIn(0, safeWindowSize - 1)
val isSelected = pageIndex == currentItem
val refinedHeight = if (isInWindow) {
calculateIndicatorHeight(
position = positionInWindow,
currentPosition = currentPosInWindow,
baseSize = baseSize,
windowSize = safeWindowSize,
)
} else {
0.dp
}
val targetWidth = if (isSelected) refinedHeight * 2 else refinedHeight
val targetShape = if (isSelected) RoundedCornerShape(16.dp) else CircleShape
val animatedWidth by animateDpAsState(targetValue = targetWidth, label = "width")
val animatedHeight by animateDpAsState(targetValue = refinedHeight, label = "height")
Box(
modifier = Modifier
.clip(indicatorShape)
.size(indicatorSize)
.padding(vertical = (baseSize - animatedHeight) / 2)
.clip(targetShape)
.width(animatedWidth)
.height(animatedHeight)
.background(
if (isSelected) activeColor else inActiveColor,
indicatorShape,
targetShape,
),
)
}
@ -112,19 +200,31 @@ private fun LazyListScope.indicatorItems(
@Preview(showBackground = true)
@Composable
private fun PagerIndicatorPreviewFirstPage() {
private fun PagerIndicatorPreview() {
TangemThemePreview {
Box(
Column(
modifier = Modifier
.background(TangemTheme.colors.background.primary)
.padding(),
contentAlignment = Alignment.Center,
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(10.dp),
) {
val pagerState = rememberPagerState(
initialPage = 0,
initialPage = 2,
pageCount = { 10 },
)
PagerIndicator(pagerState = pagerState)
val pagerState1 = rememberPagerState(
initialPage = 0,
pageCount = { 3 },
)
PagerIndicator(pagerState = pagerState1)
val pagerState2 = rememberPagerState(
initialPage = 0,
pageCount = { 1 },
)
PagerIndicator(pagerState = pagerState2)
}
}
}

View file

@ -25,7 +25,7 @@ import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.resourceReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.test.SelectNetworkFeeBottomSheetTestTags
import com.tangem.core.ui.test.SwapSelectNetworkFeeBottomSheetTestTags
import com.tangem.utils.StringsSigns
@Composable
@ -70,7 +70,7 @@ fun SelectorRowItem(
modifier = Modifier
.fillMaxWidth()
.padding(paddingValues)
.testTag(SelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
.testTag(SwapSelectNetworkFeeBottomSheetTestTags.SELECTOR_ITEM),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(

View file

@ -239,19 +239,19 @@ private fun StandardBottomSheet(
}
val newTarget =
when (val oldTarget = state.anchoredDraggableState.targetValue) {
Hidden -> if (newAnchors.hasAnchorFor(Hidden)) Hidden else oldTarget
Hidden -> if (newAnchors.hasPositionFor(Hidden)) Hidden else oldTarget
PartiallyExpanded ->
when {
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasAnchorFor(Expanded) -> Expanded
newAnchors.hasAnchorFor(Hidden) -> Hidden
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
Expanded ->
when {
newAnchors.hasAnchorFor(Expanded) -> Expanded
newAnchors.hasAnchorFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasAnchorFor(Hidden) -> Hidden
newAnchors.hasPositionFor(Expanded) -> Expanded
newAnchors.hasPositionFor(PartiallyExpanded) -> PartiallyExpanded
newAnchors.hasPositionFor(Hidden) -> Hidden
else -> oldTarget
}
}

View file

@ -102,11 +102,11 @@ class TangemSheetState(
/** Whether the sheet has an expanded state defined. */
val hasExpandedState: Boolean
get() = anchoredDraggableState.anchors.hasAnchorFor(Expanded)
get() = anchoredDraggableState.anchors.hasPositionFor(Expanded)
/** Whether the modal bottom sheet has a partially expanded state defined. */
val hasPartiallyExpandedState: Boolean
get() = anchoredDraggableState.anchors.hasAnchorFor(PartiallyExpanded)
get() = anchoredDraggableState.anchors.hasPositionFor(PartiallyExpanded)
/**
* Fully expand the bottom sheet with animation and suspend until it is fully expanded or
@ -274,8 +274,8 @@ internal fun consumeSwipeWithinBottomSheetBoundsNestedScrollConnection(
override suspend fun onPreFling(available: Velocity): Velocity {
val toFling = available.toFloat()
val currentOffset = sheetState.requireOffset()
val minAnchor = sheetState.anchoredDraggableState.anchors.minAnchor()
return if (toFling < 0 && currentOffset > minAnchor) {
val minPosition = sheetState.anchoredDraggableState.anchors.minPosition()
return if (toFling < 0 && currentOffset > minPosition) {
onFling(toFling)
// since we go to the anchor with tween settling, consume all for the best UX
available

View file

@ -1,29 +1,39 @@
package com.tangem.core.ui.components.tooltip
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.DpSize
import androidx.compose.ui.unit.dp
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.LocalWindowSize
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import kotlinx.coroutines.launch
/**
* A Tangem-themed tooltip component that displays a tooltip with the provided text when the content is clicked.
*
* @param text The text to be displayed inside the tooltip.
* @param modifier The modifier to be applied to the tooltip component.
* @param enabled If false, the tooltip will not be shown when the content is clicked.
* @param content The content that triggers the tooltip when clicked.
*/
@Composable
fun TangemTooltip(
text: String,
text: TextReference,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable (Modifier) -> Unit,
@ -33,30 +43,10 @@ fun TangemTooltip(
enabled = enabled,
tooltipContent = {
Text(
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
text = text,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary2,
)
},
content = content,
)
}
@Composable
fun TangemTooltip(
text: AnnotatedString,
modifier: Modifier = Modifier,
enabled: Boolean = true,
content: @Composable (Modifier) -> Unit,
) {
InternalTangemTooltip(
modifier = modifier,
enabled = enabled,
tooltipContent = {
Text(
modifier = Modifier.background(TangemTheme.colors.icon.secondary),
text = text,
modifier = Modifier
.background(TangemTheme.colors.icon.secondary)
.padding(horizontal = 6.dp, vertical = 8.dp),
text = text.resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary2,
)
@ -75,16 +65,22 @@ private fun InternalTangemTooltip(
) {
val tooltipState = rememberTooltipState(isPersistent = true)
val coroutineScope = rememberCoroutineScope()
val windowSize = LocalWindowSize.current.width
TooltipBox(
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(spacingBetweenTooltipAndAnchor = 8.dp),
positionProvider = TooltipDefaults.rememberTooltipPositionProvider(
positioning = TooltipAnchorPosition.Above,
spacingBetweenTooltipAndAnchor = 8.dp,
),
state = tooltipState,
modifier = modifier,
tooltip = {
PlainTooltip(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 12.dp),
caretSize = DpSize(width = 14.dp, height = 8.dp),
modifier = Modifier.padding(end = 12.dp),
shape = RoundedCornerShape(14.dp),
caretShape = TooltipDefaults.caretShape(),
maxWidth = windowSize - 24.dp,
contentColor = TangemTheme.colors.text.primary2,
containerColor = TangemTheme.colors.icon.secondary,
content = { tooltipContent() },
@ -103,24 +99,23 @@ private fun InternalTangemTooltip(
)
}
@Preview
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 720)
@Preview(showBackground = true, widthDp = 360, heightDp = 720, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTooltip_Preview() {
TangemThemePreview {
Box(
modifier = Modifier
.size(500.dp)
.background(TangemTheme.colors.background.secondary),
contentAlignment = Alignment.Center,
.fillMaxSize()
.background(TangemTheme.colors.background.tertiary),
) {
TangemTooltip(
modifier = Modifier
.background(TangemTheme.colors.background.secondary)
.size(64.dp),
text = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis.",
modifier = Modifier,
text = stringReference("Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed venenatis."),
content = { contentModifier ->
Icon(
modifier = contentModifier.size(64.dp),
modifier = contentModifier,
painter = painterResource(R.drawable.ic_token_info_24),
tint = TangemTheme.colors.icon.informative,
contentDescription = null,

View file

@ -0,0 +1,33 @@
package com.tangem.core.ui.decompose
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.State
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.bottomsheets.state.BottomSheetState
/**
* An interface describing the UI part of a component for a modular BottomSheet.
*
* Designed for use in Decompose components. It separates the UI into a title and content,
* providing access to the [BottomSheetState] to react to changes in the sheet's state (collapsed/expanded).
*/
@Stable
interface ComposableModularBottomSheetContentComponent {
/**
* Renders the title of the bottom sheet.
* @param bottomSheetState The current state of the bottom sheet. This can be used, for example,
* to change navigation buttons (e.g., hiding the "Back" button when collapsed).
*/
@Composable
fun Title(bottomSheetState: State<BottomSheetState>)
/**
* Renders the main content of the bottom sheet.
* @param bottomSheetState The current state of the bottom sheet. Useful for tracking visibility
* (e.g., for analytics or lifecycle effects when the sheet is [BottomSheetState.EXPANDED]).
*/
@Composable
fun Content(bottomSheetState: State<BottomSheetState>, modifier: Modifier)
}

View file

@ -0,0 +1,321 @@
package com.tangem.core.ui.ds.badge
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.res.painterResource
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 com.tangem.core.ui.R
import com.tangem.core.ui.ds.badge.TangemBadgeSize.*
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Tangem badge component to display a small piece of information with optional icon.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8441-83535&m=dev)
*
* @param text TextReference for the badge label.
* @param modifier Modifier to be applied to the badge.
* @param iconRes Drawable resource ID for the icon to be displayed in the badge.
* @param size [TangemBadgeSize] defining the size of the badge.
* @param shape [TangemBadgeShape] defining the shape of the badge.
* @param color [TangemBadgeColor] defining the color scheme of the badge.
* @param type [TangemBadgeType] defining the style of the badge.
* @param iconPosition [TangemBadgeIconPosition] defining icon position of the badge.
*
[REDACTED_AUTHOR]
*/
@Composable
fun TangemBadge(
text: TextReference,
modifier: Modifier = Modifier,
@DrawableRes iconRes: Int? = null,
size: TangemBadgeSize = X9,
shape: TangemBadgeShape = TangemBadgeShape.Default,
color: TangemBadgeColor = TangemBadgeColor.Gray,
type: TangemBadgeType = TangemBadgeType.Solid,
iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
) {
val iconColor = getIconColor(type = type, color = color)
Row(
horizontalArrangement = Arrangement.spacedBy(size.toContentPadding()),
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.heightIn(min = size.toHeightDp())
.clip(shape.toShape(size))
.getBackgroundColor(type = type, color = color, shape = shape.toShape(size))
.padding(size.toPaddingDp(position = iconPosition)),
) {
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.Start,
modifier = Modifier.size(size = size.toContentSize()),
label = "Start Icon Visibility",
) {
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
Icon(
painter = painterResource(id = wrappedIconRes),
contentDescription = null,
tint = iconColor,
)
}
Text(
text = text.resolveReference(),
style = size.toTextStyle(),
maxLines = 1,
color = getTextColor(type = type, color = color),
)
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemBadgeIconPosition.End,
modifier = Modifier.size(size = size.toContentSize()),
label = "End Icon Visibility",
) {
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
Icon(
painter = painterResource(id = wrappedIconRes),
contentDescription = null,
tint = iconColor,
)
}
}
}
/**
* Tangem badge shape options.
*/
enum class TangemBadgeShape {
Default,
Rounded,
;
@ReadOnlyComposable
@Composable
internal fun toShape(size: TangemBadgeSize) = RoundedCornerShape(
when (this) {
Rounded -> when (size) {
X4,
X6,
-> TangemTheme.dimens2.x4
X9 -> TangemTheme.dimens2.x25
}
Default -> when (size) {
X4 -> TangemTheme.dimens2.x1
X6,
X9,
-> 6.dp
}
},
)
}
/**
* Tangem badge size options.
*/
enum class TangemBadgeSize {
X4,
X6,
X9,
;
@ReadOnlyComposable
@Composable
internal fun toHeightDp() = when (this) {
X4 -> TangemTheme.dimens2.x4
X6 -> TangemTheme.dimens2.x6
X9 -> TangemTheme.dimens2.x9
}
@ReadOnlyComposable
@Composable
internal fun toPaddingDp(position: TangemBadgeIconPosition) = when (this) {
X4 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 4.dp, end = 6.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 6.dp, end = 4.dp)
}
X6 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 8.dp, end = 12.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 12.dp, end = 8.dp)
}
X9 -> when (position) {
TangemBadgeIconPosition.Start -> PaddingValues(start = 12.dp, end = 16.dp)
TangemBadgeIconPosition.End -> PaddingValues(start = 16.dp, end = 12.dp)
}
}
@ReadOnlyComposable
@Composable
internal fun toContentSize() = when (this) {
X4 -> TangemTheme.dimens2.x3
X6,
X9,
-> TangemTheme.dimens2.x4
}
@ReadOnlyComposable
@Composable
internal fun toContentPadding() = when (this) {
X4 -> TangemTheme.dimens2.x0_5
X6,
X9,
-> TangemTheme.dimens2.x1
}
@ReadOnlyComposable
@Composable
internal fun toTextStyle() = when (this) {
X4 -> TangemTheme.typography2.captionSemibold11
X6 -> TangemTheme.typography2.captionSemibold12
X9 -> TangemTheme.typography2.bodySemibold16
}
}
/**
* Position of the icon in the Tangem badge.
*/
enum class TangemBadgeIconPosition {
Start,
End,
}
/**
* Tangem badge type options.
*/
enum class TangemBadgeType {
Solid,
Tinted,
Outline,
}
/**
* Tangem badge color options.
*/
enum class TangemBadgeColor {
Blue,
Red,
Gray,
}
@ReadOnlyComposable
@Composable
private fun getIconColor(type: TangemBadgeType, color: TangemBadgeColor) = when (color) {
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.iconGray
TangemBadgeColor.Blue -> when (type) {
TangemBadgeType.Outline,
TangemBadgeType.Tinted,
-> TangemTheme.colors2.markers.iconBlue
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
}
TangemBadgeColor.Red -> when (type) {
TangemBadgeType.Outline,
TangemBadgeType.Tinted,
-> TangemTheme.colors2.markers.iconRed
TangemBadgeType.Solid -> TangemTheme.colors2.graphic.neutral.primaryInvertedConstant
}
}
@ReadOnlyComposable
@Composable
private fun getTextColor(type: TangemBadgeType, color: TangemBadgeColor) = when (color) {
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.textGray
TangemBadgeColor.Blue -> when (type) {
TangemBadgeType.Outline,
TangemBadgeType.Tinted,
-> TangemTheme.colors2.markers.textBlue
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
}
TangemBadgeColor.Red -> when (type) {
TangemBadgeType.Outline,
TangemBadgeType.Tinted,
-> TangemTheme.colors2.markers.textRed
TangemBadgeType.Solid -> TangemTheme.colors2.text.neutral.primaryInvertedConstant
}
}
@ReadOnlyComposable
@Composable
private fun Modifier.getBackgroundColor(type: TangemBadgeType, color: TangemBadgeColor, shape: Shape) = when (type) {
TangemBadgeType.Solid -> background(
when (color) {
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundSolidGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundSolidBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundSolidRed
},
)
TangemBadgeType.Tinted -> background(
when (color) {
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.backgroundTintedGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.backgroundTintedBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.backgroundTintedRed
},
)
TangemBadgeType.Outline -> {
border(
color = when (color) {
TangemBadgeColor.Gray -> TangemTheme.colors2.markers.borderGray
TangemBadgeColor.Blue -> TangemTheme.colors2.markers.borderTintedBlue
TangemBadgeColor.Red -> TangemTheme.colors2.markers.borderTintedRed
},
shape = shape,
width = 1.dp,
)
}
}
// region Preview
@Composable
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemBadge_Preview(@PreviewParameter(TangemBadgePreviewProvider::class) params: TangemBadgeColor) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(2) { yIndex ->
Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
repeat(TangemBadgeType.entries.size) { index ->
TangemBadge(
text = stringReference("Title"),
iconRes = R.drawable.ic_information_24,
type = TangemBadgeType.entries[index],
color = params,
shape = TangemBadgeShape.entries[yIndex % 2],
iconPosition = TangemBadgeIconPosition.entries[yIndex % 2],
)
}
}
}
}
}
}
private class TangemBadgePreviewProvider : PreviewParameterProvider<TangemBadgeColor> {
override val values: Sequence<TangemBadgeColor>
get() = sequenceOf(
TangemBadgeColor.Gray,
TangemBadgeColor.Blue,
TangemBadgeColor.Red,
)
}
// endregion

View file

@ -0,0 +1,125 @@
package com.tangem.core.ui.ds.button
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Accent Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8004-26798)
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
* @param shape TangemButtonShape defining the shape of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
fun AccentTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
shape: TangemButtonShape = TangemButtonShape.Default,
) {
TangemButtonInternal(
onClick = onClick,
modifier = modifier
.clip(shape.toShape(size))
.then(
when (state) {
TangemButtonState.Disabled,
TangemButtonState.Default,
-> Modifier.background(TangemTheme.colors2.button.backgroundPositive)
TangemButtonState.Loading,
TangemButtonState.Pressed,
-> Modifier
.background(TangemTheme.colors2.button.backgroundPositive)
.background(TangemTheme.colors2.overlay.overlaySecondary)
},
),
text = text,
contentColor = TangemTheme.colors2.text.neutral.primaryInvertedConstant,
iconRes = iconRes,
enabled = enabled,
size = size,
state = state,
iconPosition = iconPosition,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun AccentTangemButton_Preview(
@PreviewParameter(AccentTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(21.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(4) { yIndex ->
val shape = if (yIndex < 2) TangemButtonShape.Default else TangemButtonShape.Rounded
val text = if (yIndex % 2 == 1) null else stringReference("Button")
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
repeat(2) { xIndex ->
val iconPosition = if (xIndex == 1) {
TangemButtonIconPosition.Start
} else {
TangemButtonIconPosition.End
}
AccentTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
shape = shape,
iconPosition = iconPosition,
iconRes = R.drawable.ic_tangem_24,
state = params,
)
}
}
}
}
}
}
private class AccentTangemButtonPreviewProvider : PreviewParameterProvider<TangemButtonState> {
override val values: Sequence<TangemButtonState>
get() = sequenceOf(
TangemButtonState.Default,
TangemButtonState.Pressed,
TangemButtonState.Loading,
TangemButtonState.Disabled,
)
}
// endregion

View file

@ -0,0 +1,111 @@
package com.tangem.core.ui.ds.button
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Ghost Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4804)
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
fun GhostTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
) {
val contentColor = when (state) {
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
else -> TangemTheme.colors2.text.neutral.primary
}
TangemButtonInternal(
onClick = onClick,
modifier = modifier,
text = text,
contentColor = contentColor,
enabled = enabled,
size = size,
state = state,
iconPosition = iconPosition,
iconRes = iconRes,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun GhostTangemButton_Preview(
@PreviewParameter(GhostTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(21.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(4) { yIndex ->
val text = if (yIndex % 2 == 1) null else stringReference("Button")
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
repeat(2) { xIndex ->
val iconPosition = if (xIndex == 1) {
TangemButtonIconPosition.Start
} else {
TangemButtonIconPosition.End
}
GhostTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
iconPosition = iconPosition,
iconRes = R.drawable.ic_tangem_24,
state = params,
)
}
}
}
}
}
}
private class GhostTangemButtonPreviewProvider : PreviewParameterProvider<TangemButtonState> {
override val values: Sequence<TangemButtonState>
get() = sequenceOf(
TangemButtonState.Default,
TangemButtonState.Pressed,
TangemButtonState.Loading,
TangemButtonState.Disabled,
)
}
// endregion

View file

@ -0,0 +1,132 @@
package com.tangem.core.ui.ds.button
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Outline Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4800)
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
* @param shape TangemButtonShape defining the shape of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
fun OutlineTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
shape: TangemButtonShape = TangemButtonShape.Default,
) {
val backgroundModifier = when (state) {
TangemButtonState.Loading,
TangemButtonState.Pressed,
TangemButtonState.Disabled,
TangemButtonState.Default,
-> Modifier
.background(TangemTheme.colors2.surface.level1)
.border(
width = 1.dp,
color = TangemTheme.colors2.border.neutral.primary,
shape = shape.toShape(size),
)
}
val contentColor = when (state) {
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
else -> TangemTheme.colors2.text.neutral.primary
}
TangemButtonInternal(
onClick = onClick,
modifier = modifier
.clip(shape.toShape(size))
.then(backgroundModifier),
text = text,
contentColor = contentColor,
iconRes = iconRes,
enabled = enabled,
size = size,
state = state,
iconPosition = iconPosition,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun OutlineTangemButton_Preview(
@PreviewParameter(OutlineTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(21.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(4) { yIndex ->
val shape = if (yIndex < 2) TangemButtonShape.Default else TangemButtonShape.Rounded
val text = if (yIndex % 2 == 1) null else stringReference("Button")
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
repeat(2) { xIndex ->
val iconPosition = if (xIndex == 0) {
TangemButtonIconPosition.Start
} else {
TangemButtonIconPosition.End
}
OutlineTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
shape = shape,
iconPosition = iconPosition,
iconRes = R.drawable.ic_tangem_24,
state = params,
)
}
}
}
}
}
}
private class OutlineTangemButtonPreviewProvider : PreviewParameterProvider<TangemButtonState> {
override val values: Sequence<TangemButtonState>
get() = sequenceOf(
TangemButtonState.Default,
TangemButtonState.Pressed,
TangemButtonState.Loading,
TangemButtonState.Disabled,
)
}
// endregion

View file

@ -0,0 +1,127 @@
package com.tangem.core.ui.ds.button
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Primary Inverse Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=7545-78314)
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
* @param shape TangemButtonShape defining the shape of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
fun PrimaryInverseTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
shape: TangemButtonShape = TangemButtonShape.Default,
) {
val backgroundModifier = when (state) {
TangemButtonState.Default -> Modifier.background(TangemTheme.colors2.button.backgroundPrimaryInverse)
TangemButtonState.Disabled -> Modifier.background(TangemTheme.colors2.button.backgroundDisabled)
TangemButtonState.Loading,
TangemButtonState.Pressed,
-> Modifier
.background(TangemTheme.colors2.button.backgroundPrimaryInverse)
.background(TangemTheme.colors2.overlay.overlayPrimary)
}
val contentColor = when (state) {
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
else -> TangemTheme.colors2.text.neutral.primary
}
TangemButtonInternal(
onClick = onClick,
modifier = modifier
.clip(shape.toShape(size))
.then(backgroundModifier),
text = text,
contentColor = contentColor,
enabled = enabled,
size = size,
state = state,
iconPosition = iconPosition,
iconRes = iconRes,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PrimaryInverseTangemButton_Preview(
@PreviewParameter(PrimaryInverseTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(21.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level2)
.padding(8.dp),
) {
repeat(4) { yIndex ->
val shape = if (yIndex < 2) TangemButtonShape.Default else TangemButtonShape.Rounded
val text = if (yIndex % 2 == 1) null else stringReference("Button")
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
repeat(2) { xIndex ->
val iconPosition = if (xIndex == 0) {
TangemButtonIconPosition.Start
} else {
TangemButtonIconPosition.End
}
PrimaryInverseTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
shape = shape,
iconPosition = iconPosition,
iconRes = R.drawable.ic_tangem_24,
state = params,
)
}
}
}
}
}
}
private class PrimaryInverseTangemButtonPreviewProvider : PreviewParameterProvider<TangemButtonState> {
override val values: Sequence<TangemButtonState>
get() = sequenceOf(
TangemButtonState.Default,
TangemButtonState.Pressed,
TangemButtonState.Loading,
TangemButtonState.Disabled,
)
}
// endregion

View file

@ -0,0 +1,122 @@
package com.tangem.core.ui.ds.button
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Primary Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4732&t=euYo1qCxPlQl3Fa6-4)
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
* @param shape TangemButtonShape defining the shape of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
fun PrimaryTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
shape: TangemButtonShape = TangemButtonShape.Default,
) {
val backgroundModifier = when (state) {
TangemButtonState.Loading,
TangemButtonState.Default,
-> Modifier.background(TangemTheme.colors2.button.backgroundPrimary)
TangemButtonState.Disabled -> Modifier.background(TangemTheme.colors2.button.backgroundDisabled)
TangemButtonState.Pressed -> Modifier
.background(TangemTheme.colors2.button.backgroundPrimary)
.background(TangemTheme.colors2.overlay.overlaySecondary)
}
val contentColor = when (state) {
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
else -> TangemTheme.colors2.text.neutral.primaryInverted
}
TangemButtonInternal(
onClick = onClick,
modifier = modifier
.clip(shape.toShape(size))
.then(backgroundModifier),
text = text,
contentColor = contentColor,
enabled = enabled,
size = size,
state = state,
iconPosition = iconPosition,
iconRes = iconRes,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun PrimaryTangemButton_Preview(
@PreviewParameter(PrimaryTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(21.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(4) { yIndex ->
val shape = if (yIndex < 2) TangemButtonShape.Default else TangemButtonShape.Rounded
val text = if (yIndex % 2 == 1) null else stringReference("Button")
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
repeat(TangemButtonIconPosition.entries.size) { xIndex ->
PrimaryTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
shape = shape,
iconPosition = TangemButtonIconPosition.entries[xIndex],
iconRes = R.drawable.ic_tangem_24,
state = params,
)
}
}
}
}
}
}
private class PrimaryTangemButtonPreviewProvider : PreviewParameterProvider<TangemButtonState> {
override val values: Sequence<TangemButtonState>
get() = sequenceOf(
TangemButtonState.Default,
TangemButtonState.Pressed,
TangemButtonState.Loading,
TangemButtonState.Disabled,
)
}
// endregion

View file

@ -0,0 +1,125 @@
package com.tangem.core.ui.ds.button
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 com.tangem.core.ui.R
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* [Secondary Tangem button](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=5854-4796)
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
* @param shape TangemButtonShape defining the shape of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
fun SecondaryTangemButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
shape: TangemButtonShape = TangemButtonShape.Default,
) {
val backgroundModifier = when (state) {
TangemButtonState.Loading,
TangemButtonState.Default,
-> Modifier.background(TangemTheme.colors2.button.backgroundSecondary)
TangemButtonState.Disabled -> Modifier.background(TangemTheme.colors2.button.backgroundDisabled)
TangemButtonState.Pressed -> Modifier.background(TangemTheme.colors2.overlay.overlayPrimary)
}
val contentColor = when (state) {
TangemButtonState.Disabled -> TangemTheme.colors2.text.status.disabled
else -> TangemTheme.colors2.text.neutral.primary
}
TangemButtonInternal(
onClick = onClick,
modifier = modifier
.clip(shape.toShape(size))
.then(backgroundModifier),
text = text,
contentColor = contentColor,
iconRes = iconRes,
enabled = enabled,
size = size,
state = state,
iconPosition = iconPosition,
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 480)
@Preview(showBackground = true, widthDp = 480, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun SecondaryTangemButton_Preview(
@PreviewParameter(SecondaryTangemButtonPreviewProvider::class) params: TangemButtonState,
) {
TangemThemePreviewRedesign {
Row(
horizontalArrangement = Arrangement.spacedBy(21.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(8.dp),
) {
repeat(4) { yIndex ->
val shape = if (yIndex < 2) TangemButtonShape.Default else TangemButtonShape.Rounded
val text = if (yIndex % 2 == 1) null else stringReference("Button")
Column(verticalArrangement = Arrangement.spacedBy(10.dp)) {
repeat(2) { xIndex ->
val iconPosition = if (xIndex == 0) {
TangemButtonIconPosition.Start
} else {
TangemButtonIconPosition.End
}
SecondaryTangemButton(
onClick = {},
text = text,
size = TangemButtonSize.X15,
shape = shape,
iconPosition = iconPosition,
iconRes = R.drawable.ic_tangem_24,
state = params,
)
}
}
}
}
}
}
private class SecondaryTangemButtonPreviewProvider : PreviewParameterProvider<TangemButtonState> {
override val values: Sequence<TangemButtonState>
get() = sequenceOf(
TangemButtonState.Default,
TangemButtonState.Pressed,
TangemButtonState.Loading,
TangemButtonState.Disabled,
)
}
// endregion

View file

@ -0,0 +1,256 @@
package com.tangem.core.ui.ds.button
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.TextAutoSize
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.StrokeCap
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.BaseButtonTestTags
/**
* A customizable button component that supports text, icons, and different states.
*
* @param onClick Lambda to be invoked when the button is clicked.
* @param modifier Modifier to be applied to the button.
* @param text TextReference for the button label.
* @param iconRes Drawable resource ID for the icon to be displayed in the button.
* @param iconPosition Position of the icon (Start or End).
* @param enabled Boolean indicating whether the button is enabled.
* @param contentColor Color of the button content (text and icon).
* @param size TangemButtonSize defining the size of the button.
* @param state TangemButtonState defining the current state of the button.
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun TangemButtonInternal(
onClick: () -> Unit,
modifier: Modifier = Modifier,
text: TextReference? = null,
@DrawableRes iconRes: Int? = null,
iconPosition: TangemButtonIconPosition = TangemButtonIconPosition.Start,
enabled: Boolean = true,
contentColor: Color = TangemTheme.colors2.text.neutral.primary,
size: TangemButtonSize = TangemButtonSize.X15,
state: TangemButtonState = TangemButtonState.Default,
) {
Row(
modifier = modifier
.testTag(BaseButtonTestTags.BUTTON)
.height(size.toHeightDp())
.conditionalCompose(text == null) {
width(size.toHeightDp())
}
.clickableSingle(enabled = enabled, onClick = onClick, role = Role.Button)
.conditionalCompose(text != null) {
padding(horizontal = size.toPaddingDp())
}
.animateContentSize(),
horizontalArrangement = Arrangement.spacedBy(4.dp, Alignment.CenterHorizontally),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemButtonIconPosition.Start,
modifier = Modifier.size(size = size.toContentSize()),
) {
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
}
AnimatedVisibility(text != null && state != TangemButtonState.Loading) {
val wrappedText = remember(this) { requireNotNull(text) }
val textStyle = size.toTextStyle()
Text(
text = wrappedText.resolveReference(),
style = textStyle,
color = contentColor,
textAlign = TextAlign.Center,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
autoSize = TextAutoSize.StepBased(
minFontSize = 12.sp,
maxFontSize = textStyle.fontSize,
),
modifier = Modifier.testTag(BaseButtonTestTags.TEXT),
)
}
AnimatedVisibility(
visible = iconRes != null && iconPosition == TangemButtonIconPosition.End,
modifier = Modifier.size(size = size.toContentSize()),
) {
val wrappedIconRes = remember(this) { requireNotNull(iconRes) }
TangemButtonIcon(iconRes = wrappedIconRes, state = state, iconColor = contentColor, size = size)
}
}
}
@Composable
private fun TangemButtonIcon(
@DrawableRes iconRes: Int,
iconColor: Color,
state: TangemButtonState,
size: TangemButtonSize,
) {
AnimatedContent(state) { targetState ->
when (targetState) {
TangemButtonState.Loading -> CircularProgressIndicator(
color = iconColor,
strokeWidth = 2.dp,
strokeCap = StrokeCap.Round,
modifier = Modifier.padding(
when (size) {
TangemButtonSize.X7,
TangemButtonSize.X8,
TangemButtonSize.X9,
TangemButtonSize.X10,
-> 0.5.dp
TangemButtonSize.X12,
TangemButtonSize.X15,
-> 4.5.dp
},
),
)
else -> Icon(
painter = painterResource(id = iconRes),
contentDescription = null,
tint = iconColor,
)
}
}
}
/**
* Defines the shape of the Tangem button.
*/
enum class TangemButtonShape {
Default,
Rounded,
;
@ReadOnlyComposable
@Composable
internal fun toShape(size: TangemButtonSize) = RoundedCornerShape(
when (this) {
Default -> size.toShapeRadius()
Rounded -> 100.dp
},
)
}
/**
* Defines the size of the Tangem button.
*/
enum class TangemButtonSize {
X7,
X8,
X9,
X10,
X12,
X15,
;
@ReadOnlyComposable
@Composable
internal fun toHeightDp() = when (this) {
X7 -> TangemTheme.dimens2.x7
X8 -> TangemTheme.dimens2.x8
X9 -> TangemTheme.dimens2.x9
X10 -> TangemTheme.dimens2.x10
X12 -> TangemTheme.dimens2.x12
X15 -> TangemTheme.dimens2.x15
}
@ReadOnlyComposable
@Composable
internal fun toPaddingDp() = when (this) {
X7 -> TangemTheme.dimens2.x2
X8,
X9,
X10,
-> TangemTheme.dimens2.x3
X12,
X15,
-> TangemTheme.dimens2.x6
}
@ReadOnlyComposable
@Composable
internal fun toContentSize() = when (this) {
X7,
X8,
X9,
X10,
-> TangemTheme.dimens2.x5
X12,
X15,
-> TangemTheme.dimens2.x7
}
@ReadOnlyComposable
@Composable
internal fun toShapeRadius() = when (this) {
X7,
X8,
X9,
X10,
-> TangemTheme.dimens2.x2
X12 -> TangemTheme.dimens2.x3
X15 -> TangemTheme.dimens2.x4
}
@ReadOnlyComposable
@Composable
internal fun toTextStyle(): TextStyle = when (this) {
X7 -> TangemTheme.typography2.bodyRegular14
X8,
X9,
X10,
X12,
X15,
-> TangemTheme.typography2.bodySemibold16
}
}
/**
* Defines the state of the Tangem button.
*/
enum class TangemButtonState {
Default,
Disabled,
Pressed,
Loading,
}
/**
* Defines the position of the icon in the Tangem button.
*/
enum class TangemButtonIconPosition {
Start,
End,
}

View file

@ -0,0 +1,239 @@
package com.tangem.core.ui.ds.topbar
import android.content.res.Configuration
import androidx.annotation.DrawableRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
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.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import com.tangem.core.ui.R
import com.tangem.core.ui.extensions.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* 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)
*
* @param title The title text to be displayed in the center of the top bar.
* @param modifier Modifier to be applied to the top bar.
* @param subtitle Optional subtitle text to be displayed below the title.
* @param startIconRes Optional drawable resource ID for the start icon.
* @param onStartContentClick Optional click action for the start icon.
* @param endIconRes Optional drawable resource ID for the end icon.
* @param onEndContentClick Optional click action for the end icon.
* @param isGhostButtons Flag to determine if ghost button styling should be applied.
*
[REDACTED_AUTHOR]
*/
@Composable
fun TangemTopBar(
modifier: Modifier = Modifier,
title: TextReference? = null,
subtitle: TextReference? = null,
@DrawableRes startIconRes: Int? = null,
onStartContentClick: (() -> Unit)? = null,
@DrawableRes endIconRes: Int? = null,
onEndContentClick: (() -> Unit)? = null,
@DrawableRes titleIconRes: Int? = null,
titleStyle: TextStyle = TangemTheme.typography2.headingSemibold17,
isGhostButtons: Boolean = false,
) {
TangemTopBarInner(
modifier = modifier,
content = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5),
) {
TangemTopBarTitle(title = title, titleIconRes = titleIconRes, titleStyle = titleStyle)
AnimatedVisibility(
visible = subtitle != null,
label = "Subtitle Visibility",
) {
val wrappedSubtitle = remember(this) { requireNotNull(subtitle) }
Text(
text = wrappedSubtitle.resolveAnnotatedReference(),
color = TangemTheme.colors2.text.neutral.secondary,
style = TangemTheme.typography2.bodyRegular15,
textAlign = TextAlign.Center,
maxLines = 1,
)
}
}
},
startContent = if (startIconRes != null) {
{ TangemTopBarIcon(iconRes = startIconRes) }
} else {
null
},
onStartContentClick = onStartContentClick,
endContent = if (endIconRes != null) {
{ TangemTopBarIcon(iconRes = endIconRes) }
} else {
null
},
onEndContentClick = onEndContentClick,
isGhostButtons = isGhostButtons,
)
}
@Composable
private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?, titleStyle: TextStyle) {
AnimatedVisibility(
visible = title != null,
label = "Title Visibility",
) {
val wrappedTitle = remember(this) { requireNotNull(title) }
Row(
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = titleIconRes != null,
label = "Title Icon Visibility",
) {
val wrappedTitleIconRes = remember(this) {
requireNotNull(titleIconRes)
}
Icon(
imageVector = ImageVector.vectorResource(id = wrappedTitleIconRes),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier.size(TangemTheme.dimens2.x4),
)
}
Text(
text = wrappedTitle.resolveAnnotatedReference(),
color = TangemTheme.colors2.text.neutral.primary,
style = titleStyle,
textAlign = TextAlign.Center,
maxLines = 1,
)
}
}
}
@Composable
private fun TangemTopBarIcon(@DrawableRes iconRes: Int) {
Icon(
imageVector = ImageVector.vectorResource(id = iconRes),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primary,
modifier = Modifier.fillMaxSize(),
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 375)
@Preview(showBackground = true, widthDp = 375, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) params: TangemTopBarPreviewData) {
TangemThemePreviewRedesign {
TangemTopBar(
title = params.title,
subtitle = params.subtitle,
startIconRes = params.startIconRes,
endIconRes = params.endIconRes,
titleIconRes = params.titleIconRes,
isGhostButtons = params.isGhostButtons,
onStartContentClick = {},
onEndContentClick = {},
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
}
}
private class TangemTopBarPreviewData(
val title: TextReference? = null,
val subtitle: TextReference? = null,
val isGhostButtons: Boolean = false,
val titleIconRes: Int? = null,
val startIconRes: Int? = null,
val endIconRes: Int? = null,
)
private class PreviewProvider : PreviewParameterProvider<TangemTopBarPreviewData> {
override val values: Sequence<TangemTopBarPreviewData>
get() = sequenceOf(
TangemTopBarPreviewData(
title = stringReference("Title"),
startIconRes = R.drawable.ic_tangem_24,
endIconRes = R.drawable.ic_more_vertical_24,
isGhostButtons = true,
),
TangemTopBarPreviewData(
title = stringReference("Title"),
subtitle = stringReference("Subtitle"),
startIconRes = R.drawable.ic_tangem_24,
endIconRes = R.drawable.ic_more_vertical_24,
isGhostButtons = true,
),
TangemTopBarPreviewData(
title = stringReference("Title"),
subtitle = stringReference("Subtitle"),
titleIconRes = R.drawable.ic_tangem_24,
startIconRes = R.drawable.ic_tangem_24,
endIconRes = R.drawable.ic_more_vertical_24,
isGhostButtons = true,
),
TangemTopBarPreviewData(
subtitle = stringReference("Subtitle"),
titleIconRes = R.drawable.ic_tangem_24,
startIconRes = R.drawable.ic_tangem_24,
endIconRes = R.drawable.ic_more_vertical_24,
isGhostButtons = true,
),
TangemTopBarPreviewData(
title = stringReference("Title"),
endIconRes = R.drawable.ic_more_vertical_24,
isGhostButtons = true,
),
TangemTopBarPreviewData(
title = stringReference("Title"),
startIconRes = R.drawable.ic_tangem_24,
isGhostButtons = true,
),
TangemTopBarPreviewData(
title = combinedReference(
stringReference("$ 46,112"),
styledStringReference(
value = ".30",
spanStyleReference = {
TangemTheme.typography.caption1.copy(TangemTheme.colors2.text.neutral.secondary)
.toSpanStyle()
},
),
),
),
TangemTopBarPreviewData(
title = combinedReference(
stringReference("$ 46,112"),
styledStringReference(
value = ".30",
spanStyleReference = {
TangemTheme.typography.caption1.copy(TangemTheme.colors2.text.neutral.secondary)
.toSpanStyle()
},
),
),
startIconRes = R.drawable.ic_tangem_24,
endIconRes = R.drawable.ic_more_vertical_24,
),
)
}
// endregion

View file

@ -0,0 +1,90 @@
package com.tangem.core.ui.ds.topbar
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import com.tangem.core.ui.extensions.clickableSingle
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.conditionalCompose
import com.tangem.core.ui.res.TangemTheme
/**
* Internal top bar composable that arranges optional start, center, and end content.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8435-74860&m=dev)
*
* @param modifier Modifier to be applied to the top bar.
* @param content Center content of the top bar.
* @param startContent Optional start content of the top bar.
* @param onStartContentClick Optional click action for the start content.
* @param endContent Optional end content of the top bar.
* @param onEndContentClick Optional click action for the end content.
* @param isGhostButtons Flag to determine if ghost button styling should be applied.
*
[REDACTED_AUTHOR]
*/
@Composable
internal fun TangemTopBarInner(
modifier: Modifier = Modifier,
content: (@Composable () -> Unit)? = null,
startContent: (@Composable () -> Unit)? = null,
onStartContentClick: (() -> Unit)? = null,
endContent: (@Composable () -> Unit)? = null,
onEndContentClick: (() -> Unit)? = null,
isGhostButtons: Boolean = false,
) {
Box(
modifier = modifier
.height(TangemTheme.dimens2.x16)
.fillMaxWidth()
.padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3),
) {
val iconModifier = Modifier
.size(TangemTheme.dimens2.x10)
.clip(RoundedCornerShape(TangemTheme.dimens2.x25))
.background(TangemTheme.colors2.button.backgroundSecondary)
AnimatedVisibility(
visible = startContent != null,
modifier = Modifier.align(Alignment.CenterStart),
label = "Start Content Visibility",
) {
Box(
modifier = iconModifier
.conditional(onStartContentClick != null) {
clickableSingle { onStartContentClick?.invoke() }
}
.conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) },
) {
startContent?.invoke()
}
}
AnimatedVisibility(
visible = content != null,
modifier = Modifier.align(Alignment.Center),
) {
content?.invoke()
}
AnimatedVisibility(
visible = endContent != null,
modifier = Modifier.align(Alignment.CenterEnd),
label = "End Content Visibility",
) {
Box(
modifier = iconModifier
.conditional(onEndContentClick != null) {
clickableSingle { onEndContentClick?.invoke() }
}
.conditionalCompose(isGhostButtons) { padding(TangemTheme.dimens2.x1) },
) {
endContent?.invoke()
}
}
}
}

View file

@ -56,12 +56,31 @@ fun AnnotatedString.Builder.appendMarkdown(markdownText: String, node: ASTNode):
return this
}
/**
* Appends a single space character to the [AnnotatedString.Builder].
*/
fun AnnotatedString.Builder.appendSpace() = append(" ")
/**
* Appends text with the specified [Color] to the [AnnotatedString.Builder].
*
* @param text The text to append.
* @param color The [Color] to apply to the appended text.
*/
fun AnnotatedString.Builder.appendColored(text: String, color: Color) = withStyle(SpanStyle(color = color)) {
append(text)
}
/**
* Appends text with the specified [SpanStyle] to the [AnnotatedString.Builder].
*
* @param text The text to append.
* @param spanStyle The [SpanStyle] to apply to the appended text.
*/
fun AnnotatedString.Builder.appendStyled(text: String, spanStyle: SpanStyle) = withStyle(spanStyle) {
append(text)
}
/**
* Appends text from a template string to the AnnotatedString.Builder, replacing a placeholder (default "%s")
* with custom styled content provided by a lambda. The lambda allows you to insert styled or complex content

View file

@ -7,10 +7,11 @@ import androidx.compose.ui.graphics.Color
/**
* Utility class for keeping themed color reference from app theme.
*
* It necessary to use [Immutable] annotation for runtime stability.
* It is necessary to use [Immutable] annotation for runtime stability.
*
* @property value color provider from theme
*/
@Deprecated("Use TextReference with applied SpanStyleReference for colored text.")
@Immutable
data class ColorReference(val value: @Composable () -> Color)

View file

@ -54,8 +54,8 @@ fun Modifier.conditional(condition: Boolean, modifier: Modifier.() -> Modifier):
@Composable
fun Modifier.conditionalCompose(
condition: Boolean,
modifier: @Composable Modifier.() -> Modifier = { Modifier },
otherModifier: @Composable Modifier.() -> Modifier = { this },
modifier: @Composable Modifier.() -> Modifier = { Modifier },
): Modifier {
return if (condition) {
then(modifier(Modifier))

View file

@ -0,0 +1,19 @@
package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
import androidx.compose.ui.text.SpanStyle
/**
* Utility functional interface for keeping themed [SpanStyle] reference from app theme.
* It is necessary to use [Stable] annotation for runtime stability.
*/
@Stable
@FunctionalInterface
fun interface SpanStyleReference {
@ReadOnlyComposable
@Composable
operator fun invoke(): SpanStyle
}

View file

@ -1,16 +1,33 @@
package com.tangem.core.ui.extensions
import android.content.res.Configuration
import android.content.res.Resources
import androidx.annotation.PluralsRes
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.AnnotatedString.Builder
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withLink
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 com.tangem.core.res.getPluralStringSafe
import com.tangem.core.res.getStringSafe
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.utils.StringsSigns.THREE_STARS
import org.intellij.markdown.MarkdownElementTypes
import kotlin.contracts.ExperimentalContracts
@ -69,6 +86,35 @@ sealed interface TextReference {
*/
data class Combined(val refs: WrappedList<TextReference>) : TextReference
/**
* Styled string value
*
* @property value string value
* @property spanStyleReference text style reference
* @property onClick optional click action
*/
data class StyledStr(
val value: String,
val spanStyleReference: SpanStyleReference,
val onClick: (() -> Unit)? = null,
) : TextReference
/**
* Styled string resource
*
* @property id resource id
* @property formatArgs arguments. Impossible to use [kotlinx.collections.immutable.ImmutableList] because
* [Any] is unstable.
* @property spanStyleReference text style reference
* @property onClick optional click action
*/
data class StyledRes(
@StringRes val id: Int,
val formatArgs: WrappedList<Any> = WrappedList(emptyList()),
val spanStyleReference: SpanStyleReference,
val onClick: (() -> Unit)? = null,
) : TextReference
companion object {
/** Empty string as [TextReference] */
@ -139,6 +185,42 @@ fun pluralReference(
return TextReference.PluralRes(id, count, formatArgs)
}
/**
* Creates a [TextReference] using a plain string value with optional span style and click action.
*
* @param value The plain string value.
* @param spanStyleReference A [SpanStyleReference] representing the text style to be applied.
* @param onClick An optional lambda function to be invoked when the text is clicked.
* @return A [TextReference] representing the styled string with click action.
*/
fun styledStringReference(value: String, spanStyleReference: SpanStyleReference, onClick: (() -> Unit)? = null) =
TextReference.StyledStr(
value = value,
onClick = onClick,
spanStyleReference = spanStyleReference,
)
/**
* Creates a [TextReference] using a string resource ID with optional format arguments, span style, and click action.
*
* @param id The resource ID of the string.
* @param formatArgs A list of format arguments to be applied to the string resource.
* @param spanStyleReference A [SpanStyleReference] representing the text style to be applied.
* @param onClick An optional lambda function to be invoked when the text is clicked.
* @return A [TextReference] representing the styled string with click action.
*/
fun styledResourceReference(
@StringRes id: Int,
formatArgs: WrappedList<Any> = WrappedList(emptyList()),
spanStyleReference: SpanStyleReference,
onClick: (() -> Unit)? = null,
) = TextReference.StyledRes(
id = id,
formatArgs = formatArgs,
spanStyleReference = spanStyleReference,
onClick = onClick,
)
/**
* Combines multiple [TextReference] instances into a single [TextReference].
*
@ -165,9 +247,7 @@ fun combinedReference(vararg refs: TextReference): TextReference {
fun TextReference.resolveReference(): String {
return when (this) {
is TextReference.Res -> {
val args = formatArgs
.map { if (it is TextReference) it.resolveReference() else it }
.toTypedArray()
val args = formatArgs.map { if (it is TextReference) it.resolveReference() else it }.toTypedArray()
val resolvedReference = stringResourceSafe(id = id, *args)
@ -187,6 +267,12 @@ fun TextReference.resolveReference(): String {
}
}
}
is TextReference.StyledRes -> {
val args = formatArgs.map { if (it is TextReference) it.resolveReference() else it }.toTypedArray()
stringResourceSafe(id = id, *args)
}
is TextReference.StyledStr -> value
}
}
@ -194,9 +280,7 @@ fun TextReference.resolveReference(): String {
fun TextReference.resolveReference(resources: Resources): String {
return when (this) {
is TextReference.Res -> {
val args = formatArgs
.map { if (it is TextReference) it.resolveReference(resources) else it }
.toTypedArray()
val args = formatArgs.map { if (it is TextReference) it.resolveReference(resources) else it }.toTypedArray()
resources.getStringSafe(id, *args)
}
@ -210,6 +294,12 @@ fun TextReference.resolveReference(resources: Resources): String {
}
}
}
is TextReference.StyledRes -> {
val args = formatArgs.map { if (it is TextReference) it.resolveReference(resources) else it }.toTypedArray()
resources.getStringSafe(id, *args)
}
is TextReference.StyledStr -> value
}
}
@ -218,9 +308,7 @@ fun TextReference.resolveReference(resources: Resources): String {
fun TextReference.resolveAnnotatedReference(): AnnotatedString {
return when (this) {
is TextReference.Res -> {
val args = formatArgs
.map { if (it is TextReference) it.resolveReference() else it }
.toTypedArray()
val args = formatArgs.map { if (it is TextReference) it.resolveReference() else it }.toTypedArray()
formatAnnotated(stringResourceSafe(id = id, *args))
}
@ -234,6 +322,21 @@ fun TextReference.resolveAnnotatedReference(): AnnotatedString {
append(it.resolveAnnotatedReference())
}
}
is TextReference.StyledRes -> {
val args = formatArgs.map { if (it is TextReference) it.resolveReference() else it }.toTypedArray()
val text = stringResourceSafe(id = id, *args)
createStyledText(
text = text,
spanStyleReference = spanStyleReference,
onClick = onClick,
)
}
is TextReference.StyledStr -> createStyledText(
text = value,
spanStyleReference = spanStyleReference,
onClick = onClick,
)
}
}
@ -245,6 +348,8 @@ operator fun TextReference.plus(ref: TextReference): TextReference {
is TextReference.Res,
is TextReference.Str,
is TextReference.Annotated,
is TextReference.StyledRes,
is TextReference.StyledStr,
-> TextReference.Combined(refs = wrappedList(this, ref))
}
}
@ -280,4 +385,120 @@ private fun formatAnnotated(rawString: String): AnnotatedString {
*/
fun TextReference.orMaskWithStars(maskWithStars: Boolean): TextReference {
return if (maskWithStars) stringReference(THREE_STARS) else this
}
}
@ReadOnlyComposable
@Composable
private fun createStyledText(
text: String,
spanStyleReference: SpanStyleReference,
onClick: (() -> Unit)?,
): AnnotatedString = buildAnnotatedString {
if (onClick != null) {
withLink(
link = LinkAnnotation.Clickable(
tag = text,
linkInteractionListener = { onClick() },
),
block = {
appendStyled(
text = text,
spanStyle = spanStyleReference(),
)
},
)
} else {
appendStyled(
text = text,
spanStyle = spanStyleReference(),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TextReference_Preview(@PreviewParameter(TextReferencePreviewProvider::class) params: TextReference) {
TangemThemePreview {
val uriHandler = LocalUriHandler.current
Column(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors.background.primary)
.padding(4.dp),
) {
Text(
text = params.resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = styledResourceReference(
id = R.string.common_read_more,
spanStyleReference = {
TangemTheme.typography.body1.copy(TangemTheme.colors.text.accent).toSpanStyle()
},
onClick = {
uriHandler.openUri("https://tangem.com")
},
).resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
Text(
text = stringReference("To be masked").orMaskWithStars(true).resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
}
private class TextReferencePreviewProvider : PreviewParameterProvider<TextReference> {
override val values: Sequence<TextReference>
get() = sequenceOf(
stringReference("Simple string"),
resourceReference(R.string.common_tangem),
pluralReference(
id = R.plurals.common_days,
count = 5,
formatArgs = wrappedList(5),
),
styledStringReference(
value = "Styled string",
spanStyleReference = {
TangemTheme.typography.subtitle2.copy(TangemTheme.colors.text.accent).toSpanStyle()
},
),
styledResourceReference(
id = R.string.common_tangem,
spanStyleReference = {
TangemTheme.typography.caption1.copy(TangemTheme.colors.text.accent).toSpanStyle()
},
),
combinedReference(
stringReference("Simple string"),
resourceReference(R.string.common_tangem),
pluralReference(
id = R.plurals.common_days,
count = 5,
formatArgs = wrappedList(5),
),
styledStringReference(
value = "Styled string",
spanStyleReference = {
TangemTheme.typography.subtitle2.copy(TangemTheme.colors.text.accent).toSpanStyle()
},
),
styledResourceReference(
id = R.string.common_tangem,
spanStyleReference = {
TangemTheme.typography.caption1.copy(TangemTheme.colors.text.warning).toSpanStyle()
},
),
),
)
}
// endregion

View file

@ -18,6 +18,18 @@ object TangemColorPalette {
val Dark6 = Color(0xFF1E1E1E)
// endregion Dark
// region Dark Alpha
val Dark_10 = Color(0x1A1E1E1E)
val Dark_20 = Color(0x331E1E1E)
val Dark_30 = Color(0x4D1E1E1E)
val Dark_40 = Color(0x661E1E1E)
val Dark_50 = Color(0x801E1E1E)
val Dark_60 = Color(0x991E1E1E)
val Dark_70 = Color(0xB31E1E1E)
val Dark_80 = Color(0xCC1E1E1E)
val Dark_90 = Color(0xE61E1E1E)
// endregion Dark Alpha
// region Light
val Light1 = Color(0xFFF5F5F5)
val Light1V2 = Color(0xFFF4F4F4)
@ -27,6 +39,18 @@ object TangemColorPalette {
val Light5 = Color(0xFFB0B0B0)
// endregion Light
// region Light Alpha
val Light_10 = Color(0x1AFFFFFF)
val Light_20 = Color(0x33FFFFFF)
val Light_30 = Color(0x4DFFFFFF)
val Light_40 = Color(0x66FFFFFF)
val Light_50 = Color(0x80FFFFFF)
val Light_60 = Color(0x99FFFFFF)
val Light_70 = Color(0xB3FFFFFF)
val Light_80 = Color(0xCCFFFFFF)
val Light_90 = Color(0xE6FFFFFF)
// endregion Light Alpha
// region Green
val Green = Color(0xFF0C9F3D)
val Meadow = Color(0xFF1ACE80)

View file

@ -162,6 +162,7 @@ class TangemColors2 internal constructor(
backgroundSecondary: Color,
backgroundDisabled: Color,
backgroundPositive: Color,
backgroundPrimaryInverse: Color,
textPrimary: Color,
textSecondary: Color,
textDisabled: Color,
@ -178,6 +179,8 @@ class TangemColors2 internal constructor(
private set
var backgroundPositive by mutableStateOf(backgroundPositive)
private set
var backgroundPrimaryInverse by mutableStateOf(backgroundPrimaryInverse)
private set
var textPrimary by mutableStateOf(textPrimary)
private set
var textSecondary by mutableStateOf(textSecondary)
@ -198,6 +201,7 @@ class TangemColors2 internal constructor(
backgroundSecondary = other.backgroundSecondary
backgroundDisabled = other.backgroundDisabled
backgroundPositive = other.backgroundPositive
backgroundPrimaryInverse = other.backgroundPrimaryInverse
textPrimary = other.textPrimary
textSecondary = other.textSecondary
textDisabled = other.textDisabled

View file

@ -0,0 +1,39 @@
package com.tangem.core.ui.res
import androidx.compose.runtime.Immutable
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@Suppress("ConstructorParameterNaming")
@ConsistentCopyVisibility
@Immutable
data class TangemDimens2 internal constructor(
val x0: Dp = 0.dp,
val x0_5: Dp = 2.dp,
val x1: Dp = 4.dp,
val x2: Dp = 8.dp,
val x2_5: Dp = 10.dp,
val x3: Dp = 12.dp,
val x4: Dp = 16.dp,
val x5: Dp = 20.dp,
val x6: Dp = 24.dp,
val x7: Dp = 28.dp,
val x8: Dp = 32.dp,
val x9: Dp = 36.dp,
val x10: Dp = 40.dp,
val x11: Dp = 44.dp,
val x12: Dp = 48.dp,
val x13: Dp = 52.dp,
val x14: Dp = 56.dp,
val x15: Dp = 60.dp,
val x16: Dp = 64.dp,
val x17: Dp = 68.dp,
val x18: Dp = 72.dp,
val x19: Dp = 76.dp,
val x20: Dp = 80.dp,
val x21: Dp = 84.dp,
val x22: Dp = 88.dp,
val x23: Dp = 92.dp,
val x24: Dp = 96.dp,
val x25: Dp = 100.dp,
)

View file

@ -1,6 +1,8 @@
package com.tangem.core.ui.res
import android.app.Activity
import androidx.compose.foundation.ComposeFoundationFlags
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.text.selection.LocalTextSelectionColors
import androidx.compose.foundation.text.selection.TextSelectionColors
@ -27,6 +29,7 @@ import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.domain.apptheme.model.AppThemeMode
import com.valentinilk.shimmer.Shimmer
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TangemTheme(
activity: Activity,
@ -36,6 +39,9 @@ fun TangemTheme(
overrideSystemBarColors: Boolean = true,
content: @Composable () -> Unit,
) {
// TODO Research and implement in redesign [REDACTED_TASK_KEY]
ComposeFoundationFlags.isPausableCompositionInPrefetchEnabled = false
val appThemeMode by uiDependencies.appThemeModeHolder.appThemeMode
val windowSize = rememberWindowSize(activity = activity)
@ -148,11 +154,21 @@ object TangemTheme {
@ReadOnlyComposable
get() = LocalTangemTypography.current
val typography2: TangemTypography2
@Composable
@ReadOnlyComposable
get() = TangemTypography2(InterFamily)
val dimens: TangemDimens
@Composable
@ReadOnlyComposable
get() = LocalTangemDimens.current
val dimens2: TangemDimens2
@Composable
@ReadOnlyComposable
get() = LocalTangemDimens2.current
val shapes: TangemShapes
@Composable
@ReadOnlyComposable
@ -337,10 +353,18 @@ internal val LocalTangemTypography = staticCompositionLocalOf {
TangemTypography(RobotoFamily)
}
internal val LocalTangemTypography2 = staticCompositionLocalOf {
TangemTypography2(InterFamily)
}
private val LocalTangemDimens = staticCompositionLocalOf {
TangemDimens()
}
private val LocalTangemDimens2 = staticCompositionLocalOf {
TangemDimens2()
}
private val LocalTangemShapes = staticCompositionLocalOf<TangemShapes> {
error("No TangemShapes provided")
}

View file

@ -37,6 +37,32 @@ fun TangemThemePreview(
}
}
@Composable
fun TangemThemePreviewRedesign(
isDark: Boolean? = null,
alwaysShowBottomSheets: Boolean = true,
rtl: Boolean = false,
content: @Composable () -> Unit,
) {
val isDarkTheme = isDark ?: isSystemInDarkTheme()
CompositionLocalProvider(
LocalBottomSheetAlwaysVisible provides alwaysShowBottomSheets,
LocalLayoutDirection provides if (rtl) LayoutDirection.Rtl else LayoutDirection.Ltr,
) {
BoxWithConstraints {
TangemTheme(
isDark = isDarkTheme,
windowSize = rememberWindowSizePreview(maxWidth, maxHeight),
) {
TangemThemeRedesign(
content = content,
)
}
}
}
}
/**
* This is used to make the bottom sheet always visible in the Preview and should be `true` only in the Preview.
* */

View file

@ -1,4 +1,5 @@
@file:Suppress("LongMethod")
package com.tangem.core.ui.res
import androidx.compose.material3.MaterialTheme
@ -22,7 +23,7 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) {
CompositionLocalProvider(
LocalTangemColors provides themeColors,
LocalTangemColors2 provides if (LocalIsInDarkTheme.current) darkThemeColors2() else lightThemeColors2(),
LocalTangemTypography provides TangemTypography(InterFamily),
LocalTangemTypography2 provides TangemTypography2(InterFamily),
LocalRootBackgroundColor provides remember(rootBackgroundColor) { mutableStateOf(rootBackgroundColor) },
) {
content()
@ -97,9 +98,10 @@ private fun lightThemeColors2(): TangemColors2 {
)
val button = TangemColors2.Button(
backgroundPrimary = TangemColorPalette.Dark6,
backgroundSecondary = TangemColorPalette.Dark6.copy(alpha = 0.1f),
backgroundSecondary = TangemColorPalette.Dark_10,
backgroundDisabled = TangemColorPalette.Light3,
backgroundPositive = TangemColorPalette.Azure,
backgroundPrimaryInverse = TangemColorPalette.White,
textSecondary = TangemColorPalette.Dark6,
textPrimary = TangemColorPalette.Light2,
textDisabled = text.neutral.tertiary,
@ -236,9 +238,10 @@ private fun darkThemeColors2(): TangemColors2 {
)
val button = TangemColors2.Button(
backgroundPrimary = TangemColorPalette.Light1V2,
backgroundSecondary = TangemColorPalette.White.copy(alpha = 0.1f),
backgroundSecondary = TangemColorPalette.Light_10,
backgroundDisabled = TangemColorPalette.Dark5,
backgroundPositive = TangemColorPalette.Azure,
backgroundPrimaryInverse = TangemColorPalette.Light_10,
textSecondary = TangemColorPalette.Light4,
textPrimary = TangemColorPalette.Dark4,
textDisabled = text.neutral.secondary,

View file

@ -4,7 +4,6 @@ import androidx.compose.runtime.Immutable
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.TextUnit
@ -17,11 +16,6 @@ internal val RobotoFamily = FontFamily(
Font(R.font.roboto_medium, FontWeight.Medium),
)
internal val InterFamily = FontFamily(
Font(R.font.inter_regular),
Font(R.font.inter_italic, style = FontStyle.Italic),
)
@Immutable
class TangemTypography internal constructor(
fontFamily: FontFamily,

View file

@ -0,0 +1,348 @@
package com.tangem.core.ui.res
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.Font
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.TextUnitType
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.tangem.core.ui.R
internal val InterFamily = FontFamily(
Font(R.font.inter_regular),
Font(R.font.inter_italic, style = FontStyle.Italic),
)
@Stable
class TangemTypography2 internal constructor(
fontFamily: FontFamily,
) {
val titleRegular44: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 44.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0.37f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 48f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingRegular34: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 34.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.37f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 40f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingBold34: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 34.sp,
fontWeight = FontWeight.Bold,
letterSpacing = TextUnit(value = 0.37f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 40f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingRegular28: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 28.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0.36f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 36f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingBold28: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 28.sp,
fontWeight = FontWeight.Bold,
letterSpacing = TextUnit(value = 0.36f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 36f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingRegular22: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 22.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.35f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 28f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingBold22: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 22.sp,
fontWeight = FontWeight.Bold,
letterSpacing = TextUnit(value = 0.35f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 28f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingRegular20: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 20.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.38f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingSemibold20: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 20.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0.38f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 24f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingRegular17: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 17.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = -0.41f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val headingSemibold17: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 17.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = -0.2f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val bodyRegular16: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 16.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = -0.32f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val bodySemibold16: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 16.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = -0.32f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val bodyRegular15: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 15.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = -0.24f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val bodySemibold15: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 15.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = -0.1f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 20f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val bodyRegular14: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 14.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = -0.1f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val captionRegular13: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 13.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = -0.08f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val captionSemibold13: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 13.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val captionRegular12: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 12.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val captionSemibold12: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 12.sp,
fontWeight = FontWeight.Medium,
letterSpacing = TextUnit(value = 0.1f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 16f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val captionRegular11: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 11.sp,
fontWeight = FontWeight.Normal,
letterSpacing = TextUnit(value = 0.07f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 12f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
val captionSemibold11: TextStyle = TextStyle(
fontFamily = fontFamily,
fontSize = 11.sp,
fontWeight = FontWeight.SemiBold,
letterSpacing = TextUnit(value = 0.15f, type = TextUnitType.Sp),
lineHeight = TextUnit(value = 12f, type = TextUnitType.Sp),
lineHeightStyle = LineHeightStyle(
alignment = LineHeightStyle.Alignment.Center,
trim = LineHeightStyle.Trim.None,
),
)
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360, heightDp = 1500)
@Preview(showBackground = true, widthDp = 360, heightDp = 1500, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTypography2_Preview() {
TangemThemePreviewRedesign {
val typographyList = sequenceOf(
TangemTheme.typography2.titleRegular44,
TangemTheme.typography2.headingRegular34,
TangemTheme.typography2.headingBold34,
TangemTheme.typography2.headingRegular28,
TangemTheme.typography2.headingBold28,
TangemTheme.typography2.headingRegular22,
TangemTheme.typography2.headingBold22,
TangemTheme.typography2.headingRegular20,
TangemTheme.typography2.headingSemibold20,
TangemTheme.typography2.headingRegular17,
TangemTheme.typography2.headingSemibold17,
TangemTheme.typography2.bodyRegular16,
TangemTheme.typography2.bodySemibold16,
TangemTheme.typography2.bodyRegular15,
TangemTheme.typography2.bodySemibold15,
TangemTheme.typography2.bodyRegular14,
TangemTheme.typography2.captionRegular13,
TangemTheme.typography2.captionSemibold13,
TangemTheme.typography2.captionRegular12,
TangemTheme.typography2.captionSemibold12,
TangemTheme.typography2.captionRegular11,
TangemTheme.typography2.captionSemibold11,
)
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.background(TangemTheme.colors2.surface.level1)
.padding(4.dp),
) {
typographyList.forEach { textStyle ->
Box(modifier = Modifier.heightIn(min = 60.dp)) {
Text(
text = "Lorem ipsum",
style = textStyle,
color = TangemTheme.colors2.text.neutral.primary,
modifier = Modifier.align(Alignment.Center),
)
}
}
}
}
}
// endregion

View file

@ -1,6 +0,0 @@
package com.tangem.core.ui.test
object SelectNetworkFeeBottomSheetTestTags {
const val READ_MORE_TEXT = "SELECT_NETWORK_FEE_READ_MORE_TEXT"
const val SELECTOR_ITEM = "SELECT_NETWORK_FEE_SELECTOR_ITEM"
}

View file

@ -0,0 +1,22 @@
package com.tangem.core.ui.test
object SendSelectNetworkFeeBottomSheetTestTags {
const val REGULAR_FEE_ITEM = "SEND_SELECT_NETWORK_FEE_REGULAR_FEE_ITEM"
const val REGULAR_ITEM_ICON = "SEND_SELECT_NETWORK_FEE_REGULAR_ITEM_ICON"
const val REGULAR_ITEM_TITLE = "SEND_SELECT_NETWORK_FEE_REGULAR_ITEM_TITLE"
const val DOT_SIGN = "SEND_SELECT_NETWORK_FEE_DOT_SIGN"
const val TOKEN_AMOUNT = "SEND_SELECT_NETWORK_FEE_TOKEN_AMOUNT"
const val FIAT_AMOUNT = "SEND_SELECT_NETWORK_FEE_FIAT_AMOUNT"
const val CUSTOM_FEE_ITEM = "SEND_SELECT_NETWORK_FEE_CUSTOM_FEE_ITEM"
const val CUSTOM_ITEM_ICON = "SEND_SELECT_NETWORK_FEE_CUSTOM_ITEM_ICON"
const val CUSTOM_ITEM_TITLE = "SEND_SELECT_NETWORK_FEE_CUSTOM_ITEM_TITLE"
const val CUSTOM_INPUT_ITEM = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM"
const val NONCE_INPUT_ITEM = "SEND_SELECT_NETWORK_FEE_NONCE_INPUT_ITEM"
const val NONCE_INPUT_TEXT_FIELD = "SEND_SELECT_NETWORK_FEE_NONCE_INPUT_TEXT_FIELD"
const val CUSTOM_INPUT_ITEM_TITLE = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_TITLE"
const val CUSTOM_INPUT_ITEM_TOOLTIP_ICON = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_TOOLTIP_ICON"
const val CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_INPUT_TEXT_FIELD"
const val CUSTOM_INPUT_ITEM_FIAT_AMOUNT = "SEND_SELECT_NETWORK_FEE_CUSTOM_INPUT_ITEM_FIAT_AMOUNT"
}

View file

@ -0,0 +1,6 @@
package com.tangem.core.ui.test
object SwapSelectNetworkFeeBottomSheetTestTags {
const val READ_MORE_TEXT = "SWAP_SELECT_NETWORK_FEE_READ_MORE_TEXT"
const val SELECTOR_ITEM = "SWAP_SELECT_NETWORK_FEE_SELECTOR_ITEM"
}

View file

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="48dp"
android:height="48dp"
android:viewportWidth="48"
android:viewportHeight="48">
<group>
<clip-path
android:pathData="M24,0L24,0A24,24 0,0 1,48 24L48,24A24,24 0,0 1,24 48L24,48A24,24 0,0 1,0 24L0,24A24,24 0,0 1,24 0z"/>
<path
android:pathData="M24,0L24,0A24,24 0,0 1,48 24L48,24A24,24 0,0 1,24 48L24,48A24,24 0,0 1,0 24L0,24A24,24 0,0 1,24 0z"
android:strokeAlpha="0.1"
android:fillColor="#0099FF"
android:fillAlpha="0.1"/>
<path
android:pathData="M15.782,24.005C15.781,23.508 16.184,23.105 16.681,23.104L29.128,23.099L23.92,17.891C23.569,17.539 23.569,16.97 23.92,16.618C24.272,16.267 24.842,16.267 25.193,16.618L31.943,23.368C32.295,23.72 32.295,24.289 31.943,24.641L25.193,31.391C24.842,31.742 24.272,31.742 23.92,31.391C23.569,31.039 23.569,30.47 23.92,30.118L29.139,24.899L16.682,24.904C16.185,24.905 15.782,24.502 15.782,24.005Z"
android:fillColor="#0099FF"/>
</group>
</vector>