Add recipe catalog view

This commit is contained in:
2026-05-22 15:22:33 +02:00
parent ae4186d9fa
commit 6d38b8b775
14 changed files with 486 additions and 138 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -22,12 +22,14 @@
<!-- Phase 2.1 — Global search placeholder (UI-10, CONTEXT D-06) -->
<string name="search_placeholder">Szukaj…</string>
<!-- Phase 2.1 — Search screen scaffolding (curated landing in State B; results in State C) -->
<string name="search_screen_curated_title">Tu pojawią się szybkie skróty</string>
<string name="search_screen_curated_subtitle">Sugestie wyszukiwania pojawią się wraz z rozwojem aplikacji.</string>
<!-- Phase 2.1 — Search screen scaffolding (results in State C) -->
<string name="search_screen_empty_results_title">Brak wyników</string>
<string name="search_screen_empty_results_subtitle">Zacznij pisać, aby wyszukać.</string>
<!-- Phase 2.1 — Recipe catalog card meta (Phase 11 polishes copy + plurals) -->
<string name="recipe_card_minutes_format">%1$d min</string>
<string name="recipe_card_kcal_format">%1$d kcal</string>
<!-- Phase 2.1 — Search affordance a11y (UI-10, CONTEXT D-06/D-08) -->
<string name="search_open_a11y">Otwórz wyszukiwanie</string>
<string name="search_dismiss_keyboard_a11y">Wyczyść i ukryj klawiaturę</string>

View File

@@ -4,24 +4,17 @@ import dev.ulfrx.recipe.ui.screens.home.HomeViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
import dev.ulfrx.recipe.ui.screens.search.ShellSearchViewModel
import dev.ulfrx.recipe.ui.screens.search.catalog.RecipeCatalogViewModel
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
import org.koin.dsl.module
import org.koin.plugin.module.dsl.viewModel
val shellModule =
module {
// Tab ViewModels — empty-state-only this phase; feature phases extend them.
// Active-tab tracking lives in TabNavigator (a `remember`-scoped state holder
// owned by AppShell), not in a shell-level VM, so there is no ShellViewModel
// to register.
viewModel<HomeViewModel>()
viewModel<PlannerViewModel>()
viewModel<PantryViewModel>()
viewModel<ShoppingViewModel>()
// Shell-wide search VM — single global state machine (closed / open
// unfocused / open focused) shared by the SearchScreen body and the
// SearchPillRow chrome. Per-tab SearchViewModels were retired when search
// moved from per-tab inline overlay to a shell-level destination.
viewModel<ShellSearchViewModel>()
viewModel<RecipeCatalogViewModel>()
}

View File

@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.statusBars
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
@@ -14,32 +15,22 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.composables.icons.lucide.Lucide
import com.composables.icons.lucide.Search
import dev.ulfrx.recipe.ui.components.empty.EmptyState
import dev.ulfrx.recipe.ui.screens.search.catalog.RecipeCatalogGrid
import dev.ulfrx.recipe.ui.screens.search.catalog.RecipeCatalogViewModel
import dev.ulfrx.recipe.ui.theme.RecipeTheme
import org.jetbrains.compose.resources.stringResource
import recipe.composeapp.generated.resources.Res
import recipe.composeapp.generated.resources.search_screen_curated_subtitle
import recipe.composeapp.generated.resources.search_screen_curated_title
import recipe.composeapp.generated.resources.search_screen_empty_results_subtitle
import recipe.composeapp.generated.resources.search_screen_empty_results_title
/**
* Global search destination — overlays the active tab when
* [ShellSearchViewModel.state.isOpen] is true. Hosted by `AppShell`, not by the
* tab `NavDisplay`, so navigating in/out doesn't disturb per-tab back stacks.
*
* Two body modes driven by `state.isFocused`:
* - **B (unfocused)** — curated landing. v1 placeholder copy; later phases will
* surface recents, quick filters, and per-tab shortcuts here.
* - **C (focused)** — live search. v1 shows an empty-results hint until per-
* feature SearchSources are wired in Phase 5/6/8/9.
*
* The search input pill itself lives in the bottom chrome (see `SearchChrome.kt`),
* not on this screen — keeping the keyboard-adjacent affordance consistent with
* the rest of the shell.
*/
@Composable
fun SearchScreen(viewModel: ShellSearchViewModel) {
fun SearchScreen(
viewModel: ShellSearchViewModel,
catalogViewModel: RecipeCatalogViewModel,
catalogGridState: LazyGridState,
) {
val state by viewModel.state.collectAsStateWithLifecycle()
val catalogState by catalogViewModel.state.collectAsStateWithLifecycle()
Box(
modifier =
@@ -47,6 +38,7 @@ fun SearchScreen(viewModel: ShellSearchViewModel) {
.fillMaxSize()
.background(RecipeTheme.colors.background),
) {
if (state.isFocused) {
Box(
modifier =
Modifier
@@ -54,19 +46,19 @@ fun SearchScreen(viewModel: ShellSearchViewModel) {
.windowInsetsPadding(WindowInsets.statusBars)
.padding(top = RecipeTheme.spacing.xl),
) {
if (state.isFocused) {
EmptyState(
icon = Lucide.Search,
title = stringResource(Res.string.search_screen_empty_results_title),
subtitle = stringResource(Res.string.search_screen_empty_results_subtitle),
)
}
} else {
EmptyState(
icon = Lucide.Search,
title = stringResource(Res.string.search_screen_curated_title),
subtitle = stringResource(Res.string.search_screen_curated_subtitle),
RecipeCatalogGrid(
state = catalogState,
onRecipeClick = {},
gridState = catalogGridState,
modifier = Modifier.fillMaxSize(),
)
}
}
}
}

View File

@@ -0,0 +1,8 @@
package dev.ulfrx.recipe.ui.screens.search.catalog
data class RecipeCardUi(
val id: String,
val title: String,
val minutes: Int,
val kcal: Int,
)

View File

@@ -0,0 +1,219 @@
package dev.ulfrx.recipe.ui.screens.search.catalog
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.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues
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.layout.statusBars
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
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.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.composables.icons.lucide.Clock
import com.composables.icons.lucide.Flame
import com.composables.icons.lucide.Lucide
import com.composeunstyled.UnstyledButton
import com.composeunstyled.UnstyledIcon
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_kcal_format
import recipe.composeapp.generated.resources.recipe_card_minutes_format
import recipe.composeapp.generated.resources.sample_recipe
@Composable
fun RecipeCatalogGrid(
state: RecipeCatalogState,
onRecipeClick: (String) -> Unit,
modifier: Modifier = Modifier,
gridState: LazyGridState = rememberLazyGridState(),
) {
val spacing = RecipeTheme.spacing
val statusBarTop = WindowInsets.statusBars.asPaddingValues().calculateTopPadding()
LazyVerticalGrid(
columns = GridCells.Adaptive(minSize = MinRecipeCardWidth),
state = gridState,
modifier = modifier.fillMaxSize(),
contentPadding =
PaddingValues(
start = spacing.lg,
end = spacing.lg,
top = statusBarTop + spacing.lg,
bottom = GridBottomPadding,
),
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
verticalArrangement = Arrangement.spacedBy(spacing.sm),
) {
items(state.cards, key = { it.id }) { card ->
RecipeCard(card = card, onClick = { onRecipeClick(card.id) })
}
}
}
@Composable
private fun RecipeCard(
card: RecipeCardUi,
onClick: () -> Unit,
) {
val colors = RecipeTheme.colors
val typography = RecipeTheme.typography
val cardShape = RoundedCornerShape(CardCornerRadius)
UnstyledButton(
onClick = onClick,
backgroundColor = colors.surface,
contentColor = colors.content,
shape = cardShape,
borderColor = Color.Transparent,
borderWidth = 0.dp,
modifier =
Modifier
.fillMaxWidth()
.height(CardHeight)
.shadow(elevation = CardElevation, shape = cardShape, clip = false)
.clip(cardShape),
) {
Column(modifier = Modifier.fillMaxSize()) {
RecipeThumbnail()
Column(
modifier =
Modifier
.fillMaxWidth()
.weight(1f)
.padding(CardContentPadding),
) {
BasicText(
text = card.title,
style =
typography.body.copy(
color = colors.content,
fontWeight = FontWeight.SemiBold,
fontSize = CardTitleTextSize,
lineHeight = CardTitleLineHeight,
),
maxLines = 3,
overflow = TextOverflow.Ellipsis,
)
Spacer(Modifier.weight(1f))
RecipeMetaRow(card = card)
}
}
}
}
@Composable
private fun RecipeThumbnail() {
Box(
modifier =
Modifier
.fillMaxWidth()
.height(ThumbnailHeight),
) {
ThumbnailPlaceholder()
Image(
painter = painterResource(Res.drawable.sample_recipe),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.fillMaxSize(),
)
}
}
@Composable
private fun ThumbnailPlaceholder() {
Box(
modifier =
Modifier
.fillMaxSize()
.background(RecipeTheme.colors.surfaceGlass),
)
}
@Composable
private fun RecipeMetaRow(card: RecipeCardUi) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
) {
MetaItem(
icon = Lucide.Clock,
label = stringResource(Res.string.recipe_card_minutes_format, card.minutes),
)
MetaItem(
icon = Lucide.Flame,
label = stringResource(Res.string.recipe_card_kcal_format, card.kcal),
)
}
}
@Composable
private fun MetaItem(
icon: ImageVector,
label: String,
) {
val colors = RecipeTheme.colors
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(RecipeTheme.spacing.xs),
) {
UnstyledIcon(
imageVector = icon,
contentDescription = null,
tint = colors.contentMuted,
modifier = Modifier.size(MetaIconSize),
)
BasicText(
text = label,
style =
RecipeTheme.typography.label.copy(
color = colors.contentMuted,
fontSize = CardMetaTextSize,
lineHeight = CardMetaLineHeight,
),
maxLines = 1,
overflow = TextOverflow.Ellipsis,
)
}
}
private val MinRecipeCardWidth = 104.dp
private val GridBottomPadding = 96.dp
private val CardHeight = 190.dp
private val ThumbnailHeight = 87.dp
private val CardCornerRadius = 17.dp
private val CardContentPadding = 10.dp
private val CardElevation = 3.dp
private val MetaIconSize = 11.dp
private val CardTitleTextSize = 11.sp
private val CardTitleLineHeight = 14.sp
private val CardMetaTextSize = 10.sp
private val CardMetaLineHeight = 13.sp

View File

@@ -0,0 +1,5 @@
package dev.ulfrx.recipe.ui.screens.search.catalog
data class RecipeCatalogState(
val cards: List<RecipeCardUi> = emptyList(),
)

View File

@@ -0,0 +1,11 @@
package dev.ulfrx.recipe.ui.screens.search.catalog
import androidx.lifecycle.ViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
class RecipeCatalogViewModel : ViewModel() {
private val _state = MutableStateFlow(RecipeCatalogState(cards = sampleRecipeCatalogCards))
val state: StateFlow<RecipeCatalogState> = _state.asStateFlow()
}

View File

@@ -0,0 +1,95 @@
package dev.ulfrx.recipe.ui.screens.search.catalog
internal val sampleRecipeCatalogCards: List<RecipeCardUi> =
listOf(
RecipeCardUi(
id = "rcp_nalesniki",
title = "Naleśniki z twarogiem",
minutes = 25,
kcal = 320,
),
RecipeCardUi(
id = "rcp_owsianka",
title = "Owsianka z owocami i orzechami",
minutes = 10,
kcal = 280,
),
RecipeCardUi(
id = "rcp_spaghetti",
title = "Spaghetti bolognese",
minutes = 40,
kcal = 540,
),
RecipeCardUi(
id = "rcp_pierogi",
title = "Pierogi ruskie",
minutes = 90,
kcal = 460,
),
RecipeCardUi(
id = "rcp_kanapka_awokado",
title = "Kanapka z awokado i jajkiem",
minutes = 5,
kcal = 210,
),
RecipeCardUi(
id = "rcp_schabowy",
title = "Schabowy z ziemniakami",
minutes = 60,
kcal = 720,
),
RecipeCardUi(
id = "rcp_salatka_grecka",
title = "Sałatka grecka",
minutes = 15,
kcal = 310,
),
RecipeCardUi(
id = "rcp_pomidorowa",
title = "Zupa pomidorowa z ryżem",
minutes = 35,
kcal = 240,
),
RecipeCardUi(
id = "rcp_kurczak_curry",
title = "Kurczak curry z ryżem basmati",
minutes = 45,
kcal = 580,
),
RecipeCardUi(
id = "rcp_jajecznica",
title = "Jajecznica na maśle ze szczypiorkiem",
minutes = 8,
kcal = 290,
),
RecipeCardUi(
id = "rcp_risotto",
title = "Risotto z grzybami leśnymi",
minutes = 35,
kcal = 470,
),
RecipeCardUi(
id = "rcp_tortilla",
title = "Tortilla z kurczakiem i warzywami",
minutes = 20,
kcal = 430,
),
RecipeCardUi(
id = "rcp_smoothie",
title = "Smoothie bananowo-szpinakowe",
minutes = 5,
kcal = 180,
),
RecipeCardUi(
id = "rcp_losos",
title = "Łosoś pieczony z brokułami",
minutes = 30,
kcal = 510,
),
RecipeCardUi(
id = "rcp_nadziewane_papryki",
title = "Papryki nadziewane kaszą i warzywami",
minutes = 55,
kcal = 390,
),
)

View File

@@ -9,6 +9,7 @@ import androidx.compose.animation.togetherWith
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
@@ -24,6 +25,7 @@ import dev.ulfrx.recipe.ui.components.glass.LocalGlassBackdropState
import dev.ulfrx.recipe.ui.components.glass.rememberGlassBackdropState
import dev.ulfrx.recipe.ui.screens.search.SearchScreen
import dev.ulfrx.recipe.ui.screens.search.ShellSearchViewModel
import dev.ulfrx.recipe.ui.screens.search.catalog.RecipeCatalogViewModel
import dev.ulfrx.recipe.ui.theme.RecipeTheme
import org.koin.compose.viewmodel.koinViewModel
@@ -32,6 +34,8 @@ import org.koin.compose.viewmodel.koinViewModel
fun AppShell(modifier: Modifier = Modifier) {
val navigator = remember { TabNavigator() }
val searchVm: ShellSearchViewModel = koinViewModel()
val catalogVm: RecipeCatalogViewModel = koinViewModel()
val catalogGridState = rememberLazyGridState()
val searchState by searchVm.state.collectAsStateWithLifecycle()
val backdropState = rememberGlassBackdropState()
@@ -56,7 +60,11 @@ fun AppShell(modifier: Modifier = Modifier) {
label = "AppShell body",
) { searchOpen ->
if (searchOpen) {
SearchScreen(viewModel = searchVm)
SearchScreen(
viewModel = searchVm,
catalogViewModel = catalogVm,
catalogGridState = catalogGridState,
)
} else {
RootNavDisplay(
navigator = navigator,

View File

@@ -104,7 +104,8 @@ private fun DockBarExpanded(
}
},
) {
val anim = rememberDockOverlayAnimations(
val anim =
rememberDockOverlayAnimations(
pressState = pressState,
activeIndex = activeIndex,
tabBounds = tabBounds,

View File

@@ -28,11 +28,11 @@ import kotlinx.coroutines.launch
import kotlin.math.abs
private val PressOverlayBleed = 4.dp
private const val SlideOutwardStiffness = Spring.StiffnessMediumLow
private const val SlideSettleStiffness = Spring.StiffnessHigh
private const val OverlayFadeInDurationMs = 120
private const val OverlayFadeOutDurationMs = 40
private const val SettleEpsilonPx = 0.5f
private const val SLIDE_OUTWARD_STIFFNESS = Spring.StiffnessMediumLow
private const val SLIDE_SETTLE_STIFFNESS = Spring.StiffnessHigh
private const val OVERLAY_FADE_IN_DURATION_MS = 120
private const val OVERLAY_FADE_OUT_DURATION_MS = 40
private const val SETTLE_EPSILON_PX = 0.5f
internal data class DockOverlayAnimations(
val overlayCenterX: Float,
@@ -72,23 +72,27 @@ internal fun rememberDockOverlayAnimations(
activeCenterX,
spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = SlideSettleStiffness,
visibilityThreshold = SettleEpsilonPx,
stiffness = SLIDE_SETTLE_STIFFNESS,
visibilityThreshold = SETTLE_EPSILON_PX,
),
)
}
!wasPressed -> {
wasPressed = true
centerAnim.animateTo(
clampedPressX,
spring(
dampingRatio = Spring.DampingRatioNoBouncy,
stiffness = SlideOutwardStiffness,
visibilityThreshold = SettleEpsilonPx,
stiffness = SLIDE_OUTWARD_STIFFNESS,
visibilityThreshold = SETTLE_EPSILON_PX,
),
)
}
else -> centerAnim.snapTo(clampedPressX)
else -> {
centerAnim.snapTo(clampedPressX)
}
}
}
@@ -101,13 +105,14 @@ internal fun rememberDockOverlayAnimations(
activeAlphaAnim.snapTo(0f)
overlayAlphaAnim.animateTo(
targetValue = 1f,
animationSpec = tween(OverlayFadeInDurationMs, easing = FastOutSlowInEasing),
animationSpec = tween(OVERLAY_FADE_IN_DURATION_MS, easing = FastOutSlowInEasing),
)
} else {
if (overlayAlphaAnim.value <= 0f) return@LaunchedEffect
releaseSlideStartX = centerAnim.value
if (overlayAlphaAnim.value < 1f) {
val tailMs = ((1f - overlayAlphaAnim.value) * OverlayFadeInDurationMs)
val tailMs =
((1f - overlayAlphaAnim.value) * OVERLAY_FADE_IN_DURATION_MS)
.toInt()
.coerceAtLeast(0)
if (tailMs > 0) {
@@ -116,19 +121,19 @@ internal fun rememberDockOverlayAnimations(
}
snapshotFlow {
!centerAnim.isRunning &&
abs(centerAnim.value - activeCenterXState.value) < SettleEpsilonPx
abs(centerAnim.value - activeCenterXState.value) < SETTLE_EPSILON_PX
}.first { it }
coroutineScope {
launch {
overlayAlphaAnim.animateTo(
targetValue = 0f,
animationSpec = tween(OverlayFadeOutDurationMs, easing = FastOutSlowInEasing),
animationSpec = tween(OVERLAY_FADE_OUT_DURATION_MS, easing = FastOutSlowInEasing),
)
}
launch {
activeAlphaAnim.animateTo(
targetValue = 1f,
animationSpec = tween(OverlayFadeOutDurationMs, easing = FastOutSlowInEasing),
animationSpec = tween(OVERLAY_FADE_OUT_DURATION_MS, easing = FastOutSlowInEasing),
)
}
}
@@ -136,15 +141,19 @@ internal fun rememberDockOverlayAnimations(
}
}
val releaseSlideProgress = run {
val releaseSlideProgress =
run {
val start = releaseSlideStartX
if (start == null) {
0f
} else {
val target = activeCenterXState.value
val total = abs(target - start)
if (total < 1f) 0f
else (abs(centerAnim.value - start) / total).coerceIn(0f, 1f)
if (total < 1f) {
0f
} else {
(abs(centerAnim.value - start) / total).coerceIn(0f, 1f)
}
}
}
val overlayPeakProgress = overlayAlphaAnim.value * (1f - releaseSlideProgress)

View File

@@ -13,18 +13,18 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.util.lerp
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.times
import androidx.compose.ui.util.lerp
import dev.ulfrx.recipe.ui.components.glass.GlassSurface
import dev.ulfrx.recipe.ui.theme.RecipeTheme
import kotlin.math.roundToInt
private val PressOverlayVerticalInset = 0.dp
private val ActiveIndicatorVerticalInset = 5.dp
private const val PressOverlayScale = 1.22f
private const val PRESS_OVERLAY_SCALE = 1.22f
@Composable
internal fun DockSubstrate(cornerRadius: Dp) {
@@ -49,7 +49,8 @@ internal fun DockActiveIndicatorLayer(
val bbox = activeIndicatorBboxFor(bounds, dockWidthPx, density)
Box(
modifier = Modifier
modifier =
Modifier
.offset { IntOffset(bbox.leftPx.roundToInt(), 0) }
.width(with(density) { bbox.widthPx.toDp() })
.fillMaxHeight()
@@ -77,13 +78,14 @@ internal fun DockPressOverlayLayer(
val dockHeightPx = with(density) { dockHeight.toPx() }
val activeInsetPx = with(density) { ActiveIndicatorVerticalInset.toPx() }
val activeStartScaleY = if (dockHeightPx > 0f) (dockHeightPx - 2 * activeInsetPx) / dockHeightPx else 1f
val scaleX = lerp(1f, PressOverlayScale, overlayPeakProgress)
val scaleY = lerp(activeStartScaleY, PressOverlayScale, overlayPeakProgress)
val scaleX = lerp(1f, PRESS_OVERLAY_SCALE, overlayPeakProgress)
val scaleY = lerp(activeStartScaleY, PRESS_OVERLAY_SCALE, overlayPeakProgress)
val cornerRadius = (dockHeight - 2 * PressOverlayVerticalInset) / 2
val leftPx = overlayCenterX - overlayWidthPx / 2f
GlassSurface(
modifier = Modifier
modifier =
Modifier
.offset { IntOffset(leftPx.roundToInt(), 0) }
.width(with(density) { overlayWidthPx.toDp() })
.fillMaxHeight()
@@ -91,8 +93,7 @@ internal fun DockPressOverlayLayer(
.graphicsLayer {
this.scaleX = scaleX
this.scaleY = scaleY
}
.alpha(overlayAlpha),
}.alpha(overlayAlpha),
cornerRadius = cornerRadius,
glassStyle = RecipeTheme.glass.dockPress,
tint = RecipeTheme.colors.surfaceGlassOverlay,

View File

@@ -33,8 +33,8 @@ import kotlin.math.roundToInt
private val DockTabIconSize = 18.dp
private val DockTabIconLabelGap = 2.dp
private const val DockTabLabelFontSizeSp = 11
private const val DockTabLabelLineHeightSp = 13
private const val DOCK_TAB_LABEL_FONT_SIZE_SP = 11
private const val DOCK_TAB_LABEL_LINE_HEIGHT_SP = 13
@Composable
internal fun DockTabRow(
@@ -53,7 +53,8 @@ internal fun DockTabRow(
) {
destinations.forEachIndexed { index, destination ->
val cellBounds = tabBounds[index]
val contentOffsetPx = if (cellBounds != null && dockWidthPx > 0f) {
val contentOffsetPx =
if (cellBounds != null && dockWidthPx > 0f) {
val bbox = activeIndicatorBboxFor(cellBounds, dockWidthPx, density)
val cellCenterX = cellBounds.offsetXPx + cellBounds.widthPx / 2f
bbox.centerPx - cellCenterX
@@ -65,7 +66,8 @@ internal fun DockTabRow(
isActive = index == activeIndex,
contentOffsetPx = contentOffsetPx,
onSelect = { onTabSelectFromA11y(destination) },
modifier = Modifier
modifier =
Modifier
.weight(1f)
.fillMaxHeight()
.onGloballyPositioned { coords ->
@@ -94,7 +96,8 @@ private fun DockTabItem(
val a11yLabel = if (isActive) "$label, aktywna" else label
val tint = RecipeTheme.colors.content
Box(
modifier = modifier.semantics {
modifier =
modifier.semantics {
role = Role.Tab
selected = isActive
contentDescription = a11yLabel
@@ -119,10 +122,11 @@ private fun DockTabItem(
Spacer(modifier = Modifier.size(DockTabIconLabelGap))
BasicText(
text = label,
style = RecipeTheme.typography.label.copy(
style =
RecipeTheme.typography.label.copy(
color = tint,
fontSize = DockTabLabelFontSizeSp.sp,
lineHeight = DockTabLabelLineHeightSp.sp,
fontSize = DOCK_TAB_LABEL_FONT_SIZE_SP.sp,
lineHeight = DOCK_TAB_LABEL_LINE_HEIGHT_SP.sp,
),
)
}