Updated on 2026-08-14
This commit is contained in:
commit
21ae19f625
938 changed files with 40685 additions and 7487 deletions
|
|
@ -0,0 +1,5 @@
|
|||
package com.tangem.core.ui
|
||||
|
||||
interface DesignFeatureToggles {
|
||||
val isRedesignEnabled: Boolean
|
||||
}
|
||||
|
|
@ -16,4 +16,6 @@ interface UiDependencies {
|
|||
val globalSnackbarHostState: SnackbarHostState
|
||||
|
||||
val eventMessageHandler: EventMessageHandler
|
||||
|
||||
val designFeatureToggles: DesignFeatureToggles
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 = "")
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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?,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
122
core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
Normal file
122
core/ui/src/main/java/com/tangem/core/ui/components/chip/Chip.kt
Normal 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 = {},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -8,4 +8,5 @@ data class SearchBarUM(
|
|||
val onQueryChange: (String) -> Unit,
|
||||
val isActive: Boolean,
|
||||
val onActiveChange: (Boolean) -> Unit,
|
||||
val onClearClick: () -> Unit = {},
|
||||
)
|
||||
|
|
@ -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)
|
||||
|
|
@ -7,4 +7,5 @@ enum class IconTint {
|
|||
Accent,
|
||||
Warning,
|
||||
Inactive,
|
||||
Informative,
|
||||
}
|
||||
|
|
@ -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 ->
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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>?,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
344
core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
Normal file
344
core/ui/src/main/java/com/tangem/core/ui/ds/badge/TangemBadge.kt
Normal 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
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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,
|
||||
)
|
||||
105
core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt
Normal file
105
core/ui/src/main/java/com/tangem/core/ui/ds/tabs/TangemTab.kt
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
|
|
@ -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].
|
||||
*
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
* */
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
12
core/ui/src/main/res/drawable/ic_check_default_24.xml
Normal file
12
core/ui/src/main/res/drawable/ic_check_default_24.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_down_dynamic_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_down_dynamic_24.xml
Normal 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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
9
core/ui/src/main/res/drawable/ic_heart_broken_32.xml
Normal file
9
core/ui/src/main/res/drawable/ic_heart_broken_32.xml
Normal 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>
|
||||
29
core/ui/src/main/res/drawable/ic_p2p_logo.xml
Normal file
29
core/ui/src/main/res/drawable/ic_p2p_logo.xml
Normal 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>
|
||||
18
core/ui/src/main/res/drawable/ic_show_more_news_48.xml
Normal file
18
core/ui/src/main/res/drawable/ic_show_more_news_48.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_static_dynamic_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_static_dynamic_24.xml
Normal 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>
|
||||
9
core/ui/src/main/res/drawable/ic_up_dynamic_24.xml
Normal file
9
core/ui/src/main/res/drawable/ic_up_dynamic_24.xml
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue