Updated on 2026-08-14

This commit is contained in:
Tangem 2026-01-22 17:05:04 +03:00
commit 21ae19f625
938 changed files with 40685 additions and 7487 deletions

View file

@ -0,0 +1,5 @@
package com.tangem.core.ui
interface DesignFeatureToggles {
val isRedesignEnabled: Boolean
}

View file

@ -16,4 +16,6 @@ interface UiDependencies {
val globalSnackbarHostState: SnackbarHostState
val eventMessageHandler: EventMessageHandler
val designFeatureToggles: DesignFeatureToggles
}

View file

@ -170,6 +170,8 @@ fun PrimaryButtonIconStart(
modifier: Modifier = Modifier,
showProgress: Boolean = false,
enabled: Boolean = true,
size: TangemButtonSize = TangemButtonSize.Default,
shape: Shape = size.toShape(),
) {
TangemButton(
modifier = modifier,
@ -180,6 +182,8 @@ fun PrimaryButtonIconStart(
enabled = enabled,
showProgress = showProgress,
textStyle = TangemTheme.typography.subtitle1,
size = size,
shape = shape,
)
}
// endregion PrimaryButton

View file

@ -1,51 +0,0 @@
package com.tangem.core.ui.components
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.res.TangemThemePreview
import com.tangem.core.ui.res.TangemTheme
/**
* Currency placeholder icon
*
* @param id currency id
* @param modifier modifier
*/
@Composable
fun CurrencyPlaceholderIcon(id: String, modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier.background(color = TangemTheme.colors.icon.secondary, shape = CircleShape),
) {
Text(
text = id.firstOrNull()?.titlecase().orEmpty(),
modifier = Modifier.padding(TangemTheme.dimens.spacing4),
color = TangemTheme.colors.text.primary2,
textAlign = TextAlign.Center,
// FIXME("Incorrect typography. Replace with typography from design system")
style = TangemTheme.typography.h3.copy(fontWeight = FontWeight.Bold),
)
}
}
// region preview
@Preview(showBackground = true, heightDp = 40, widthDp = 40)
@Preview(showBackground = true, heightDp = 40, widthDp = 40, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_CurrencyPlaceholderIcon() {
TangemThemePreview {
CurrencyPlaceholderIcon(id = "DAI")
}
}
// endregion preview

View file

@ -70,17 +70,6 @@ fun BasicDialog(
)
}
@Composable
fun SimpleOkDialog(message: String, onDismissDialog: () -> Unit) {
TangemDialog(
type = DialogType.Message(message),
confirmButton = DialogButtonUM(onClick = onDismissDialog),
onDismissDialog = onDismissDialog,
title = null,
dismissButton = null,
)
}
/**
* Dialog with text field
*
@ -124,34 +113,6 @@ fun TextInputDialog(
)
}
@Composable
fun TextInputDialog(
fieldValue: String,
confirmButton: DialogButtonUM,
onDismissDialog: () -> Unit,
onValueChange: (String) -> Unit,
textFieldParams: AdditionalTextInputDialogUM = remember { AdditionalTextInputDialogUM() },
title: String? = null,
dismissButton: DialogButtonUM? = null,
isDismissable: Boolean = true,
) {
TangemDialog(
type = DialogType.SimpleTextInput(
value = fieldValue,
onValueChange = onValueChange,
params = textFieldParams,
),
confirmButton = confirmButton,
onDismissDialog = onDismissDialog,
title = title,
dismissButton = dismissButton,
properties = DialogProperties(
dismissOnBackPress = isDismissable,
dismissOnClickOutside = isDismissable,
),
)
}
@Composable
fun SelectorDialog(
selectedItemIndex: Int,
@ -441,14 +402,6 @@ private sealed class DialogType {
// endregion Defaults
// region Preview
@Composable
private fun SimpleOkDialogPreview() {
SimpleOkDialog(
message = "All protected passwords will be deleted from the " +
"secure storage, you must enter the wallet password to work with the app.",
) {}
}
@Composable
private fun BasicDialogPreview() {
BasicDialog(
@ -461,15 +414,6 @@ private fun BasicDialogPreview() {
)
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_SimpleOkDialog() {
TangemThemePreview {
SimpleOkDialogPreview()
}
}
@Preview(showBackground = true)
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable

View file

@ -1,142 +0,0 @@
package com.tangem.core.ui.components
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.SubcomposeLayout
import androidx.compose.ui.text.TextLayoutResult
import androidx.compose.ui.text.TextStyle
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.TextAlign
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.TextUnit
import com.tangem.core.ui.components.atoms.text.BoundCounter
/**
* https://stackoverflow.com/questions/69083061/how-to-make-middle-ellipsis-in-text-with-jetpack-compose
*/
@Deprecated("Use EllipsisText with TextEllipsis.Middle ellipsis instead")
@Suppress("LongMethod")
@Composable
fun MiddleEllipsisText(
text: String,
modifier: Modifier = Modifier,
color: Color = Color.Unspecified,
fontSize: TextUnit = TextUnit.Unspecified,
fontStyle: FontStyle? = null,
fontWeight: FontWeight? = null,
fontFamily: FontFamily? = null,
letterSpacing: TextUnit = TextUnit.Unspecified,
textDecoration: TextDecoration? = null,
textAlign: TextAlign? = null,
lineHeight: TextUnit = TextUnit.Unspecified,
softWrap: Boolean = true,
onTextLayout: (TextLayoutResult) -> Unit = {},
style: TextStyle = LocalTextStyle.current,
) {
// some letters, like "r", will have less width when placed right before "."
// adding a space to prevent such case
val layoutText = remember(text) { "$text $ellipsisText" }
val textLayoutResultState = remember(layoutText) {
mutableStateOf<TextLayoutResult?>(null)
}
SubcomposeLayout(modifier) { constraints ->
// result is ignored - we only need to fill our textLayoutResult
subcompose("measure") {
Text(
text = layoutText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
maxLines = 1,
onTextLayout = { textLayoutResultState.value = it },
style = style,
)
}.first().measure(Constraints())
// to allow smart cast
val textLayoutResult = textLayoutResultState.value
?: // shouldn't happen - onTextLayout is called before subcompose finishes
return@SubcomposeLayout layout(0, 0) {}
val placeable = subcompose("visible") {
val finalText = remember(text, textLayoutResult, constraints.maxWidth) {
if (
text.isEmpty() ||
textLayoutResult.getBoundingBox(text.indices.last).right <= constraints.maxWidth
) {
// text not including ellipsis fits on the first line.
return@remember text
}
val ellipsisWidth = layoutText.indices.toList()
.takeLast(ELLIPSIS_CHARACTERS_COUNT)
.let widthLet@{ indices ->
// fix this bug: https://issuetracker.google.com/issues/197146630
// in this case width is invalid
for (i in indices) {
val width = textLayoutResult.getBoundingBox(i).width
if (width > 0) {
return@widthLet width * ELLIPSIS_CHARACTERS_COUNT
}
}
// this should not happen, because
// this error occurs only for the last character in the string
error("all ellipsis chars have invalid width")
}
val availableWidth = constraints.maxWidth - ellipsisWidth
val startCounter = BoundCounter(text, textLayoutResult) { it }
val endCounter = BoundCounter(text, textLayoutResult) { text.indices.last - it }
while (availableWidth - startCounter.width - endCounter.width > 0) {
val possibleEndWidth = endCounter.widthWithNextChar()
if (
startCounter.width >= possibleEndWidth &&
availableWidth - startCounter.width - possibleEndWidth >= 0
) {
endCounter.addNextChar()
} else if (availableWidth - startCounter.widthWithNextChar() - endCounter.width >= 0) {
startCounter.addNextChar()
} else {
break
}
}
startCounter.string.trimEnd() + ellipsisText + endCounter.string.reversed().trimStart()
}
Text(
text = finalText,
color = color,
fontSize = fontSize,
fontStyle = fontStyle,
fontWeight = fontWeight,
fontFamily = fontFamily,
letterSpacing = letterSpacing,
textDecoration = textDecoration,
textAlign = textAlign,
lineHeight = lineHeight,
softWrap = softWrap,
onTextLayout = onTextLayout,
style = style,
)
}[0].measure(constraints)
layout(placeable.width, placeable.height) {
placeable.place(0, 0)
}
}
}
private const val ELLIPSIS_CHARACTERS_COUNT = 3
private const val ELLIPSIS_CHARACTER = '.'
private val ellipsisText = List(ELLIPSIS_CHARACTERS_COUNT) { ELLIPSIS_CHARACTER }.joinToString(separator = "")

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

@ -62,6 +62,7 @@ private fun getTextColorByType(type: AuditLabelUM.Type): Color {
AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention
AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent
AuditLabelUM.Type.Info -> TangemTheme.colors.text.primary2
AuditLabelUM.Type.General -> TangemTheme.colors.text.secondary
}
}
@ -73,6 +74,7 @@ private fun getBackgroundColorByType(type: AuditLabelUM.Type): Color {
AuditLabelUM.Type.Warning -> TangemTheme.colors.text.attention.copy(alpha = 0.1f)
AuditLabelUM.Type.Permit -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
AuditLabelUM.Type.Info -> TangemTheme.colors.text.accent
AuditLabelUM.Type.General -> TangemTheme.colors.control.default
}
}
@ -83,6 +85,7 @@ private fun getPaddingByType(type: AuditLabelUM.Type): PaddingValues {
AuditLabelUM.Type.Prohibition,
AuditLabelUM.Type.Warning,
AuditLabelUM.Type.Permit,
AuditLabelUM.Type.General,
-> PaddingValues(horizontal = 4.dp)
AuditLabelUM.Type.Info -> PaddingValues(horizontal = 6.dp, vertical = 1.dp)
}

View file

@ -25,5 +25,8 @@ data class AuditLabelUM(val text: TextReference, val type: Type) {
/** Blue with white text. Like, "best rate" */
Info,
/** Gray. Like, "info" */
General,
}
}

View file

@ -1,32 +1,25 @@
package com.tangem.core.ui.components.bottomsheets.internal
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.focusable
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.key.*
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ModalBottomSheetWithBackHandling(
onDismissRequest: () -> Unit,
onBack: (() -> Unit),
modifier: Modifier = Modifier,
onBack: (() -> Unit)? = null,
sheetState: SheetState = rememberModalBottomSheetState(),
sheetMaxWidth: Dp = BottomSheetDefaults.SheetMaxWidth,
shape: Shape = BottomSheetDefaults.ExpandedShape,
@ -38,28 +31,9 @@ fun ModalBottomSheetWithBackHandling(
contentWindowInsets: @Composable () -> WindowInsets = { BottomSheetDefaults.windowInsets },
content: @Composable ColumnScope.() -> Unit,
) {
BackHandler(enabled = sheetState.targetValue != SheetValue.Hidden) {
onBack()
}
val requester = remember { FocusRequester() }
val backPressedDispatcherOwner = LocalOnBackPressedDispatcherOwner.current
ModalBottomSheet(
onDismissRequest = onDismissRequest,
modifier = modifier
.focusRequester(requester)
.focusable()
.onPreviewKeyEvent { keyEvent ->
if (keyEvent.key == Key.Back &&
keyEvent.type == KeyEventType.KeyUp &&
!keyEvent.nativeKeyEvent.isCanceled
) {
backPressedDispatcherOwner?.onBackPressedDispatcher?.onBackPressed()
return@onPreviewKeyEvent true
}
return@onPreviewKeyEvent false
},
modifier = modifier,
sheetState = sheetState,
sheetMaxWidth = sheetMaxWidth,
shape = shape,
@ -69,18 +43,17 @@ fun ModalBottomSheetWithBackHandling(
scrimColor = scrimColor,
dragHandle = dragHandle,
contentWindowInsets = contentWindowInsets,
properties = ModalBottomSheetDefaults.properties(
// Set false otherwise the onPreviewKeyEvent doesn't work at all.
// The functionality of shouldDismissOnBackPress is achieved by the BackHandler.
shouldDismissOnBackPress = false,
properties = ModalBottomSheetProperties(
shouldDismissOnBackPress = onBack == null,
),
content = content,
)
content = {
content()
LaunchedEffect(Unit) {
delay(timeMillis = 200)
requester.requestFocus()
}
BackHandler(enabled = onBack != null && sheetState.targetValue != SheetValue.Hidden) {
onBack?.invoke()
}
},
)
}
@OptIn(ExperimentalMaterial3Api::class)

View file

@ -27,6 +27,7 @@ import com.tangem.core.ui.components.buttons.common.TangemButtonIconPosition
import com.tangem.core.ui.components.buttons.common.TangemButtonsDefaults
import com.tangem.core.ui.components.icons.HighlightedIcon
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
@ -109,7 +110,7 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier:
modifier = Modifier
.fillMaxWidth()
.padding(top = TangemTheme.dimens.spacing8),
text = body.resolveReference(),
text = body.resolveAnnotatedReference(),
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.secondary,
textAlign = TextAlign.Center,
@ -124,7 +125,6 @@ private fun ContentContainer(state: MessageBottomSheetUMV2.InfoBlock, modifier:
}
}
@Suppress("CanBeNonNullable")
@Composable
private fun BottomSheetIconContainer(
icon: MessageBottomSheetUMV2.Icon?,

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 = {
@ -207,32 +207,18 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheet(
noinline onBack: (() -> Unit)? = null,
noinline bsContent: @Composable ColumnScope.() -> Unit,
) {
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
// region Preview

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,
@ -247,32 +247,18 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicModalBottomSheetWit
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
ModalBottomSheetWithBackHandling(
modifier = modifier,
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = Color.Transparent,
shape = TangemTheme.shapes.roundedCornersLarge,
contentWindowInsets = { WindowInsetsZero },
onBack = onBack,
dragHandle = null,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
// region Preview

View file

@ -2,7 +2,6 @@ package com.tangem.core.ui.components.bottomsheets.sheet
import androidx.compose.foundation.layout.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ModalBottomSheet
import androidx.compose.material3.SheetState
import androidx.compose.material3.SheetValue.Expanded
import androidx.compose.material3.rememberModalBottomSheetState
@ -134,7 +133,8 @@ inline fun <reified T : TangemBottomSheetConfigContent> PreviewBottomSheet(
sheetState = SheetState(
skipPartiallyExpanded = skipPartiallyExpanded,
initialValue = Expanded,
density = LocalDensity.current,
positionalThreshold = { 0f },
velocityThreshold = { 0f },
),
onBack = null,
containerColor = containerColor,
@ -181,30 +181,16 @@ inline fun <reified T : TangemBottomSheetConfigContent> BasicBottomSheet(
}
}
if (onBack != null) {
ModalBottomSheetWithBackHandling(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
} else {
ModalBottomSheet(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}
ModalBottomSheetWithBackHandling(
modifier = modifier.statusBarsPadding(),
onDismissRequest = config.onDismissRequest,
sheetState = sheetState,
containerColor = containerColor,
shape = TangemTheme.shapes.bottomSheetLarge,
contentWindowInsets = { WindowInsetsZero },
dragHandle = { TangemBottomSheetDraggableHeader(color = containerColor) },
onBack = onBack,
content = bsContent,
scrimColor = TangemTheme.colors.overlay.secondary,
)
}

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

@ -0,0 +1,57 @@
package com.tangem.core.ui.components.haze
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import com.tangem.core.ui.res.LocalHazeState
import com.tangem.core.ui.res.LocalPowerSavingState
import com.tangem.core.ui.res.LocalRootBackgroundColor
import dev.chrisbanes.haze.*
import dev.chrisbanes.haze.materials.CupertinoMaterials
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@OptIn(ExperimentalHazeMaterialsApi::class)
@Composable
internal fun ProvideHaze(content: @Composable () -> Unit) {
val hazeState = rememberHazeState()
CompositionLocalProvider(
LocalHazeState provides hazeState,
LocalHazeStyle provides CupertinoMaterials.ultraThin(),
) {
content()
}
}
/**
* Applies a haze effect to the [Modifier] with consideration of global haze settings and power saving mode.
*
* @param configure A lambda to configure the [HazeEffectScope].
* @return A [Modifier] with the configured haze effect applied.
*/
@Composable
fun Modifier.hazeEffectTangem(
style: HazeStyle = HazeStyle.Unspecified,
configure: HazeEffectScope.() -> Unit = {},
): Modifier {
val powerSavingEnabled = LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsState()
val hazeState = LocalHazeState.current
val isGlobalBlurEnabled = hazeState.blurEnabled && !powerSavingEnabled.value
val rootBackground by LocalRootBackgroundColor.current
return hazeEffect(hazeState, style) {
fallbackTint = HazeTint(rootBackground)
if (isGlobalBlurEnabled) {
configure()
}
blurEnabled = isGlobalBlurEnabled
}
}
/**
* Applies a haze source to the [Modifier] using the current global haze state.
*/
@Composable
fun Modifier.hazeSourceTangem(zIndex: Float = 0f, key: Any? = null) =
this.hazeSource(LocalHazeState.current, zIndex, key)

View file

@ -7,4 +7,5 @@ enum class IconTint {
Accent,
Warning,
Inactive,
Informative,
}

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

@ -50,7 +50,7 @@ fun Label(state: LabelUM, modifier: Modifier = Modifier) {
val backgroundColor by animateColorAsState(
targetValue = when (state.style) {
LabelStyle.ACCENT -> TangemTheme.colors.text.accent.copy(alpha = 0.1f)
LabelStyle.REGULAR -> TangemTheme.colors.control.unchecked
LabelStyle.REGULAR -> TangemTheme.colors.control.default
LabelStyle.WARNING -> TangemTheme.colors.text.warning.copy(alpha = 0.1f)
},
)
@ -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

@ -12,6 +12,7 @@ data class LabelUM(
@DrawableRes val icon: Int? = null,
val onIconClick: (() -> Unit)? = null,
val onClick: (() -> Unit)? = null,
val maxLines: Int = Int.MAX_VALUE,
)
@Immutable

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

@ -0,0 +1,66 @@
package com.tangem.core.ui.components.powersaving
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.PowerManager
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.core.content.ContextCompat
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.compose.LocalLifecycleOwner
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
class PowerSavingState(
appContext: Context,
lifecycle: androidx.lifecycle.Lifecycle,
) {
private val powerManager =
appContext.getSystemService(Context.POWER_SERVICE) as? PowerManager ?: error("PowerManager not available")
val isPowerSavingModeEnabled: StateFlow<Boolean>
field = MutableStateFlow(powerManager.isPowerSaveMode)
init {
val intentFilter = IntentFilter().apply {
addAction(PowerManager.ACTION_POWER_SAVE_MODE_CHANGED)
}
val receiver = PowerSavingModeReceiver()
ContextCompat.registerReceiver(
appContext,
receiver,
intentFilter,
ContextCompat.RECEIVER_NOT_EXPORTED,
)
lifecycle.addObserver(object : DefaultLifecycleObserver {
override fun onDestroy(owner: LifecycleOwner) {
appContext.unregisterReceiver(receiver)
}
})
}
inner class PowerSavingModeReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == PowerManager.ACTION_POWER_SAVE_MODE_CHANGED) {
isPowerSavingModeEnabled.value = powerManager.isPowerSaveMode
}
}
}
}
@Composable
internal fun rememberPowerSavingState(): PowerSavingState {
val appContext = LocalContext.current.applicationContext
val lifecycle = LocalLifecycleOwner.current.lifecycle
return remember(appContext, lifecycle) {
PowerSavingState(appContext = appContext, lifecycle = lifecycle)
}
}

View file

@ -1,86 +0,0 @@
package com.tangem.core.ui.components.rows
import androidx.compose.animation.AnimatedContent
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.SpacerH28
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreview
/**
* Simple clickable action row, without input and icon
*
* https://www.figma.com/file/14ISV23YB1yVW1uNVwqrKv/Android?type=design&node-id=2100-807&mode=design&t=Ygv5sohTTHYAQcBS-4
*/
@Composable
fun SimpleActionRow(title: String, description: String, modifier: Modifier = Modifier, isClickable: Boolean = true) {
Box(
modifier = modifier
.background(color = TangemTheme.colors.background.action)
.height(TangemTheme.dimens.size44)
.fillMaxWidth(),
) {
Column(
modifier = Modifier
.padding(end = TangemTheme.dimens.spacing48)
.align(Alignment.CenterStart),
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens.spacing8),
) {
AnimatedContent(targetState = title, label = "") { title ->
Text(
text = title,
style = TangemTheme.typography.caption2,
color = TangemTheme.colors.text.secondary,
)
}
AnimatedContent(targetState = description, label = "") { description ->
Text(
text = description,
style = TangemTheme.typography.body2,
color = TangemTheme.colors.text.primary1,
)
}
}
if (isClickable) {
Icon(
painter = painterResource(id = R.drawable.ic_chevron_right_24),
contentDescription = null,
modifier = Modifier
.align(alignment = Alignment.CenterEnd)
.padding(end = TangemTheme.dimens.spacing12),
tint = TangemTheme.colors.icon.informative,
)
}
}
}
@Preview
@Composable
private fun SimpleActionRowPreview() {
Column {
TangemThemePreview(isDark = false) {
SimpleActionRow(
title = "Title",
description = "Description",
)
}
SpacerH28()
TangemThemePreview(isDark = false) {
SimpleActionRow(
title = "Title",
description = "Description",
)
}
}
}

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,17 +0,0 @@
package com.tangem.core.ui.components.states
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
data class SelectableItemsState<T>(
val selectedItem: Item<T>,
val items: ImmutableList<Item<T>>,
)
data class Item<T>(
val id: Int,
val startText: TextReference,
val endText: TextReference,
val isSelected: Boolean,
val data: T,
)

View file

@ -2,6 +2,7 @@ package com.tangem.core.ui.components.token.internal
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
@ -43,16 +44,33 @@ internal fun TokenFiatAmount(state: TokenFiatAmountState?, isBalanceHidden: Bool
isFlickering = state.isFlickering,
)
}
is TokenFiatAmountState.Icon -> {
IconAmount(modifier = modifier, state = state)
}
is TokenFiatAmountState.Loading -> {
RectangleShimmer(modifier = modifier.placeholderSize(), radius = 4.dp)
}
is TokenFiatAmountState.Locked -> {
LockedRectangle(modifier = modifier.placeholderSize())
}
is TokenFiatAmountState.Empty -> {
// Empty box for proper measurements
Box(modifier)
}
null -> Unit
}
}
@Composable
private fun IconAmount(state: TokenFiatAmountState.Icon, modifier: Modifier = Modifier) {
Icon(
modifier = modifier,
painter = rememberVectorPainter(image = ImageVector.vectorResource(state.iconRes)),
tint = state.tint.color(),
contentDescription = null,
)
}
@Composable
private fun ContentFiatAmount(
text: String,
@ -76,11 +94,7 @@ private fun ContentFiatAmount(
Icon(
modifier = Modifier.size(12.dp),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = when (icon.tint) {
IconTint.Accent -> TangemTheme.colors.icon.accent
IconTint.Warning -> TangemTheme.colors.icon.attention
IconTint.Inactive -> TangemTheme.colors.icon.inactive
},
tint = icon.tint.color(),
contentDescription = null,
)
}
@ -91,6 +105,14 @@ private fun ContentFiatAmount(
}
}
@Composable
private fun IconTint.color() = when (this) {
IconTint.Accent -> TangemTheme.colors.icon.accent
IconTint.Warning -> TangemTheme.colors.icon.attention
IconTint.Inactive -> TangemTheme.colors.icon.inactive
IconTint.Informative -> TangemTheme.colors.icon.informative
}
@Composable
private fun FiatAmountText(
text: String,

View file

@ -90,7 +90,7 @@ private fun YieldSupplyApyLabel(apy: TextReference?, isActive: Boolean, modifier
color = if (isActive) {
TangemTheme.colors.text.accent.copy(alpha = 0.1f)
} else {
TangemTheme.colors.control.unchecked
TangemTheme.colors.control.default
},
shape = TangemTheme.shapes.roundedCornersSmall2,
),

View file

@ -43,7 +43,7 @@ internal fun YieldSupplyPromoBanner(state: PromoBannerState.Content, modifier: M
LaunchedEffect(state) {
state.onPromoShown()
}
val bgColor = TangemTheme.colors.control.unchecked
val bgColor = TangemTheme.colors.control.default
Column(modifier = modifier) {
Row(
modifier = Modifier

View file

@ -235,9 +235,16 @@ sealed class TokenItemState {
val isFlickering: Boolean = false,
) : FiatAmountState()
data class Icon(
val iconRes: Int,
val tint: IconTint = IconTint.Inactive,
) : FiatAmountState()
data object Loading : FiatAmountState()
data object Locked : FiatAmountState()
data object Empty : FiatAmountState()
}
@Immutable

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

@ -29,6 +29,7 @@ import kotlinx.collections.immutable.ImmutableList
* @param txHistoryItems transactions
* @param modifier modifier
*/
@Suppress("CanBeNonNullable")
fun LazyListScope.txHistoryItems(
state: TxHistoryState,
txHistoryItems: LazyPagingItems<TxHistoryState.TxHistoryItemState>?,

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,344 @@
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 badgeUM [TangemBadgeUM] containing all the badge parameters.
* @param modifier Modifier to be applied to the badge.
*
[REDACTED_AUTHOR]
*/
@Composable
fun TangemBadge(badgeUM: TangemBadgeUM, modifier: Modifier = Modifier) {
TangemBadge(
text = badgeUM.text,
iconRes = badgeUM.iconRes,
size = badgeUM.size,
shape = badgeUM.shape,
color = badgeUM.color,
type = badgeUM.type,
iconPosition = badgeUM.iconPosition,
modifier = modifier,
)
}
/**
* 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,27 @@
package com.tangem.core.ui.ds.badge
import androidx.annotation.DrawableRes
import com.tangem.core.ui.ds.badge.TangemBadgeSize.X9
import com.tangem.core.ui.extensions.TextReference
/**
* UI model for [TangemBadge] component
*
* @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.
*/
class TangemBadgeUM(
val text: TextReference,
@DrawableRes val iconRes: Int? = null,
val size: TangemBadgeSize = X9,
val shape: TangemBadgeShape = TangemBadgeShape.Default,
val color: TangemBadgeColor = TangemBadgeColor.Gray,
val type: TangemBadgeType = TangemBadgeType.Solid,
val iconPosition: TangemBadgeIconPosition = TangemBadgeIconPosition.Start,
)

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,127 @@
package com.tangem.core.ui.ds.checkbox
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
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.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEach
import com.tangem.core.ui.R
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
/**
* Tangem checkbox component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8437-82586&m=dev)
*
* @param isChecked Boolean value representing the current state of the checkbox.
* @param onCheckedChange Lambda function to be invoked when the checkbox state changes.
* @param modifier Modifier to be applied to the badge.
* @param isRounded Boolean value to determine if the checkbox has rounded corners or is circular.
* @param isEnabled Boolean value to determine if the checkbox is enabled or disabled.
*/
@Composable
fun TangemCheckbox(
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
isRounded: Boolean = true,
isEnabled: Boolean = true,
) {
val shape = if (isRounded) {
RoundedCornerShape(TangemTheme.dimens2.x1)
} else {
CircleShape
}
Box(
modifier = modifier
.size(TangemTheme.dimens2.x5)
.then(
if (isChecked) {
Modifier.background(
color = TangemTheme.colors2.border.status.accent,
shape = shape,
)
} else {
Modifier.border(
color = TangemTheme.colors2.border.neutral.secondary,
shape = shape,
width = 1.dp,
)
},
)
.clip(shape = shape)
.toggleable(
value = isChecked,
onValueChange = onCheckedChange,
enabled = isEnabled,
role = Role.Checkbox,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
),
) {
if (isChecked) {
Icon(
painter = rememberVectorPainter(ImageVector.vectorResource(R.drawable.ic_check_default_24)),
contentDescription = null,
tint = TangemTheme.colors2.graphic.neutral.primaryInvertedConstant,
modifier = Modifier
.align(Alignment.Center)
.size(TangemTheme.dimens2.x4),
)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemCheckbox_Preview() {
val previewData = mutableListOf(
TangemCheckboxPreviewData(isChecked = false, isRounded = true),
TangemCheckboxPreviewData(isChecked = false, isRounded = false),
TangemCheckboxPreviewData(isChecked = true, isRounded = true),
TangemCheckboxPreviewData(isChecked = true, isRounded = false),
)
TangemThemePreviewRedesign {
Row(
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x6),
) {
previewData.fastForEach { data ->
TangemCheckbox(
isChecked = data.isChecked,
isRounded = data.isRounded,
onCheckedChange = { },
modifier = Modifier,
)
}
}
}
}
private data class TangemCheckboxPreviewData(
val isChecked: Boolean,
val isRounded: Boolean,
)
// endregion

View file

@ -0,0 +1,189 @@
package com.tangem.core.ui.ds.row
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.Measurable
import androidx.compose.ui.layout.Placeable
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Constraints
import com.tangem.core.ui.res.TangemTheme
import kotlin.math.max
/**
* A custom layout composable that arranges its children in a row with specific layout IDs.
*/
internal enum class TangemRowLayoutId {
HEAD, START_TOP, END_TOP, START_BOTTOM, END_BOTTOM, TAIL, EXTRA_TOP
}
/**
* A custom layout composable that arranges its children in a row with specific layout IDs.
*
* @param modifier Modifier to be applied to the layout.
* @param contentPadding Padding values to be applied around the content.
* @param content Composable content to be laid out within the TangemRow.
*/
@Suppress("LongMethod")
@Composable
internal fun TangemRowContainer(
modifier: Modifier = Modifier,
contentPadding: PaddingValues = PaddingValues(TangemTheme.dimens2.x3),
content: @Composable () -> Unit,
) {
val density = LocalDensity.current
val localDirection = LocalLayoutDirection.current
val verticalPadding = with(density) { TangemTheme.dimens2.x1.roundToPx() }
val contentTopPadding = with(density) { contentPadding.calculateTopPadding().roundToPx() }
val contentBottomPadding = with(density) { contentPadding.calculateBottomPadding().roundToPx() }
val contentStartPadding = with(density) { contentPadding.calculateLeftPadding(localDirection).roundToPx() }
val contentEndPadding = with(density) { contentPadding.calculateRightPadding(localDirection).roundToPx() }
Layout(
content = content,
modifier = modifier,
) { measurables, constraints ->
val layoutWidth = constraints.maxWidth - contentStartPadding - contentEndPadding
val startTopMinWidth = (layoutWidth * TITLE_MIN_WIDTH_COEFFICIENT).toInt()
val startBottomMinWidth = (layoutWidth * PRICE_MIN_WIDTH_COEFFICIENT).toInt()
// Head composable measurement
val headPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.HEAD,
constraints = constraints,
)
// Tail composable measurement
val tailPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.TAIL,
constraints = constraints,
)
val availableWidthForBody = layoutWidth - headPlaceable.widthOrZero() - tailPlaceable.widthOrZero()
// End top composable width must take the whole free space but is not greater the Start top min size
val endTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.END_TOP,
constraints = constraints.copy(
minWidth = 0,
maxWidth = availableWidthForBody - startTopMinWidth,
),
)
// End bottom composable width must take the whole free space but is not greater the Start bottom min size
val endBottomPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.END_BOTTOM,
constraints = constraints.copy(
minWidth = 0,
maxWidth = availableWidthForBody - startBottomMinWidth,
),
)
/* Start top composable will take take the whole REMAINING width space width but no less than minimum */
val startTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.START_TOP,
constraints = constraints.copy(
minWidth = startTopMinWidth,
maxWidth = max(
a = startTopMinWidth,
b = availableWidthForBody - endTopPlaceable.widthOrZero(),
),
),
)
/* Start bottom composable will take take the whole REMAINING width space width but no less than minimum */
val startBottomPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.START_BOTTOM,
constraints = constraints.copy(
minWidth = startBottomMinWidth,
maxWidth = max(
a = startBottomMinWidth,
b = availableWidthForBody - endBottomPlaceable.widthOrZero(),
),
),
)
val extraTopPlaceable = measurables.measure(
layoutId = TangemRowLayoutId.EXTRA_TOP,
constraints = constraints,
)
val mainLayoutHeight = maxOf(
headPlaceable.heightOrZero(),
tailPlaceable.heightOrZero(),
startTopPlaceable.heightOrZero() + startBottomPlaceable.heightOrZero() + verticalPadding,
endTopPlaceable.heightOrZero() + endBottomPlaceable.heightOrZero() + verticalPadding,
)
val mainContentTopPadding = if (extraTopPlaceable != null) {
extraTopPlaceable.heightOrZero()
} else {
contentTopPadding
}
val layoutHeight = mainLayoutHeight + mainContentTopPadding + contentBottomPadding
layout(width = constraints.maxWidth, height = layoutHeight) {
extraTopPlaceable?.placeRelative(x = 0, y = 0)
headPlaceable?.placeRelative(
x = contentStartPadding,
y = mainContentTopPadding + (mainLayoutHeight - headPlaceable.height).div(other = 2),
)
startTopPlaceable?.placeRelative(
x = contentStartPadding + headPlaceable.widthOrZero(),
y = mainContentTopPadding + if (startBottomPlaceable == null) {
(mainLayoutHeight - startTopPlaceable.height).div(2)
} else {
0
},
)
startBottomPlaceable?.placeRelative(
x = contentStartPadding + headPlaceable.widthOrZero(),
y = mainContentTopPadding + if (startTopPlaceable == null) {
(mainLayoutHeight - startBottomPlaceable.height).div(2)
} else {
startTopPlaceable.heightOrZero() + verticalPadding
},
)
endTopPlaceable?.placeRelative(
x = layoutWidth - endTopPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentEndPadding,
y = mainContentTopPadding + if (endBottomPlaceable == null) {
(mainLayoutHeight - endTopPlaceable.height).div(2)
} else {
0
},
)
endBottomPlaceable?.placeRelative(
x = layoutWidth - endBottomPlaceable.widthOrZero() - tailPlaceable.widthOrZero() + contentEndPadding,
y = mainContentTopPadding + if (endTopPlaceable == null) {
(mainLayoutHeight - endBottomPlaceable.height).div(2)
} else {
endTopPlaceable.heightOrZero() + verticalPadding
},
)
tailPlaceable?.placeRelative(
x = layoutWidth - tailPlaceable.width + contentEndPadding,
y = mainContentTopPadding + (mainLayoutHeight - tailPlaceable.height).div(other = 2),
)
}
}
}
private const val TITLE_MIN_WIDTH_COEFFICIENT = 0.3
private const val PRICE_MIN_WIDTH_COEFFICIENT = 0.32
private fun List<Measurable>.measure(layoutId: TangemRowLayoutId, constraints: Constraints): Placeable? {
return firstOrNull { it.layoutId == layoutId }?.measure(constraints)
}
private fun Placeable?.widthOrZero(): Int = this?.width ?: 0
private fun Placeable?.heightOrZero(): Int = this?.height ?: 0

View file

@ -0,0 +1,167 @@
package com.tangem.core.ui.ds.row.token
import android.content.res.Configuration
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.composed
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.layout.layoutId
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
import com.tangem.core.ui.components.currency.icon.CurrencyIcon
import com.tangem.core.ui.ds.row.TangemRowContainer
import com.tangem.core.ui.ds.row.TangemRowLayoutId
import com.tangem.core.ui.ds.row.token.internal.*
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.core.ui.test.TokenElementsTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
/**
* Composable function that represents a Tangem token row in a list.
*
* [Token Row](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8207-17583&t=k8dyaykorsNocGVq-4)
*
* @param tokenRowUM The user model containing the data for the token row.
* @param isBalanceHidden A boolean indicating whether the balance should be hidden.
* @param reorderableTokenListState The state of the reorderable lazy list, if applicable.
* @param modifier The modifier to be applied to the row.
*/
@Composable
fun TangemTokenRow(
tokenRowUM: TangemTokenRowUM,
isBalanceHidden: Boolean,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
TangemRowContainer(
content = {
CurrencyIcon(
state = tokenRowUM.iconState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.HEAD)
.padding(end = TangemTheme.dimens2.x2)
.testTag(TokenElementsTestTags.TOKEN_ICON),
)
TokenRowPromoBanner(
promoBannerUM = tokenRowUM.promoBannerUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.EXTRA_TOP)
.testTag(TokenElementsTestTags.TOKEN_YIELD_PROMO_BANNER)
.padding(horizontal = TangemTheme.dimens2.x3)
.fillMaxWidth(),
)
TokenRowTitle(
titleUM = tokenRowUM.titleUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_TOP)
.padding(end = TangemTheme.dimens2.x2)
.testTag(tag = TokenElementsTestTags.TOKEN_TITLE),
)
TokenRowSubtitle(
subtitleUM = tokenRowUM.subtitleUM,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.START_BOTTOM)
.padding(end = TangemTheme.dimens2.x2)
.testTag(tag = TokenElementsTestTags.TOKEN_PRICE),
)
TokenRowEndTopContent(
endContentUM = tokenRowUM.topEndContentUM,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_TOP)
.testTag(tag = TokenElementsTestTags.TOKEN_FIAT_AMOUNT),
)
TokenRowEndBottomContent(
endContentUM = tokenRowUM.bottomEndContentUM,
isBalanceHidden = isBalanceHidden,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.END_BOTTOM)
.testTag(tag = TokenElementsTestTags.TOKEN_CRYPTO_AMOUNT),
)
TokenRowTail(
tailUM = tokenRowUM.tailUM,
reorderableTokenListState = reorderableTokenListState,
modifier = Modifier
.layoutId(layoutId = TangemRowLayoutId.TAIL)
.testTag(TokenElementsTestTags.TOKEN_NON_FIAT_BLOCK),
)
},
modifier = modifier.tokenClickable(tokenRowUM = tokenRowUM),
)
}
@OptIn(ExperimentalFoundationApi::class)
private fun Modifier.tokenClickable(tokenRowUM: TangemTokenRowUM): Modifier = composed {
val hapticFeedback = LocalHapticFeedback.current
val onClick = tokenRowUM.onItemClick
val onLongClick = tokenRowUM.onItemLongClick
val onHapticLongClick = if (onLongClick != null) {
{
hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress)
onLongClick(tokenRowUM)
}
} else {
null
}
when {
onClick == null && onLongClick == null -> this
onClick == null && onLongClick != null -> combinedClickable(onClick = {}, onLongClick = onHapticLongClick)
onClick != null && onLongClick == null -> combinedClickable(onClick = { onClick(tokenRowUM) })
onClick != null && onLongClick != null -> {
combinedClickable(onClick = { onClick(tokenRowUM) }, onLongClick = onHapticLongClick)
}
else -> this
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTokenRow_Preview(
@PreviewParameter(TangemTokenRowPreviewProvider::class) tokenRowUM: TangemTokenRowUM,
) {
TangemThemePreviewRedesign {
TangemTokenRow(
tokenRowUM = tokenRowUM,
isBalanceHidden = false,
reorderableTokenListState = null,
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
)
}
}
private class TangemTokenRowPreviewProvider : CollectionPreviewParameterProvider<TangemTokenRowUM>(
collection = listOf(
TangemTokenRowPreviewData.defaultState,
TangemTokenRowPreviewData.defaultEllipsisState,
TangemTokenRowPreviewData.tokenState,
TangemTokenRowPreviewData.customTokenState,
TangemTokenRowPreviewData.draggableState,
TangemTokenRowPreviewData.draggableStateV2,
TangemTokenRowPreviewData.loadingState,
TangemTokenRowPreviewData.unreachableState,
TangemTokenRowPreviewData.accountState,
TangemTokenRowPreviewData.accountLetterState,
TangemTokenRowPreviewData.accountEllipsisState,
TangemTokenRowPreviewData.promoBannerState,
),
)
// endregion

View file

@ -0,0 +1,172 @@
package com.tangem.core.ui.ds.row.token
import androidx.compose.runtime.Immutable
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.ds.badge.TangemBadgeUM
import com.tangem.core.ui.extensions.ColorReference2
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.res.TangemTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
@Immutable
sealed class TangemTokenRowUM {
/** Unique id */
abstract val id: String
/** Token icon state */
abstract val iconState: CurrencyIconState
/** Token title UM (in one row with [topEndContentUM]) */
abstract val titleUM: TitleUM
/** Token subtitle UM (in one row with [bottomEndContentUM]) */
abstract val subtitleUM: SubtitleUM
/** Top end content UM (e.i. fiat amount in one row with [titleUM]) */
abstract val topEndContentUM: EndContentUM
/** Bottom end content UM (e.i. crypto amount in one row with [subtitleUM]) */
abstract val bottomEndContentUM: EndContentUM
/** Token row tail UM */
abstract val tailUM: TailUM
/** Promo banner UM */
abstract val promoBannerUM: PromoBannerUM
/** Callback which will be called when an item is clicked */
abstract val onItemClick: ((TangemTokenRowUM) -> Unit)?
/** Callback which will be called when an item is long clicked */
abstract val onItemLongClick: ((TangemTokenRowUM) -> Unit)?
/**
* Content state of [TangemTokenRowUM]
*/
data class Content(
override val id: String,
override val iconState: CurrencyIconState,
override val titleUM: TitleUM,
override val subtitleUM: SubtitleUM,
override val topEndContentUM: EndContentUM,
override val bottomEndContentUM: EndContentUM,
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty,
override val tailUM: TailUM = TailUM.Empty,
override val onItemClick: ((TangemTokenRowUM) -> Unit)?,
override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?,
) : TangemTokenRowUM()
/**
* Loading state of [TangemTokenRowUM]
*/
data class Loading(
override val id: String,
override val iconState: CurrencyIconState,
override val titleUM: TitleUM = TitleUM.Loading,
override val subtitleUM: SubtitleUM = SubtitleUM.Loading,
) : TangemTokenRowUM() {
override val topEndContentUM: EndContentUM = EndContentUM.Loading
override val bottomEndContentUM: EndContentUM = EndContentUM.Loading
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
override val tailUM: TailUM = TailUM.Empty
override val onItemClick: ((TangemTokenRowUM) -> Unit)? = null
override val onItemLongClick: ((TangemTokenRowUM) -> Unit)? = null
}
/**
* Actionable state of [TangemTokenRowUM]
*/
data class Actionable(
override val id: String,
override val iconState: CurrencyIconState,
override val titleUM: TitleUM,
override val subtitleUM: SubtitleUM,
override val tailUM: TailUM,
override val onItemClick: ((TangemTokenRowUM) -> Unit)?,
override val onItemLongClick: ((TangemTokenRowUM) -> Unit)?,
override val topEndContentUM: EndContentUM = EndContentUM.Empty,
override val bottomEndContentUM: EndContentUM = EndContentUM.Empty,
) : TangemTokenRowUM() {
override val promoBannerUM: PromoBannerUM = PromoBannerUM.Empty
}
@Immutable
sealed class TitleUM {
data class Content(
val text: TextReference,
val hasPending: Boolean = false,
val isAvailable: Boolean = true,
val badge: TangemBadgeUM? = null,
val onBadgeClick: (() -> Unit)? = null,
) : TitleUM()
data object Loading : TitleUM()
data object Empty : TitleUM()
}
@Immutable
sealed class SubtitleUM {
data class Content(
val text: TextReference,
val isAvailable: Boolean = true,
val isFlickering: Boolean = false,
val icons: ImmutableList<IconUM> = persistentListOf(),
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
val badge: TangemBadgeUM? = null,
) : SubtitleUM()
data object Loading : SubtitleUM()
data object Empty : SubtitleUM()
}
@Immutable
sealed class EndContentUM {
data class Content(
val text: TextReference,
val isAvailable: Boolean = true,
val isFlickering: Boolean = false,
val icons: ImmutableList<IconUM> = persistentListOf(),
val priceChangeUM: PriceChangeState = PriceChangeState.Unknown,
) : EndContentUM()
data object Loading : EndContentUM()
data object Empty : EndContentUM()
}
@Immutable
sealed class PromoBannerUM {
data class Content(
val title: TextReference,
val onPromoBannerClick: () -> Unit,
val onCloseClick: () -> Unit,
val onPromoShown: () -> Unit = {},
) : PromoBannerUM()
data object Empty : PromoBannerUM()
}
@Immutable
sealed class TailUM {
data class Text(
val text: TextReference,
) : TailUM()
data object Draggable : TailUM()
data object Empty : TailUM()
}
data class IconUM(
val iconRes: Int,
val tintReference: ColorReference2 = ColorReference2 { TangemTheme.colors2.graphic.neutral.primary },
)
}

View file

@ -0,0 +1,266 @@
package com.tangem.core.ui.ds.row.token.internal
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.SpanStyle
import com.tangem.core.ui.R
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.combinedReference
import com.tangem.core.ui.extensions.stringReference
import com.tangem.core.ui.extensions.styledResourceReference
import com.tangem.core.ui.extensions.styledStringReference
import com.tangem.core.ui.res.TangemColorPalette
import com.tangem.core.ui.res.TangemTheme
import com.tangem.utils.StringsSigns
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
internal object TangemTokenRowPreviewData {
private val priceChangeState: PriceChangeState.Content
get() = PriceChangeState.Content(
type = PriceChangeType.UP,
valueInPercent = "2.45%",
)
val promoBannerUM: TangemTokenRowUM.PromoBannerUM.Content
get() = TangemTokenRowUM.PromoBannerUM.Content(
title = stringReference("Earn yield by supplying your crypto assets"),
onPromoBannerClick = {},
onPromoShown = {},
onCloseClick = {},
)
val titleUM: TangemTokenRowUM.TitleUM.Content
get() = TangemTokenRowUM.TitleUM.Content(
text = stringReference(value = "Polygon"),
hasPending = true,
)
val subtitleUM: TangemTokenRowUM.SubtitleUM.Content
get() = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference(value = "$ 0.6631"),
priceChangeUM = priceChangeState,
)
val topEndContentUM: TangemTokenRowUM.EndContentUM.Content
get() = TangemTokenRowUM.EndContentUM.Content(
text = combinedReference(
stringReference("$ 500"),
styledStringReference(".17", {
SpanStyle(
color = TangemTheme.colors2.text.neutral.secondary,
fontWeight = TangemTheme.typography2.bodyRegular16.fontWeight,
)
}),
),
)
val bottomEndContentUM: TangemTokenRowUM.EndContentUM.Content
get() = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("Title"),
)
private val accountResIcon: CurrencyIconState.CryptoPortfolio.Icon
get() = CurrencyIconState.CryptoPortfolio.Icon(
resId = R.drawable.ic_rounded_star_24,
color = Color.Blue,
isGrayscale = false,
)
private val accountLetterIcon: CurrencyIconState.CryptoPortfolio.Letter
get() = CurrencyIconState.CryptoPortfolio.Letter(
char = stringReference("A"),
color = Color.Blue,
isGrayscale = false,
)
private val coinIconState
get() = CurrencyIconState.CoinIcon(
url = null,
fallbackResId = R.drawable.img_polygon_22,
isGrayscale = false,
shouldShowCustomBadge = false,
)
private val tokenIconState
get() = CurrencyIconState.TokenIcon(
url = null,
topBadgeIconResId = R.drawable.img_polygon_22,
fallbackTint = TangemColorPalette.Black,
fallbackBackground = TangemColorPalette.Meadow,
isGrayscale = false,
shouldShowCustomBadge = false,
)
private val customTokenIconState
get() = CurrencyIconState.CustomTokenIcon(
tint = TangemColorPalette.Black,
background = TangemColorPalette.Meadow,
topBadgeIconResId = R.drawable.img_polygon_22,
isGrayscale = false,
)
val defaultState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val defaultEllipsisState: TangemTokenRowUM.Content
get() = defaultState.copy(
titleUM = titleUM.copy(text = stringReference("Polygon Polygon Polygon Polygon")),
subtitleUM = subtitleUM,
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = combinedReference(
stringReference("$ 500"),
styledStringReference(".11232131237", {
SpanStyle(
color = TangemTheme.colors2.text.neutral.secondary,
fontWeight = TangemTheme.typography2.bodyRegular16.fontWeight,
)
}),
),
icons = persistentListOf(
TangemTokenRowUM.IconUM(R.drawable.ic_staking_mini_10),
TangemTokenRowUM.IconUM(R.drawable.ic_attention_12),
TangemTokenRowUM.IconUM(R.drawable.ic_error_sync_24),
),
),
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val tokenState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = tokenIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val customTokenState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = customTokenIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
promoBannerUM = TangemTokenRowUM.PromoBannerUM.Empty,
tailUM = TangemTokenRowUM.TailUM.Empty,
onItemClick = {},
onItemLongClick = {},
)
val draggableState: TangemTokenRowUM.Actionable
get() = TangemTokenRowUM.Actionable(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
tailUM = TangemTokenRowUM.TailUM.Draggable,
onItemClick = {},
onItemLongClick = {},
)
val draggableStateV2: TangemTokenRowUM.Actionable
get() = TangemTokenRowUM.Actionable(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = titleUM,
subtitleUM = subtitleUM,
topEndContentUM = topEndContentUM,
bottomEndContentUM = bottomEndContentUM,
tailUM = TangemTokenRowUM.TailUM.Draggable,
onItemClick = {},
onItemLongClick = {},
)
val loadingState: TangemTokenRowUM.Loading
get() = TangemTokenRowUM.Loading(
id = UUID.randomUUID().toString(),
iconState = coinIconState,
titleUM = TangemTokenRowUM.TitleUM.Loading,
subtitleUM = TangemTokenRowUM.SubtitleUM.Loading,
)
val unreachableState: TangemTokenRowUM.Content
get() = defaultState.copy(
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference(StringsSigns.DASH_SIGN),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = styledResourceReference(
id = R.string.common_unreachable,
spanStyleReference = { SpanStyle(color = TangemTheme.colors2.text.status.attention) },
),
),
)
val accountState: TangemTokenRowUM.Content
get() = TangemTokenRowUM.Content(
id = UUID.randomUUID().toString(),
iconState = accountResIcon,
titleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference(value = "Portfolio"),
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("24 tokens"),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("22,129.65 $"),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("+ $ 1,245.32"),
priceChangeUM = priceChangeState,
),
onItemClick = {},
onItemLongClick = {},
)
val accountLetterState: TangemTokenRowUM.Content
get() = accountState.copy(
iconState = accountLetterIcon,
)
val accountEllipsisState: TangemTokenRowUM.Content
get() = accountState.copy(
titleUM = TangemTokenRowUM.TitleUM.Content(
text = stringReference(value = "Portfolio Portfolio Portfolio Portfolio"),
),
subtitleUM = TangemTokenRowUM.SubtitleUM.Content(
text = stringReference("24 tokens 24 tokens 24 tokens 24 tokens"),
),
topEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("22,129.6129387147653025 $"),
),
bottomEndContentUM = TangemTokenRowUM.EndContentUM.Content(
text = stringReference("+ $ 1,245.31093284302752"),
priceChangeUM = priceChangeState,
),
)
val promoBannerState: TangemTokenRowUM.Content
get() = defaultState.copy(
promoBannerUM = promoBannerUM,
)
}

View file

@ -0,0 +1,100 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
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.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndBottomContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
is TangemTokenRowUM.EndContentUM.Content -> Content(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
modifier = modifier.width(TangemTheme.dimens2.x10),
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
when (endContentUM.priceChangeUM) {
is PriceChangeState.Content -> TokenRowPriceChangeContent(
priceChangeState = endContentUM.priceChangeUM,
isFlickering = endContentUM.isFlickering,
isAvailable = endContentUM.isAvailable,
)
PriceChangeState.Unknown -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowEndBottomContent_Preview(
@PreviewParameter(TokenRowEndBottomContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
TokenRowEndBottomContent(
endContentUM = params,
isBalanceHidden = false,
)
}
}
private class TokenRowEndBottomContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.bottomEndContentUM,
)
}
// endregion

View file

@ -0,0 +1,115 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.*
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
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.util.fastForEach
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.orMaskWithStars
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowEndTopContent(
endContentUM: TangemTokenRowUM.EndContentUM,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
when (endContentUM) {
is TangemTokenRowUM.EndContentUM.Content -> Content(
modifier = modifier,
endContentUM = endContentUM,
isBalanceHidden = isBalanceHidden,
)
TangemTokenRowUM.EndContentUM.Empty -> Unit
TangemTokenRowUM.EndContentUM.Loading -> TextShimmer(
style = TangemTheme.typography2.bodySemibold16,
modifier = modifier.width(TangemTheme.dimens2.x18),
radius = TangemTheme.dimens2.x25,
)
}
}
@Composable
private fun Content(
endContentUM: TangemTokenRowUM.EndContentUM.Content,
isBalanceHidden: Boolean,
modifier: Modifier = Modifier,
) {
Row(
modifier = modifier,
verticalAlignment = Alignment.CenterVertically,
) {
AnimatedVisibility(
visible = endContentUM.icons.isNotEmpty(),
) {
Row(
modifier = Modifier.padding(horizontal = TangemTheme.dimens2.x1),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x1),
) {
endContentUM.icons.fastForEach { icon ->
Icon(
modifier = Modifier.size(TangemTheme.dimens2.x3),
painter = rememberVectorPainter(image = ImageVector.vectorResource(icon.iconRes)),
tint = icon.tintReference(),
contentDescription = null,
)
}
}
}
Text(
modifier = Modifier,
text = endContentUM.text.orMaskWithStars(isBalanceHidden).resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.bodySemibold16.applyBladeBrush(
isEnabled = endContentUM.isFlickering,
textColor = if (endContentUM.isAvailable) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowEndTopContent_Preview(
@PreviewParameter(TokenRowEndContentPreviewProvider::class) params: TangemTokenRowUM.EndContentUM,
) {
TangemThemePreviewRedesign {
TokenRowEndTopContent(
endContentUM = params,
isBalanceHidden = false,
)
}
}
private class TokenRowEndContentPreviewProvider : PreviewParameterProvider<TangemTokenRowUM.EndContentUM> {
override val values: Sequence<TangemTokenRowUM.EndContentUM>
get() = sequenceOf(
TangemTokenRowPreviewData.topEndContentUM,
)
}
// endregion

View file

@ -0,0 +1,71 @@
package com.tangem.core.ui.ds.row.token.internal
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.marketprice.PriceChangeType
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.res.TangemTheme
@Composable
internal fun RowScope.TokenRowPriceChangeContent(
priceChangeState: PriceChangeState.Content,
isFlickering: Boolean,
isAvailable: Boolean = true,
) {
AnimatedContent(
targetState = priceChangeState.type,
label = "Update the price change's arrow",
modifier = Modifier.padding(start = TangemTheme.dimens2.x1),
) { animatedType ->
Icon(
painter = rememberVectorPainter(
ImageVector.vectorResource(
when (animatedType) {
PriceChangeType.UP -> R.drawable.ic_up_dynamic_24
PriceChangeType.DOWN -> R.drawable.ic_down_dynamic_24
PriceChangeType.NEUTRAL -> R.drawable.ic_static_dynamic_24
},
),
),
tint = when (animatedType) {
PriceChangeType.UP -> TangemTheme.colors2.graphic.status.accent
PriceChangeType.DOWN -> TangemTheme.colors2.graphic.status.warning
PriceChangeType.NEUTRAL -> TangemTheme.colors2.graphic.neutral.secondary
},
contentDescription = null,
modifier = Modifier.size(TangemTheme.dimens2.x3),
)
}
AnimatedContent(
targetState = priceChangeState.valueInPercent,
label = "Update the price text",
modifier = Modifier.padding(start = TangemTheme.dimens2.x0_5),
) { animatedText ->
Text(
text = animatedText,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = isFlickering,
textColor = if (isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
}
}

View file

@ -0,0 +1,107 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.ripple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
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.painterResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM, modifier: Modifier = Modifier) {
when (promoBannerUM) {
is TangemTokenRowUM.PromoBannerUM.Content -> TokenRowPromoBanner(
promoBannerUM = promoBannerUM,
modifier = modifier,
)
TangemTokenRowUM.PromoBannerUM.Empty -> Unit
}
}
@Composable
internal fun TokenRowPromoBanner(promoBannerUM: TangemTokenRowUM.PromoBannerUM.Content, modifier: Modifier = Modifier) {
LaunchedEffect(promoBannerUM) {
promoBannerUM.onPromoShown()
}
val bgColor = TangemTheme.colors.control.default
Column(modifier = modifier) {
Row(
modifier = Modifier
.background(color = bgColor, shape = RoundedCornerShape(TangemTheme.dimens2.x4))
.clickable(onClick = promoBannerUM.onPromoBannerClick)
.padding(horizontal = TangemTheme.dimens2.x3, vertical = TangemTheme.dimens2.x2)
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.ic_analytics_up_24),
contentDescription = null,
tint = TangemTheme.colors.icon.accent,
modifier = Modifier
.padding(end = TangemTheme.dimens2.x2)
.size(TangemTheme.dimens2.x4),
)
Text(
text = promoBannerUM.title.resolveReference(),
style = TangemTheme.typography2.captionSemibold12,
color = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.weight(1f)
.padding(end = TangemTheme.dimens2.x2),
)
Icon(
painter = painterResource(id = R.drawable.ic_close_24),
contentDescription = null,
tint = TangemTheme.colors2.text.neutral.secondary,
modifier = Modifier
.size(TangemTheme.dimens2.x4)
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = ripple(bounded = false),
onClick = { promoBannerUM.onCloseClick() },
),
)
}
Box(
modifier = Modifier.fillMaxWidth(),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_rectangle_bottom),
contentDescription = null,
tint = bgColor,
modifier = Modifier
.size(width = TangemTheme.dimens2.x3, height = TangemTheme.dimens2.x2),
)
}
}
}
@Preview(widthDp = 360, showBackground = true)
@Preview(widthDp = 360, showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
@Composable
private fun Preview_TokenRowPromoBanner() {
TangemThemePreviewRedesign {
TokenRowPromoBanner(
promoBannerUM = TangemTokenRowPreviewData.promoBannerUM,
modifier = Modifier.fillMaxWidth(),
)
}
}

View file

@ -0,0 +1,100 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.width
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.text.style.TextOverflow
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.components.TextShimmer
import com.tangem.core.ui.components.marketprice.PriceChangeState
import com.tangem.core.ui.components.text.applyBladeBrush
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowSubtitle(subtitleUM: TangemTokenRowUM.SubtitleUM, modifier: Modifier = Modifier) {
when (subtitleUM) {
is TangemTokenRowUM.SubtitleUM.Content -> SubtitleContent(
subtitleUM = subtitleUM,
modifier = modifier,
)
TangemTokenRowUM.SubtitleUM.Loading -> TextShimmer(
style = TangemTheme.typography2.captionSemibold12,
modifier = modifier.width(TangemTheme.dimens2.x8),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.SubtitleUM.Empty -> Unit
}
}
@Composable
private fun SubtitleContent(subtitleUM: TangemTokenRowUM.SubtitleUM.Content, modifier: Modifier = Modifier) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
AnimatedVisibility(
visible = subtitleUM.badge != null,
) {
val wrappedBadge = remember(this) { requireNotNull(subtitleUM.badge) }
TangemBadge(wrappedBadge)
}
Text(
text = subtitleUM.text.resolveAnnotatedReference(),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionSemibold12.applyBladeBrush(
isEnabled = subtitleUM.isFlickering,
textColor = if (subtitleUM.isAvailable) {
TangemTheme.colors2.text.neutral.secondary
} else {
TangemTheme.colors2.text.status.disabled
},
),
)
when (subtitleUM.priceChangeUM) {
is PriceChangeState.Content -> TokenRowPriceChangeContent(
priceChangeState = subtitleUM.priceChangeUM,
isFlickering = subtitleUM.isFlickering,
isAvailable = subtitleUM.isAvailable,
)
PriceChangeState.Unknown -> Unit
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowSubtitle_Preview(
@PreviewParameter(TokenRowSubtitlePreviewProvider::class) params: TangemTokenRowUM.SubtitleUM,
) {
TangemThemePreviewRedesign {
TokenRowSubtitle(
subtitleUM = params,
)
}
}
private class TokenRowSubtitlePreviewProvider : PreviewParameterProvider<TangemTokenRowUM.SubtitleUM> {
override val values: Sequence<TangemTokenRowUM.SubtitleUM>
get() = sequenceOf(
TangemTokenRowPreviewData.subtitleUM,
TangemTokenRowUM.SubtitleUM.Loading,
)
}
// endregion

View file

@ -0,0 +1,81 @@
package com.tangem.core.ui.ds.row.token.internal
import androidx.compose.animation.AnimatedContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import com.tangem.core.ui.R
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.resolveAnnotatedReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.test.OrganizeTokensScreenTestTags
import org.burnoutcrew.reorderable.ReorderableLazyListState
import org.burnoutcrew.reorderable.detectReorder
@Composable
internal fun TokenRowTail(
tailUM: TangemTokenRowUM.TailUM,
reorderableTokenListState: ReorderableLazyListState?,
modifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = tailUM,
label = "Update non content fiat block",
modifier = modifier,
contentKey = { it::class.java },
) { animatedState ->
val innerModifier = Modifier.padding(start = TangemTheme.dimens2.x2)
when (animatedState) {
TangemTokenRowUM.TailUM.Empty -> Unit
is TangemTokenRowUM.TailUM.Draggable -> DraggableImage(
reorderableTokenListState = reorderableTokenListState,
modifier = innerModifier,
)
is TangemTokenRowUM.TailUM.Text -> ContentText(text = animatedState.text, modifier = innerModifier)
}
}
}
@Composable
private fun DraggableImage(reorderableTokenListState: ReorderableLazyListState?, modifier: Modifier = Modifier) {
Box(
modifier = modifier
.size(size = TangemTheme.dimens2.x6)
.then(
other = if (reorderableTokenListState != null) {
Modifier.detectReorder(reorderableTokenListState)
} else {
Modifier
},
)
.testTag(OrganizeTokensScreenTestTags.DRAGGABLE_IMAGE),
contentAlignment = Alignment.Center,
) {
Icon(
painter = painterResource(id = R.drawable.ic_drag_24),
tint = TangemTheme.colors2.graphic.neutral.tertiaryConstant,
contentDescription = null,
)
}
}
@Composable
private fun ContentText(text: TextReference, modifier: Modifier = Modifier) {
Text(
text = text.resolveAnnotatedReference(),
color = TangemTheme.colors2.text.neutral.tertiary,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = TangemTheme.typography2.captionRegular12,
modifier = modifier,
)
}

View file

@ -0,0 +1,101 @@
package com.tangem.core.ui.ds.row.token.internal
import android.content.res.Configuration
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.Image
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.width
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.res.painterResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import com.tangem.core.ui.R
import com.tangem.core.ui.components.TextShimmer
import com.tangem.core.ui.ds.badge.TangemBadge
import com.tangem.core.ui.ds.row.token.TangemTokenRowUM
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
@Composable
internal fun TokenRowTitle(titleUM: TangemTokenRowUM.TitleUM, modifier: Modifier = Modifier) {
when (titleUM) {
is TangemTokenRowUM.TitleUM.Content -> ContentTitle(titleUM = titleUM, modifier = modifier)
TangemTokenRowUM.TitleUM.Loading -> TextShimmer(
style = TangemTheme.typography2.bodySemibold16,
modifier = modifier.width(TangemTheme.dimens2.x18),
radius = TangemTheme.dimens2.x25,
)
TangemTokenRowUM.TitleUM.Empty -> Unit
}
}
@Composable
private fun ContentTitle(titleUM: TangemTokenRowUM.TitleUM.Content, modifier: Modifier = Modifier) {
Row(
modifier = modifier,
horizontalArrangement = Arrangement.spacedBy(space = TangemTheme.dimens2.x4),
verticalAlignment = Alignment.CenterVertically,
) {
/*
* If currency name has a long width, then it will completely displace the image.
* So we need to use [weight] to avoid displacement.
*/
Text(
text = titleUM.text.resolveReference(),
modifier = Modifier.weight(weight = 1f, fill = false),
color = if (titleUM.isAvailable) {
TangemTheme.colors2.text.neutral.primary
} else {
TangemTheme.colors2.text.status.disabled
},
overflow = TextOverflow.Ellipsis,
maxLines = 1,
style = TangemTheme.typography2.bodySemibold16,
)
AnimatedVisibility(
visible = titleUM.hasPending,
modifier = Modifier.align(alignment = Alignment.CenterVertically),
) {
Image(
painter = painterResource(id = R.drawable.img_loader_15),
contentDescription = null,
)
}
AnimatedVisibility(
visible = titleUM.badge != null,
modifier = Modifier.conditional(
condition = titleUM.onBadgeClick != null,
modifier = { clickable(onClick = requireNotNull(titleUM.onBadgeClick)) },
),
) {
val wrappedBadge = remember(this) { requireNotNull(titleUM.badge) }
TangemBadge(wrappedBadge)
}
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TokenRowTitle_Preview() {
TangemThemePreviewRedesign {
TokenRowTitle(
titleUM = TangemTokenRowPreviewData.titleUM,
modifier = Modifier.fillMaxWidth(),
)
}
}
// endregion

View file

@ -0,0 +1,271 @@
package com.tangem.core.ui.ds.tabs
import android.content.res.Configuration
import androidx.compose.animation.core.animateDpAsState
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.coerceAtLeast
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import com.tangem.core.ui.extensions.TextReference
import com.tangem.core.ui.extensions.conditional
import com.tangem.core.ui.extensions.resolveReference
import com.tangem.core.ui.res.TangemTheme
import com.tangem.core.ui.res.TangemThemePreviewRedesign
import com.tangem.utils.extensions.indexOfFirstOrNull
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
/**
* Tangem segmented picker component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8452-16487&m=dev)
*
* @param tangemSegmentedPickerUM Model representing the segmented picker.
* @param modifier Modifier to be applied to the segmented picker.
* @param onClick Lambda function to be invoked when a segment is clicked.
*/
@Composable
fun TangemSegmentedPicker(
tangemSegmentedPickerUM: TangemSegmentedPickerUM,
modifier: Modifier = Modifier,
onClick: (TangemSegmentUM) -> Unit,
) {
TangemSegmentedPicker(
items = tangemSegmentedPickerUM.items,
modifier = modifier,
initialSelectedItem = tangemSegmentedPickerUM.initialSelectedItem,
hasSeparator = tangemSegmentedPickerUM.hasSeparator,
isFixed = tangemSegmentedPickerUM.isFixed,
isAltSurface = tangemSegmentedPickerUM.isAltSurface,
onClick = onClick,
)
}
/**
* Tangem segmented picker component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8452-16487&m=dev)
*
* @param items List of TangemSegmentUM representing the segments in the picker.
* @param modifier Modifier to be applied to the segmented picker.
* @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment.
* @param hasSeparator Boolean indicating whether there is a separator between segments.
* @param isFixed Boolean indicating whether the picker has a fixed width.
* @param isAltSurface Boolean indicating whether to use an alternative surface style.
* @param onClick Lambda function to be invoked when a segment is clicked.
*/
@Composable
fun TangemSegmentedPicker(
items: ImmutableList<TangemSegmentUM>,
modifier: Modifier = Modifier,
initialSelectedItem: TangemSegmentUM? = null,
hasSeparator: Boolean = false,
isFixed: Boolean = false,
isAltSurface: Boolean = false,
onClick: (TangemSegmentUM) -> Unit,
) {
if (items.isEmpty() || items.size == 1) return
val density = LocalDensity.current
val itemsWidths = remember { mutableStateListOf(*Array(items.size) { 0.dp }) }
val selectedIndex = remember { mutableStateOf(items.indexOfFirstOrNull { it == initialSelectedItem } ?: 0) }
val segmentHeight = remember { mutableStateOf(0.dp) }
val shape = RoundedCornerShape(TangemTheme.dimens2.x25)
val spacing = if (hasSeparator) {
TangemTheme.dimens2.x4
} else {
TangemTheme.dimens2.x1
}
Box(
modifier = modifier
.clip(shape)
.background(
color = if (isAltSurface) {
TangemTheme.colors2.tabs.backgroundQuaternary
} else {
TangemTheme.colors2.tabs.backgroundSecondary
},
)
.padding(TangemTheme.dimens2.x0_5),
) {
SegmentSelection(
itemsWidths = itemsWidths,
selectedIndex = selectedIndex.value,
segmentHeight = segmentHeight.value,
spacing = spacing,
)
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(spacing),
) {
items.fastForEachIndexed { index, item ->
Segment(
item = item,
index = index,
isFixed = isFixed,
selectedIndex = selectedIndex,
onClick = { onClick(item) },
modifier = Modifier
.onGloballyPositioned {
with(density) {
itemsWidths[index] = it.size.width.toDp()
segmentHeight.value = it.size.height.toDp().coerceAtLeast(segmentHeight.value)
}
},
)
}
}
}
}
@Composable
private fun SegmentSelection(itemsWidths: SnapshotStateList<Dp>, selectedIndex: Int, segmentHeight: Dp, spacing: Dp) {
val indicatorOffset by animateDpAsState(
targetValue = itemsWidths.take(selectedIndex).fold(0.dp, Dp::plus) + spacing * selectedIndex,
animationSpec = tween(durationMillis = 300),
label = "indicatorOffset",
)
val indicatorWidth by animateDpAsState(
targetValue = itemsWidths[selectedIndex],
animationSpec = tween(durationMillis = 300),
label = "indicatorWidth",
)
Box(
modifier = Modifier
.offset {
IntOffset(x = indicatorOffset.roundToPx(), y = 0)
}
.size(width = indicatorWidth, height = segmentHeight)
.shadow(
elevation = TangemTheme.dimens2.x1,
shape = RoundedCornerShape(TangemTheme.dimens2.x25),
)
.background(
color = TangemTheme.colors2.tabs.backgroundTertiary,
),
)
}
@Composable
private fun RowScope.Segment(
item: TangemSegmentUM,
index: Int,
isFixed: Boolean,
selectedIndex: MutableState<Int>,
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
Box(
modifier = modifier
.conditional(isFixed) {
weight(1f)
}
.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() },
) {
selectedIndex.value = index
onClick()
},
) {
Text(
text = item.title.resolveReference(),
style = TangemTheme.typography2.bodySemibold15,
color = if (selectedIndex.value == index) {
TangemTheme.colors2.tabs.textTertiary
} else {
TangemTheme.colors2.tabs.textSecondary
},
maxLines = 1,
modifier = Modifier
.align(Alignment.Center)
.padding(
horizontal = TangemTheme.dimens2.x3,
vertical = TangemTheme.dimens2.x2,
),
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemSegmentedPicker_Preview(@PreviewParameter(PreviewProvider::class) params: TangemSegmentedPickerUM) {
var selectedItem by remember { mutableStateOf(params.items.first()) }
TangemThemePreviewRedesign {
Box(
modifier = Modifier
.fillMaxWidth()
.background(TangemTheme.colors2.surface.level1)
.padding(TangemTheme.dimens2.x4),
) {
TangemSegmentedPicker(
isFixed = params.isFixed,
hasSeparator = params.hasSeparator,
isAltSurface = params.isAltSurface,
items = params.items,
initialSelectedItem = params.items.last(),
onClick = { selectedItem = it },
)
}
}
}
private class PreviewProvider : PreviewParameterProvider<TangemSegmentedPickerUM> {
private val items = persistentListOf(
TangemSegmentUM(id = "1", title = TextReference.Str("Segment")),
TangemSegmentUM(id = "2", title = TextReference.Str("Segment Two")),
TangemSegmentUM(id = "3", title = TextReference.Str("Seg 3")),
)
override val values: Sequence<TangemSegmentedPickerUM>
get() = sequenceOf(
TangemSegmentedPickerUM(
items = items,
hasSeparator = false,
isFixed = false,
isAltSurface = false,
),
TangemSegmentedPickerUM(
items = items,
hasSeparator = true,
isFixed = false,
isAltSurface = true,
),
TangemSegmentedPickerUM(
items = items,
hasSeparator = false,
isFixed = true,
isAltSurface = false,
),
TangemSegmentedPickerUM(
items = items,
hasSeparator = true,
isFixed = true,
isAltSurface = true,
),
)
}
// endregion

View file

@ -0,0 +1,32 @@
package com.tangem.core.ui.ds.tabs
import com.tangem.core.ui.extensions.TextReference
import kotlinx.collections.immutable.ImmutableList
/**
* Model for Tangem segmented picker component.
*
* @param items List of TangemSegmentUM representing the segments in the picker.
* @param initialSelectedItem Optional TangemSegmentUM representing the initially selected segment.
* @param hasSeparator Boolean indicating whether there is a separator between segments.
* @param isFixed Boolean indicating whether the picker has a fixed width.
* @param isAltSurface Boolean indicating whether to use an alternative surface style.
*/
data class TangemSegmentedPickerUM(
val items: ImmutableList<TangemSegmentUM>,
val initialSelectedItem: TangemSegmentUM? = null,
val hasSeparator: Boolean = false,
val isFixed: Boolean = false,
val isAltSurface: Boolean = false,
)
/**
* Model for a single segment in the Tangem segmented picker.
*
* @param id String representing the unique identifier of the segment.
* @param title TextReference representing the title of the segment.
*/
data class TangemSegmentUM(
val id: String,
val title: TextReference,
)

View file

@ -0,0 +1,105 @@
package com.tangem.core.ui.ds.tabs
import android.content.res.Configuration
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.selection.toggleable
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.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.tooling.preview.Preview
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 tab component.
* [Figma](https://www.figma.com/design/RU7AIgwHtGdMfy83T5UOoR/Core-Library?node-id=8448-74386&m=dev)
*
* @param text TextReference representing the text to be displayed on the tab.
* @param isChecked Boolean value representing the current state of the tab.
* @param onCheckedChange Lambda function to be invoked when the tab state changes.
* @param modifier Modifier to be applied to the tab.
* @param isEnabled Boolean value to determine if the tab is enabled or disabled.
*/
@Composable
fun TangemTab(
text: TextReference,
isChecked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
isEnabled: Boolean = true,
) {
val shape = RoundedCornerShape(TangemTheme.dimens2.x25)
val backgroundColor = if (isChecked) {
TangemTheme.colors2.tabs.backgroundPrimary
} else {
TangemTheme.colors2.tabs.textPrimary
}
val textColor = if (isChecked) {
TangemTheme.colors2.tabs.textPrimary
} else {
TangemTheme.colors2.tabs.textSecondary
}
Box(
modifier = modifier
.background(
color = backgroundColor,
shape = shape,
)
.clip(shape = shape)
.padding(all = TangemTheme.dimens2.x3)
.toggleable(
value = isChecked,
onValueChange = onCheckedChange,
enabled = isEnabled,
role = Role.Checkbox,
interactionSource = remember { MutableInteractionSource() },
indication = ripple(),
),
) {
Text(
text = text.resolveReference(),
style = TangemTheme.typography2.bodySemibold16,
color = textColor,
maxLines = 1,
)
}
}
// region Preview
@Composable
@Preview(showBackground = true, widthDp = 360)
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
private fun TangemTab_Preview() {
TangemThemePreviewRedesign {
Column(
verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x6),
modifier = Modifier.background(TangemTheme.colors2.surface.level1),
) {
TangemTab(
text = stringReference("Tab Title"),
isChecked = true,
onCheckedChange = {},
)
TangemTab(
text = stringReference("Tab Title"),
isChecked = false,
onCheckedChange = {},
)
}
}
}
// endregion

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

@ -2,15 +2,18 @@ package com.tangem.core.ui.extensions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.Stable
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 or ColorReference2 for color.")
@Immutable
data class ColorReference(val value: @Composable () -> Color)
@ -30,4 +33,17 @@ fun themedColor(value: @Composable () -> Color): ColorReference {
@Composable
fun ColorReference.resolveReference(): Color {
return value()
}
/**
* Utility functional interface for keeping themed [Color] reference from app theme.
* It is necessary to use [Stable] annotation for runtime stability.
*/
@Stable
@FunctionalInterface
fun interface ColorReference2 {
@ReadOnlyComposable
@Composable
operator fun invoke(): Color
}

View file

@ -9,6 +9,7 @@ import androidx.compose.runtime.Immutable
* It necessary to use [Immutable] annotation because all sealed interface has runtime stability.
* All subclasses are stable.
*/
@Immutable
sealed class ImageReference {
data class Url(val url: String) : ImageReference()

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,30 @@
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.text.AnnotatedString
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.*
import androidx.compose.ui.text.AnnotatedString.Builder
import androidx.compose.ui.text.buildAnnotatedString
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 +83,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 +182,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 +244,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 +264,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 +277,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 +291,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 +305,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 +319,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 +345,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 +382,121 @@ 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() },
styles = TextLinkStyles(style = spanStyleReference()),
),
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

@ -169,19 +169,19 @@ class TangemColors internal constructor(
@Stable
class Control internal constructor(
checked: Color,
unchecked: Color,
default: Color,
key: Color,
) {
var checked by mutableStateOf(checked)
private set
var unchecked by mutableStateOf(unchecked)
var default by mutableStateOf(default)
private set
var key by mutableStateOf(key)
private set
fun update(other: Control) {
checked = other.checked
unchecked = other.unchecked
default = other.default
key = other.key
}
}

View file

@ -1,4 +1,5 @@
@file:Suppress("LongParameterList")
package com.tangem.core.ui.res
import androidx.compose.runtime.Stable
@ -20,6 +21,7 @@ class TangemColors2 internal constructor(
val fill: Fill,
val skeleton: Skeleton,
val markers: Markers,
val tabs: Tabs,
) {
@Stable
@ -162,6 +164,7 @@ class TangemColors2 internal constructor(
backgroundSecondary: Color,
backgroundDisabled: Color,
backgroundPositive: Color,
backgroundPrimaryInverse: Color,
textPrimary: Color,
textSecondary: Color,
textDisabled: Color,
@ -178,6 +181,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 +203,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
@ -520,6 +526,42 @@ class TangemColors2 internal constructor(
}
}
@Stable
class Tabs internal constructor(
textPrimary: Color,
textSecondary: Color,
textTertiary: Color,
backgroundPrimary: Color,
backgroundSecondary: Color,
backgroundTertiary: Color,
backgroundQuaternary: Color,
) {
var textPrimary by mutableStateOf(textPrimary)
private set
var textSecondary by mutableStateOf(textSecondary)
private set
var textTertiary by mutableStateOf(textTertiary)
private set
var backgroundPrimary by mutableStateOf(backgroundPrimary)
private set
var backgroundSecondary by mutableStateOf(backgroundSecondary)
private set
var backgroundTertiary by mutableStateOf(backgroundTertiary)
private set
var backgroundQuaternary by mutableStateOf(backgroundQuaternary)
private set
fun update(other: Tabs) {
textPrimary = other.textPrimary
textSecondary = other.textSecondary
textTertiary = other.textTertiary
backgroundPrimary = other.backgroundPrimary
backgroundSecondary = other.backgroundSecondary
backgroundTertiary = other.backgroundTertiary
backgroundQuaternary = other.backgroundQuaternary
}
}
fun update(other: TangemColors2) {
text.update(other.text)
graphic.update(other.graphic)
@ -532,5 +574,6 @@ class TangemColors2 internal constructor(
fill.update(other.fill)
skeleton.update(other.skeleton)
markers.update(other.markers)
tabs.update(other.tabs)
}
}

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
@ -16,6 +18,8 @@ import com.tangem.core.ui.UiDependencies
import com.tangem.core.ui.components.LocalSystemBarsIconsController
import com.tangem.core.ui.components.SystemBarsIconsController
import com.tangem.core.ui.components.TangemShimmer
import com.tangem.core.ui.components.powersaving.PowerSavingState
import com.tangem.core.ui.components.powersaving.rememberPowerSavingState
import com.tangem.core.ui.components.text.BladeAnimation
import com.tangem.core.ui.components.text.rememberBladeAnimation
import com.tangem.core.ui.haptic.DefaultHapticManager
@ -26,7 +30,9 @@ import com.tangem.core.ui.windowsize.WindowSize
import com.tangem.core.ui.windowsize.rememberWindowSize
import com.tangem.domain.apptheme.model.AppThemeMode
import com.valentinilk.shimmer.Shimmer
import dev.chrisbanes.haze.HazeState
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun TangemTheme(
activity: Activity,
@ -36,6 +42,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)
@ -48,7 +57,13 @@ fun TangemTheme(
overrideSystemBarColors = overrideSystemBarColors,
typography = typography,
dimens = dimens,
content = content,
content = {
if (uiDependencies.designFeatureToggles.isRedesignEnabled) {
TangemThemeRedesign { content() }
} else {
content()
}
},
)
}
@ -108,6 +123,7 @@ fun TangemTheme(
LocalWindowSize provides windowSize,
LocalBladeAnimation provides rememberBladeAnimation(),
LocalSystemBarsIconsController provides systemBarsIconsController,
LocalPowerSavingState provides rememberPowerSavingState(),
) {
CompositionLocalProvider(
LocalTangemShimmer provides TangemShimmer,
@ -148,11 +164,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
@ -244,7 +270,7 @@ internal fun lightThemeColors(redesign: Boolean = false): TangemColors {
),
control = TangemColors.Control(
checked = TangemColorPalette.Dark6,
unchecked = TangemColorPalette.Light2,
default = TangemColorPalette.Light2,
key = TangemColorPalette.White,
),
stroke = TangemColors.Stroke(
@ -298,7 +324,7 @@ internal fun darkThemeColors(): TangemColors {
),
control = TangemColors.Control(
checked = TangemColorPalette.Azure,
unchecked = TangemColorPalette.Dark4,
default = TangemColorPalette.Dark4,
key = TangemColorPalette.White,
),
stroke = TangemColors.Stroke(
@ -337,10 +363,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")
}
@ -379,6 +413,18 @@ val LocalBladeAnimation = staticCompositionLocalOf<BladeAnimation> {
error("No MainBottomSheetColor provided")
}
val LocalHazeState = staticCompositionLocalOf<HazeState> {
error("No HazeState provided")
}
val LocalRedesignEnabled = staticCompositionLocalOf<Boolean> {
false
}
val LocalPowerSavingState = compositionLocalOf<PowerSavingState> {
error("No PowerSavingState provided")
}
/**
* Determines whether the dark theme should be used based on the given [AppThemeMode].
*

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,8 +1,13 @@
@file:Suppress("LongMethod")
package com.tangem.core.ui.res
import androidx.compose.foundation.layout.Box
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import com.tangem.core.ui.components.haze.ProvideHaze
import com.tangem.core.ui.components.haze.hazeSourceTangem
/**
* Provides additional theming for redesigned components.
@ -20,12 +25,17 @@ fun TangemThemeRedesign(content: @Composable () -> Unit) {
colorScheme = tangemColorScheme(colors = themeColors),
) {
CompositionLocalProvider(
LocalRedesignEnabled provides true,
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()
ProvideHaze {
Box(Modifier.hazeSourceTangem(zIndex = 1f)) {
content()
}
}
}
}
}
@ -97,9 +107,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,
@ -154,6 +165,15 @@ private fun lightThemeColors2(): TangemColors2 {
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Light2,
textSecondary = TangemColorPalette.Dark4,
textTertiary = TangemColorPalette.Black,
backgroundPrimary = TangemColorPalette.Dark6,
backgroundSecondary = TangemColorPalette.Dark_10,
backgroundTertiary = TangemColorPalette.White,
backgroundQuaternary = TangemColorPalette.Dark_20,
)
return TangemColors2(
text = text,
graphic = graphic,
@ -166,6 +186,7 @@ private fun lightThemeColors2(): TangemColors2 {
field = field,
skeleton = skeleton,
markers = markers,
tabs = tabs,
)
}
@ -236,9 +257,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,
@ -293,6 +315,15 @@ private fun darkThemeColors2(): TangemColors2 {
borderTintedBlue = TangemColorPalette.Azure.copy(alpha = 0.1f),
borderTintedRed = TangemColorPalette.Amaranth.copy(alpha = 0.1f),
)
val tabs = TangemColors2.Tabs(
textPrimary = TangemColorPalette.Dark4,
textSecondary = TangemColorPalette.Light5,
textTertiary = TangemColorPalette.White,
backgroundPrimary = TangemColorPalette.Light1,
backgroundSecondary = TangemColorPalette.Light_10,
backgroundTertiary = TangemColorPalette.Light_10,
backgroundQuaternary = TangemColorPalette.Light_10,
)
return TangemColors2(
text = text,
graphic = graphic,
@ -305,5 +336,6 @@ private fun darkThemeColors2(): TangemColors2 {
field = field,
skeleton = skeleton,
markers = markers,
tabs = tabs,
)
}

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

@ -1,15 +0,0 @@
package com.tangem.core.ui.utils
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
fun Modifier.disableNestedScroll(): Modifier = nestedScroll(DisableParentConnection)
private object DisableParentConnection : NestedScrollConnection {
override fun onPostScroll(consumed: Offset, available: Offset, source: NestedScrollSource): Offset {
return available.copy(x = 0f)
}
}

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,12L9.785,18.221C9.89,18.357 10.099,18.349 10.193,18.205L18.5,5.5"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5.28,4C4.294,4 3.679,5.064 4.176,5.911L10.896,17.369C11.389,18.21 12.611,18.21 13.104,17.369L19.824,5.911C20.321,5.064 19.706,4 18.72,4H5.28Z"
android:fillColor="#000000"/>
</vector>

View file

@ -4,7 +4,9 @@
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M19,8H5V10H19V8ZM19,14H5V16H19V14Z"
android:fillColor="#B0B0B0"
android:fillType="evenOdd"/>
android:pathData="M5,8H19M5,16H19"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="round"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="32dp"
android:height="32dp"
android:viewportWidth="32"
android:viewportHeight="32">
<path
android:pathData="M16,28.467L14.067,26.707C7.2,20.48 2.667,16.36 2.667,11.333C2.667,7.213 5.894,4 10,4C10.894,4 11.76,4.16 12.587,4.44L17.334,12.467L12,19.133L16,28.467ZM22,4C26.107,4 29.334,7.213 29.334,11.333C29.334,16.36 24.8,20.48 17.934,26.707L16,28.467L14.667,19.133L20.667,12.467L17.134,5.693C18.494,4.627 20.227,4 22,4Z"
android:fillColor="#FF3333"/>
</vector>

View file

@ -0,0 +1,29 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="744dp"
android:height="744dp"
android:viewportWidth="744"
android:viewportHeight="744">
<path
android:pathData="M0,0h744v744h-744z"
android:fillColor="#136BFF"/>
<path
android:pathData="M110,318C108.89,318 108,318.89 108,320V380C108,381.11 108.89,382 110,382H266C267.11,382 268,381.11 268,380V320C268,318.89 267.11,318 266,318H110ZM247,362C247.55,362 248,361.55 248,361V339C248,338.45 247.55,338 247,338H129C128.45,338 128,338.45 128,339V361C128,361.55 128.45,362 129,362H247Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M128,408C128,406.89 127.11,406 126,406H110C108.89,406 108,406.89 108,408V424C108,425.11 108.89,426 110,426H126C127.11,426 128,425.11 128,424V408Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M496,408C496,406.89 495.11,406 494,406H478C476.89,406 476,406.89 476,408V424C476,425.11 476.89,426 478,426H494C495.11,426 496,425.11 496,424V408Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M292,408C292,406.89 292.89,406 294,406H450C451.11,406 452,406.89 452,408V424C452,425.11 451.11,426 450,426H294C292.89,426 292,425.11 292,424V408Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M476,320C476,318.89 476.89,318 478,318H634C635.1,318 636,318.89 636,320V380C636,381.11 635.1,382 634,382H478C476.89,382 476,381.11 476,380V320ZM615,338C615.55,338 616,338.45 616,339V361C616,361.55 615.55,362 615,362H497C496.45,362 496,361.55 496,361V339C496,338.45 496.45,338 497,338H615Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
<path
android:pathData="M294,318C292.89,318 292,318.89 292,320V336C292,337.11 292.89,338 294,338H431C431.55,338 432,338.45 432,339V361C432,361.55 431.55,362 431,362H294C292.89,362 292,362.89 292,364V380C292,381.11 292.89,382 294,382H450C451.11,382 452,381.11 452,380V320C452,318.89 451.11,318 450,318H294Z"
android:fillColor="#ffffff"/>
</vector>

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>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,12m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"
android:fillColor="#000000"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5.28,20C4.294,20 3.679,18.936 4.176,18.089L10.896,6.631C11.389,5.79 12.611,5.79 13.104,6.631L19.824,18.089C20.321,18.936 19.706,20 18.72,20H5.28Z"
android:fillColor="#000000"/>
</vector>