From 0b1cebf923b21341351bb28e0a39b0a46f1a3627 Mon Sep 17 00:00:00 2001 From: Brandon McAnsh Date: Wed, 12 Aug 2026 17:30:32 -0400 Subject: [PATCH] fix(v2): host the tab bar as a bottom overlay, not a scaffold bottomBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit As a Material2 Scaffold bottomBar the tab bar couldn't win either way: hiding it for a modal collapsed the scaffold's bottom slot and re-laid-out the content beneath (re-fanning the wallet's collapsing card stack), while keeping it visible drew it on top of the modal (bottomBar is painted over content). Move it to a bottom overlay over full-height nav content, as its own docs always described. The body no longer resizes when the bar hides, so hiding it for a BottomBar modal (restored) or a bill is free — no jump — and the modal renders over the now-absent bar. LocalTabBarPadding is provided from the bar's tallest measured height, latched and gated to tab homes, so a modal-driven hide doesn't shrink the inset. Content now also scrolls edge-to-edge under the frosted bar, which is what the Haze blur wants. --- .../app/internal/ui/AppNavigationBar.kt | 11 +++-- .../app/internal/ui/navigation/AppContent.kt | 46 +++++++++++++++---- 2 files changed, 43 insertions(+), 14 deletions(-) diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt index d4fb9ff92..35afd328b 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/AppNavigationBar.kt @@ -27,6 +27,7 @@ import com.flipcash.app.core.ui.rememberNavigationBarState import com.flipcash.app.featureflags.FeatureFlag import com.flipcash.app.featureflags.LocalFeatureFlags import com.flipcash.app.session.LocalSessionController +import com.getcode.manager.BottomBarManager import com.getcode.navigation.core.CodeNavigator import com.getcode.theme.CodeTheme import kotlinx.coroutines.flow.flowOf @@ -54,6 +55,11 @@ internal fun AppNavigationBar( val selectedTab = navigator.backStack.firstNotNullOfOrNull { (it as? AppRoute)?.asNavBarTab() } val topTab = (navigator.currentRouteKey as? AppRoute)?.asNavBarTab() + // A BottomBar modal (e.g. Add Money) renders in the nav content, above this bar; hide the bar so + // it doesn't draw over the modal. Safe because the bar is a bottom overlay (not a scaffold + // bottomBar), so hiding it doesn't resize the content beneath (see NewAppContent). + val bottomBarMessages by BottomBarManager.messages.collectAsStateWithLifecycle() + // A bill/tip card renders at the app root above everything; hide the bar so it doesn't show // beneath the presented bill. val session = LocalSessionController.current @@ -66,11 +72,8 @@ internal fun AppNavigationBar( .then(modifier), contentAlignment = Alignment.BottomCenter, ) { - // Keep the bar in place for BottomBar modals (e.g. Add Money): the modal's scrim renders over - // it via z-order, and hiding it would collapse the scaffold's bottom slot and re-lay-out the - // tab content beneath (visibly re-fanning the wallet's collapsing card stack). AnimatedVisibility( - visible = topTab != null && !billUp, + visible = topTab != null && bottomBarMessages.isEmpty() && !billUp, enter = slideInVertically { it } + fadeIn(), exit = slideOutVertically { it } + fadeOut(), ) { diff --git a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt index dd070471e..f13283eb7 100644 --- a/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt +++ b/apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/ui/navigation/AppContent.kt @@ -9,9 +9,17 @@ import androidx.compose.animation.slideInHorizontally import androidx.compose.animation.slideOutHorizontally import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp import androidx.navigation3.runtime.NavKey import dev.chrisbanes.haze.hazeSource import dev.chrisbanes.haze.rememberHazeState @@ -30,6 +38,7 @@ import com.getcode.navigation.core.CodeNavigator import com.getcode.navigation.results.NavResultStateRegistry import com.getcode.navigation.scenes.ModalBottomSheetSceneStrategy import com.getcode.ui.components.bars.BarManager +import com.getcode.ui.core.measured import com.getcode.ui.theme.CodeScaffold import dev.theolm.rinku.DeepLink @@ -135,16 +144,22 @@ internal fun NewAppContent( onPendingAction: (DeeplinkAction) -> Unit = {}, ) { val hazeState = rememberHazeState() - CodeScaffold( - bottomBar = { - AppNavigationBar(navigator = codeNavigator, hazeState = hazeState) - } - ) { padding -> - CompositionLocalProvider(LocalTabBarPadding provides padding) { - // Mark the nav content as the haze source so the frosted tab bar blurs what scrolls - // beneath it. - Box(modifier = Modifier.hazeSource(hazeState)) { - AppNavHost( + // The tab bar is a bottom OVERLAY, not a scaffold bottomBar: the nav content stays full-height, so + // hiding the bar for a modal (or bill) never resizes it — which otherwise re-fanned the wallet's + // collapsing card stack — and content scrolls edge-to-edge under the frosted bar. Reserve space for + // it via LocalTabBarPadding, latched to the tallest measured height and only on tab homes, so a + // modal-driven hide doesn't shrink the inset. + var tabBarHeight by remember { mutableStateOf(0.dp) } + val onTabHome = (codeNavigator.currentRouteKey as? AppRoute)?.asNavBarTab() != null + val tabBarPadding = if (onTabHome) PaddingValues(bottom = tabBarHeight) else PaddingValues() + + CodeScaffold { _ -> + Box(modifier = Modifier.fillMaxSize()) { + CompositionLocalProvider(LocalTabBarPadding provides tabBarPadding) { + // Mark the nav content as the haze source so the frosted tab bar blurs what scrolls + // beneath it. + Box(modifier = Modifier.hazeSource(hazeState)) { + AppNavHost( navigator = codeNavigator, resultStateRegistry = resultStateRegistry, decorators = listOf( @@ -207,7 +222,18 @@ internal fun NewAppContent( onPendingAction = onPendingAction, ), ) + } } + + // Bottom overlay over the full-height nav content. Latch the tallest height so the + // reserved inset above stays stable when the bar hides for a modal/bill. + AppNavigationBar( + navigator = codeNavigator, + hazeState = hazeState, + modifier = Modifier + .align(Alignment.BottomCenter) + .measured { if (it.height > tabBarHeight) tabBarHeight = it.height }, + ) } } } \ No newline at end of file