Redesign recipe detail screen
This commit is contained in:
@@ -191,7 +191,9 @@ private fun Modifier.expandingHeight(
|
|||||||
* that match an in-flight settle become no-ops — no flag, no race.
|
* that match an in-flight settle become no-ops — no flag, no race.
|
||||||
*/
|
*/
|
||||||
@Stable
|
@Stable
|
||||||
private class PillExpansion(initial: Float) {
|
private class PillExpansion(
|
||||||
|
initial: Float,
|
||||||
|
) {
|
||||||
var progress by mutableFloatStateOf(initial)
|
var progress by mutableFloatStateOf(initial)
|
||||||
private set
|
private set
|
||||||
var fullHeightPx by mutableIntStateOf(0)
|
var fullHeightPx by mutableIntStateOf(0)
|
||||||
@@ -200,19 +202,27 @@ private class PillExpansion(initial: Float) {
|
|||||||
private var target: Float = initial
|
private var target: Float = initial
|
||||||
private var settleJob: Job? = null
|
private var settleJob: Job? = null
|
||||||
|
|
||||||
fun dragBy(delta: Float, range: Float) {
|
fun dragBy(
|
||||||
|
delta: Float,
|
||||||
|
range: Float,
|
||||||
|
) {
|
||||||
settleJob?.cancel()
|
settleJob?.cancel()
|
||||||
progress = (progress - delta / range).coerceIn(0f, 1f)
|
progress = (progress - delta / range).coerceIn(0f, 1f)
|
||||||
target = progress
|
target = progress
|
||||||
}
|
}
|
||||||
|
|
||||||
fun animateTo(scope: CoroutineScope, target: Float, initialVelocity: Float = 0f) {
|
fun animateTo(
|
||||||
|
scope: CoroutineScope,
|
||||||
|
target: Float,
|
||||||
|
initialVelocity: Float = 0f,
|
||||||
|
) {
|
||||||
if (this.target == target && settleJob?.isActive == true) return
|
if (this.target == target && settleJob?.isActive == true) return
|
||||||
this.target = target
|
this.target = target
|
||||||
settleJob?.cancel()
|
settleJob?.cancel()
|
||||||
settleJob =
|
settleJob =
|
||||||
scope.launch {
|
scope.launch {
|
||||||
Animatable(progress).also { it.updateBounds(0f, 1f) }
|
Animatable(progress)
|
||||||
|
.also { it.updateBounds(0f, 1f) }
|
||||||
.animateTo(
|
.animateTo(
|
||||||
targetValue = target,
|
targetValue = target,
|
||||||
animationSpec =
|
animationSpec =
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ class HorizonCalendarHolder(
|
|||||||
companion object {
|
companion object {
|
||||||
private const val DEFAULT_HORIZON_DAYS = 7
|
private const val DEFAULT_HORIZON_DAYS = 7
|
||||||
|
|
||||||
fun defaultHorizon(today: LocalDate = todayInSystemTz()): LocalDate =
|
fun defaultHorizon(today: LocalDate = todayInSystemTz()): LocalDate = today.plus(DatePeriod(days = DEFAULT_HORIZON_DAYS - 1))
|
||||||
today.plus(DatePeriod(days = DEFAULT_HORIZON_DAYS - 1))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,10 @@ class OverlayDismisser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val LocalOverlayDismisser = staticCompositionLocalOf<OverlayDismisser> {
|
val LocalOverlayDismisser =
|
||||||
error("OverlayDismisser not provided — wrap your composable in AppShell or supply one explicitly.")
|
staticCompositionLocalOf<OverlayDismisser> {
|
||||||
}
|
error("OverlayDismisser not provided — wrap your composable in AppShell or supply one explicitly.")
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RegisterDismissibleOverlay(
|
fun RegisterDismissibleOverlay(
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package dev.ulfrx.recipe.ui.components.recipe
|
package dev.ulfrx.recipe.ui.components.recipe
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.border
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -12,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer
|
|||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -69,14 +69,13 @@ fun IngredientRow(
|
|||||||
modifier =
|
modifier =
|
||||||
modifier
|
modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.clip(RoundedCornerShape(CornerRadius))
|
|
||||||
.background(colors.surface)
|
|
||||||
.animateContentSize(),
|
.animateContentSize(),
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
.heightIn(min = MinRowHeight)
|
||||||
.padding(horizontal = PaddingHorizontal, vertical = PaddingVertical),
|
.padding(horizontal = PaddingHorizontal, vertical = PaddingVertical),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.sm),
|
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.sm),
|
||||||
@@ -86,7 +85,7 @@ fun IngredientRow(
|
|||||||
style =
|
style =
|
||||||
typography.body.copy(
|
typography.body.copy(
|
||||||
color = colors.content,
|
color = colors.content,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = NameTextSize,
|
fontSize = NameTextSize,
|
||||||
lineHeight = LineHeight,
|
lineHeight = LineHeight,
|
||||||
),
|
),
|
||||||
@@ -137,7 +136,7 @@ private fun AmountLabel(
|
|||||||
style =
|
style =
|
||||||
typography.body.copy(
|
typography.body.copy(
|
||||||
color = colors.content,
|
color = colors.content,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.SemiBold,
|
||||||
fontSize = NameTextSize,
|
fontSize = NameTextSize,
|
||||||
lineHeight = LineHeight,
|
lineHeight = LineHeight,
|
||||||
),
|
),
|
||||||
@@ -263,14 +262,14 @@ internal fun RecipeIngredientSlotUi.scaledBy(servings: Int) =
|
|||||||
id = id,
|
id = id,
|
||||||
)
|
)
|
||||||
|
|
||||||
private val CornerRadius = 12.dp
|
private val MinRowHeight = 48.dp
|
||||||
private val PaddingHorizontal = 12.dp
|
private val PaddingHorizontal = 12.dp
|
||||||
private val PaddingVertical = 16.dp
|
private val PaddingVertical = 12.dp
|
||||||
private val NameTextSize = 13.sp
|
private val NameTextSize = 12.sp
|
||||||
private val UnitTextSize = 12.sp
|
private val UnitTextSize = 11.sp
|
||||||
private val LineHeight = 18.sp
|
private val LineHeight = 16.sp
|
||||||
private val ToggleSize = 28.dp
|
private val ToggleSize = 24.dp
|
||||||
private val ToggleIconSize = 14.dp
|
private val ToggleIconSize = 12.dp
|
||||||
private val OptionCornerRadius = 10.dp
|
private val OptionCornerRadius = 10.dp
|
||||||
private val OptionPadding = 12.dp
|
private val OptionPadding = 12.dp
|
||||||
private val OptionMetaGap = 2.dp
|
private val OptionMetaGap = 2.dp
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
@@ -45,6 +46,7 @@ fun NutritionSummary(
|
|||||||
nutrition: RecipeNutritionUi,
|
nutrition: RecipeNutritionUi,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
|
val colors = RecipeTheme.colors
|
||||||
Row(
|
Row(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.sm),
|
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.sm),
|
||||||
@@ -53,21 +55,25 @@ fun NutritionSummary(
|
|||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
value = nutrition.kcal.toString(),
|
value = nutrition.kcal.toString(),
|
||||||
label = stringResource(Res.string.nutrition_macro_kcal),
|
label = stringResource(Res.string.nutrition_macro_kcal),
|
||||||
|
valueColor = colors.content,
|
||||||
)
|
)
|
||||||
MacroCard(
|
MacroCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
value = stringResource(Res.string.nutrition_grams_format, nutrition.protein),
|
value = stringResource(Res.string.nutrition_grams_format, nutrition.protein),
|
||||||
label = stringResource(Res.string.nutrition_macro_protein),
|
label = stringResource(Res.string.nutrition_macro_protein),
|
||||||
|
valueColor = colors.macroProtein,
|
||||||
)
|
)
|
||||||
MacroCard(
|
MacroCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
value = stringResource(Res.string.nutrition_grams_format, nutrition.fat),
|
value = stringResource(Res.string.nutrition_grams_format, nutrition.fat),
|
||||||
label = stringResource(Res.string.nutrition_macro_fat),
|
label = stringResource(Res.string.nutrition_macro_fat),
|
||||||
|
valueColor = colors.macroFat,
|
||||||
)
|
)
|
||||||
MacroCard(
|
MacroCard(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
value = stringResource(Res.string.nutrition_grams_format, nutrition.carbs),
|
value = stringResource(Res.string.nutrition_grams_format, nutrition.carbs),
|
||||||
label = stringResource(Res.string.nutrition_macro_carbs),
|
label = stringResource(Res.string.nutrition_macro_carbs),
|
||||||
|
valueColor = colors.macroCarbs,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,6 +82,7 @@ fun NutritionSummary(
|
|||||||
private fun MacroCard(
|
private fun MacroCard(
|
||||||
value: String,
|
value: String,
|
||||||
label: String,
|
label: String,
|
||||||
|
valueColor: Color,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
) {
|
) {
|
||||||
val colors = RecipeTheme.colors
|
val colors = RecipeTheme.colors
|
||||||
@@ -91,7 +98,7 @@ private fun MacroCard(
|
|||||||
text = value,
|
text = value,
|
||||||
style =
|
style =
|
||||||
RecipeTheme.typography.body.copy(
|
RecipeTheme.typography.body.copy(
|
||||||
color = colors.content,
|
color = valueColor,
|
||||||
fontWeight = FontWeight.Bold,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = ValueTextSize,
|
fontSize = ValueTextSize,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package dev.ulfrx.recipe.ui.components.recipe
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.requiredHeight
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composables.icons.lucide.Minus
|
||||||
|
import com.composables.icons.lucide.Plus
|
||||||
|
import com.composeunstyled.UnstyledButton
|
||||||
|
import com.composeunstyled.UnstyledIcon
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.GlassSurface
|
||||||
|
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RecipeServingsStepper(
|
||||||
|
servings: Int,
|
||||||
|
servingsRange: IntRange,
|
||||||
|
decrementContentDescription: String,
|
||||||
|
incrementContentDescription: String,
|
||||||
|
onServingsChange: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val colors = RecipeTheme.colors
|
||||||
|
GlassSurface(
|
||||||
|
modifier = modifier.height(STEPPER_HEIGHT),
|
||||||
|
cornerRadius = STEPPER_HEIGHT / 2,
|
||||||
|
glassStyle = RecipeTheme.glass.chipOnGlass,
|
||||||
|
tint = RecipeTheme.colors.surfaceGlass.copy(alpha = 0.45f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.xs),
|
||||||
|
) {
|
||||||
|
StepperButton(
|
||||||
|
icon = Lucide.Minus,
|
||||||
|
contentDescription = decrementContentDescription,
|
||||||
|
enabled = servings > servingsRange.first,
|
||||||
|
onClick = { onServingsChange(servings - 1) },
|
||||||
|
)
|
||||||
|
BasicText(
|
||||||
|
text = servings.toString(),
|
||||||
|
style =
|
||||||
|
RecipeTheme.typography.body.copy(
|
||||||
|
color = colors.content,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = SERVINGS_VALUE_TEXT_SIZE,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
),
|
||||||
|
modifier = Modifier.width(SERVINGS_VALUE_WIDTH),
|
||||||
|
)
|
||||||
|
StepperButton(
|
||||||
|
icon = Lucide.Plus,
|
||||||
|
contentDescription = incrementContentDescription,
|
||||||
|
enabled = servings < servingsRange.last,
|
||||||
|
onClick = { onServingsChange(servings + 1) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun StepperButton(
|
||||||
|
icon: ImageVector,
|
||||||
|
contentDescription: String,
|
||||||
|
enabled: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
val colors = RecipeTheme.colors
|
||||||
|
UnstyledButton(
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
modifier = Modifier.width(STEPPER_BUTTON_WIDTH).requiredHeight(STEPPER_TAP_TARGET_HEIGHT),
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
||||||
|
UnstyledIcon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
tint = if (enabled) colors.content else colors.contentMuted.copy(alpha = 0.45f),
|
||||||
|
modifier = Modifier.size(STEPPER_ICON_SIZE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val STEPPER_HEIGHT = 28.dp
|
||||||
|
private val STEPPER_TAP_TARGET_HEIGHT = 44.dp
|
||||||
|
private val STEPPER_BUTTON_WIDTH = 28.dp
|
||||||
|
private val STEPPER_ICON_SIZE = 12.dp
|
||||||
|
private val SERVINGS_VALUE_WIDTH = 18.dp
|
||||||
|
private val SERVINGS_VALUE_TEXT_SIZE = 12.sp
|
||||||
@@ -0,0 +1,170 @@
|
|||||||
|
package dev.ulfrx.recipe.ui.screens.recipedetail
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
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.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
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.layout.size
|
||||||
|
import androidx.compose.foundation.text.BasicText
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.composables.icons.lucide.Clock
|
||||||
|
import com.composables.icons.lucide.Lucide
|
||||||
|
import com.composeunstyled.UnstyledIcon
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.GlassSurface
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackdropState
|
||||||
|
import dev.ulfrx.recipe.ui.components.glass.rememberGlassBackdropState
|
||||||
|
import dev.ulfrx.recipe.ui.components.recipe.RecipeServingsStepper
|
||||||
|
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.recipe_card_minutes_format
|
||||||
|
import recipe.composeapp.generated.resources.recipe_detail_servings_decrement_a11y
|
||||||
|
import recipe.composeapp.generated.resources.recipe_detail_servings_increment_a11y
|
||||||
|
import recipe.composeapp.generated.resources.sample_recipe
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun RecipeDetailHero(
|
||||||
|
title: String,
|
||||||
|
cookingMinutes: Int,
|
||||||
|
servings: Int,
|
||||||
|
onServingsChange: (Int) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val colors = RecipeTheme.colors
|
||||||
|
val typography = RecipeTheme.typography
|
||||||
|
val spacing = RecipeTheme.spacing
|
||||||
|
val heroBackdrop = rememberGlassBackdropState()
|
||||||
|
|
||||||
|
Box(modifier = modifier.fillMaxWidth().height(HERO_HEIGHT)) {
|
||||||
|
GlassBackdropSource(state = heroBackdrop, modifier = Modifier.fillMaxSize()) {
|
||||||
|
Image(
|
||||||
|
painter = painterResource(Res.drawable.sample_recipe),
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize().background(colors.surfaceGlass),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalGlassBackdropState provides heroBackdrop) {
|
||||||
|
GlassSurface(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(HERO_BAND_INSET),
|
||||||
|
cornerRadius = HERO_BAND_CORNER,
|
||||||
|
glassStyle = RecipeTheme.glass.heroBand,
|
||||||
|
recordAsSource = true,
|
||||||
|
tint = RecipeTheme.colors.surfaceGlass.copy(alpha = 0.45f)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier =
|
||||||
|
Modifier.padding(
|
||||||
|
horizontal = HERO_BAND_PADDING_H,
|
||||||
|
vertical = HERO_BAND_PADDING_V,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
BasicText(
|
||||||
|
text = title,
|
||||||
|
style =
|
||||||
|
typography.display.copy(
|
||||||
|
color = colors.content,
|
||||||
|
fontSize = TITLE_TEXT_SIZE,
|
||||||
|
lineHeight = TITLE_LINE_HEIGHT,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(spacing.lg))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
) {
|
||||||
|
MetaChip(
|
||||||
|
text = stringResource(Res.string.recipe_card_minutes_format, cookingMinutes),
|
||||||
|
icon = Lucide.Clock,
|
||||||
|
)
|
||||||
|
RecipeServingsStepper(
|
||||||
|
servings = servings,
|
||||||
|
servingsRange = MIN_RECIPE_SERVINGS..MAX_RECIPE_SERVINGS,
|
||||||
|
decrementContentDescription = stringResource(Res.string.recipe_detail_servings_decrement_a11y),
|
||||||
|
incrementContentDescription = stringResource(Res.string.recipe_detail_servings_increment_a11y),
|
||||||
|
onServingsChange = onServingsChange,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun MetaChip(
|
||||||
|
text: String,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val colors = RecipeTheme.colors
|
||||||
|
GlassSurface(
|
||||||
|
modifier = modifier,
|
||||||
|
cornerRadius = CHIP_CORNER_RADIUS,
|
||||||
|
glassStyle = RecipeTheme.glass.chipOnGlass,
|
||||||
|
tint = RecipeTheme.colors.surfaceGlass.copy(alpha = 0.45f)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = CHIP_PADDING_H, vertical = CHIP_PADDING_V),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(CHIP_GAP),
|
||||||
|
) {
|
||||||
|
if (icon != null) {
|
||||||
|
UnstyledIcon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = colors.content,
|
||||||
|
modifier = Modifier.size(CHIP_ICON_SIZE),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
BasicText(
|
||||||
|
text = text,
|
||||||
|
style =
|
||||||
|
RecipeTheme.typography.body.copy(
|
||||||
|
color = colors.content,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
fontSize = CHIP_TEXT_SIZE,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val HERO_HEIGHT = 280.dp
|
||||||
|
private val HERO_BAND_INSET = 16.dp
|
||||||
|
private val HERO_BAND_CORNER = 22.dp
|
||||||
|
private val HERO_BAND_PADDING_H = 16.dp
|
||||||
|
private val HERO_BAND_PADDING_V = 14.dp
|
||||||
|
|
||||||
|
private val CHIP_CORNER_RADIUS = 14.dp
|
||||||
|
private val CHIP_PADDING_H = 10.dp
|
||||||
|
private val CHIP_PADDING_V = 7.dp
|
||||||
|
private val CHIP_GAP = 5.dp
|
||||||
|
private val CHIP_ICON_SIZE = 11.dp
|
||||||
|
private val CHIP_TEXT_SIZE = 11.sp
|
||||||
|
|
||||||
|
private val TITLE_TEXT_SIZE = 17.sp
|
||||||
|
private val TITLE_LINE_HEIGHT = 21.sp
|
||||||
@@ -3,8 +3,8 @@ package dev.ulfrx.recipe.ui.screens.recipedetail
|
|||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
import androidx.compose.animation.fadeIn
|
import androidx.compose.animation.fadeIn
|
||||||
import androidx.compose.animation.fadeOut
|
import androidx.compose.animation.fadeOut
|
||||||
import androidx.compose.foundation.Image
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
@@ -32,10 +32,7 @@ import androidx.compose.runtime.getValue
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Brush
|
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
|
||||||
import androidx.compose.ui.layout.ContentScale
|
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
import androidx.compose.ui.semantics.semantics
|
import androidx.compose.ui.semantics.semantics
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
@@ -50,10 +47,7 @@ import com.composables.core.Sheet
|
|||||||
import com.composables.core.SheetDetent
|
import com.composables.core.SheetDetent
|
||||||
import com.composables.core.rememberModalBottomSheetState
|
import com.composables.core.rememberModalBottomSheetState
|
||||||
import com.composables.icons.lucide.Calendar
|
import com.composables.icons.lucide.Calendar
|
||||||
import com.composables.icons.lucide.Clock
|
|
||||||
import com.composables.icons.lucide.Lucide
|
import com.composables.icons.lucide.Lucide
|
||||||
import com.composables.icons.lucide.Minus
|
|
||||||
import com.composables.icons.lucide.Plus
|
|
||||||
import com.composeunstyled.UnstyledButton
|
import com.composeunstyled.UnstyledButton
|
||||||
import com.composeunstyled.UnstyledIcon
|
import com.composeunstyled.UnstyledIcon
|
||||||
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
|
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
|
||||||
@@ -66,20 +60,14 @@ import dev.ulfrx.recipe.ui.components.recipe.RecipeIngredientSlotUi
|
|||||||
import dev.ulfrx.recipe.ui.components.recipe.RecipeNutritionUi
|
import dev.ulfrx.recipe.ui.components.recipe.RecipeNutritionUi
|
||||||
import dev.ulfrx.recipe.ui.components.recipe.scaledBy
|
import dev.ulfrx.recipe.ui.components.recipe.scaledBy
|
||||||
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
import org.jetbrains.compose.resources.painterResource
|
|
||||||
import org.jetbrains.compose.resources.stringResource
|
import org.jetbrains.compose.resources.stringResource
|
||||||
import recipe.composeapp.generated.resources.Res
|
import recipe.composeapp.generated.resources.Res
|
||||||
import recipe.composeapp.generated.resources.nutrition_label
|
import recipe.composeapp.generated.resources.nutrition_label
|
||||||
import recipe.composeapp.generated.resources.recipe_card_minutes_format
|
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_handle_a11y
|
import recipe.composeapp.generated.resources.recipe_detail_handle_a11y
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_plan_button
|
import recipe.composeapp.generated.resources.recipe_detail_plan_button
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_section_ingredients
|
import recipe.composeapp.generated.resources.recipe_detail_section_ingredients
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_section_steps
|
import recipe.composeapp.generated.resources.recipe_detail_section_steps
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_servings_decrement_a11y
|
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_servings_increment_a11y
|
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_servings_label
|
|
||||||
import recipe.composeapp.generated.resources.recipe_detail_step_number_format
|
import recipe.composeapp.generated.resources.recipe_detail_step_number_format
|
||||||
import recipe.composeapp.generated.resources.sample_recipe
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RecipeDetailSheet(
|
fun RecipeDetailSheet(
|
||||||
@@ -93,16 +81,19 @@ fun RecipeDetailSheet(
|
|||||||
detents = listOf(SheetDetent.Hidden, SheetDetent.FullyExpanded),
|
detents = listOf(SheetDetent.Hidden, SheetDetent.FullyExpanded),
|
||||||
)
|
)
|
||||||
val ready = state as? RecipeDetailState.Ready
|
val ready = state as? RecipeDetailState.Ready
|
||||||
|
val hasReadyRecipe = ready != null
|
||||||
|
|
||||||
LaunchedEffect(ready != null) {
|
LaunchedEffect(hasReadyRecipe) {
|
||||||
sheetState.targetDetent = if (ready != null) SheetDetent.FullyExpanded else SheetDetent.Hidden
|
sheetState.targetDetent = if (hasReadyRecipe) SheetDetent.FullyExpanded else SheetDetent.Hidden
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only caller of dismiss(): a drag that settles the sheet at Hidden while the VM still holds
|
// Only caller of dismiss(): a drag that settles the sheet at Hidden while the VM still holds
|
||||||
// the recipe. Programmatic closes must set targetDetent = Hidden and let this fire — calling
|
// the recipe. Programmatic closes must set targetDetent = Hidden and let this fire — calling
|
||||||
// dismiss() directly would clear the recipe mid-animation and blank the closing sheet.
|
// dismiss() directly would clear the recipe mid-animation and blank the closing sheet.
|
||||||
|
// Keys are the sheet's settled state, NOT hasReadyRecipe — keying on the latter would fire
|
||||||
|
// the effect at open time (before the sheet leaves Hidden) and immediately dismiss the recipe.
|
||||||
LaunchedEffect(sheetState.isIdle, sheetState.currentDetent) {
|
LaunchedEffect(sheetState.isIdle, sheetState.currentDetent) {
|
||||||
if (sheetState.isIdle && sheetState.currentDetent == SheetDetent.Hidden && ready != null) {
|
if (sheetState.isIdle && sheetState.currentDetent == SheetDetent.Hidden && hasReadyRecipe) {
|
||||||
viewModel.dismiss()
|
viewModel.dismiss()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -138,7 +129,6 @@ private fun BottomSheetScope.RecipeDetailContent(
|
|||||||
onPlanRecipe: (String) -> Unit,
|
onPlanRecipe: (String) -> Unit,
|
||||||
) {
|
) {
|
||||||
val colors = RecipeTheme.colors
|
val colors = RecipeTheme.colors
|
||||||
val typography = RecipeTheme.typography
|
|
||||||
val spacing = RecipeTheme.spacing
|
val spacing = RecipeTheme.spacing
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
val bottomInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
val bottomInset = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
||||||
@@ -153,28 +143,17 @@ private fun BottomSheetScope.RecipeDetailContent(
|
|||||||
Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(SHEET_HEIGHT_FRACTION)) {
|
Box(modifier = Modifier.fillMaxWidth().fillMaxHeight(SHEET_HEIGHT_FRACTION)) {
|
||||||
GlassBackdropSource(state = backdrop, modifier = Modifier.fillMaxSize()) {
|
GlassBackdropSource(state = backdrop, modifier = Modifier.fillMaxSize()) {
|
||||||
Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
Column(modifier = Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||||
RecipeHero()
|
RecipeDetailHero(
|
||||||
|
title = detail.title,
|
||||||
|
cookingMinutes = detail.cookingMinutes,
|
||||||
|
servings = servings,
|
||||||
|
onServingsChange = onServingsChange,
|
||||||
|
)
|
||||||
|
|
||||||
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = spacing.lg)) {
|
Column(modifier = Modifier.fillMaxWidth().padding(horizontal = spacing.lg)) {
|
||||||
BasicText(
|
|
||||||
text = detail.title,
|
|
||||||
style =
|
|
||||||
typography.display.copy(
|
|
||||||
color = colors.content,
|
|
||||||
fontSize = TITLE_TEXT_SIZE,
|
|
||||||
lineHeight = TITLE_LINE_HEIGHT,
|
|
||||||
fontWeight = FontWeight.Bold,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
Spacer(Modifier.height(spacing.sm))
|
|
||||||
MetaRow(minutes = detail.cookingMinutes)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(spacing.xl))
|
Spacer(Modifier.height(spacing.xl))
|
||||||
NutritionSection(nutrition = detail.nutrition.scaledBy(servings))
|
NutritionSection(nutrition = detail.nutrition.scaledBy(servings))
|
||||||
|
|
||||||
Spacer(Modifier.height(spacing.xl))
|
|
||||||
ServingsSection(servings = servings, onServingsChange = onServingsChange)
|
|
||||||
|
|
||||||
Spacer(Modifier.height(spacing.xl))
|
Spacer(Modifier.height(spacing.xl))
|
||||||
IngredientsSection(
|
IngredientsSection(
|
||||||
ingredients = detail.ingredients,
|
ingredients = detail.ingredients,
|
||||||
@@ -207,61 +186,13 @@ private fun BottomSheetScope.RecipeDetailContent(
|
|||||||
modifier =
|
modifier =
|
||||||
Modifier
|
Modifier
|
||||||
.align(Alignment.TopEnd)
|
.align(Alignment.TopEnd)
|
||||||
.padding(top = spacing.lg, end = spacing.lg),
|
.padding(top = spacing.xl, end = spacing.lg),
|
||||||
onClick = { onPlanRecipe(detail.id) },
|
onClick = { onPlanRecipe(detail.id) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun RecipeHero() {
|
|
||||||
val colors = RecipeTheme.colors
|
|
||||||
Box(modifier = Modifier.fillMaxWidth().height(HERO_HEIGHT)) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize().background(colors.surfaceGlass))
|
|
||||||
Image(
|
|
||||||
painter = painterResource(Res.drawable.sample_recipe),
|
|
||||||
contentDescription = null,
|
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = Modifier.fillMaxSize(),
|
|
||||||
)
|
|
||||||
Box(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.background(
|
|
||||||
Brush.verticalGradient(
|
|
||||||
colorStops =
|
|
||||||
arrayOf(
|
|
||||||
0.5f to Color.Transparent,
|
|
||||||
1f to colors.background,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun MetaRow(minutes: Int) {
|
|
||||||
val colors = RecipeTheme.colors
|
|
||||||
Row(
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.xs),
|
|
||||||
) {
|
|
||||||
UnstyledIcon(
|
|
||||||
imageVector = Lucide.Clock,
|
|
||||||
contentDescription = null,
|
|
||||||
tint = colors.contentMuted,
|
|
||||||
modifier = Modifier.size(META_ICON_SIZE),
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = stringResource(Res.string.recipe_card_minutes_format, minutes),
|
|
||||||
style = RecipeTheme.typography.label.copy(color = colors.contentMuted),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun Section(
|
private fun Section(
|
||||||
title: String,
|
title: String,
|
||||||
@@ -293,25 +224,6 @@ private fun NutritionSection(nutrition: RecipeNutritionUi) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ServingsSection(
|
|
||||||
servings: Int,
|
|
||||||
onServingsChange: (Int) -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.SpaceBetween,
|
|
||||||
) {
|
|
||||||
SectionTitle(text = stringResource(Res.string.recipe_detail_servings_label))
|
|
||||||
ServingsStepper(
|
|
||||||
servings = servings,
|
|
||||||
onDecrement = { onServingsChange(servings - 1) },
|
|
||||||
onIncrement = { onServingsChange(servings + 1) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun IngredientsSection(
|
private fun IngredientsSection(
|
||||||
ingredients: List<RecipeIngredientSlotUi>,
|
ingredients: List<RecipeIngredientSlotUi>,
|
||||||
@@ -319,9 +231,19 @@ private fun IngredientsSection(
|
|||||||
substitutions: Map<String, String>,
|
substitutions: Map<String, String>,
|
||||||
onSelectSubstitution: (slotId: String, optionId: String) -> Unit,
|
onSelectSubstitution: (slotId: String, optionId: String) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
val colors = RecipeTheme.colors
|
||||||
|
val cardShape = RoundedCornerShape(INGREDIENTS_CARD_CORNER)
|
||||||
Section(title = stringResource(Res.string.recipe_detail_section_ingredients)) {
|
Section(title = stringResource(Res.string.recipe_detail_section_ingredients)) {
|
||||||
Column(verticalArrangement = Arrangement.spacedBy(INGREDIENT_ROW_GAP)) {
|
Column(
|
||||||
ingredients.forEach { slot ->
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(cardShape)
|
||||||
|
.background(colors.surface)
|
||||||
|
.border(width = CARD_BORDER_WIDTH, color = colors.borderCard, shape = cardShape),
|
||||||
|
) {
|
||||||
|
ingredients.forEachIndexed { index, slot ->
|
||||||
|
if (index > 0) IngredientDivider()
|
||||||
IngredientRow(
|
IngredientRow(
|
||||||
slot = slot.scaledBy(servings),
|
slot = slot.scaledBy(servings),
|
||||||
selectedOptionId = substitutions[slot.id] ?: slot.default.id,
|
selectedOptionId = substitutions[slot.id] ?: slot.default.id,
|
||||||
@@ -337,6 +259,18 @@ private fun IngredientsSection(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun IngredientDivider() {
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = INGREDIENT_DIVIDER_INSET)
|
||||||
|
.height(INGREDIENT_DIVIDER_THICKNESS)
|
||||||
|
.background(RecipeTheme.colors.separator),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StepsSection(steps: List<String>) {
|
private fun StepsSection(steps: List<String>) {
|
||||||
Section(title = stringResource(Res.string.recipe_detail_section_steps)) {
|
Section(title = stringResource(Res.string.recipe_detail_section_steps)) {
|
||||||
@@ -348,70 +282,6 @@ private fun StepsSection(steps: List<String>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ServingsStepper(
|
|
||||||
servings: Int,
|
|
||||||
onDecrement: () -> Unit,
|
|
||||||
onIncrement: () -> Unit,
|
|
||||||
) {
|
|
||||||
val colors = RecipeTheme.colors
|
|
||||||
Row(
|
|
||||||
modifier =
|
|
||||||
Modifier
|
|
||||||
.clip(RoundedCornerShape(percent = 50))
|
|
||||||
.background(colors.surface)
|
|
||||||
.padding(horizontal = RecipeTheme.spacing.xs),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.xs),
|
|
||||||
) {
|
|
||||||
StepperButton(
|
|
||||||
icon = Lucide.Minus,
|
|
||||||
contentDescription = stringResource(Res.string.recipe_detail_servings_decrement_a11y),
|
|
||||||
enabled = servings > MIN_RECIPE_SERVINGS,
|
|
||||||
onClick = onDecrement,
|
|
||||||
)
|
|
||||||
BasicText(
|
|
||||||
text = servings.toString(),
|
|
||||||
style =
|
|
||||||
RecipeTheme.typography.body.copy(
|
|
||||||
color = colors.content,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
),
|
|
||||||
modifier = Modifier.width(SERVINGS_VALUE_WIDTH),
|
|
||||||
)
|
|
||||||
StepperButton(
|
|
||||||
icon = Lucide.Plus,
|
|
||||||
contentDescription = stringResource(Res.string.recipe_detail_servings_increment_a11y),
|
|
||||||
enabled = servings < MAX_RECIPE_SERVINGS,
|
|
||||||
onClick = onIncrement,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun StepperButton(
|
|
||||||
icon: ImageVector,
|
|
||||||
contentDescription: String,
|
|
||||||
enabled: Boolean,
|
|
||||||
onClick: () -> Unit,
|
|
||||||
) {
|
|
||||||
val colors = RecipeTheme.colors
|
|
||||||
UnstyledButton(
|
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
|
||||||
modifier = Modifier.size(STEPPER_BUTTON_SIZE),
|
|
||||||
) {
|
|
||||||
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
||||||
UnstyledIcon(
|
|
||||||
imageVector = icon,
|
|
||||||
contentDescription = contentDescription,
|
|
||||||
tint = if (enabled) colors.content else colors.contentMuted.copy(alpha = 0.45f),
|
|
||||||
modifier = Modifier.size(STEPPER_ICON_SIZE),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun StepRow(
|
private fun StepRow(
|
||||||
number: Int,
|
number: Int,
|
||||||
@@ -424,8 +294,8 @@ private fun StepRow(
|
|||||||
style =
|
style =
|
||||||
RecipeTheme.typography.body.copy(
|
RecipeTheme.typography.body.copy(
|
||||||
color = colors.contentMuted,
|
color = colors.contentMuted,
|
||||||
fontWeight = FontWeight.Medium,
|
fontWeight = FontWeight.Bold,
|
||||||
fontSize = STEP_TEXT_SIZE,
|
fontSize = STEP_NUMBER_TEXT_SIZE,
|
||||||
),
|
),
|
||||||
modifier = Modifier.width(STEP_NUMBER_WIDTH),
|
modifier = Modifier.width(STEP_NUMBER_WIDTH),
|
||||||
)
|
)
|
||||||
@@ -489,21 +359,18 @@ private const val SCRIM_FADE_MILLIS = 250
|
|||||||
|
|
||||||
private val SCRIM_COLOR = Color.Black.copy(alpha = 0.45f)
|
private val SCRIM_COLOR = Color.Black.copy(alpha = 0.45f)
|
||||||
private val SHEET_CORNER_RADIUS = 28.dp
|
private val SHEET_CORNER_RADIUS = 28.dp
|
||||||
private val HERO_HEIGHT = 200.dp
|
|
||||||
private val HANDLE_WIDTH = 36.dp
|
private val HANDLE_WIDTH = 36.dp
|
||||||
private val HANDLE_HEIGHT = 5.dp
|
private val HANDLE_HEIGHT = 5.dp
|
||||||
private val INGREDIENT_ROW_GAP = 6.dp
|
private val INGREDIENTS_CARD_CORNER = 16.dp
|
||||||
private val META_ICON_SIZE = 14.dp
|
private val INGREDIENT_DIVIDER_INSET = 12.dp
|
||||||
private val STEPPER_BUTTON_SIZE = 30.dp
|
private val INGREDIENT_DIVIDER_THICKNESS = 1.dp
|
||||||
private val STEPPER_ICON_SIZE = 14.dp
|
private val CARD_BORDER_WIDTH = 1.dp
|
||||||
private val SERVINGS_VALUE_WIDTH = 28.dp
|
|
||||||
private val STEP_NUMBER_WIDTH = 20.dp
|
private val STEP_NUMBER_WIDTH = 20.dp
|
||||||
private val PLAN_BUTTON_HEIGHT = 36.dp
|
private val PLAN_BUTTON_HEIGHT = 36.dp
|
||||||
private val PLAN_BUTTON_ICON_SIZE = 14.dp
|
private val PLAN_BUTTON_ICON_SIZE = 14.dp
|
||||||
|
|
||||||
private val TITLE_TEXT_SIZE = 24.sp
|
|
||||||
private val TITLE_LINE_HEIGHT = 28.sp
|
|
||||||
private val SECTION_HEADER_TEXT_SIZE = 11.sp
|
private val SECTION_HEADER_TEXT_SIZE = 11.sp
|
||||||
private val SECTION_HEADER_TRACKING = 1.sp
|
private val SECTION_HEADER_TRACKING = 1.sp
|
||||||
private val STEP_TEXT_SIZE = 14.sp
|
private val STEP_NUMBER_TEXT_SIZE = 11.sp
|
||||||
private val STEP_LINE_HEIGHT = 20.sp
|
private val STEP_TEXT_SIZE = 13.sp
|
||||||
|
private val STEP_LINE_HEIGHT = 19.sp
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public data class RecipeColors(
|
|||||||
val separator: Color,
|
val separator: Color,
|
||||||
val borderCard: Color,
|
val borderCard: Color,
|
||||||
val destructive: Color,
|
val destructive: Color,
|
||||||
|
val macroProtein: Color,
|
||||||
|
val macroFat: Color,
|
||||||
|
val macroCarbs: Color,
|
||||||
)
|
)
|
||||||
|
|
||||||
public val LightRecipeColors: RecipeColors =
|
public val LightRecipeColors: RecipeColors =
|
||||||
@@ -33,6 +36,9 @@ public val LightRecipeColors: RecipeColors =
|
|||||||
separator = Color(0xFFE5E1DA),
|
separator = Color(0xFFE5E1DA),
|
||||||
borderCard = Color(0xFFE5E1DA).copy(alpha = 0.60f),
|
borderCard = Color(0xFFE5E1DA).copy(alpha = 0.60f),
|
||||||
destructive = Color(0xFFC0392B),
|
destructive = Color(0xFFC0392B),
|
||||||
|
macroProtein = Color(0xFF3B82F6),
|
||||||
|
macroFat = Color(0xFFD97706),
|
||||||
|
macroCarbs = Color(0xFFEA580C),
|
||||||
)
|
)
|
||||||
|
|
||||||
public val DarkRecipeColors: RecipeColors =
|
public val DarkRecipeColors: RecipeColors =
|
||||||
@@ -48,4 +54,7 @@ public val DarkRecipeColors: RecipeColors =
|
|||||||
separator = Color(0xFF383B40),
|
separator = Color(0xFF383B40),
|
||||||
borderCard = Color(0xFFFFFFFF).copy(alpha = 0.08f),
|
borderCard = Color(0xFFFFFFFF).copy(alpha = 0.08f),
|
||||||
destructive = Color(0xFFE57368),
|
destructive = Color(0xFFE57368),
|
||||||
|
macroProtein = Color(0xFF60A5FA),
|
||||||
|
macroFat = Color(0xFFFBBF24),
|
||||||
|
macroCarbs = Color(0xFFFB923C),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -31,14 +31,36 @@ data object RecipeGlass {
|
|||||||
/** Calm refraction with strong frost — for large surfaces where [menu] would read as a murky lens. */
|
/** Calm refraction with strong frost — for large surfaces where [menu] would read as a murky lens. */
|
||||||
val panel: RecipeGlassStyle =
|
val panel: RecipeGlassStyle =
|
||||||
RecipeGlassStyle(
|
RecipeGlassStyle(
|
||||||
refraction = 0.03f,
|
refraction = 0.10f,
|
||||||
curve = 0.25f,
|
curve = 0.3f,
|
||||||
edge = 0.01f,
|
edge = 0.01f,
|
||||||
dispersion = 0.0f,
|
dispersion = 0.03f,
|
||||||
saturation = 0.5f,
|
saturation = 0.5f,
|
||||||
contrast = 1.3f,
|
contrast = 1.5f,
|
||||||
frost = 28.dp,
|
frost = 28.dp,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val heroBand: RecipeGlassStyle =
|
||||||
|
RecipeGlassStyle(
|
||||||
|
refraction = 0.05f,
|
||||||
|
curve = 0.20f,
|
||||||
|
edge = 0f,
|
||||||
|
dispersion = 0.03f,
|
||||||
|
saturation = 0.5f,
|
||||||
|
contrast = 1.5f,
|
||||||
|
frost = 5.dp,
|
||||||
|
)
|
||||||
|
|
||||||
|
val chipOnGlass: RecipeGlassStyle =
|
||||||
|
RecipeGlassStyle(
|
||||||
|
refraction = 0f,
|
||||||
|
curve = 0f,
|
||||||
|
edge = 0.1f,
|
||||||
|
dispersion = 0.03f,
|
||||||
|
saturation = 0.5f,
|
||||||
|
contrast = 1.5f,
|
||||||
|
frost = 5.dp,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
data class RecipeGlassStyle(
|
data class RecipeGlassStyle(
|
||||||
|
|||||||
Reference in New Issue
Block a user