Add recipe catalog view
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 268 KiB |
@@ -22,12 +22,14 @@
|
|||||||
<!-- Phase 2.1 — Global search placeholder (UI-10, CONTEXT D-06) -->
|
<!-- Phase 2.1 — Global search placeholder (UI-10, CONTEXT D-06) -->
|
||||||
<string name="search_placeholder">Szukaj…</string>
|
<string name="search_placeholder">Szukaj…</string>
|
||||||
|
|
||||||
<!-- Phase 2.1 — Search screen scaffolding (curated landing in State B; results in State C) -->
|
<!-- Phase 2.1 — Search screen scaffolding (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>
|
|
||||||
<string name="search_screen_empty_results_title">Brak wyników</string>
|
<string name="search_screen_empty_results_title">Brak wyników</string>
|
||||||
<string name="search_screen_empty_results_subtitle">Zacznij pisać, aby wyszukać.</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) -->
|
<!-- 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_open_a11y">Otwórz wyszukiwanie</string>
|
||||||
<string name="search_dismiss_keyboard_a11y">Wyczyść i ukryj klawiaturę</string>
|
<string name="search_dismiss_keyboard_a11y">Wyczyść i ukryj klawiaturę</string>
|
||||||
|
|||||||
@@ -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.pantry.PantryViewModel
|
||||||
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
|
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
|
||||||
import dev.ulfrx.recipe.ui.screens.search.ShellSearchViewModel
|
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 dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koin.plugin.module.dsl.viewModel
|
import org.koin.plugin.module.dsl.viewModel
|
||||||
|
|
||||||
val shellModule =
|
val shellModule =
|
||||||
module {
|
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<HomeViewModel>()
|
||||||
viewModel<PlannerViewModel>()
|
viewModel<PlannerViewModel>()
|
||||||
viewModel<PantryViewModel>()
|
viewModel<PantryViewModel>()
|
||||||
viewModel<ShoppingViewModel>()
|
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<ShellSearchViewModel>()
|
||||||
|
viewModel<RecipeCatalogViewModel>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.statusBars
|
import androidx.compose.foundation.layout.statusBars
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
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.Lucide
|
||||||
import com.composables.icons.lucide.Search
|
import com.composables.icons.lucide.Search
|
||||||
import dev.ulfrx.recipe.ui.components.empty.EmptyState
|
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 dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
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.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_subtitle
|
||||||
import recipe.composeapp.generated.resources.search_screen_empty_results_title
|
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
|
@Composable
|
||||||
fun SearchScreen(viewModel: ShellSearchViewModel) {
|
fun SearchScreen(
|
||||||
|
viewModel: ShellSearchViewModel,
|
||||||
|
catalogViewModel: RecipeCatalogViewModel,
|
||||||
|
catalogGridState: LazyGridState,
|
||||||
|
) {
|
||||||
val state by viewModel.state.collectAsStateWithLifecycle()
|
val state by viewModel.state.collectAsStateWithLifecycle()
|
||||||
|
val catalogState by catalogViewModel.state.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier =
|
modifier =
|
||||||
@@ -47,26 +38,27 @@ fun SearchScreen(viewModel: ShellSearchViewModel) {
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.background(RecipeTheme.colors.background),
|
.background(RecipeTheme.colors.background),
|
||||||
) {
|
) {
|
||||||
Box(
|
if (state.isFocused) {
|
||||||
modifier =
|
Box(
|
||||||
Modifier
|
modifier =
|
||||||
.fillMaxSize()
|
Modifier
|
||||||
.windowInsetsPadding(WindowInsets.statusBars)
|
.fillMaxSize()
|
||||||
.padding(top = RecipeTheme.spacing.xl),
|
.windowInsetsPadding(WindowInsets.statusBars)
|
||||||
) {
|
.padding(top = RecipeTheme.spacing.xl),
|
||||||
if (state.isFocused) {
|
) {
|
||||||
EmptyState(
|
EmptyState(
|
||||||
icon = Lucide.Search,
|
icon = Lucide.Search,
|
||||||
title = stringResource(Res.string.search_screen_empty_results_title),
|
title = stringResource(Res.string.search_screen_empty_results_title),
|
||||||
subtitle = stringResource(Res.string.search_screen_empty_results_subtitle),
|
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),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
RecipeCatalogGrid(
|
||||||
|
state = catalogState,
|
||||||
|
onRecipeClick = {},
|
||||||
|
gridState = catalogGridState,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
)
|
||||||
@@ -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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package dev.ulfrx.recipe.ui.screens.search.catalog
|
||||||
|
|
||||||
|
data class RecipeCatalogState(
|
||||||
|
val cards: List<RecipeCardUi> = emptyList(),
|
||||||
|
)
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
),
|
||||||
|
)
|
||||||
@@ -9,6 +9,7 @@ import androidx.compose.animation.togetherWith
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.getValue
|
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.components.glass.rememberGlassBackdropState
|
||||||
import dev.ulfrx.recipe.ui.screens.search.SearchScreen
|
import dev.ulfrx.recipe.ui.screens.search.SearchScreen
|
||||||
import dev.ulfrx.recipe.ui.screens.search.ShellSearchViewModel
|
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 dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
import org.koin.compose.viewmodel.koinViewModel
|
import org.koin.compose.viewmodel.koinViewModel
|
||||||
|
|
||||||
@@ -32,6 +34,8 @@ import org.koin.compose.viewmodel.koinViewModel
|
|||||||
fun AppShell(modifier: Modifier = Modifier) {
|
fun AppShell(modifier: Modifier = Modifier) {
|
||||||
val navigator = remember { TabNavigator() }
|
val navigator = remember { TabNavigator() }
|
||||||
val searchVm: ShellSearchViewModel = koinViewModel()
|
val searchVm: ShellSearchViewModel = koinViewModel()
|
||||||
|
val catalogVm: RecipeCatalogViewModel = koinViewModel()
|
||||||
|
val catalogGridState = rememberLazyGridState()
|
||||||
val searchState by searchVm.state.collectAsStateWithLifecycle()
|
val searchState by searchVm.state.collectAsStateWithLifecycle()
|
||||||
val backdropState = rememberGlassBackdropState()
|
val backdropState = rememberGlassBackdropState()
|
||||||
|
|
||||||
@@ -56,7 +60,11 @@ fun AppShell(modifier: Modifier = Modifier) {
|
|||||||
label = "AppShell body",
|
label = "AppShell body",
|
||||||
) { searchOpen ->
|
) { searchOpen ->
|
||||||
if (searchOpen) {
|
if (searchOpen) {
|
||||||
SearchScreen(viewModel = searchVm)
|
SearchScreen(
|
||||||
|
viewModel = searchVm,
|
||||||
|
catalogViewModel = catalogVm,
|
||||||
|
catalogGridState = catalogGridState,
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
RootNavDisplay(
|
RootNavDisplay(
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
|||||||
@@ -104,13 +104,14 @@ private fun DockBarExpanded(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
) {
|
) {
|
||||||
val anim = rememberDockOverlayAnimations(
|
val anim =
|
||||||
pressState = pressState,
|
rememberDockOverlayAnimations(
|
||||||
activeIndex = activeIndex,
|
pressState = pressState,
|
||||||
tabBounds = tabBounds,
|
activeIndex = activeIndex,
|
||||||
dockWidthPx = dockWidthPx,
|
tabBounds = tabBounds,
|
||||||
density = LocalDensity.current,
|
dockWidthPx = dockWidthPx,
|
||||||
)
|
density = LocalDensity.current,
|
||||||
|
)
|
||||||
DockSubstrate(cornerRadius = height / 2)
|
DockSubstrate(cornerRadius = height / 2)
|
||||||
DockActiveIndicatorLayer(
|
DockActiveIndicatorLayer(
|
||||||
activeIndex = activeIndex,
|
activeIndex = activeIndex,
|
||||||
|
|||||||
@@ -28,11 +28,11 @@ import kotlinx.coroutines.launch
|
|||||||
import kotlin.math.abs
|
import kotlin.math.abs
|
||||||
|
|
||||||
private val PressOverlayBleed = 4.dp
|
private val PressOverlayBleed = 4.dp
|
||||||
private const val SlideOutwardStiffness = Spring.StiffnessMediumLow
|
private const val SLIDE_OUTWARD_STIFFNESS = Spring.StiffnessMediumLow
|
||||||
private const val SlideSettleStiffness = Spring.StiffnessHigh
|
private const val SLIDE_SETTLE_STIFFNESS = Spring.StiffnessHigh
|
||||||
private const val OverlayFadeInDurationMs = 120
|
private const val OVERLAY_FADE_IN_DURATION_MS = 120
|
||||||
private const val OverlayFadeOutDurationMs = 40
|
private const val OVERLAY_FADE_OUT_DURATION_MS = 40
|
||||||
private const val SettleEpsilonPx = 0.5f
|
private const val SETTLE_EPSILON_PX = 0.5f
|
||||||
|
|
||||||
internal data class DockOverlayAnimations(
|
internal data class DockOverlayAnimations(
|
||||||
val overlayCenterX: Float,
|
val overlayCenterX: Float,
|
||||||
@@ -72,23 +72,27 @@ internal fun rememberDockOverlayAnimations(
|
|||||||
activeCenterX,
|
activeCenterX,
|
||||||
spring(
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||||
stiffness = SlideSettleStiffness,
|
stiffness = SLIDE_SETTLE_STIFFNESS,
|
||||||
visibilityThreshold = SettleEpsilonPx,
|
visibilityThreshold = SETTLE_EPSILON_PX,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
!wasPressed -> {
|
!wasPressed -> {
|
||||||
wasPressed = true
|
wasPressed = true
|
||||||
centerAnim.animateTo(
|
centerAnim.animateTo(
|
||||||
clampedPressX,
|
clampedPressX,
|
||||||
spring(
|
spring(
|
||||||
dampingRatio = Spring.DampingRatioNoBouncy,
|
dampingRatio = Spring.DampingRatioNoBouncy,
|
||||||
stiffness = SlideOutwardStiffness,
|
stiffness = SLIDE_OUTWARD_STIFFNESS,
|
||||||
visibilityThreshold = SettleEpsilonPx,
|
visibilityThreshold = SETTLE_EPSILON_PX,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> centerAnim.snapTo(clampedPressX)
|
|
||||||
|
else -> {
|
||||||
|
centerAnim.snapTo(clampedPressX)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,34 +105,35 @@ internal fun rememberDockOverlayAnimations(
|
|||||||
activeAlphaAnim.snapTo(0f)
|
activeAlphaAnim.snapTo(0f)
|
||||||
overlayAlphaAnim.animateTo(
|
overlayAlphaAnim.animateTo(
|
||||||
targetValue = 1f,
|
targetValue = 1f,
|
||||||
animationSpec = tween(OverlayFadeInDurationMs, easing = FastOutSlowInEasing),
|
animationSpec = tween(OVERLAY_FADE_IN_DURATION_MS, easing = FastOutSlowInEasing),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
if (overlayAlphaAnim.value <= 0f) return@LaunchedEffect
|
if (overlayAlphaAnim.value <= 0f) return@LaunchedEffect
|
||||||
releaseSlideStartX = centerAnim.value
|
releaseSlideStartX = centerAnim.value
|
||||||
if (overlayAlphaAnim.value < 1f) {
|
if (overlayAlphaAnim.value < 1f) {
|
||||||
val tailMs = ((1f - overlayAlphaAnim.value) * OverlayFadeInDurationMs)
|
val tailMs =
|
||||||
.toInt()
|
((1f - overlayAlphaAnim.value) * OVERLAY_FADE_IN_DURATION_MS)
|
||||||
.coerceAtLeast(0)
|
.toInt()
|
||||||
|
.coerceAtLeast(0)
|
||||||
if (tailMs > 0) {
|
if (tailMs > 0) {
|
||||||
overlayAlphaAnim.animateTo(1f, tween(tailMs, easing = FastOutSlowInEasing))
|
overlayAlphaAnim.animateTo(1f, tween(tailMs, easing = FastOutSlowInEasing))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
!centerAnim.isRunning &&
|
!centerAnim.isRunning &&
|
||||||
abs(centerAnim.value - activeCenterXState.value) < SettleEpsilonPx
|
abs(centerAnim.value - activeCenterXState.value) < SETTLE_EPSILON_PX
|
||||||
}.first { it }
|
}.first { it }
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
launch {
|
launch {
|
||||||
overlayAlphaAnim.animateTo(
|
overlayAlphaAnim.animateTo(
|
||||||
targetValue = 0f,
|
targetValue = 0f,
|
||||||
animationSpec = tween(OverlayFadeOutDurationMs, easing = FastOutSlowInEasing),
|
animationSpec = tween(OVERLAY_FADE_OUT_DURATION_MS, easing = FastOutSlowInEasing),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
launch {
|
launch {
|
||||||
activeAlphaAnim.animateTo(
|
activeAlphaAnim.animateTo(
|
||||||
targetValue = 1f,
|
targetValue = 1f,
|
||||||
animationSpec = tween(OverlayFadeOutDurationMs, easing = FastOutSlowInEasing),
|
animationSpec = tween(OVERLAY_FADE_OUT_DURATION_MS, easing = FastOutSlowInEasing),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,17 +141,21 @@ internal fun rememberDockOverlayAnimations(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val releaseSlideProgress = run {
|
val releaseSlideProgress =
|
||||||
val start = releaseSlideStartX
|
run {
|
||||||
if (start == null) {
|
val start = releaseSlideStartX
|
||||||
0f
|
if (start == null) {
|
||||||
} else {
|
0f
|
||||||
val target = activeCenterXState.value
|
} else {
|
||||||
val total = abs(target - start)
|
val target = activeCenterXState.value
|
||||||
if (total < 1f) 0f
|
val total = abs(target - start)
|
||||||
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)
|
val overlayPeakProgress = overlayAlphaAnim.value * (1f - releaseSlideProgress)
|
||||||
|
|
||||||
return DockOverlayAnimations(
|
return DockOverlayAnimations(
|
||||||
|
|||||||
@@ -13,18 +13,18 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.draw.alpha
|
import androidx.compose.ui.draw.alpha
|
||||||
import androidx.compose.ui.graphics.graphicsLayer
|
import androidx.compose.ui.graphics.graphicsLayer
|
||||||
import androidx.compose.ui.platform.LocalDensity
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.util.lerp
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.times
|
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.components.glass.GlassSurface
|
||||||
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
import dev.ulfrx.recipe.ui.theme.RecipeTheme
|
||||||
import kotlin.math.roundToInt
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
private val PressOverlayVerticalInset = 0.dp
|
private val PressOverlayVerticalInset = 0.dp
|
||||||
private val ActiveIndicatorVerticalInset = 5.dp
|
private val ActiveIndicatorVerticalInset = 5.dp
|
||||||
private const val PressOverlayScale = 1.22f
|
private const val PRESS_OVERLAY_SCALE = 1.22f
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DockSubstrate(cornerRadius: Dp) {
|
internal fun DockSubstrate(cornerRadius: Dp) {
|
||||||
@@ -49,16 +49,17 @@ internal fun DockActiveIndicatorLayer(
|
|||||||
val bbox = activeIndicatorBboxFor(bounds, dockWidthPx, density)
|
val bbox = activeIndicatorBboxFor(bounds, dockWidthPx, density)
|
||||||
|
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.offset { IntOffset(bbox.leftPx.roundToInt(), 0) }
|
Modifier
|
||||||
.width(with(density) { bbox.widthPx.toDp() })
|
.offset { IntOffset(bbox.leftPx.roundToInt(), 0) }
|
||||||
.fillMaxHeight()
|
.width(with(density) { bbox.widthPx.toDp() })
|
||||||
.padding(vertical = ActiveIndicatorVerticalInset)
|
.fillMaxHeight()
|
||||||
.alpha(alpha)
|
.padding(vertical = ActiveIndicatorVerticalInset)
|
||||||
.background(
|
.alpha(alpha)
|
||||||
color = RecipeTheme.colors.chromeActive,
|
.background(
|
||||||
shape = RoundedCornerShape(50),
|
color = RecipeTheme.colors.chromeActive,
|
||||||
),
|
shape = RoundedCornerShape(50),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,22 +78,22 @@ internal fun DockPressOverlayLayer(
|
|||||||
val dockHeightPx = with(density) { dockHeight.toPx() }
|
val dockHeightPx = with(density) { dockHeight.toPx() }
|
||||||
val activeInsetPx = with(density) { ActiveIndicatorVerticalInset.toPx() }
|
val activeInsetPx = with(density) { ActiveIndicatorVerticalInset.toPx() }
|
||||||
val activeStartScaleY = if (dockHeightPx > 0f) (dockHeightPx - 2 * activeInsetPx) / dockHeightPx else 1f
|
val activeStartScaleY = if (dockHeightPx > 0f) (dockHeightPx - 2 * activeInsetPx) / dockHeightPx else 1f
|
||||||
val scaleX = lerp(1f, PressOverlayScale, overlayPeakProgress)
|
val scaleX = lerp(1f, PRESS_OVERLAY_SCALE, overlayPeakProgress)
|
||||||
val scaleY = lerp(activeStartScaleY, PressOverlayScale, overlayPeakProgress)
|
val scaleY = lerp(activeStartScaleY, PRESS_OVERLAY_SCALE, overlayPeakProgress)
|
||||||
|
|
||||||
val cornerRadius = (dockHeight - 2 * PressOverlayVerticalInset) / 2
|
val cornerRadius = (dockHeight - 2 * PressOverlayVerticalInset) / 2
|
||||||
val leftPx = overlayCenterX - overlayWidthPx / 2f
|
val leftPx = overlayCenterX - overlayWidthPx / 2f
|
||||||
GlassSurface(
|
GlassSurface(
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.offset { IntOffset(leftPx.roundToInt(), 0) }
|
Modifier
|
||||||
.width(with(density) { overlayWidthPx.toDp() })
|
.offset { IntOffset(leftPx.roundToInt(), 0) }
|
||||||
.fillMaxHeight()
|
.width(with(density) { overlayWidthPx.toDp() })
|
||||||
.padding(vertical = PressOverlayVerticalInset)
|
.fillMaxHeight()
|
||||||
.graphicsLayer {
|
.padding(vertical = PressOverlayVerticalInset)
|
||||||
this.scaleX = scaleX
|
.graphicsLayer {
|
||||||
this.scaleY = scaleY
|
this.scaleX = scaleX
|
||||||
}
|
this.scaleY = scaleY
|
||||||
.alpha(overlayAlpha),
|
}.alpha(overlayAlpha),
|
||||||
cornerRadius = cornerRadius,
|
cornerRadius = cornerRadius,
|
||||||
glassStyle = RecipeTheme.glass.dockPress,
|
glassStyle = RecipeTheme.glass.dockPress,
|
||||||
tint = RecipeTheme.colors.surfaceGlassOverlay,
|
tint = RecipeTheme.colors.surfaceGlassOverlay,
|
||||||
|
|||||||
@@ -33,8 +33,8 @@ import kotlin.math.roundToInt
|
|||||||
|
|
||||||
private val DockTabIconSize = 18.dp
|
private val DockTabIconSize = 18.dp
|
||||||
private val DockTabIconLabelGap = 2.dp
|
private val DockTabIconLabelGap = 2.dp
|
||||||
private const val DockTabLabelFontSizeSp = 11
|
private const val DOCK_TAB_LABEL_FONT_SIZE_SP = 11
|
||||||
private const val DockTabLabelLineHeightSp = 13
|
private const val DOCK_TAB_LABEL_LINE_HEIGHT_SP = 13
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DockTabRow(
|
internal fun DockTabRow(
|
||||||
@@ -53,30 +53,32 @@ internal fun DockTabRow(
|
|||||||
) {
|
) {
|
||||||
destinations.forEachIndexed { index, destination ->
|
destinations.forEachIndexed { index, destination ->
|
||||||
val cellBounds = tabBounds[index]
|
val cellBounds = tabBounds[index]
|
||||||
val contentOffsetPx = if (cellBounds != null && dockWidthPx > 0f) {
|
val contentOffsetPx =
|
||||||
val bbox = activeIndicatorBboxFor(cellBounds, dockWidthPx, density)
|
if (cellBounds != null && dockWidthPx > 0f) {
|
||||||
val cellCenterX = cellBounds.offsetXPx + cellBounds.widthPx / 2f
|
val bbox = activeIndicatorBboxFor(cellBounds, dockWidthPx, density)
|
||||||
bbox.centerPx - cellCenterX
|
val cellCenterX = cellBounds.offsetXPx + cellBounds.widthPx / 2f
|
||||||
} else {
|
bbox.centerPx - cellCenterX
|
||||||
0f
|
} else {
|
||||||
}
|
0f
|
||||||
|
}
|
||||||
DockTabItem(
|
DockTabItem(
|
||||||
destination = destination,
|
destination = destination,
|
||||||
isActive = index == activeIndex,
|
isActive = index == activeIndex,
|
||||||
contentOffsetPx = contentOffsetPx,
|
contentOffsetPx = contentOffsetPx,
|
||||||
onSelect = { onTabSelectFromA11y(destination) },
|
onSelect = { onTabSelectFromA11y(destination) },
|
||||||
modifier = Modifier
|
modifier =
|
||||||
.weight(1f)
|
Modifier
|
||||||
.fillMaxHeight()
|
.weight(1f)
|
||||||
.onGloballyPositioned { coords ->
|
.fillMaxHeight()
|
||||||
onTabBoundsChange(
|
.onGloballyPositioned { coords ->
|
||||||
index,
|
onTabBoundsChange(
|
||||||
TabBounds(
|
index,
|
||||||
offsetXPx = coords.positionInParent().x,
|
TabBounds(
|
||||||
widthPx = coords.size.width.toFloat(),
|
offsetXPx = coords.positionInParent().x,
|
||||||
),
|
widthPx = coords.size.width.toFloat(),
|
||||||
)
|
),
|
||||||
},
|
)
|
||||||
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,15 +96,16 @@ private fun DockTabItem(
|
|||||||
val a11yLabel = if (isActive) "$label, aktywna" else label
|
val a11yLabel = if (isActive) "$label, aktywna" else label
|
||||||
val tint = RecipeTheme.colors.content
|
val tint = RecipeTheme.colors.content
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier.semantics {
|
modifier =
|
||||||
role = Role.Tab
|
modifier.semantics {
|
||||||
selected = isActive
|
role = Role.Tab
|
||||||
contentDescription = a11yLabel
|
selected = isActive
|
||||||
onClick {
|
contentDescription = a11yLabel
|
||||||
onSelect()
|
onClick {
|
||||||
true
|
onSelect()
|
||||||
}
|
true
|
||||||
},
|
}
|
||||||
|
},
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
@@ -119,11 +122,12 @@ private fun DockTabItem(
|
|||||||
Spacer(modifier = Modifier.size(DockTabIconLabelGap))
|
Spacer(modifier = Modifier.size(DockTabIconLabelGap))
|
||||||
BasicText(
|
BasicText(
|
||||||
text = label,
|
text = label,
|
||||||
style = RecipeTheme.typography.label.copy(
|
style =
|
||||||
color = tint,
|
RecipeTheme.typography.label.copy(
|
||||||
fontSize = DockTabLabelFontSizeSp.sp,
|
color = tint,
|
||||||
lineHeight = DockTabLabelLineHeightSp.sp,
|
fontSize = DOCK_TAB_LABEL_FONT_SIZE_SP.sp,
|
||||||
),
|
lineHeight = DOCK_TAB_LABEL_LINE_HEIGHT_SP.sp,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user