Updated on 2026-08-14
This commit is contained in:
commit
3428284898
821 changed files with 37777 additions and 8160 deletions
|
|
@ -32,7 +32,7 @@ import com.tangem.core.ui.res.TangemThemePreview
|
|||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
|
||||
enum class AccountIconSize {
|
||||
Default, Large, Medium, Small, ExtraSmall, RedesignedDefault, RedesignExtraSmall
|
||||
Default, Large, Medium, Small, ExtraSmall, RedesignedDefault, RedesignExtraSmall, RedesignLarge, Contact
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -132,6 +132,8 @@ fun AccountCharIcon(char: Char, color: Color, size: AccountIconSize, modifier: M
|
|||
AccountIconSize.ExtraSmall -> TangemTheme.typography.caption1
|
||||
AccountIconSize.RedesignedDefault -> TangemTheme.typography2.headingSemibold28
|
||||
AccountIconSize.RedesignExtraSmall -> TangemTheme.typography2.captionMedium11
|
||||
AccountIconSize.RedesignLarge -> TangemTheme.typography3.heading.medium
|
||||
AccountIconSize.Contact -> TangemTheme.typography3.body.medium
|
||||
}
|
||||
|
||||
val textSize by animateFloatAsState(
|
||||
|
|
@ -166,6 +168,8 @@ private fun AccountIconSize.iconSizeInDp(): Dp = when (this) {
|
|||
AccountIconSize.ExtraSmall -> 8.dp
|
||||
AccountIconSize.RedesignedDefault -> 20.dp
|
||||
AccountIconSize.RedesignExtraSmall -> 8.dp
|
||||
AccountIconSize.RedesignLarge -> 32.dp
|
||||
AccountIconSize.Contact -> 20.dp
|
||||
}
|
||||
|
||||
fun AccountIconSize.toBoxSize(): Dp = when (this) {
|
||||
|
|
@ -176,6 +180,8 @@ fun AccountIconSize.toBoxSize(): Dp = when (this) {
|
|||
AccountIconSize.ExtraSmall -> 14.dp
|
||||
AccountIconSize.RedesignedDefault -> 40.dp
|
||||
AccountIconSize.RedesignExtraSmall -> 16.dp
|
||||
AccountIconSize.RedesignLarge -> 80.dp
|
||||
AccountIconSize.Contact -> 40.dp
|
||||
}
|
||||
|
||||
private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
|
||||
|
|
@ -186,6 +192,8 @@ private fun AccountIconSize.boxShapeSizeInDp(): Dp = when (this) {
|
|||
AccountIconSize.ExtraSmall -> 4.dp
|
||||
AccountIconSize.RedesignedDefault -> 12.dp
|
||||
AccountIconSize.RedesignExtraSmall -> 6.dp
|
||||
AccountIconSize.RedesignLarge -> 80.dp
|
||||
AccountIconSize.Contact -> 100.dp
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
|
|
@ -228,7 +236,9 @@ private fun Sample() {
|
|||
AccountIconSize.Small -> AccountIconSize.ExtraSmall
|
||||
AccountIconSize.ExtraSmall -> AccountIconSize.RedesignedDefault
|
||||
AccountIconSize.RedesignedDefault -> AccountIconSize.RedesignExtraSmall
|
||||
AccountIconSize.RedesignExtraSmall -> AccountIconSize.Default
|
||||
AccountIconSize.RedesignExtraSmall -> AccountIconSize.RedesignLarge
|
||||
AccountIconSize.RedesignLarge -> AccountIconSize.Contact
|
||||
AccountIconSize.Contact -> AccountIconSize.Default
|
||||
}
|
||||
}) { Text("Change") }
|
||||
|
||||
|
|
|
|||
|
|
@ -51,7 +51,10 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
) {
|
||||
var watchedCounter by remember { mutableIntStateOf(1) }
|
||||
var isPressed by remember { mutableStateOf(value = false) }
|
||||
val storyState by remember(config) {
|
||||
// Key on content (stories + repeatability), not the whole config object: an unrelated change
|
||||
// to the config instance (e.g. a caller folding a changing flag into it) must not recreate the
|
||||
// state machine and rewind stories to the first page.
|
||||
val storyState by remember(config.stories, config.isRestartable) {
|
||||
mutableStateOf(
|
||||
StoriesStepStateMachine(
|
||||
stories = config.stories,
|
||||
|
|
@ -59,7 +62,9 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
),
|
||||
)
|
||||
}
|
||||
BackHandler(onBack = { config.onClose(watchedCounter) })
|
||||
if (config.isCloseButtonVisible) {
|
||||
BackHandler(onBack = { config.onClose(watchedCounter) })
|
||||
}
|
||||
|
||||
val isPaused = isPressed || isPauseStories
|
||||
|
||||
|
|
@ -90,22 +95,24 @@ inline fun <reified T : StoryConfig> StoriesContainer(
|
|||
paused = isPaused,
|
||||
onStepFinish = onNextClick,
|
||||
)
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(R.drawable.ic_close_24),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.constant,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 14.dp, end = 16.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
onClick = { config.onClose(watchedCounter) },
|
||||
)
|
||||
.testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON),
|
||||
)
|
||||
if (config.isCloseButtonVisible) {
|
||||
Icon(
|
||||
painter = rememberVectorPainter(
|
||||
image = ImageVector.vectorResource(R.drawable.ic_close_24),
|
||||
),
|
||||
tint = TangemTheme.colors.icon.constant,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.align(Alignment.End)
|
||||
.padding(top = 14.dp, end = 16.dp)
|
||||
.clickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = LocalIndication.current,
|
||||
onClick = { config.onClose(watchedCounter) },
|
||||
)
|
||||
.testTag(SwapStoriesScreenTestTags.CLOSE_BUTTON),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,18 @@ import kotlinx.collections.immutable.ImmutableList
|
|||
/**
|
||||
* Config for stories component
|
||||
*
|
||||
* @property stories configuration list
|
||||
* @property stories configuration list
|
||||
* @property isRestartable indicates than stories progressions starts
|
||||
* @property isCloseButtonVisible whether the top-right close button (and back handler) is shown.
|
||||
* Set `false` for non-closable stories (e.g. a root intro screen); [onClose] is then irrelevant.
|
||||
* @property onClose invoked with the number of watched stories when the user closes them.
|
||||
* Only meaningful when [isCloseButtonVisible] is `true`; defaults to a no-op for non-closable stories.
|
||||
*/
|
||||
interface StoriesContentConfig<T : StoryConfig> {
|
||||
val stories: ImmutableList<T>
|
||||
val isRestartable: Boolean
|
||||
val onClose: (Int) -> Unit
|
||||
val isCloseButtonVisible: Boolean get() = true
|
||||
val onClose: (Int) -> Unit get() = {}
|
||||
}
|
||||
|
||||
interface StoryConfig {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
|
|
@ -32,6 +33,8 @@ import androidx.compose.ui.text.style.TextDecoration
|
|||
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.currency.icon.CurrencyIcon
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.components.icons.identicon.IdentIcon
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM
|
||||
import com.tangem.core.ui.components.transactions.state.TransactionItemUM.Content.Direction
|
||||
|
|
@ -70,55 +73,92 @@ fun TransactionItem(state: TransactionItemUM, isBalanceHidden: Boolean, modifier
|
|||
|
||||
@Composable
|
||||
private fun ContentItem(state: TransactionItemUM.Content, isBalanceHidden: Boolean, modifier: Modifier = Modifier) {
|
||||
val rowModifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = state.onClick)
|
||||
.testTag(TransactionHistoryItemTestTags.ITEM)
|
||||
|
||||
TangemRowContainer(
|
||||
modifier = rowModifier,
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens2.x4,
|
||||
vertical = TangemTheme.dimens2.x3,
|
||||
),
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable(onClick = state.onClick)
|
||||
.testTag(TransactionHistoryItemTestTags.ITEM),
|
||||
) {
|
||||
StatusCircle(
|
||||
iconRes = state.iconRes,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.testTag(TransactionHistoryItemTestTags.STATUS_PREFIX + state.status.testTagSuffix),
|
||||
TangemRowContainer(
|
||||
contentPadding = PaddingValues(
|
||||
horizontal = TangemTheme.dimens2.x4,
|
||||
vertical = TangemTheme.dimens2.x3,
|
||||
),
|
||||
) {
|
||||
StatusCircle(
|
||||
iconRes = state.iconRes,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.HEAD)
|
||||
.padding(end = TangemTheme.dimens2.x3)
|
||||
.size(TangemTheme.dimens2.x10)
|
||||
.testTag(TransactionHistoryItemTestTags.STATUS_PREFIX + state.status.testTagSuffix),
|
||||
)
|
||||
TitleText(
|
||||
title = state.title,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.TITLE),
|
||||
)
|
||||
SubtitleText(
|
||||
subtitle = state.subtitle,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5),
|
||||
)
|
||||
state.amount?.let { amount ->
|
||||
AmountText(
|
||||
amount = amount,
|
||||
status = state.status,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.AMOUNT),
|
||||
)
|
||||
}
|
||||
CurrencyText(
|
||||
symbol = state.currencySymbol,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5)
|
||||
.testTag(TransactionHistoryItemTestTags.CURRENCY),
|
||||
)
|
||||
}
|
||||
state.warning?.let { warning ->
|
||||
WarningLine(
|
||||
warning = warning,
|
||||
modifier = Modifier.padding(
|
||||
start = TangemTheme.dimens2.x4,
|
||||
end = TangemTheme.dimens2.x4,
|
||||
bottom = TangemTheme.dimens2.x3,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun WarningLine(warning: TextReference, modifier: Modifier = Modifier) {
|
||||
val attention = TangemTheme.colors2.text.status.attention
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x2),
|
||||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_alert_triangle_20),
|
||||
contentDescription = null,
|
||||
tint = attention,
|
||||
modifier = Modifier.size(TangemTheme.dimens2.x5),
|
||||
)
|
||||
TitleText(
|
||||
title = state.title,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.TITLE),
|
||||
)
|
||||
SubtitleText(
|
||||
subtitle = state.subtitle,
|
||||
status = state.status,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.START_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5),
|
||||
)
|
||||
AmountText(
|
||||
amount = state.amount,
|
||||
status = state.status,
|
||||
isBalanceHidden = isBalanceHidden,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_TOP)
|
||||
.testTag(TransactionHistoryItemTestTags.AMOUNT),
|
||||
)
|
||||
CurrencyText(
|
||||
symbol = state.currencySymbol,
|
||||
modifier = Modifier
|
||||
.layoutId(TangemRowLayoutId.END_BOTTOM)
|
||||
.padding(top = TangemTheme.dimens2.x0_5)
|
||||
.testTag(TransactionHistoryItemTestTags.CURRENCY),
|
||||
Text(
|
||||
text = warning.resolveReference(),
|
||||
color = attention,
|
||||
style = TangemTheme.typography2.captionMedium12,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -262,6 +302,21 @@ private fun SubtitleText(subtitle: ContentSubtitle, status: Status, modifier: Mo
|
|||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
is ContentSubtitle.Asset -> InlineImageSubtitle(
|
||||
template = stringResourceSafe(subtitle.direction.templateResId(), subtitle.symbol),
|
||||
color = tertiary,
|
||||
afterIconColor = if (isFailed) tertiary else primary,
|
||||
modifier = modifier,
|
||||
) {
|
||||
subtitle.icon?.let { iconState ->
|
||||
CurrencyIcon(
|
||||
state = iconState,
|
||||
shouldDisplayNetwork = false,
|
||||
withFixedSize = false,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -491,4 +546,73 @@ private fun Preview_TransactionItem_Swap() {
|
|||
}
|
||||
}
|
||||
|
||||
@Preview(showBackground = true, widthDp = 360)
|
||||
@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview_TransactionItem_Express() {
|
||||
TangemThemePreviewRedesign {
|
||||
PreviewColumn(
|
||||
items = listOf(
|
||||
TransactionItemUM.Content(
|
||||
txHash = "exp-swap-u",
|
||||
amount = "-390.00",
|
||||
currencySymbol = "USDT",
|
||||
time = "",
|
||||
status = Status.Unconfirmed,
|
||||
direction = Direction.OUTGOING,
|
||||
onClick = {},
|
||||
iconRes = R.drawable.ic_exchange_vertical_24,
|
||||
title = stringReference("Swapping"),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = ContentSubtitle.Direction.TO,
|
||||
symbol = "POL",
|
||||
icon = CurrencyIconState.CoinIcon(
|
||||
url = null,
|
||||
fallbackResId = R.drawable.ic_custom_token_44,
|
||||
isGrayscale = false,
|
||||
shouldShowCustomBadge = false,
|
||||
),
|
||||
),
|
||||
timestamp = 0L,
|
||||
warning = stringReference("KYC verification required by provider"),
|
||||
),
|
||||
TransactionItemUM.Content(
|
||||
txHash = "exp-onramp-c",
|
||||
amount = "+0.006339",
|
||||
currencySymbol = "BTC",
|
||||
time = "",
|
||||
status = Status.Confirmed,
|
||||
direction = Direction.INCOMING,
|
||||
onClick = {},
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
title = stringReference("Topped up"),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = ContentSubtitle.Direction.FROM,
|
||||
symbol = "SEK",
|
||||
icon = null,
|
||||
),
|
||||
timestamp = 0L,
|
||||
),
|
||||
TransactionItemUM.Content(
|
||||
txHash = "exp-onramp-f",
|
||||
amount = "0.006339",
|
||||
currencySymbol = "BTC",
|
||||
time = "",
|
||||
status = Status.Failed,
|
||||
direction = Direction.INCOMING,
|
||||
onClick = {},
|
||||
iconRes = R.drawable.ic_tangem_card_24,
|
||||
title = stringReference("Top up failed"),
|
||||
subtitle = ContentSubtitle.Asset(
|
||||
direction = ContentSubtitle.Direction.FROM,
|
||||
symbol = "SEK",
|
||||
icon = null,
|
||||
),
|
||||
timestamp = 0L,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// endregion
|
||||
|
|
@ -3,6 +3,7 @@ package com.tangem.core.ui.components.transactions.state
|
|||
import androidx.annotation.DrawableRes
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.tangem.core.ui.components.currency.icon.CurrencyIconState
|
||||
import com.tangem.core.ui.ds.image.DeviceIconUM
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
|
||||
|
|
@ -22,12 +23,13 @@ sealed interface TransactionItemUM {
|
|||
/**
|
||||
* Content state.
|
||||
*
|
||||
* @property amount signed numeric value, e.g. "+0.500913" / "-350.31"; no currency symbol embedded
|
||||
* @property amount signed numeric value, e.g. "+0.500913" / "-350.31"; no currency symbol embedded.
|
||||
* `null` hides the numeric value while [currencySymbol] still shows.
|
||||
* @property currencySymbol currency symbol shown alongside [amount], e.g. "BTC", "USDT"
|
||||
*/
|
||||
data class Content(
|
||||
override val txHash: String,
|
||||
val amount: String,
|
||||
val amount: String?,
|
||||
val currencySymbol: String,
|
||||
val time: String,
|
||||
val status: Status,
|
||||
|
|
@ -37,6 +39,7 @@ sealed interface TransactionItemUM {
|
|||
val title: TextReference,
|
||||
val subtitle: ContentSubtitle,
|
||||
val timestamp: Long,
|
||||
val warning: TextReference? = null,
|
||||
) : TransactionItemUM {
|
||||
|
||||
@Immutable
|
||||
|
|
@ -95,6 +98,19 @@ sealed interface TransactionItemUM {
|
|||
val deviceIconUM: DeviceIconUM,
|
||||
) : ContentSubtitle
|
||||
|
||||
/**
|
||||
* Counterparty asset ticker — renders as "to/from: <icon> <SYMBOL>". Used for express rows
|
||||
* (swap counterparty currency / onramp fiat), e.g. "to: ◎ POL" or "from: 🇸🇪 SEK".
|
||||
*
|
||||
* @property icon resolved counterparty currency icon, rendered via `CurrencyIcon`. `null` when no icon
|
||||
* is available (e.g. onramp fiat carries no `CryptoCurrency`) — the ticker then renders without a leading icon.
|
||||
*/
|
||||
data class Asset(
|
||||
val direction: Direction,
|
||||
val symbol: String,
|
||||
val icon: CurrencyIconState?,
|
||||
) : ContentSubtitle
|
||||
|
||||
enum class Direction { TO, FROM }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,297 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.core.ui.ds2.glowring
|
||||
|
||||
import android.os.Build
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.BoxScope
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
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.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.res.generated.TangemColors3
|
||||
|
||||
/**
|
||||
* Design-system v2 (DS3) **Glow Ring** — an animated angular-gradient halo that runs around a
|
||||
* rounded-rect outline, like lights chasing along the border.
|
||||
*
|
||||
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=4933-126&m=dev)
|
||||
*
|
||||
* @param modifier Modifier for the whole component; also defines its size when there is no [content].
|
||||
* @param variant Color theme of the gradient — see [TangemGlowRing.Variant].
|
||||
* @param cornerRadius Corner radius of the ring; should match the radius of the wrapped surface.
|
||||
* @param animated When `false`, the ring is rendered static (no rotation).
|
||||
* @param quality Rendering strategy; defaults to [TangemGlowRing.Quality.Auto] (device-appropriate).
|
||||
* Force [TangemGlowRing.Quality.LayeredStrokes] to preview the pre-Android-12 fallback on any device.
|
||||
* @param contentDescription Accessibility label; pass a value when the ring conveys state (e.g. error),
|
||||
* leave `null` when it is purely decorative.
|
||||
* @param content Optional content drawn inside/over the ring.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemGlowRing(
|
||||
modifier: Modifier = Modifier,
|
||||
variant: TangemGlowRing.Variant = TangemGlowRing.Variant.Magic,
|
||||
cornerRadius: Dp = 24.dp,
|
||||
animated: Boolean = true,
|
||||
quality: TangemGlowRing.Quality = TangemGlowRing.Quality.Auto,
|
||||
contentDescription: String? = null,
|
||||
content: @Composable BoxScope.() -> Unit = {},
|
||||
) {
|
||||
val resolved = remember(quality) { resolveQuality(quality) }
|
||||
val stops = rememberGlowRingStops(variant, animated)
|
||||
val metrics = remember {
|
||||
GlowRingMetrics(coreWidth = 2.dp, ringWidth = 4.dp, blurMid = 8.dp, blurBottom = 16.dp)
|
||||
}
|
||||
|
||||
val angle = if (animated) {
|
||||
val transition = rememberInfiniteTransition(label = "glowRing")
|
||||
val rotation by transition.animateFloat(
|
||||
initialValue = GLOW_RING_START_ANGLE,
|
||||
targetValue = 270f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(
|
||||
durationMillis = 24_000,
|
||||
easing = CubicBezierEasing(a = 0.1f, b = 0f, c = 0.9f, d = 1f),
|
||||
),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "angle",
|
||||
)
|
||||
rotation
|
||||
} else {
|
||||
GLOW_RING_START_ANGLE
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = if (contentDescription != null) {
|
||||
modifier.semantics { this.contentDescription = contentDescription }
|
||||
} else {
|
||||
modifier
|
||||
},
|
||||
) {
|
||||
val ringModifier = Modifier.matchParentSize()
|
||||
when (resolved) {
|
||||
ResolvedGlowRingQuality.Blur -> BlurGlowRing(
|
||||
angle = angle,
|
||||
stops = stops,
|
||||
cornerRadius = cornerRadius,
|
||||
metrics = metrics,
|
||||
modifier = ringModifier,
|
||||
)
|
||||
ResolvedGlowRingQuality.LayeredStrokes -> LayeredStrokesGlowRing(
|
||||
angle = angle,
|
||||
stops = stops,
|
||||
cornerRadius = cornerRadius,
|
||||
metrics = metrics,
|
||||
modifier = ringModifier,
|
||||
)
|
||||
}
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
/** Sweep start angle, also reused as the static angle when [TangemGlowRing] is not animated (Figma: -90°). */
|
||||
private const val GLOW_RING_START_ANGLE = -90f
|
||||
|
||||
/**
|
||||
* Resolves the gradient stops for [variant] from the DS3 `colors3.glow` tokens. The
|
||||
* [TangemGlowRing.Variant.Magic] variant continuously ping-pongs between gradient A (`glow.magic`) and
|
||||
* gradient B (`glow.magicBlend`) while [animated] is `true`; every other variant has a single static
|
||||
* gradient.
|
||||
*/
|
||||
@Composable
|
||||
private fun rememberGlowRingStops(variant: TangemGlowRing.Variant, animated: Boolean): List<Pair<Float, Color>> {
|
||||
val glow = TangemTheme.colors3.glow
|
||||
if (variant != TangemGlowRing.Variant.Magic || !animated) {
|
||||
return variant.stops(glow)
|
||||
}
|
||||
val morphTransition = rememberInfiniteTransition(label = "glowRingMorph")
|
||||
val mix by morphTransition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = 1f,
|
||||
animationSpec = infiniteRepeatable(
|
||||
// 6s A→B half-period; Reverse makes a 12s ping-pong (Figma morphDur = 12s).
|
||||
animation = tween(durationMillis = 6_000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Reverse,
|
||||
),
|
||||
label = "morphMix",
|
||||
)
|
||||
return morphedMagicStops(glow.magic.steps(), glow.magicBlend.steps(), mix)
|
||||
}
|
||||
|
||||
/** Public API surface of [TangemGlowRing]. */
|
||||
object TangemGlowRing {
|
||||
|
||||
/** Color theme of the glow ring gradient. */
|
||||
enum class Variant {
|
||||
/**
|
||||
* Multi-color "magic" gradient that continuously auto-morphs (ping-pongs) between separated
|
||||
* orange / blue / purple arcs and a continuous fully-saturated blend.
|
||||
*/
|
||||
Magic,
|
||||
|
||||
/** Green success glow. */
|
||||
Success,
|
||||
|
||||
/** Red error glow. */
|
||||
Error,
|
||||
|
||||
/** Orange/amber warning glow. */
|
||||
Warning,
|
||||
|
||||
/** Blue informational glow. */
|
||||
Info,
|
||||
}
|
||||
|
||||
/**
|
||||
* Rendering strategy for the glow.
|
||||
*
|
||||
* [Auto] picks the best renderer for the current device — a real Gaussian blur on Android 12+
|
||||
* (API 31) and a layered-stroke approximation on older versions. The explicit values force one
|
||||
* renderer regardless of API level; they exist mainly for previews / Storybook so the
|
||||
* pre-Android-12 fallback can be inspected on a modern device. Product code should use [Auto].
|
||||
*/
|
||||
enum class Quality {
|
||||
/** Auto-detect the renderer from the device API level (recommended). */
|
||||
Auto,
|
||||
|
||||
/** Force the Android 12+ real-blur renderer. */
|
||||
Blur,
|
||||
|
||||
/** Force the pre-Android-12 layered-stroke fallback. */
|
||||
LayeredStrokes,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves [quality] to a concrete renderer. [TangemGlowRing.Quality.Auto] picks a real blur on
|
||||
* Android 12+ (API 31) and falls back to stacked translucent strokes on older versions; the explicit
|
||||
* values force their renderer regardless of API level.
|
||||
*/
|
||||
private fun resolveQuality(quality: TangemGlowRing.Quality): ResolvedGlowRingQuality = when (quality) {
|
||||
TangemGlowRing.Quality.Blur -> ResolvedGlowRingQuality.Blur
|
||||
TangemGlowRing.Quality.LayeredStrokes -> ResolvedGlowRingQuality.LayeredStrokes
|
||||
TangemGlowRing.Quality.Auto -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
ResolvedGlowRingQuality.Blur
|
||||
} else {
|
||||
ResolvedGlowRingQuality.LayeredStrokes
|
||||
}
|
||||
}
|
||||
|
||||
/** Concrete rendering strategy chosen by [resolveQuality]. */
|
||||
private enum class ResolvedGlowRingQuality { Blur, LayeredStrokes }
|
||||
|
||||
/**
|
||||
* Builds the angular-gradient stops for [this] variant from its DS3 `colors3.glow` token group. Every
|
||||
* variant token exposes the same 10 [steps] — solid arcs at steps 1/4/7, a faint arc at 9 and transparent
|
||||
* gaps elsewhere — which [glowStops] lays out as evenly-spaced, seamlessly-looping stops.
|
||||
*/
|
||||
private fun TangemGlowRing.Variant.stops(glow: TangemColors3.Glow): List<Pair<Float, Color>> = glowStops(
|
||||
when (this) {
|
||||
TangemGlowRing.Variant.Magic -> glow.magic.steps()
|
||||
TangemGlowRing.Variant.Success -> glow.success.steps()
|
||||
TangemGlowRing.Variant.Error -> glow.error.steps()
|
||||
TangemGlowRing.Variant.Warning -> glow.warning.steps()
|
||||
TangemGlowRing.Variant.Info -> glow.info.steps()
|
||||
},
|
||||
)
|
||||
|
||||
/**
|
||||
* Blends the Magic gradients A ([magic] = `glow.magic`) and B ([magicBlend] = `glow.magicBlend`) at
|
||||
* [mix] (`0` = A, `1` = B). Both token groups share the same stop positions, so the morph is a direct
|
||||
* per-step color lerp. Mirrors the reference rig's auto-morph (ping-pong) between gradient A and B.
|
||||
*/
|
||||
private fun morphedMagicStops(magic: List<Color>, magicBlend: List<Color>, mix: Float): List<Pair<Float, Color>> {
|
||||
val m = mix.coerceIn(0f, 1f)
|
||||
return glowStops(List(magic.size) { lerp(magic[it], magicBlend[it], m) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Lays the glow [steps] out as an angular gradient: evenly spaced from `0`, with step 1 repeated at `1.0`
|
||||
* so the rotation loops seamlessly. Transparent steps create the gaps between the glowing arcs.
|
||||
*/
|
||||
private fun glowStops(steps: List<Color>): List<Pair<Float, Color>> {
|
||||
val count = steps.size
|
||||
return steps.mapIndexed { index, color -> index.toFloat() / count to color } + (1f to steps.first())
|
||||
}
|
||||
|
||||
private fun TangemColors3.Glow.Magic.steps(): List<Color> =
|
||||
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||
|
||||
private fun TangemColors3.Glow.MagicBlend.steps(): List<Color> =
|
||||
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||
|
||||
private fun TangemColors3.Glow.Success.steps(): List<Color> =
|
||||
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||
|
||||
private fun TangemColors3.Glow.Error.steps(): List<Color> =
|
||||
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||
|
||||
private fun TangemColors3.Glow.Warning.steps(): List<Color> =
|
||||
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||
|
||||
private fun TangemColors3.Glow.Info.steps(): List<Color> =
|
||||
listOf(step1, step2, step3, step4, step5, step6, step7, step8, step9, step10)
|
||||
|
||||
@Preview(name = "Light", showBackground = true)
|
||||
@Preview(name = "Dark", uiMode = android.content.res.Configuration.UI_MODE_NIGHT_YES, showBackground = true)
|
||||
@Composable
|
||||
private fun TangemGlowRingPreview() {
|
||||
TangemThemePreviewRedesign {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.background(TangemTheme.colors3.bg.primary)
|
||||
.padding(24.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(24.dp),
|
||||
) {
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
TangemGlowRing(
|
||||
modifier = Modifier.size(120.dp, 72.dp),
|
||||
variant = TangemGlowRing.Variant.Magic,
|
||||
)
|
||||
TangemGlowRing(
|
||||
modifier = Modifier.size(120.dp, 72.dp),
|
||||
variant = TangemGlowRing.Variant.Success,
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
TangemGlowRing(
|
||||
modifier = Modifier.size(120.dp, 72.dp),
|
||||
variant = TangemGlowRing.Variant.Error,
|
||||
)
|
||||
TangemGlowRing(
|
||||
modifier = Modifier.size(120.dp, 72.dp),
|
||||
variant = TangemGlowRing.Variant.Warning,
|
||||
)
|
||||
}
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(24.dp)) {
|
||||
TangemGlowRing(
|
||||
modifier = Modifier.size(120.dp, 72.dp),
|
||||
variant = TangemGlowRing.Variant.Info,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,231 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.core.ui.ds2.glowring
|
||||
|
||||
import androidx.compose.foundation.Canvas
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.BlurredEdgeTreatment
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.geometry.CornerRadius
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.RoundRect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.Path
|
||||
import androidx.compose.ui.graphics.PathFillType
|
||||
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||
import androidx.compose.ui.graphics.drawscope.clipPath
|
||||
import androidx.compose.ui.graphics.drawscope.withTransform
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Token-driven measurements shared by both renderers, mirroring the Figma component anatomy:
|
||||
* a crisp [coreWidth] core line plus two wider, blurred glow bands ([ringWidth] stroked, blurred by
|
||||
* [blurMid] and [blurBottom]).
|
||||
*/
|
||||
internal data class GlowRingMetrics(
|
||||
val coreWidth: Dp, // crisp core stroke (top layer)
|
||||
val ringWidth: Dp, // glow band stroke (mid + bottom layers)
|
||||
val blurMid: Dp, // mid glow blur radius
|
||||
val blurBottom: Dp, // widest glow blur radius
|
||||
)
|
||||
|
||||
/**
|
||||
* Tier 1 — works on every API level, no blur or shader. Approximates the blurred glow by stacking the
|
||||
* same breathing angular-gradient ring several times: progressively wider + fainter bands under a crisp
|
||||
* core. Everything is clipped to the rounded box, so only the inner half of each band shows → inner glow.
|
||||
*/
|
||||
@Composable
|
||||
internal fun LayeredStrokesGlowRing(
|
||||
angle: Float,
|
||||
stops: List<Pair<Float, Color>>,
|
||||
cornerRadius: Dp,
|
||||
metrics: GlowRingMetrics,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier.clip(RoundedCornerShape(cornerRadius))) {
|
||||
Canvas(Modifier.fillMaxSize()) {
|
||||
val r = cornerRadius.toPx()
|
||||
// widest & faintest first, crisp core last
|
||||
drawBreathingRing(
|
||||
stops = stops,
|
||||
angleDeg = angle,
|
||||
cornerRadiusPx = r,
|
||||
strokePx = (metrics.ringWidth + metrics.blurBottom).toPx(),
|
||||
alpha = 0.06f,
|
||||
)
|
||||
drawBreathingRing(
|
||||
stops = stops,
|
||||
angleDeg = angle,
|
||||
cornerRadiusPx = r,
|
||||
strokePx = (metrics.ringWidth + metrics.blurMid).toPx(),
|
||||
alpha = 0.12f,
|
||||
)
|
||||
drawBreathingRing(
|
||||
stops = stops,
|
||||
angleDeg = angle,
|
||||
cornerRadiusPx = r,
|
||||
strokePx = metrics.ringWidth.toPx(),
|
||||
alpha = 0.30f,
|
||||
)
|
||||
drawBreathingRing(
|
||||
stops = stops,
|
||||
angleDeg = angle,
|
||||
cornerRadiusPx = r,
|
||||
strokePx = metrics.coreWidth.toPx(),
|
||||
alpha = 1.0f,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tier 2 — Android 12+ (API 31). Reproduces the Figma anatomy directly: three stacked breathing
|
||||
* angular-gradient rings with real blur (bottom widest, mid, top crisp). Each layer bleeds with
|
||||
* [BlurredEdgeTreatment.Unbounded]; the surrounding [clip] to the rounded box keeps only the inner
|
||||
* bloom, producing the inner glow.
|
||||
*/
|
||||
@Composable
|
||||
internal fun BlurGlowRing(
|
||||
angle: Float,
|
||||
stops: List<Pair<Float, Color>>,
|
||||
cornerRadius: Dp,
|
||||
metrics: GlowRingMetrics,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Box(modifier.clip(RoundedCornerShape(cornerRadius))) {
|
||||
// bottom — widest halo
|
||||
BreathingRing(
|
||||
angle = angle,
|
||||
stops = stops,
|
||||
cornerRadius = cornerRadius,
|
||||
strokeWidth = metrics.ringWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.blur(radius = metrics.blurBottom, edgeTreatment = BlurredEdgeTreatment.Unbounded),
|
||||
)
|
||||
// mid
|
||||
BreathingRing(
|
||||
angle = angle,
|
||||
stops = stops,
|
||||
cornerRadius = cornerRadius,
|
||||
strokeWidth = metrics.ringWidth,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.blur(radius = metrics.blurMid, edgeTreatment = BlurredEdgeTreatment.Unbounded),
|
||||
)
|
||||
// top — crisp core
|
||||
BreathingRing(
|
||||
angle = angle,
|
||||
stops = stops,
|
||||
cornerRadius = cornerRadius,
|
||||
strokeWidth = metrics.coreWidth,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BreathingRing(
|
||||
angle: Float,
|
||||
stops: List<Pair<Float, Color>>,
|
||||
cornerRadius: Dp,
|
||||
strokeWidth: Dp,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
Canvas(modifier) {
|
||||
drawBreathingRing(
|
||||
stops = stops,
|
||||
angleDeg = angle,
|
||||
cornerRadiusPx = cornerRadius.toPx(),
|
||||
strokePx = strokeWidth.toPx(),
|
||||
alpha = 1f,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws one angular-gradient ring band clipped to the rounded-rect stroke outline. The gradient is a
|
||||
* sweep whose colour seam is rotated by [angleDeg] (via [rotatedStops]) and whose vertical squish
|
||||
* breathes between W/2 and W/8 over the rotation (`rxM = mid + amp·cos(2φ)`), reproducing the morphing
|
||||
* arcs of the reference rig.
|
||||
*/
|
||||
private fun DrawScope.drawBreathingRing(
|
||||
stops: List<Pair<Float, Color>>,
|
||||
angleDeg: Float,
|
||||
cornerRadiusPx: Float,
|
||||
strokePx: Float,
|
||||
alpha: Float,
|
||||
) {
|
||||
val w = size.width
|
||||
val h = size.height
|
||||
if (w <= 0f || h <= 0f) return
|
||||
val center = Offset(w / 2f, h / 2f)
|
||||
|
||||
// Breathing horizontal radius of the gradient ellipse → vertical squish of the angle sampling.
|
||||
val maxRx = w / 2f
|
||||
val minRx = w / 8f
|
||||
val mid = (maxRx + minRx) / 2f
|
||||
val amp = max((maxRx - minRx) / 2f, 0f)
|
||||
val phaseRad = Math.toRadians(angleDeg.toDouble()).toFloat()
|
||||
val rxM = mid + amp * cos(2f * phaseRad)
|
||||
val scaleY = h / 2f / max(rxM, 1f)
|
||||
|
||||
val r = min(cornerRadiusPx, min(w, h) / 2f)
|
||||
val o = strokePx / 2f
|
||||
val ring = Path().apply {
|
||||
fillType = PathFillType.EvenOdd
|
||||
addRoundRect(
|
||||
RoundRect(rect = Rect(Offset(-o, -o), Size(w + 2f * o, h + 2f * o)), cornerRadius = CornerRadius(r + o)),
|
||||
)
|
||||
addRoundRect(
|
||||
RoundRect(
|
||||
rect = Rect(Offset(o, o), Size(w - 2f * o, h - 2f * o)),
|
||||
cornerRadius = CornerRadius(max(r - o, 0f)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
val brush = Brush.sweepGradient(colorStops = rotatedStops(stops, angleDeg), center = center)
|
||||
val big = max(w, h) * 4f
|
||||
clipPath(ring) {
|
||||
withTransform({ scale(scaleX = 1f, scaleY = scaleY, pivot = center) }) {
|
||||
drawRect(
|
||||
brush = brush,
|
||||
topLeft = Offset(center.x - big / 2f, center.y - big / 2f),
|
||||
size = Size(big, big),
|
||||
alpha = alpha,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compose's [Brush.sweepGradient] has no start-angle parameter, so the colour seam is rotated by
|
||||
* shifting every stop position by `deg/360` (wrapping around the loop) and re-anchoring boundary stops
|
||||
* at 0 and 1 with the interpolated wrap colour. Mirrors `rotatedStops` from the reference rig.
|
||||
*/
|
||||
private fun rotatedStops(base: List<Pair<Float, Color>>, deg: Float): Array<Pair<Float, Color>> {
|
||||
val d = (deg / 360f % 1f + 1f) % 1f
|
||||
val uniq = base.dropLast(1) // drop the duplicate wrap stop at 1.0
|
||||
val shifted = uniq
|
||||
.map { (p, c) -> ((p + d) % 1f + 1f) % 1f to c }
|
||||
.sortedBy { it.first }
|
||||
val first = shifted.first()
|
||||
val last = shifted.last()
|
||||
val span = first.first + 1f - last.first
|
||||
val wrapFraction = if (span > 1e-6f) (1f - last.first) / span else 0f
|
||||
val wrapColor = lerp(last.second, first.second, wrapFraction)
|
||||
return (listOf(0f to wrapColor) + shifted + listOf(1f to wrapColor)).toTypedArray()
|
||||
}
|
||||
|
|
@ -52,11 +52,13 @@ open class BigDecimalCryptoFormatStyled(
|
|||
fun BigDecimalFormatScope.crypto(
|
||||
symbol: String,
|
||||
decimals: Int,
|
||||
ignoreSymbolPosition: Boolean = false,
|
||||
locale: Locale = Locale.getDefault(),
|
||||
): BigDecimalCryptoFormat {
|
||||
return BigDecimalCryptoFormat(
|
||||
symbol = symbol,
|
||||
decimals = decimals,
|
||||
shouldIgnoreSymbolPosition = ignoreSymbolPosition,
|
||||
locale = locale,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -190,11 +190,13 @@ object TangemTheme {
|
|||
@ReadOnlyComposable
|
||||
get() = LocalTangemTypography3.current
|
||||
|
||||
@Deprecated("Use plain dp")
|
||||
val dimens: TangemDimens
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
get() = LocalTangemDimens.current
|
||||
|
||||
@Deprecated("Use plain dp")
|
||||
val dimens2: TangemDimens2
|
||||
@Composable
|
||||
@ReadOnlyComposable
|
||||
|
|
|
|||
|
|
@ -2,4 +2,5 @@ package com.tangem.core.ui.test
|
|||
|
||||
object AppSettingsScreenTestTags {
|
||||
const val CURRENCY_BUTTON = "APP_SETTINGS_SCREEN_CURRENCY_BUTTON"
|
||||
const val BACK_BUTTON = "APP_SETTINGS_SCREEN_BACK_BUTTON"
|
||||
}
|
||||
25
core/ui/src/main/res/drawable/ic_address_book_24.xml
Normal file
25
core/ui/src/main/res/drawable/ic_address_book_24.xml
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<!--
|
||||
~ Copyright (C) 2026 The Android Open Source Project
|
||||
~
|
||||
~ Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ you may not use this file except in compliance with the License.
|
||||
~ You may obtain a copy of the License at
|
||||
~
|
||||
~ http://www.apache.org/licenses/LICENSE-2.0
|
||||
~
|
||||
~ Unless required by applicable law or agreed to in writing, software
|
||||
~ distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ See the License for the specific language governing permissions and
|
||||
~ limitations under the License.
|
||||
-->
|
||||
<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="M17.213,3.014C18.508,3.147 19.5,4.257 19.5,5.583V18.417C19.5,19.831 18.372,21 16.95,21H7.05C5.629,21 4.5,19.831 4.5,18.417V17.895H3C2.448,17.895 2,17.447 2,16.895C2,16.342 2.448,15.894 3,15.894H4.5V12.984H3C2.448,12.984 2,12.536 2,11.984C2,11.432 2.448,10.984 3,10.984H4.5V8.075H3C2.448,8.075 2,7.627 2,7.075C2,6.523 2.448,6.075 3,6.075H4.5V5.583C4.5,4.169 5.628,2.999 7.05,2.999H16.95L17.213,3.014ZM10.61,12.887C9.492,12.89 8.573,13.739 8.46,14.828L8.448,15.049V15.325C8.448,15.614 8.563,15.891 8.767,16.095C8.971,16.299 9.248,16.413 9.536,16.413H14.465C15.065,16.413 15.552,15.926 15.552,15.325V15.049C15.549,13.931 14.7,13.012 13.611,12.898L13.39,12.887H10.61ZM12,7.588C10.84,7.588 9.899,8.528 9.899,9.688C9.9,10.849 10.84,11.789 12,11.789C13.16,11.789 14.1,10.849 14.101,9.688C14.101,8.528 13.16,7.588 12,7.588Z"
|
||||
android:fillColor="#0090F9"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:pathData="M9.149,16.445C9.427,16.482 9.711,16.5 10,16.5V18C9.646,18 9.297,17.974 8.955,17.93L9.149,16.445ZM10.947,17.188L11.007,17.641L11.044,17.93C10.702,17.974 10.354,18 10,18V16.5C10.289,16.5 10.573,16.482 10.851,16.445L10.947,17.188ZM6.044,15.158C6.492,15.502 6.985,15.789 7.513,16.008L6.938,17.392C6.287,17.122 5.681,16.769 5.131,16.346L6.044,15.158ZM14.868,16.346C14.318,16.769 13.712,17.122 13.061,17.392L12.997,17.234L12.487,16.008C13.015,15.789 13.508,15.502 13.956,15.158L14.868,16.346ZM3.992,12.487C4.211,13.015 4.498,13.508 4.842,13.956L4.247,14.413L3.653,14.868C3.231,14.318 2.877,13.712 2.607,13.061L2.772,12.994L3.992,12.487ZM17.228,12.994L17.392,13.061C17.122,13.712 16.769,14.318 16.346,14.868L15.158,13.956C15.502,13.508 15.789,13.015 16.008,12.487L17.228,12.994ZM2,10C2,9.646 2.025,9.297 2.069,8.955L2.812,9.052L3.555,9.149C3.518,9.427 3.5,9.711 3.5,10C3.5,10.289 3.518,10.573 3.555,10.851L2.069,11.044C2.025,10.702 2,10.354 2,10ZM18,10C18,10.354 17.974,10.702 17.93,11.044L17.188,10.948L16.445,10.851C16.482,10.573 16.5,10.289 16.5,10C16.5,9.711 16.482,9.427 16.445,9.149L17.93,8.955C17.974,9.297 18,9.646 18,10ZM4.842,6.044C4.498,6.492 4.211,6.985 3.992,7.513L2.607,6.938C2.877,6.287 3.231,5.681 3.653,5.131L4.842,6.044ZM16.346,5.131C16.769,5.681 17.122,6.287 17.392,6.938L16.008,7.513C15.789,6.985 15.502,6.492 15.158,6.044L16.346,5.131ZM7.513,3.992C6.985,4.211 6.492,4.498 6.044,4.842L5.131,3.653C5.681,3.231 6.287,2.877 6.938,2.607L7.513,3.992ZM13.061,2.607C13.712,2.877 14.318,3.231 14.868,3.653L13.956,4.842C13.508,4.498 13.015,4.211 12.487,3.992L13.061,2.607ZM10,2C10.354,2 10.702,2.025 11.044,2.069L10.948,2.812L10.947,2.811L10.851,3.555C10.573,3.518 10.289,3.5 10,3.5C9.711,3.5 9.427,3.518 9.149,3.555L8.955,2.069C9.297,2.025 9.646,2 10,2Z"
|
||||
android:fillColor="#0090F9"/>
|
||||
</vector>
|
||||
Loading…
Add table
Add a link
Reference in a new issue