From bcd9b329c5b76014bcdfb5ec510231914e04a27c Mon Sep 17 00:00:00 2001 From: ulfrxdev Date: Thu, 4 Jun 2026 23:27:36 +0200 Subject: [PATCH] Redesign recipe detail view --- .../composeResources/values/strings.xml | 2 +- .../ui/components/button/CircleButton.kt | 75 +++++++++++++ .../screens/recipedetail/RecipeDetailHero.kt | 101 +++++++----------- 3 files changed, 114 insertions(+), 64 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/button/CircleButton.kt diff --git a/composeApp/src/commonMain/composeResources/values/strings.xml b/composeApp/src/commonMain/composeResources/values/strings.xml index 239b407..0e5f0b4 100644 --- a/composeApp/src/commonMain/composeResources/values/strings.xml +++ b/composeApp/src/commonMain/composeResources/values/strings.xml @@ -42,7 +42,6 @@ Zamień składnik - Zaplanuj Porcje Składniki Kroki @@ -86,6 +85,7 @@ Zaplanuj posiłek + Dodaj posiłek do planu Wróć do szczegółów przepisu Dodaj Dodaj posiłek do planu diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/button/CircleButton.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/button/CircleButton.kt new file mode 100644 index 0000000..c0eabd2 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/components/button/CircleButton.kt @@ -0,0 +1,75 @@ +package dev.ulfrx.recipe.ui.components.button + +import androidx.compose.animation.core.FastOutSlowInEasing +import androidx.compose.animation.core.animateFloatAsState +import androidx.compose.animation.core.tween +import androidx.compose.foundation.background +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.interaction.collectIsPressedAsState +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.scale +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.composeunstyled.UnstyledButton +import com.composeunstyled.UnstyledIcon +import dev.ulfrx.recipe.ui.theme.RecipeTheme + +@Composable +fun CircleButton( + icon: ImageVector, + contentDescription: String, + modifier: Modifier = Modifier, + size: Dp = 48.dp, + tint: Color = RecipeTheme.colors.surface, + iconTint: Color = RecipeTheme.colors.content, + iconSize: Dp = 24.dp, + borderTint: Color = RecipeTheme.colors.borderCard, + borderWidth: Dp = 1.dp, + onClick: () -> Unit, +) { + val interactionSource = remember { MutableInteractionSource() } + val isPressed by interactionSource.collectIsPressedAsState() + + val scale by animateFloatAsState( + targetValue = if (isPressed) 1.15f else 1f, + animationSpec = tween(durationMillis = 120, easing = FastOutSlowInEasing), + label = "CircleGlassButton scale", + ) + + UnstyledButton( + onClick = onClick, + contentPadding = PaddingValues(0.dp), + interactionSource = interactionSource, + indication = null, + modifier = modifier + .scale(scale) + .size(size), + backgroundColor = tint, + borderColor = borderTint, + borderWidth = borderWidth, + shape = CircleShape, + ) { + Box( + modifier = Modifier.fillMaxSize(), + contentAlignment = Alignment.Center, + ) { + UnstyledIcon( + imageVector = icon, + contentDescription = contentDescription, + tint = iconTint, + modifier = Modifier.size(iconSize), + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipedetail/RecipeDetailHero.kt b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipedetail/RecipeDetailHero.kt index a88bb26..ee44fe0 100644 --- a/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipedetail/RecipeDetailHero.kt +++ b/composeApp/src/commonMain/kotlin/dev/ulfrx/recipe/ui/screens/recipedetail/RecipeDetailHero.kt @@ -5,7 +5,6 @@ import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio @@ -13,7 +12,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.BasicText import androidx.compose.runtime.Composable @@ -31,14 +29,14 @@ import androidx.compose.ui.unit.sp import com.composables.icons.lucide.Calendar import com.composables.icons.lucide.Clock import com.composables.icons.lucide.Lucide -import com.composeunstyled.UnstyledButton import com.composeunstyled.UnstyledIcon +import dev.ulfrx.recipe.ui.components.button.CircleButton import dev.ulfrx.recipe.ui.theme.RecipeTheme import org.jetbrains.compose.resources.painterResource import org.jetbrains.compose.resources.stringResource import recipe.composeapp.generated.resources.Res +import recipe.composeapp.generated.resources.meal_plan_editor_title_a11y import recipe.composeapp.generated.resources.recipe_card_minutes_format -import recipe.composeapp.generated.resources.recipe_detail_plan_button import recipe.composeapp.generated.resources.sample_recipe @Composable @@ -83,38 +81,31 @@ internal fun RecipeDetailHero( Spacer(Modifier.height(spacing.lg)) - BasicText( - text = title, - style = - typography.display.copy( - color = colors.content, - fontSize = TITLE_FONT_SIZE, - lineHeight = TITLE_LINE_HEIGHT, - fontWeight = FontWeight.Bold, - textAlign = TextAlign.Center, - ), - ) - - Spacer(Modifier.height(spacing.lg)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(spacing.sm), + Row(modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) { + Column( + modifier = Modifier.weight(1f), + horizontalAlignment = Alignment.Start, + verticalArrangement =Arrangement.spacedBy(spacing.lg), ) { - MetaChip( - icon = Lucide.Clock, - text = stringResource(Res.string.recipe_card_minutes_format, cookingMinutes), + BasicText( + text = title, + style = + typography.display.copy( + color = colors.content, + fontSize = TITLE_FONT_SIZE, + lineHeight = TITLE_LINE_HEIGHT, + fontWeight = FontWeight.Bold, + textAlign = TextAlign.Left, + ), ) + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(spacing.sm)) { + MetaChip( + icon = Lucide.Clock, + text = stringResource(Res.string.recipe_card_minutes_format, cookingMinutes), + ) + } } - PlanButton( - text = stringResource(Res.string.recipe_detail_plan_button), - onClick = onPlanClick, - ) + PlanButton(onClick = onPlanClick) } } } @@ -150,33 +141,19 @@ private fun MetaChip( @Composable private fun PlanButton( - text: String, onClick: () -> Unit, ) { - val colors = RecipeTheme.colors - UnstyledButton( + CircleButton( onClick = onClick, - shape = CHIP_SHAPE, - backgroundColor = colors.accent, - contentColor = colors.surface, - contentPadding = PaddingValues(horizontal = PLAN_PADDING_H, vertical = PLAN_PADDING_V), - ) { - UnstyledIcon( - imageVector = Lucide.Calendar, - contentDescription = null, - tint = colors.surface, - modifier = Modifier.size(CHIP_ICON_SIZE), - ) - Spacer(Modifier.width(CHIP_ICON_GAP)) - BasicText( - text = text, - style = - RecipeTheme.typography.label.copy( - color = colors.surface, - fontWeight = FontWeight.Bold, - ), - ) - } + icon = Lucide.Calendar, + contentDescription = stringResource(Res.string.meal_plan_editor_title_a11y), + size = PLAN_BUTTON_SIZE, + iconSize = PLAN_BUTTON_ICON_SIZE, + tint = RecipeTheme.colors.surface, + iconTint = RecipeTheme.colors.accent, + borderTint = RecipeTheme.colors.borderCard, + borderWidth = 1.dp, + ) } private const val BANNER_ASPECT_RATIO = 16f / 9f @@ -187,15 +164,13 @@ private val BANNER_SHADOW_COLOR = Color.Black.copy(alpha = 0.45f) // Leave room for the sheet handle (8dp top padding + 5dp handle) plus breathing room. private val HERO_TOP_PADDING = 32.dp -private val TITLE_FONT_SIZE = 24.sp -private val TITLE_LINE_HEIGHT = 28.sp +private val TITLE_FONT_SIZE = 19.sp +private val TITLE_LINE_HEIGHT = 20.sp private val CHIP_SHAPE = RoundedCornerShape(percent = 50) private val CHIP_PADDING_H = 12.dp private val CHIP_PADDING_V = 7.dp private val CHIP_ICON_SIZE = 14.dp private val CHIP_ICON_GAP = 5.dp - -// Plan button is slightly more padded than meta chips so it reads as a CTA, not just a coloured chip. -private val PLAN_PADDING_H = 14.dp -private val PLAN_PADDING_V = 9.dp +private val PLAN_BUTTON_SIZE = 50.dp +private val PLAN_BUTTON_ICON_SIZE = 25.dp