Coverage Summary for Class: SurferCategoryHeroCardKt (com.georgeci.moneysurfer.uikit.components.category)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| SurferCategoryHeroCardKt |
100%
(1/1)
|
66.7%
(2/3)
|
35.7%
(5/14)
|
91.7%
(55/60)
|
86.2%
(511/593)
|
package com.georgeci.moneysurfer.uikit.components.category
import androidx.compose.foundation.background
import androidx.compose.foundation.border
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.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.georgeci.moneysurfer.uikit.components.SurferCategoryBubble
import com.georgeci.moneysurfer.uikit.components.SurferCategoryPalette
import com.georgeci.moneysurfer.uikit.components.base.SurferSplitAmount
import com.georgeci.moneysurfer.uikit.components.base.SurferSplitAmountTier
import com.georgeci.moneysurfer.uikit.icons.SurferIcons
import com.georgeci.moneysurfer.uikit.preview.SurferComponentPreview
import com.georgeci.moneysurfer.uikit.theme.AppTheme
/**
* Hero block on the Category Details screen, washed in the category's own [tint].
*
* The tint arrives resolved rather than as a raw hue — [SurferCategoryPalette] owns the
* hue-to-colour mapping, and taking a `Color` keeps this component usable for the system
* Transfer category, whose colour is fixed rather than derived.
*
* The wash is a low-alpha gradient over the theme surface instead of a solid tonal container:
* a category hue is user-chosen and arbitrary, so painting a whole card in it at full strength
* would fight the theme and leave text at unpredictable contrast. Every glyph here stays on
* theme colours; only the background carries the hue.
*/
@Composable
fun SurferCategoryHeroCard(
icon: ImageVector,
tint: Color,
name: String,
label: String,
formattedAmount: String,
modifier: Modifier = Modifier,
subtitle: String? = null,
) {
Box(
modifier = modifier
.fillMaxWidth()
.clip(AppTheme.shapes.large)
.background(
Brush.linearGradient(
listOf(
tint.copy(alpha = HeroTintAlpha),
AppTheme.materialColors.surface,
),
),
)
.border(
width = 1.dp,
color = AppTheme.materialColors.outlineVariant,
shape = AppTheme.shapes.large,
),
) {
// Decorative tonal blob, clipped by the card. Anchored past the corner so only the
// falloff shows, echoing the dashboard balance hero at a smaller scale.
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.offset(x = BlobOffset, y = -BlobOffset)
.size(BlobSize)
.clip(CircleShape)
.background(tint.copy(alpha = BlobAlpha)),
)
Column(modifier = Modifier.padding(20.dp)) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(14.dp),
) {
SurferCategoryBubble(icon = icon, tint = tint, size = 56.dp)
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
style = AppTheme.typography.labelMedium.copy(letterSpacing = 0.5.sp),
color = AppTheme.materialColors.onSurfaceVariant,
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
Text(
text = name,
style = AppTheme.typography.titleLarge,
color = AppTheme.materialColors.onSurface,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
)
}
}
Spacer(Modifier.height(14.dp))
SurferSplitAmount(
formattedAmount = formattedAmount,
tier = SurferSplitAmountTier.Hero,
color = AppTheme.materialColors.onSurface,
)
if (!subtitle.isNullOrBlank()) {
Spacer(Modifier.height(6.dp))
Text(
text = subtitle,
style = AppTheme.typography.bodyMedium,
color = AppTheme.materialColors.onSurfaceVariant,
)
}
}
}
}
private const val HeroTintAlpha = 0.22f
private const val BlobAlpha = 0.18f
private val BlobSize = 160.dp
private val BlobOffset = 40.dp
@Preview
@Composable
private fun SurferCategoryHeroCardPreview() {
SurferComponentPreview {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
SurferCategoryHeroCard(
icon = SurferIcons.Receipt,
tint = SurferCategoryPalette.tints[5],
name = "Dining",
label = "SPENT IN APRIL",
formattedAmount = "€168.55",
subtitle = "14 transactions · 2 subcategories",
)
SurferCategoryHeroCard(
icon = SurferIcons.Cash,
tint = SurferCategoryPalette.tints[0],
name = "Salary",
label = "INCOME · APRIL",
formattedAmount = "€3,200.00",
)
}
}
}