diff --git a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt index fa6f78a262..899a888c8a 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/components/dropdownmenu/TangemDropdownMenu.kt @@ -79,7 +79,7 @@ private fun DropdownMenuContent( content: @Composable ColumnScope.() -> Unit, ) { // Menu open/close animation. - val transition = updateTransition(expandedStates, "DropDownMenu") + val transition = rememberTransition(expandedStates, "DropDownMenu") val scale by transition.animateFloat( transitionSpec = { diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt index 9abdda6653..f1bf0d3fdb 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/checkbox/TangemCheckbox.kt @@ -48,9 +48,9 @@ fun TangemCheckbox( isEnabled: Boolean = true, ) { val shape = if (isRounded) { - RoundedCornerShape(TangemTheme.dimens2.x1) - } else { CircleShape + } else { + RoundedCornerShape(TangemTheme.dimens2.x1) } Box( modifier = modifier diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt new file mode 100644 index 0000000000..69a475c2a8 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenu.kt @@ -0,0 +1,288 @@ +package com.tangem.core.ui.ds.contextmenu + +import android.content.res.Configuration +import androidx.compose.animation.core.* +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.HorizontalDivider +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.TransformOrigin +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.testTag +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.* +import androidx.compose.ui.window.Popup +import androidx.compose.ui.window.PopupPositionProvider +import androidx.compose.ui.window.PopupProperties +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.extensions.stringReference +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import com.tangem.core.ui.test.PopUpMenuTestTags +import dev.chrisbanes.haze.rememberHazeState +import kotlin.math.max +import kotlin.math.min + +/** + * Just copy paste [DropdownMenu] from material3 with deleting vertical paddings. + */ +@Composable +fun TangemContextMenu( + expanded: Boolean, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, + offset: DpOffset = DpOffset.Zero, + properties: PopupProperties = PopupProperties(focusable = true), + content: @Composable ColumnScope.() -> Unit, +) { + val expandedStates = remember { MutableTransitionState(false) } + expandedStates.targetState = expanded + + if (expandedStates.currentState || expandedStates.targetState) { + val transformOriginState = remember { mutableStateOf(TransformOrigin.Center) } + val density = LocalDensity.current + val popupPositionProvider = DropdownMenuPositionProvider( + offset, + density, + ) { parentBounds, menuBounds -> + transformOriginState.value = calculateTransformOrigin(parentBounds, menuBounds) + } + + Popup( + onDismissRequest = onDismissRequest, + popupPositionProvider = popupPositionProvider, + properties = properties, + ) { + DropdownMenuContent( + expandedStates = expandedStates, + transformOriginState = transformOriginState, + modifier = modifier, + content = content, + ) + } + } +} + +private const val IN_TRANSITION_DURATION = 120 +private const val OUT_TRANSITION_DURATION = 75 + +@Suppress("ReusedModifierInstance", "MagicNumber") +@Composable +private fun DropdownMenuContent( + expandedStates: MutableTransitionState, + transformOriginState: MutableState, + modifier: Modifier = Modifier, + content: @Composable ColumnScope.() -> Unit, +) { + // Menu open/close animation. + val transition = rememberTransition(expandedStates, "DropDownMenu") + + val scale by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween( + durationMillis = IN_TRANSITION_DURATION, + easing = LinearOutSlowInEasing, + ) + } else { + // Expanded to dismissed. + tween( + durationMillis = 1, + delayMillis = OUT_TRANSITION_DURATION - 1, + ) + } + }, + label = "", + ) { isExpanded -> + if (isExpanded) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0.8f + } + } + + val alpha by transition.animateFloat( + transitionSpec = { + if (false isTransitioningTo true) { + // Dismissed to expanded + tween(durationMillis = 30) + } else { + // Expanded to dismissed. + tween(durationMillis = OUT_TRANSITION_DURATION) + } + }, + label = "", + ) { isExpanded -> + if (isExpanded) { + // Menu is expanded. + 1f + } else { + // Menu is dismissed. + 0f + } + } + Card( + modifier = Modifier + .clip(RoundedCornerShape(TangemTheme.dimens2.x5)) + .graphicsLayer { + scaleX = scale + scaleY = scale + this.alpha = alpha + transformOrigin = transformOriginState.value + }, + elevation = CardDefaults.cardElevation(), + ) { + Column( + modifier = modifier + .width(IntrinsicSize.Max) + .verticalScroll(rememberScrollState()) + .clip(RoundedCornerShape(TangemTheme.dimens2.x5)) + .background(TangemTheme.colors2.contextMenu.background) + .testTag(PopUpMenuTestTags.CONTAINER), + content = content, + ) + } +} + +private fun calculateTransformOrigin(parentBounds: IntRect, menuBounds: IntRect): TransformOrigin { + val pivotX = when { + menuBounds.left >= parentBounds.right -> 0f + menuBounds.right <= parentBounds.left -> 1f + menuBounds.width == 0 -> 0f + else -> { + val intersectionCenter = + (max(parentBounds.left, menuBounds.left) + min(parentBounds.right, menuBounds.right)) / 2 + (intersectionCenter - menuBounds.left).toFloat() / menuBounds.width + } + } + val pivotY = when { + menuBounds.top >= parentBounds.bottom -> 0f + menuBounds.bottom <= parentBounds.top -> 1f + menuBounds.height == 0 -> 0f + else -> { + val intersectionCenter = + (max(parentBounds.top, menuBounds.top) + min(parentBounds.bottom, menuBounds.bottom)) / 2 + (intersectionCenter - menuBounds.top).toFloat() / menuBounds.height + } + } + return TransformOrigin(pivotX, pivotY) +} + +private val MenuVerticalMargin = 48.dp + +@Immutable +internal data class DropdownMenuPositionProvider( + val contentOffset: DpOffset, + val density: Density, + val onPositionCalculated: (IntRect, IntRect) -> Unit = { _, _ -> }, +) : PopupPositionProvider { + override fun calculatePosition( + anchorBounds: IntRect, + windowSize: IntSize, + layoutDirection: LayoutDirection, + popupContentSize: IntSize, + ): IntOffset { + // The min margin above and below the menu, relative to the screen. + val verticalMargin = with(density) { MenuVerticalMargin.roundToPx() } + // The content offset specified using the dropdown offset parameter. + val contentOffsetX = with(density) { contentOffset.x.roundToPx() } + val contentOffsetY = with(density) { contentOffset.y.roundToPx() } + + // Compute horizontal position. + val toRight = anchorBounds.left + contentOffsetX + val toLeft = anchorBounds.right - contentOffsetX - popupContentSize.width + val toDisplayRight = windowSize.width - popupContentSize.width + val toDisplayLeft = 0 + val x = if (layoutDirection == LayoutDirection.Ltr) { + sequenceOf( + toRight, + toLeft, + // If the anchor gets outside of the window on the left, we want to position + // toDisplayLeft for proximity to the anchor. Otherwise, toDisplayRight. + if (anchorBounds.left >= 0) toDisplayRight else toDisplayLeft, + ) + } else { + sequenceOf( + toLeft, + toRight, + // If the anchor gets outside of the window on the right, we want to position + // toDisplayRight for proximity to the anchor. Otherwise, toDisplayLeft. + if (anchorBounds.right <= windowSize.width) toDisplayLeft else toDisplayRight, + ) + }.firstOrNull { + it >= 0 && it + popupContentSize.width <= windowSize.width + } ?: toLeft + + // Compute vertical position. + val toBottom = maxOf(anchorBounds.bottom + contentOffsetY, verticalMargin) + val toTop = anchorBounds.top - contentOffsetY - popupContentSize.height + val toCenter = anchorBounds.top - popupContentSize.height / 2 + val toDisplayBottom = windowSize.height - popupContentSize.height - verticalMargin + val y = sequenceOf(toBottom, toTop, toCenter, toDisplayBottom).firstOrNull { element -> + element >= verticalMargin && + element + popupContentSize.height <= windowSize.height - verticalMargin + } ?: toTop + + onPositionCalculated( + anchorBounds, + IntRect( + left = x, + top = y, + right = x + popupContentSize.width, + bottom = y + popupContentSize.height, + ), + ) + return IntOffset(x, y) + } +} + +// region Preview +@Composable +@Preview(showBackground = true, widthDp = 360) +@Preview(showBackground = true, widthDp = 360, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemContextMenu_Preview() { + TangemThemePreviewRedesign { + val hazeState = rememberHazeState() + Column( + modifier = Modifier + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1) + .hazeSourceTangem(state = hazeState, zIndex = -1f), + ) { + TangemContextMenu( + expanded = true, + onDismissRequest = { }, + modifier = Modifier.hazeEffectTangem(state = hazeState), + ) { + TangemContextMenuCheckboxItem( + title = stringReference("Sort by balance"), + isChecked = true, + onClick = {}, + ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors2.border.neutral.quaternary, + ) + TangemContextMenuCheckboxItem( + title = stringReference("Group tokens"), + isChecked = false, + onClick = {}, + ) + } + } + } +} +// endregion \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt new file mode 100644 index 0000000000..6ff6196041 --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/contextmenu/TangemContextMenuCheckboxItem.kt @@ -0,0 +1,46 @@ +package com.tangem.core.ui.ds.contextmenu + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.ds.checkbox.TangemCheckbox +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.extensions.clickableSingle +import com.tangem.core.ui.extensions.resolveReference +import com.tangem.core.ui.res.TangemTheme + +/** + * Item with checkbox for [TangemContextMenu]. + */ +@Composable +fun TangemContextMenuCheckboxItem(title: TextReference, isChecked: Boolean, onClick: () -> Unit) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .width(238.dp) + .clickableSingle(onClick = onClick) + .padding( + vertical = TangemTheme.dimens2.x5, + horizontal = TangemTheme.dimens2.x4, + ), + ) { + Text( + text = title.resolveReference(), + style = TangemTheme.typography2.headingSemibold17, + color = TangemTheme.colors2.text.neutral.primary, + ) + TangemCheckbox( + modifier = Modifier, + isRounded = true, + isChecked = isChecked, + onCheckedChange = { /* no-op */ }, + ) + } +} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt index b38389acc5..c9dca9b4fc 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBar.kt @@ -5,19 +5,23 @@ import androidx.annotation.DrawableRes import androidx.compose.animation.* import androidx.compose.foundation.background import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape 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.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.lerp 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 androidx.compose.ui.unit.Dp import com.tangem.core.ui.R import com.tangem.core.ui.extensions.* import com.tangem.core.ui.res.TangemTheme @@ -30,11 +34,9 @@ import com.tangem.core.ui.res.TangemThemePreviewRedesign * @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. + * @param startActionUM Optional action data for the start action icon. + * @param endActionUM Optional action data for the end action icon. + * @param titleIconRes Optional drawable resource ID for the icon to be displayed next to the title. * [REDACTED_AUTHOR] */ @@ -43,56 +45,95 @@ 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, + startActionUM: TangemTopBarActionUM? = null, + endActionUM: TangemTopBarActionUM? = null, @DrawableRes titleIconRes: Int? = null, - titleStyle: TextStyle = TangemTheme.typography2.headingSemibold17, - isGhostButtons: Boolean = false, ) { - TangemTopBarInner( + TangemTopBar( + title = title, + subtitle = subtitle, + titleIconRes = titleIconRes, modifier = modifier, - content = { - Column( - modifier = Modifier.fillMaxWidth(), - 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) } + startContent = if (startActionUM != null) { + { TangemTopBarActionContent(startActionUM) } } else { null }, - onStartContentClick = onStartContentClick, - endContent = if (endIconRes != null) { - { TangemTopBarIcon(iconRes = endIconRes) } + endContent = if (endActionUM != null) { + { TangemTopBarActionContent(endActionUM) } } else { null }, - onEndContentClick = onEndContentClick, - isGhostButtons = isGhostButtons, ) } +/** + * 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. + * +[REDACTED_AUTHOR] + */ @Composable -private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?, titleStyle: TextStyle) { +fun TangemTopBar( + modifier: Modifier = Modifier, + title: TextReference? = null, + subtitle: TextReference? = null, + @DrawableRes titleIconRes: Int? = null, + startContent: @Composable (() -> Unit)? = null, + endContent: @Composable (() -> Unit)? = null, +) { + Box( + modifier = modifier + .fillMaxWidth() + .height(TangemTheme.dimens2.x16) + .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3), + ) { + AnimatedVisibility( + visible = startContent != null, + modifier = Modifier.align(Alignment.CenterStart), + label = "Start Content Visibility", + ) { + startContent?.invoke() + } + + Column( + modifier = Modifier + .align(Alignment.Center) + .fillMaxWidth(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(TangemTheme.dimens2.x0_5), + ) { + TangemTopBarTitle(title = title, titleIconRes = titleIconRes) + 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, + ) + } + } + AnimatedVisibility( + visible = endContent != null, + modifier = Modifier.align(Alignment.CenterEnd), + label = "End Content Visibility", + ) { + endContent?.invoke() + } + } +} + +@Composable +private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Int?) { AnimatedVisibility( visible = title != null, label = "Title Visibility", @@ -127,7 +168,7 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: Text( text = wrappedTitle.resolveAnnotatedReference(), color = TangemTheme.colors2.text.neutral.primary, - style = titleStyle, + style = TangemTheme.typography2.headingSemibold17, textAlign = TextAlign.Center, maxLines = 1, ) @@ -136,12 +177,29 @@ private fun TangemTopBarTitle(title: TextReference?, @DrawableRes titleIconRes: } @Composable -private fun TangemTopBarIcon(@DrawableRes iconRes: Int) { +fun TangemTopBarActionContent( + actionUM: TangemTopBarActionUM, + modifier: Modifier = Modifier, + iconSize: Dp = TangemTheme.dimens2.x8, +) { + val background = lerp( + start = Color.Transparent, + stop = TangemTheme.colors2.button.backgroundSecondary, + fraction = actionUM.ghostModeProgress, + ) + val padding = (TangemTheme.dimens2.x10 - iconSize) / 2 Icon( - imageVector = ImageVector.vectorResource(id = iconRes), + imageVector = ImageVector.vectorResource(id = actionUM.iconRes), contentDescription = null, tint = TangemTheme.colors2.graphic.neutral.primary, - modifier = Modifier.fillMaxSize(), + modifier = modifier + .size(TangemTheme.dimens2.x10) + .clip(CircleShape) + .conditional(actionUM.isActionable) { background(background) } + .conditionalCompose(actionUM.isActionable && actionUM.onClick != null) { + clickableSingle(onClick = requireNotNull(actionUM.onClick)) + } + .padding(padding), ) } @@ -154,13 +212,10 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param 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), + startContent = params.startActionUM?.let { { TangemTopBarActionContent(it) } }, + endContent = params.endActionUM?.let { { TangemTopBarActionContent(it) } }, ) } } @@ -168,10 +223,9 @@ private fun TangemTopBar_Preview(@PreviewParameter(PreviewProvider::class) param 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, + val startActionUM: TangemTopBarActionUM? = null, + val endActionUM: TangemTopBarActionUM? = null, ) private class PreviewProvider : PreviewParameterProvider { @@ -179,41 +233,79 @@ private class PreviewProvider : PreviewParameterProvider Unit)? = null, + @param:FloatRange(0.0, 1.0) val ghostModeProgress: Float = 0f, +) \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt b/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt deleted file mode 100644 index e445ea4a30..0000000000 --- a/core/ui/src/main/java/com/tangem/core/ui/ds/topbar/TangemTopBarInner.kt +++ /dev/null @@ -1,95 +0,0 @@ -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 androidx.compose.ui.platform.LocalDensity -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, -) { - val statusBarHeight = with(LocalDensity.current) { WindowInsets.statusBars.getTop(density = this).toDp() } - Box( - modifier = modifier - .height(TangemTheme.dimens2.x16 + statusBarHeight) - .fillMaxWidth() - .padding(top = statusBarHeight) - .padding(TangemTheme.dimens2.x4, TangemTheme.dimens2.x3), - ) { - val iconModifier = Modifier - .size(TangemTheme.dimens2.x10) - .clip(RoundedCornerShape(TangemTheme.dimens2.x25)) - .conditionalCompose(isGhostButtons) { - 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() - } - } - } -} \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt index 0906b9626d..9b81ff94d3 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColorPalette.kt @@ -20,6 +20,7 @@ object TangemColorPalette { // endregion Dark // region Dark Alpha + val Dark_05 = Color(0x0D1E1E1E) val Dark_10 = Color(0x1A1E1E1E) val Dark_20 = Color(0x331E1E1E) val Dark_30 = Color(0x4D1E1E1E) diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt index b9b0c54c51..b1775d1934 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemColors2.kt @@ -22,6 +22,7 @@ class TangemColors2 internal constructor( val skeleton: Skeleton, val markers: Markers, val tabs: Tabs, + val contextMenu: ContextMenu, ) { @Stable @@ -332,15 +333,23 @@ class TangemColors2 internal constructor( class Neutral internal constructor( primary: Color, secondary: Color, + tertiary: Color, + quaternary: Color, ) { var primary by mutableStateOf(primary) private set var secondary by mutableStateOf(secondary) private set + var tertiary by mutableStateOf(tertiary) + private set + var quaternary by mutableStateOf(quaternary) + private set fun update(other: Neutral) { primary = other.primary secondary = other.secondary + tertiary = other.tertiary + quaternary = other.quaternary } } @@ -612,6 +621,18 @@ class TangemColors2 internal constructor( } } + @Stable + class ContextMenu internal constructor( + background: Color, + ) { + var background by mutableStateOf(background) + private set + + fun update(other: ContextMenu) { + background = other.background + } + } + fun update(other: TangemColors2) { text.update(other.text) graphic.update(other.graphic) @@ -625,5 +646,6 @@ class TangemColors2 internal constructor( skeleton.update(other.skeleton) markers.update(other.markers) tabs.update(other.tabs) + contextMenu.update(other.contextMenu) } } \ No newline at end of file diff --git a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt index 93a3647d65..9468bd8eb8 100644 --- a/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt +++ b/core/ui/src/main/java/com/tangem/core/ui/res/TangemThemeRedesign.kt @@ -79,6 +79,8 @@ private fun lightThemeColors2(): TangemColors2 { neutral = TangemColors2.Border.Neutral( primary = TangemColorPalette.Light3, secondary = TangemColorPalette.Light5, + tertiary = TangemColorPalette.Light_10, + quaternary = TangemColorPalette.Dark_10, ), status = TangemColors2.Border.Status( accent = TangemColorPalette.Azure, @@ -186,6 +188,9 @@ private fun lightThemeColors2(): TangemColors2 { backgroundTertiary = TangemColorPalette.White, backgroundQuaternary = TangemColorPalette.Dark_20, ) + val contextMenu = TangemColors2.ContextMenu( + background = TangemColorPalette.Dark_05, + ) return TangemColors2( text = text, graphic = graphic, @@ -199,6 +204,7 @@ private fun lightThemeColors2(): TangemColors2 { skeleton = skeleton, markers = markers, tabs = tabs, + contextMenu = contextMenu, ) } @@ -241,6 +247,8 @@ private fun darkThemeColors2(): TangemColors2 { neutral = TangemColors2.Border.Neutral( primary = TangemColorPalette.Dark4, secondary = TangemColorPalette.Dark4, + tertiary = TangemColorPalette.Light_10, + quaternary = TangemColorPalette.Light_10, ), status = TangemColors2.Border.Status( accent = TangemColorPalette.Azure, @@ -348,6 +356,9 @@ private fun darkThemeColors2(): TangemColors2 { backgroundTertiary = TangemColorPalette.Light_10, backgroundQuaternary = TangemColorPalette.Light_10, ) + val contextMenu = TangemColors2.ContextMenu( + background = TangemColorPalette.Light_10, + ) return TangemColors2( text = text, graphic = graphic, @@ -361,5 +372,6 @@ private fun darkThemeColors2(): TangemColors2 { skeleton = skeleton, markers = markers, tabs = tabs, + contextMenu = contextMenu, ) } \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt index f1cf7347f8..66cb344ef0 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/entity/StoryBookPage.kt @@ -45,6 +45,11 @@ internal data class TangemTokenRowStory( val onBalanceHiddenToggle: () -> Unit, ) : StoryBookPage +internal data class TangemContextMenuStory( + val isExpanded: Boolean, + val onExpandedChange: (Boolean) -> Unit, +) : StoryBookPage + internal data class TangemHeaderRowStory( val isBalanceHidden: Boolean, val onBalanceHiddenToggle: () -> Unit, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/Build.kt new file mode 100644 index 0000000000..87ed7bf20e --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/Build.kt @@ -0,0 +1,17 @@ +package com.tangem.feature.tester.presentation.storybook.page.contextmenu + +import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemContextMenuStory { + return TangemContextMenuStory( + isExpanded = false, + onExpandedChange = { expanded -> + updateStory { it.copy(isExpanded = expanded) } + }, + ) +} + +internal val tangemContextMenuStoryFactory + get() = storyPageFactory(StateUpdater::build) \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/TangemContextMenuStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/TangemContextMenuStory.kt new file mode 100644 index 0000000000..8bac2d248c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/contextmenu/TangemContextMenuStory.kt @@ -0,0 +1,80 @@ +package com.tangem.feature.tester.presentation.storybook.page.contextmenu + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.HorizontalDivider +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.DpOffset +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.PrimaryButton +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.ds.contextmenu.TangemContextMenu +import com.tangem.core.ui.ds.contextmenu.TangemContextMenuCheckboxItem +import com.tangem.core.ui.extensions.TextReference +import com.tangem.core.ui.res.TangemTheme +import dev.chrisbanes.haze.rememberHazeState +import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory as TangemContextMenuStoryState + +@Composable +internal fun TangemContextMenuStory(state: TangemContextMenuStoryState, modifier: Modifier = Modifier) { + val hazeState = rememberHazeState() + LazyColumn( + contentPadding = PaddingValues(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + modifier = modifier + .statusBarsPadding() + .fillMaxSize() + .background(TangemTheme.colors2.surface.level1) + .hazeSourceTangem(state = hazeState, zIndex = -1f), + ) { + item("context_menu") { + Column( + verticalArrangement = Arrangement.spacedBy(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + ) { + Text( + text = "TangemContextMenu", + style = TangemTheme.typography2.headingSemibold17, + color = TangemTheme.colors2.text.neutral.primary, + ) + + Box { + PrimaryButton( + text = "Show Context Menu", + onClick = { state.onExpandedChange(true) }, + ) + + TangemContextMenu( + expanded = state.isExpanded, + onDismissRequest = { state.onExpandedChange(false) }, + offset = DpOffset(0.dp, 4.dp), + modifier = Modifier.hazeEffectTangem(hazeState), + ) { + TangemContextMenuCheckboxItem( + title = TextReference.Str("Sort by balance"), + isChecked = true, + onClick = {}, + ) + HorizontalDivider( + thickness = 0.5.dp, + color = TangemTheme.colors2.border.neutral.quaternary, + ) + TangemContextMenuCheckboxItem( + title = TextReference.Str("Group tokens"), + isChecked = false, + onClick = {}, + ) + } + } + } + } + } +} \ No newline at end of file diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt index eb98c23c7e..6fc4111d73 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookListScreen.kt @@ -18,12 +18,13 @@ import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory import com.tangem.feature.tester.presentation.storybook.page.background.northernLightsStoryFactory import com.tangem.feature.tester.presentation.storybook.page.badge.tangemBadgeStoryFactory import com.tangem.feature.tester.presentation.storybook.page.buttons.buttonsStoryFactory -import com.tangem.feature.tester.presentation.storybook.page.opportunities.opportunitiesBGStoryFactory -import com.tangem.feature.tester.presentation.storybook.page.message.tangemMessageStoryFactory import com.tangem.feature.tester.presentation.storybook.page.checkbox.tangemCheckboxStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.contextmenu.tangemContextMenuStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.headerrow.tangemHeaderRowStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.message.tangemMessageStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.opportunities.opportunitiesBGStoryFactory import com.tangem.feature.tester.presentation.storybook.page.tabs.tangemSegmentedPickerStoryFactory import com.tangem.feature.tester.presentation.storybook.page.tokenrow.tangemTokenRowStoryFactory -import com.tangem.feature.tester.presentation.storybook.page.headerrow.tangemHeaderRowStoryFactory private data class StoryItem(val title: String, val factory: StoryPageFactory) @@ -37,6 +38,7 @@ private fun buildStories() = listOf( StoryItem(title = "☑️ Checkbox", factory = tangemCheckboxStoryFactory), StoryItem(title = "🪙 Token Row", factory = tangemTokenRowStoryFactory), StoryItem(title = "📑 Header Row", factory = tangemHeaderRowStoryFactory), + StoryItem(title = "📋 Context Menu", factory = tangemContextMenuStoryFactory), ) @Composable diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt index 58ec323705..f3e58f0000 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/ui/StoryBookScreen.kt @@ -12,6 +12,7 @@ import com.tangem.feature.tester.presentation.storybook.entity.StoryBookUM import com.tangem.feature.tester.presentation.storybook.entity.StoryList import com.tangem.feature.tester.presentation.storybook.entity.TangemCheckboxStory import com.tangem.feature.tester.presentation.storybook.entity.TangemHeaderRowStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemContextMenuStory import com.tangem.feature.tester.presentation.storybook.entity.TangemMessageStory import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory @@ -24,6 +25,7 @@ import com.tangem.feature.tester.presentation.storybook.page.message.TangemMessa import com.tangem.feature.tester.presentation.storybook.page.tabs.TangemSegmentedPickerStory import com.tangem.feature.tester.presentation.storybook.page.tokenrow.TangemTokenRowStory import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory +import com.tangem.feature.tester.presentation.storybook.page.contextmenu.TangemContextMenuStory @Composable internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) { @@ -45,6 +47,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) TangemSegmentedPickerStory -> TangemSegmentedPickerStory() is TangemTokenRowStory -> TangemTokenRowStory(state = storyState) is TangemHeaderRowStory -> TangemHeaderRowStory(state = storyState) + is TangemContextMenuStory -> TangemContextMenuStory(state = storyState) } } } \ No newline at end of file diff --git a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt index 8438399294..7782f92342 100644 --- a/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt +++ b/features/wallet/impl/src/main/java/com/tangem/feature/wallet/presentation/wallet/ui/components/common/WalletTopBar.kt @@ -1,23 +1,22 @@ package com.tangem.feature.wallet.presentation.wallet.ui.components.common import android.content.res.Configuration +import androidx.compose.foundation.layout.statusBarsPadding import androidx.compose.material3.* 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.platform.testTag import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview -import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.tangem.core.ui.components.haze.hazeEffectTangem import com.tangem.core.ui.ds.topbar.TangemTopBar +import com.tangem.core.ui.ds.topbar.TangemTopBarActionUM import com.tangem.core.ui.ds.topbar.collapsing.TangemCollapsingAppBarBehavior import com.tangem.core.ui.ds.topbar.collapsing.rememberTangemExitUntilCollapsedScrollBehavior import com.tangem.core.ui.extensions.TextReference import com.tangem.core.ui.extensions.stringReference -import com.tangem.core.ui.res.LocalPowerSavingState import com.tangem.core.ui.res.TangemTheme import com.tangem.core.ui.res.TangemThemePreview import com.tangem.core.ui.res.TangemThemePreviewRedesign @@ -46,23 +45,27 @@ internal fun WalletTopBar( color = Color.Unspecified, contentColor = Color.Unspecified, modifier = Modifier.hazeEffectTangem { - progressive = - HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) + progressive = HazeProgressive.verticalGradient(startIntensity = 1f, endIntensity = 0f) }, ) { - val isPowerSaving by LocalPowerSavingState.current.isPowerSavingModeEnabled.collectAsStateWithLifecycle() - val wrappedBalance = remember(behavior.state.collapsedFraction) { if (behavior.state.collapsedFraction > VISIBILITY_THRESHOLD) walletBalance else null } TangemTopBar( title = wrappedBalance, - startIconRes = R.drawable.ic_tangem_24, - endIconRes = R.drawable.ic_more_default_24, - onEndContentClick = topBarConfig.onDetailsClick, - isGhostButtons = !isPowerSaving, + startActionUM = TangemTopBarActionUM( + iconRes = R.drawable.ic_tangem_24, + isActionable = false, + ), + endActionUM = TangemTopBarActionUM( + iconRes = R.drawable.ic_more_default_24, + isActionable = true, + onClick = topBarConfig.onDetailsClick, + ghostModeProgress = behavior.state.collapsedFraction, + ), modifier = Modifier + .statusBarsPadding() .testTag(MainScreenTestTags.TOP_BAR), ) }