Redesign recipe detail view
This commit is contained in:
@@ -42,7 +42,6 @@
|
||||
<string name="ingredient_substitute_a11y">Zamień składnik</string>
|
||||
|
||||
<!-- Phase 2.1 — Recipe detail sheet (UI-only view; planner wiring lands in later phases) -->
|
||||
<string name="recipe_detail_plan_button">Zaplanuj</string>
|
||||
<string name="recipe_detail_servings_label">Porcje</string>
|
||||
<string name="recipe_detail_section_ingredients">Składniki</string>
|
||||
<string name="recipe_detail_section_steps">Kroki</string>
|
||||
@@ -86,6 +85,7 @@
|
||||
|
||||
<!-- Phase 6 — Meal plan editor (UI-first; planStore wiring lands in later phases) -->
|
||||
<string name="meal_plan_editor_title">Zaplanuj posiłek</string>
|
||||
<string name="meal_plan_editor_title_a11y">Dodaj posiłek do planu</string>
|
||||
<string name="meal_plan_editor_back_a11y">Wróć do szczegółów przepisu</string>
|
||||
<string name="meal_plan_editor_confirm">Dodaj</string>
|
||||
<string name="meal_plan_editor_confirm_a11y">Dodaj posiłek do planu</string>
|
||||
|
||||
@@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user