Switch from navigation 2 to navigation 3

This commit is contained in:
2026-05-12 22:42:36 +02:00
parent 15d2d9ad13
commit 8f4903a055
11 changed files with 219 additions and 215 deletions

View File

@@ -90,7 +90,7 @@ kotlin {
implementation(libs.kotlinx.serializationJson) implementation(libs.kotlinx.serializationJson)
implementation(libs.multiplatform.settings) implementation(libs.multiplatform.settings)
implementation(libs.lokksmith.compose) implementation(libs.lokksmith.compose)
implementation(libs.navigation.compose) implementation(libs.navigation3.ui)
implementation(libs.compose.unstyled) implementation(libs.compose.unstyled)
implementation(libs.compose.icons.lucide) implementation(libs.compose.icons.lucide)
implementation(libs.liquid) implementation(libs.liquid)

View File

@@ -12,8 +12,9 @@ import org.koin.plugin.module.dsl.viewModel
val shellModule = val shellModule =
module { module {
// Tab ViewModels — empty-state-only this phase; feature phases extend them. // Tab ViewModels — empty-state-only this phase; feature phases extend them.
// Active-tab tracking lives in NavController (currentBackStackEntryAsState), // Active-tab tracking lives in TabNavigator (a `remember`-scoped state holder
// not in a shell-level VM, so there is no ShellViewModel to register. // owned by AppShell), not in a shell-level VM, so there is no ShellViewModel
// to register.
viewModel<PlannerViewModel>() viewModel<PlannerViewModel>()
viewModel<RecipesViewModel>() viewModel<RecipesViewModel>()
viewModel<PantryViewModel>() viewModel<PantryViewModel>()

View File

@@ -16,8 +16,11 @@ import recipe.composeapp.generated.resources.shell_tab_shopping
/** /**
* The 4 bottom-bar destinations in left→right order per CONTEXT D-03: * The 4 bottom-bar destinations in left→right order per CONTEXT D-03:
* Planner / Recipes / Pantry / Shopping. The first entry (Planner) is the * Planner / Recipes / Pantry / Shopping. The first entry (Planner) is the
* default landing tab — CONTEXT D-03 departs from REQUIREMENTS' literal listing * default landing tab — CONTEXT D-03 departs from REQUIREMENTS' literal
* order, which research confirmed is non-binding. * listing order, which research confirmed is non-binding.
*
* Each destination carries its tab's *root* [Screen] (e.g. [Screen.Planner.Home])
* so the shell's [TabNavigator] knows where each tab's back stack starts.
* *
* Per-tab contextual chrome (search button on Recipes / Pantry, future filter * Per-tab contextual chrome (search button on Recipes / Pantry, future filter
* buttons elsewhere) is owned by each screen via the slot pattern in * buttons elsewhere) is owned by each screen via the slot pattern in
@@ -25,27 +28,27 @@ import recipe.composeapp.generated.resources.shell_tab_shopping
* intentionally minimal: route + label + icon, nothing about feature affordances. * intentionally minimal: route + label + icon, nothing about feature affordances.
*/ */
enum class BottomBarDestination( enum class BottomBarDestination(
val graphRoute: Any, val startDestination: Screen,
val labelRes: StringResource, val labelRes: StringResource,
val icon: ImageVector, val icon: ImageVector,
) { ) {
Planner( Planner(
graphRoute = PlannerGraph, startDestination = Screen.Planner.Home,
labelRes = Res.string.shell_tab_planner, labelRes = Res.string.shell_tab_planner,
icon = Lucide.CalendarDays, icon = Lucide.CalendarDays,
), ),
Recipes( Recipes(
graphRoute = RecipesGraph, startDestination = Screen.Recipes.Home,
labelRes = Res.string.shell_tab_recipes, labelRes = Res.string.shell_tab_recipes,
icon = Lucide.BookOpenText, icon = Lucide.BookOpenText,
), ),
Pantry( Pantry(
graphRoute = PantryGraph, startDestination = Screen.Pantry.Home,
labelRes = Res.string.shell_tab_pantry, labelRes = Res.string.shell_tab_pantry,
icon = Lucide.Package, icon = Lucide.Package,
), ),
Shopping( Shopping(
graphRoute = ShoppingGraph, startDestination = Screen.Shopping.Home,
labelRes = Res.string.shell_tab_shopping, labelRes = Res.string.shell_tab_shopping,
icon = Lucide.ShoppingCart, icon = Lucide.ShoppingCart,
), ),

View File

@@ -1,28 +0,0 @@
package dev.ulfrx.recipe.navigation
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.NavHostController
/**
* Multi-back-stack tab navigation per UI-03 + RESEARCH § Pattern 1 (lines 304-339).
*
* Applies the canonical four-flag incantation:
* - `popUpTo(graph.findStartDestination().id) { saveState = true }` — saves the
* current tab's stack so re-selecting the tab later restores it.
* - `launchSingleTop = true` — selecting an already-active tab does NOT push a
* duplicate onto the back stack.
* - `restoreState = true` — when the destination tab is re-selected, restore its
* saved state instead of recreating it. CRITICAL: without this flag, ViewModels
* are re-created on every reselection (RESEARCH § Pitfall B).
*
* @param graphRoute the @Serializable graph route (e.g. PlannerGraph, RecipesGraph)
*/
fun NavHostController.navigateToTab(graphRoute: Any) {
navigate(graphRoute) {
popUpTo(graph.findStartDestination().id) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
}

View File

@@ -0,0 +1,94 @@
package dev.ulfrx.recipe.navigation
import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.togetherWith
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.NavDisplay
import dev.ulfrx.recipe.ui.screens.pantry.PantryScreen
import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerScreen
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesScreen
import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingScreen
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
import org.koin.compose.viewmodel.koinViewModel
/**
* Nav 3 host for the 4-tab shell. Renders **one** [NavDisplay] for the active
* tab, swapped via [AnimatedContent] when [TabNavigator.activeTab] changes.
*
* ## Why one display per active tab, not one shared display
* Nav 3's [NavDisplay] takes a single back stack. A shell with parallel tab
* stacks therefore needs either:
* - one display that re-keys when the tab changes (this implementation), or
* - four always-composed displays stacked z-order, alpha-toggled by tab.
*
* The re-keyed approach matches the reference [To-Do-CMP](https://github.com/stevdza-san/To-Do-CMP)
* structure 1:1, gives a clean 180 ms cross-fade between tabs, and avoids the
* predictive-back-handler arbitration headache that comes with multiple live
* NavDisplays competing for the gesture (see `NavDisplay.kt` source —
* `NavigationBackHandler` is enabled when `previousEntries.isNotEmpty()`, which
* is per-display and would mis-fire if multiple displays were alive at once).
*
* ## ViewModel lifetime
* No `rememberViewModelStoreNavEntryDecorator` is installed, so `koinViewModel`
* inside `entry<…>` resolves through the **host** `ViewModelStoreOwner`
* (Android: the Activity; iOS: the root Compose owner). That keeps tab VMs
* alive across tab switches even though the per-tab [NavDisplay] is unmounted
* during cross-fade — matches the previous Nav-2 multi-back-stack behaviour
* (`saveState=true`/`restoreState=true`).
*
* Phase 5+ introduces detail screens with their own VM scopes; at that point
* we'll add `rememberViewModelStoreNavEntryDecorator()` for **detail** entries
* specifically (passed via `entryDecorators = listOf(...)`).
*/
@Composable
fun RootNavDisplay(
navigator: TabNavigator,
modifier: Modifier = Modifier,
) {
AnimatedContent(
targetState = navigator.activeTab,
modifier = modifier,
transitionSpec = {
fadeIn(tween(durationMillis = 180)) togetherWith
fadeOut(tween(durationMillis = 180))
},
label = "RootNavDisplay tab cross-fade",
) { tab ->
NavDisplay(
backStack = navigator.backStackFor(tab),
modifier = Modifier.fillMaxSize(),
onBack = { navigator.goBack(tab) },
entryProvider = entryProvider {
entry<Screen.Planner.Home> {
val vm: PlannerViewModel = koinViewModel()
PlannerScreen(viewModel = vm)
}
entry<Screen.Recipes.Home> {
val vm: RecipesViewModel = koinViewModel()
val searchVm: RecipesSearchViewModel = koinViewModel()
RecipesScreen(viewModel = vm, searchViewModel = searchVm)
}
entry<Screen.Pantry.Home> {
val vm: PantryViewModel = koinViewModel()
val searchVm: PantrySearchViewModel = koinViewModel()
PantryScreen(viewModel = vm, searchViewModel = searchVm)
}
entry<Screen.Shopping.Home> {
val vm: ShoppingViewModel = koinViewModel()
ShoppingScreen(viewModel = vm)
}
},
)
}
}

View File

@@ -1,97 +0,0 @@
package dev.ulfrx.recipe.navigation
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.navigation
import dev.ulfrx.recipe.ui.screens.pantry.PantryScreen
import dev.ulfrx.recipe.ui.screens.pantry.PantrySearchViewModel
import dev.ulfrx.recipe.ui.screens.pantry.PantryViewModel
import dev.ulfrx.recipe.ui.screens.planner.PlannerScreen
import dev.ulfrx.recipe.ui.screens.planner.PlannerViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesScreen
import dev.ulfrx.recipe.ui.screens.recipes.RecipesSearchViewModel
import dev.ulfrx.recipe.ui.screens.recipes.RecipesViewModel
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingScreen
import dev.ulfrx.recipe.ui.screens.shopping.ShoppingViewModel
import org.koin.compose.viewmodel.koinViewModel
/**
* Root of the app shell's navigation. Hosts ONE root [NavHost] containing four
* [navigation] sub-graphs (one per tab) so each tab preserves its own back stack
* independently across tab switches (RESEARCH § Pattern 1; UI-03).
*
* Default start destination: [PlannerGraph] per CONTEXT D-03.
*
* Per-tab ViewModel scoping: each composable<*Home> block retrieves the parent
* graph's [androidx.navigation.NavBackStackEntry] via
* `navController.getBackStackEntry(*Graph)` and passes it as `viewModelStoreOwner`
* to `koinViewModel(...)`. This makes per-tab VMs survive within the graph
* (RESEARCH § Pattern 2) — Phase 5 detail screens inherit cleanly.
*/
@Composable
fun RootNavHost(
navController: NavHostController,
modifier: Modifier = Modifier,
) {
NavHost(
navController = navController,
startDestination = PlannerGraph,
modifier = modifier.fillMaxSize(),
) {
// ---- Planner graph (default landing — D-03) ----
navigation<PlannerGraph>(startDestination = PlannerHome) {
composable<PlannerHome> { entry ->
val parent =
remember(entry) {
navController.getBackStackEntry(PlannerGraph)
}
val vm: PlannerViewModel = koinViewModel(viewModelStoreOwner = parent)
PlannerScreen(viewModel = vm)
}
// future: composable<PlannerDetail>{ ... }
}
// ---- Recipes graph ----
navigation<RecipesGraph>(startDestination = RecipesHome) {
composable<RecipesHome> { entry ->
val parent =
remember(entry) {
navController.getBackStackEntry(RecipesGraph)
}
val vm: RecipesViewModel = koinViewModel(viewModelStoreOwner = parent)
val searchVm: RecipesSearchViewModel = koinViewModel(viewModelStoreOwner = parent)
RecipesScreen(viewModel = vm, searchViewModel = searchVm)
}
}
// ---- Pantry graph ----
navigation<PantryGraph>(startDestination = PantryHome) {
composable<PantryHome> { entry ->
val parent =
remember(entry) {
navController.getBackStackEntry(PantryGraph)
}
val vm: PantryViewModel = koinViewModel(viewModelStoreOwner = parent)
val searchVm: PantrySearchViewModel = koinViewModel(viewModelStoreOwner = parent)
PantryScreen(viewModel = vm, searchViewModel = searchVm)
}
}
// ---- Shopping graph ----
navigation<ShoppingGraph>(startDestination = ShoppingHome) {
composable<ShoppingHome> { entry ->
val parent =
remember(entry) {
navController.getBackStackEntry(ShoppingGraph)
}
val vm: ShoppingViewModel = koinViewModel(viewModelStoreOwner = parent)
ShoppingScreen(viewModel = vm)
}
}
}
}

View File

@@ -1,33 +0,0 @@
package dev.ulfrx.recipe.navigation
import kotlinx.serialization.Serializable
/**
* Type-safe route definitions for the 4-tab app shell (CONTEXT D-03).
* Each tab graph has a serializable route type and a home (start) destination.
* Phase 5+ extends each graph with detail destinations (RESEARCH § Pattern 1).
*/
@Serializable
data object PlannerGraph
@Serializable
data object PlannerHome
@Serializable
data object RecipesGraph
@Serializable
data object RecipesHome
@Serializable
data object PantryGraph
@Serializable
data object PantryHome
@Serializable
data object ShoppingGraph
@Serializable
data object ShoppingHome

View File

@@ -0,0 +1,35 @@
package dev.ulfrx.recipe.navigation
import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
/**
* Type-safe Nav 3 destinations. Each leaf is a `@Serializable` `NavKey` so the
* back stack can be persisted (Nav 3 uses kotlinx-serialization for restoration).
*
* Screens are grouped by tab so future detail destinations (Phase 5+) slot in
* without polluting the top-level namespace — e.g. `Screen.Recipes.Detail(id)`.
* The grouping is purely a code-organisation convenience; Nav 3 treats each
* leaf as an independent NavKey regardless of nesting.
*/
sealed interface Screen : NavKey {
sealed interface Planner : Screen {
@Serializable
data object Home : Planner
}
sealed interface Recipes : Screen {
@Serializable
data object Home : Recipes
}
sealed interface Pantry : Screen {
@Serializable
data object Home : Pantry
}
sealed interface Shopping : Screen {
@Serializable
data object Home : Shopping
}
}

View File

@@ -0,0 +1,51 @@
package dev.ulfrx.recipe.navigation
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.mutableStateListOf
@Stable
class TabNavigator(
initialTab: BottomBarDestination = BottomBarDestination.Default,
) {
private val backStacks: Map<BottomBarDestination, SnapshotStateList<Screen>> =
BottomBarDestination.entries.associateWith { dest -> mutableStateListOf(dest.startDestination) }
var activeTab: BottomBarDestination by mutableStateOf(initialTab)
private set
val activeBackStack: SnapshotStateList<Screen>
get() = backStacks.getValue(activeTab)
fun backStackFor(tab: BottomBarDestination): SnapshotStateList<Screen> =
backStacks.getValue(tab)
fun selectTab(tab: BottomBarDestination) {
if (tab == activeTab) {
popToRoot(tab)
} else {
activeTab = tab
}
}
fun navigateTo(screen: Screen) {
activeBackStack.add(screen)
}
fun goBack(tab: BottomBarDestination = activeTab) {
val stack = backStacks.getValue(tab)
if (stack.size > 1) {
stack.removeAt(stack.lastIndex)
}
}
private fun popToRoot(tab: BottomBarDestination) {
val stack = backStacks.getValue(tab)
while (stack.size > 1) {
stack.removeAt(stack.lastIndex)
}
}
}

View File

@@ -20,24 +20,14 @@ import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
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.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavBackStackEntry
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.ulfrx.recipe.navigation.BottomBarDestination import dev.ulfrx.recipe.navigation.BottomBarDestination
import dev.ulfrx.recipe.navigation.PantryGraph import dev.ulfrx.recipe.navigation.RootNavDisplay
import dev.ulfrx.recipe.navigation.PlannerGraph import dev.ulfrx.recipe.navigation.TabNavigator
import dev.ulfrx.recipe.navigation.RecipesGraph
import dev.ulfrx.recipe.navigation.RootNavHost
import dev.ulfrx.recipe.navigation.ShoppingGraph
import dev.ulfrx.recipe.navigation.navigateToTab
import dev.ulfrx.recipe.ui.components.dock.DockBar import dev.ulfrx.recipe.ui.components.dock.DockBar
import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource import dev.ulfrx.recipe.ui.components.glass.GlassBackdropSource
import dev.ulfrx.recipe.ui.theme.RecipeTheme import dev.ulfrx.recipe.ui.theme.RecipeTheme
@@ -48,7 +38,7 @@ import dev.ulfrx.recipe.ui.theme.RecipeTheme
* *
* ## Layout * ## Layout
* - Background: full-screen [RecipeTheme.colors.background] under the safe area. * - Background: full-screen [RecipeTheme.colors.background] under the safe area.
* - Body: [RootNavHost] consumes the full screen, wrapped in [GlassBackdropSource] * - Body: [RootNavDisplay] consumes the full screen, wrapped in [GlassBackdropSource]
* so Liquid chrome samples the screen body through `LocalGlassBackdropState`. * so Liquid chrome samples the screen body through `LocalGlassBackdropState`.
* - Bottom chrome (overlay): the active screen contributes its own contextual * - Bottom chrome (overlay): the active screen contributes its own contextual
* chrome via [ShellChromeState] / [LocalShellChrome] (see [ShellChrome.kt]). * chrome via [ShellChromeState] / [LocalShellChrome] (see [ShellChrome.kt]).
@@ -61,9 +51,20 @@ import dev.ulfrx.recipe.ui.theme.RecipeTheme
* - Pitfall F: navigationBars + ime padding only; no `safeContentPadding()`. * - Pitfall F: navigationBars + ime padding only; no `safeContentPadding()`.
* *
* ## Active-tab tracking * ## Active-tab tracking
* Derived from the NavHost's current back stack entry hierarchy via [hasRoute]. * Read directly from [TabNavigator.activeTab], which is `MutableState<…>` so
* No mirror state in a ShellViewModel is needed — `currentBackStackEntryAsState()` * recomposition is automatic on tab switch. No mirror state needed — the
* is the single source of truth, and re-derives synchronously after every nav. * navigator is the single source of truth.
*
* ## Why TabNavigator and not the AndroidX NavController
* Phase 2.1 originally wired the dock through a single Nav-2 `NavHost` with
* four nested `navigation<…>` sub-graphs using `popUpTo + saveState +
* restoreState` for multi-back-stack. Nav 3 replaces that with an app-owned
* back stack (a `SnapshotStateList<NavKey>`), so [TabNavigator] holds **one
* stack per tab** and [RootNavDisplay] renders only the active one inside a
* `NavDisplay`. The implementation closely mirrors the
* [To-Do-CMP reference](https://github.com/stevdza-san/To-Do-CMP) pattern —
* `Navigator` + `NavDisplay(entryProvider = …)` — extended for the dock's
* parallel-stack requirement.
* *
* ## What used to live here, and why it moved * ## What used to live here, and why it moved
* The previous version of this file knew about `RecipesSearchViewModel` and * The previous version of this file knew about `RecipesSearchViewModel` and
@@ -76,12 +77,7 @@ import dev.ulfrx.recipe.ui.theme.RecipeTheme
@Preview @Preview
@Composable @Composable
fun AppShell(modifier: Modifier = Modifier) { fun AppShell(modifier: Modifier = Modifier) {
val navController = rememberNavController() val navigator = remember { TabNavigator() }
val backStack by navController.currentBackStackEntryAsState()
val activeTab =
remember(backStack) {
backStack?.toBottomBarDestination() ?: BottomBarDestination.Default
}
// Single chrome-state holder for the lifetime of the shell. Provided to // Single chrome-state holder for the lifetime of the shell. Provided to
// descendants via LocalShellChrome. // descendants via LocalShellChrome.
@@ -99,11 +95,11 @@ fun AppShell(modifier: Modifier = Modifier) {
.fillMaxSize() .fillMaxSize()
.background(RecipeTheme.colors.background), .background(RecipeTheme.colors.background),
) { ) {
// Body — RootNavHost fills the available space and is the shared source // Body — RootNavDisplay fills the available space and is the shared source
// layer for Liquid chrome sampling via GlassBackdropSource (plan 02.1-03). // layer for Liquid chrome sampling via GlassBackdropSource (plan 02.1-03).
GlassBackdropSource(modifier = Modifier.fillMaxSize()) { GlassBackdropSource(modifier = Modifier.fillMaxSize()) {
RootNavHost( RootNavDisplay(
navController = navController, navigator = navigator,
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) )
} }
@@ -140,8 +136,8 @@ fun AppShell(modifier: Modifier = Modifier) {
chrome.bottomOverlay?.invoke() chrome.bottomOverlay?.invoke()
} else { } else {
DefaultDockRow( DefaultDockRow(
activeTab = activeTab, activeTab = navigator.activeTab,
onTabSelect = { dest -> navController.navigateToTab(dest.graphRoute) }, onTabSelect = navigator::selectTab,
trailingSlot = chrome.trailingSlot, trailingSlot = chrome.trailingSlot,
) )
} }
@@ -176,20 +172,3 @@ private fun DefaultDockRow(
} }
} }
} }
/**
* Maps a [NavBackStackEntry]'s current route hierarchy to a [BottomBarDestination].
* CMP nav-compose 2.9.2 supports type-safe [hasRoute] matching against
* @Serializable graph types.
*/
private fun NavBackStackEntry?.toBottomBarDestination(): BottomBarDestination? {
if (this == null) return null
val hierarchy = destination.hierarchy
return when {
hierarchy.any { it.hasRoute(PlannerGraph::class) } -> BottomBarDestination.Planner
hierarchy.any { it.hasRoute(RecipesGraph::class) } -> BottomBarDestination.Recipes
hierarchy.any { it.hasRoute(PantryGraph::class) } -> BottomBarDestination.Pantry
hierarchy.any { it.hasRoute(ShoppingGraph::class) } -> BottomBarDestination.Shopping
else -> null
}
}

View File

@@ -19,7 +19,7 @@ ktor = "3.4.2"
lokksmith = "0.13.0" lokksmith = "0.13.0"
logback = "1.5.32" logback = "1.5.32"
multiplatformSettings = "1.3.0" multiplatformSettings = "1.3.0"
navigation-compose = "2.9.2" navigation3 = "1.1.1"
compose-unstyled = "1.49.9" compose-unstyled = "1.49.9"
compose-icons = "2.2.1" compose-icons = "2.2.1"
liquid = "1.1.1" liquid = "1.1.1"
@@ -88,8 +88,7 @@ ktor-serializationKotlinxJsonMpp = { module = "io.ktor:ktor-serialization-kotlin
lokksmith-compose = { module = "dev.lokksmith:lokksmith-compose", version.ref = "lokksmith" } lokksmith-compose = { module = "dev.lokksmith:lokksmith-compose", version.ref = "lokksmith" }
multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" } multiplatform-settings = { module = "com.russhwolf:multiplatform-settings", version.ref = "multiplatformSettings" }
# Phase 2.1 — App shell foundation (UI-03, UI-04, UI-09, UI-10) navigation3-ui = { module = "org.jetbrains.androidx.navigation3:navigation3-ui", version.ref = "navigation3" }
navigation-compose = { module = "org.jetbrains.androidx.navigation:navigation-compose", version.ref = "navigation-compose" }
compose-unstyled = { module = "com.composables:composeunstyled", version.ref = "compose-unstyled" } compose-unstyled = { module = "com.composables:composeunstyled", version.ref = "compose-unstyled" }
compose-icons-lucide = { module = "com.composables:icons-lucide-cmp", version.ref = "compose-icons" } compose-icons-lucide = { module = "com.composables:icons-lucide-cmp", version.ref = "compose-icons" }
liquid = { module = "io.github.fletchmckee.liquid:liquid", version.ref = "liquid" } liquid = { module = "io.github.fletchmckee.liquid:liquid", version.ref = "liquid" }