diff --git a/core/ui/src/main/java/com/tangem/core/ui/ds2/fade/TangemFade.kt b/core/ui/src/main/java/com/tangem/core/ui/ds2/fade/TangemFade.kt new file mode 100644 index 0000000000..dba8e035dd --- /dev/null +++ b/core/ui/src/main/java/com/tangem/core/ui/ds2/fade/TangemFade.kt @@ -0,0 +1,166 @@ +@file:Suppress("MagicNumber") + +package com.tangem.core.ui.ds2.fade + +import android.content.res.Configuration +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.haze.hazeEffectTangem +import com.tangem.core.ui.res.LocalHazeState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.core.ui.res.TangemThemePreviewRedesign +import dev.chrisbanes.haze.HazeProgressive +import dev.chrisbanes.haze.HazeTint + +private const val SOLID_RATIO = 40f / 96f +private const val HARD_ALPHA = 0.95f +private const val SOFT_ALPHA = 0.6f +private val FADE_HEIGHT = 96.dp +private val BLUR_RADIUS = 20.dp + +/** + * Design-system fade overlay used at the top or bottom edge of scrollable content. + * + * Renders a 96dp-tall gradient (and optional solid block in the [TangemFade.Variant.Hard] + * variant) tinted with `colors3.bg.primary`, so it blends with the page background and masks + * content as it scrolls under the edge. Place it inside a [Box] and align with + * [androidx.compose.ui.Alignment.TopCenter] / [androidx.compose.ui.Alignment.BottomCenter] + * depending on [position]. + * + * @param position which edge the fade is anchored to — controls the gradient direction. + * @param variant [TangemFade.Variant.Hard] adds an opaque solid block next to the edge for a + * harder cut-off, [TangemFade.Variant.Soft] is gradient-only and gentler. + * @param blur when `true`, the content under the fade is blurred via Haze (radius 20dp, + * progressive intensity matching [position]). + * @param backgroundColor base color of the fade gradient — defaults to `colors3.bg.primary` so + * the fade blends with the standard page background. + * @param modifier modifier applied to the fade's root. + */ +@Composable +fun TangemFade( + position: TangemFade.Position, + modifier: Modifier = Modifier, + variant: TangemFade.Variant = TangemFade.Variant.Soft, + blur: Boolean = false, + backgroundColor: Color = TangemTheme.colors3.bg.primary, +) { + val hazeState = LocalHazeState.current + val brush = buildBrush(color = backgroundColor, position = position, variant = variant) + + val blurModifier = if (blur) { + Modifier.hazeEffectTangem(state = hazeState) { + blurRadius = BLUR_RADIUS + fallbackTint = HazeTint(Color.Transparent) + progressive = HazeProgressive.verticalGradient( + startIntensity = if (position == TangemFade.Position.Top) 1f else 0f, + endIntensity = if (position == TangemFade.Position.Top) 0f else 1f, + preferPerformance = true, + ) + } + } else { + Modifier + } + + Box( + modifier = modifier + .fillMaxWidth() + .height(FADE_HEIGHT) + .then(blurModifier) + .background(brush), + ) +} + +private fun buildBrush(color: Color, position: TangemFade.Position, variant: TangemFade.Variant): Brush { + val opaque = color.copy(alpha = HARD_ALPHA) + val soft = color.copy(alpha = SOFT_ALPHA) + val transparent = Color.Transparent + + return when (variant) { + TangemFade.Variant.Hard -> when (position) { + TangemFade.Position.Top -> Brush.verticalGradient( + colorStops = arrayOf( + 0f to opaque, + SOLID_RATIO to opaque, + 1f to transparent, + ), + ) + TangemFade.Position.Bottom -> Brush.verticalGradient( + colorStops = arrayOf( + 0f to transparent, + 1f - SOLID_RATIO to opaque, + 1f to opaque, + ), + ) + } + TangemFade.Variant.Soft -> when (position) { + TangemFade.Position.Top -> Brush.verticalGradient(colors = listOf(soft, transparent)) + TangemFade.Position.Bottom -> Brush.verticalGradient(colors = listOf(transparent, soft)) + } + } +} + +object TangemFade { + enum class Position { Top, Bottom } + enum class Variant { Hard, Soft } +} + +@Composable +@Preview(showBackground = true) +@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES) +private fun TangemFade_Preview() { + TangemThemePreviewRedesign { + Column( + verticalArrangement = Arrangement.spacedBy(12.dp), + modifier = Modifier + .background(TangemTheme.colors3.bg.secondary) + .padding(12.dp), + ) { + TangemFade.Variant.entries.forEach { variant -> + TangemFade.Position.entries.forEach { position -> + FadePreviewRow(variant = variant, position = position) + } + } + } + } +} + +@Composable +private fun FadePreviewRow(variant: TangemFade.Variant, position: TangemFade.Position) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text( + text = "${variant.name} / ${position.name}", + style = TangemTheme.typography.caption2, + color = TangemTheme.colors3.text.primary, + ) + Box( + modifier = Modifier + .fillMaxWidth() + .height(120.dp) + .background(TangemTheme.colors3.bg.accent.blue), + ) { + TangemFade( + position = position, + variant = variant, + modifier = Modifier.align( + when (position) { + TangemFade.Position.Top -> Alignment.TopCenter + TangemFade.Position.Bottom -> Alignment.BottomCenter + }, + ), + ) + } + } +} \ 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 637a15eedf..d3149250d0 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 @@ -7,6 +7,7 @@ import com.tangem.core.ui.ds.message.TangemMessageEffect import com.tangem.core.ui.ds.topbar.TangemTopBarType import com.tangem.core.ui.ds2.badge.TangemBadge import com.tangem.core.ui.ds2.button.TangemButton +import com.tangem.core.ui.ds2.fade.TangemFade import com.tangem.core.ui.ds2.loader.TangemLoaderSize import com.tangem.core.ui.ds2.shimmers.TextShimmerStyle @@ -176,6 +177,15 @@ internal data class TangemButtonStory( } } +internal data class TangemFadeStory( + val variant: TangemFade.Variant, + val position: TangemFade.Position, + val isBlur: Boolean, + val onVariantChange: (TangemFade.Variant) -> Unit, + val onPositionChange: (TangemFade.Position) -> Unit, + val onBlurToggle: () -> Unit, +) : DsStoryBookPage + internal data class TangemBadgeV2Story( val variant: TangemBadge.Variant, val status: TangemBadge.Status, diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt index 1bc217e693..57b7f91cc1 100644 --- a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/DsComponentsListScreen.kt @@ -17,6 +17,7 @@ import com.tangem.feature.tester.presentation.storybook.entity.DsComponentsListS import com.tangem.feature.tester.presentation.storybook.entity.StoryPageFactory import com.tangem.feature.tester.presentation.storybook.page.ds.badge.tangemBadgeV2StoryFactory import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemButtonStoryFactory +import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory @@ -27,6 +28,7 @@ private fun buildDsStories() = listOf( DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory), DsStoryItem(title = "🏷️ TangemBadge", factory = tangemBadgeV2StoryFactory), DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory), + DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory), ) @Composable diff --git a/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/fade/Build.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/fade/Build.kt new file mode 100644 index 0000000000..9fa47f0e0c --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/fade/Build.kt @@ -0,0 +1,26 @@ +package com.tangem.feature.tester.presentation.storybook.page.ds.fade + +import com.tangem.core.ui.ds2.fade.TangemFade +import com.tangem.feature.tester.presentation.storybook.entity.TangemFadeStory +import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater +import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory + +internal fun StateUpdater.build(): TangemFadeStory { + return TangemFadeStory( + variant = TangemFade.Variant.Hard, + position = TangemFade.Position.Top, + isBlur = false, + onVariantChange = { variant -> + updateStory { it.copy(variant = variant) } + }, + onPositionChange = { position -> + updateStory { it.copy(position = position) } + }, + onBlurToggle = { + updateStory { it.copy(isBlur = !it.isBlur) } + }, + ) +} + +internal val tangemFadeStoryFactory + 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/ds/fade/TangemFadeStory.kt b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/fade/TangemFadeStory.kt new file mode 100644 index 0000000000..48cb741a28 --- /dev/null +++ b/features/tester/impl/src/main/java/com/tangem/feature/tester/presentation/storybook/page/ds/fade/TangemFadeStory.kt @@ -0,0 +1,245 @@ +@file:Suppress("MagicNumber") + +package com.tangem.feature.tester.presentation.storybook.page.ds.fade + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +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.geometry.Offset +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.TileMode +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.unit.dp +import com.tangem.core.ui.components.haze.hazeSourceTangem +import com.tangem.core.ui.ds2.fade.TangemFade +import com.tangem.core.ui.res.LocalHazeState +import com.tangem.core.ui.res.TangemTheme +import com.tangem.feature.tester.presentation.storybook.entity.TangemFadeStory + +@Composable +internal fun TangemFadeStory(state: TangemFadeStory, modifier: Modifier = Modifier) { + val hazeState = LocalHazeState.current + DisposableEffect(state.isBlur) { + val wasBlurEnabled = hazeState.blurEnabled + hazeState.blurEnabled = state.isBlur + onDispose { hazeState.blurEnabled = wasBlurEnabled } + } + Column( + modifier = modifier + .fillMaxSize() + .background(TangemTheme.colors.background.primary) + .padding(vertical = 16.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + ComponentPreview(state = state) + VariantSelector(selected = state.variant, onSelect = state.onVariantChange) + PositionSelector(selected = state.position, onSelect = state.onPositionChange) + Toggles(state = state) + } +} + +@Composable +private fun ComponentPreview(state: TangemFadeStory) { + Box( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .height(240.dp) + .clip(RoundedCornerShape(16.dp)), + ) { + Backdrop( + modifier = Modifier + .fillMaxSize() + .hazeSourceTangem(zIndex = 0f), + ) + TangemFade( + position = state.position, + variant = state.variant, + blur = state.isBlur, + modifier = Modifier.align( + when (state.position) { + TangemFade.Position.Top -> Alignment.TopCenter + TangemFade.Position.Bottom -> Alignment.BottomCenter + }, + ), + ) + } +} + +@Composable +private fun Backdrop(modifier: Modifier = Modifier) { + val bands = remember { + listOf( + Color(0xFFFF1744), + Color(0xFFFF9100), + Color(0xFFFFEA00), + Color(0xFF00E676), + Color(0xFF00B8D4), + Color(0xFF2962FF), + Color(0xFFD500F9), + ) + } + val stops = remember(bands) { + buildList { + bands.forEachIndexed { index, color -> + val start = index.toFloat() / bands.size + val end = (index + 1).toFloat() / bands.size + add(start to color) + add(end to color) + } + }.toTypedArray() + } + val tilePx = with(LocalDensity.current) { 160.dp.toPx() } + Box( + modifier = modifier.background( + brush = Brush.linearGradient( + colorStops = stops, + start = Offset(0f, 0f), + end = Offset(tilePx, tilePx), + tileMode = TileMode.Repeated, + ), + ), + ) +} + +@Composable +private fun VariantSelector(selected: TangemFade.Variant, onSelect: (TangemFade.Variant) -> Unit) { + Section(label = "Variant") { + ChipGrid( + items = TangemFade.Variant.entries, + label = { it.name }, + isSelected = { it == selected }, + onSelect = onSelect, + ) + } +} + +@Composable +private fun PositionSelector(selected: TangemFade.Position, onSelect: (TangemFade.Position) -> Unit) { + Section(label = "Position") { + ChipGrid( + items = TangemFade.Position.entries, + label = { it.name }, + isSelected = { it == selected }, + onSelect = onSelect, + ) + } +} + +@Composable +private fun Toggles(state: TangemFadeStory) { + Section(label = "Flags") { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + ToggleRow(label = "blur", checked = state.isBlur, onToggle = state.onBlurToggle) + } + } +} + +@Composable +private fun Section(label: String, content: @Composable () -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + Text( + modifier = Modifier.padding(horizontal = 16.dp), + text = label, + style = TangemTheme.typography.subtitle1, + color = TangemTheme.colors.text.primary1, + ) + content() + } +} + +@Composable +private fun ChipGrid(items: List, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) { + val shape = RoundedCornerShape(50) + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 16.dp) + .clip(shape) + .background(TangemTheme.colors2.surface.level2) + .border( + width = 1.dp, + color = TangemTheme.colors2.border.neutral.secondary, + shape = shape, + ) + .padding(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + items.forEach { item -> + Chip( + label = label(item), + selected = isSelected(item), + onClick = { onSelect(item) }, + modifier = Modifier.weight(1f), + ) + } + } +} + +@Composable +private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) { + val chipShape = RoundedCornerShape(50) + Box( + contentAlignment = Alignment.Center, + modifier = modifier + .clip(chipShape) + .background( + if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2, + ) + .clickable(onClick = onClick) + .padding(vertical = 8.dp, horizontal = 4.dp), + ) { + Text( + text = label, + style = TangemTheme.typography.caption2, + color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary, + ) + } +} + +@Composable +private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(12.dp)) + .background(TangemTheme.colors2.surface.level2) + .clickable(onClick = onToggle) + .padding(horizontal = 12.dp, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text( + text = label, + style = TangemTheme.typography.body1, + color = TangemTheme.colors.text.primary1, + ) + Text( + text = if (checked) "ON" else "OFF", + style = TangemTheme.typography.caption2, + color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary, + ) + } +} \ No newline at end of file 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 e5f0906ef8..a261cb5e27 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 @@ -4,6 +4,31 @@ import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedContent import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier +import com.tangem.feature.tester.presentation.storybook.entity.ButtonsStory +import com.tangem.feature.tester.presentation.storybook.entity.DeviceIconStory +import com.tangem.feature.tester.presentation.storybook.entity.DsComponentsListStory +import com.tangem.feature.tester.presentation.storybook.entity.NorthernLightsStory +import com.tangem.feature.tester.presentation.storybook.entity.OpportunitiesBGStory +import com.tangem.feature.tester.presentation.storybook.entity.PlaceholderStory +import com.tangem.feature.tester.presentation.storybook.entity.ProgressIndicatorStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemBadgeV2Story +import com.tangem.feature.tester.presentation.storybook.entity.TangemButtonStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemFadeStory +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.TangemLoaderStory +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.TangemPagerIndicatorStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchFieldStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemSegmentedPickerStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTabStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTokenRowStory +import com.tangem.feature.tester.presentation.storybook.entity.TangemTopBarStory +import com.tangem.feature.tester.presentation.storybook.entity.TypographyStory import com.tangem.feature.tester.presentation.storybook.entity.* import com.tangem.feature.tester.presentation.storybook.page.background.NorthernLightsStory import com.tangem.feature.tester.presentation.storybook.page.badge.TangemBadgeStory @@ -14,6 +39,7 @@ import com.tangem.feature.tester.presentation.storybook.page.deviceicon.DeviceIc import com.tangem.feature.tester.presentation.storybook.page.ds.DsComponentsListStory import com.tangem.feature.tester.presentation.storybook.page.ds.badge.TangemBadgeV2Story import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemButtonStory +import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory @@ -64,6 +90,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier) is TangemButtonStory -> TangemButtonStory(state = storyState) is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState) is TangemShimmerStory -> TangemShimmerStory(state = storyState) + is TangemFadeStory -> TangemFadeStory(state = storyState) } } } \ No newline at end of file