From eb78814f648fd5b7f15220819149280ea6888e42 Mon Sep 17 00:00:00 2001 From: net <96362337+netqo@users.noreply.github.com> Date: Wed, 27 May 2026 06:47:04 -0300 Subject: [PATCH] refactor(polish): match mockup spec for the bottom navigation bar Replaces the Material 3 NavigationBar (which carried its own surface, indicator pill and icon tinting defaults) with a custom Row that mirrors the mockup bottomNav exactly (mockup/js/components.js): nav: h-16, border-t border-line, bg-[#0B0B12], grid 5 cols button: stacked icon + 9px tracked label, violet when active, muted otherwise icon: 18dp, stroked (no fill), stroke-width 2 Each tab uses a custom vector drawable ported verbatim from the mockup SVG path data: ic_tab_lobby (house), ic_tab_wallet (rect + clasp), ic_tab_history (clock), ic_tab_news (document with text lines), ic_tab_profile (head + shoulders). Drawables live in res/drawable so the Icon composable can tint them by route activity without per-tab Compose code. Cleanups noted in self-review: * Top border was drawn at y=0, which (because drawLine centers strokes on the given coordinate) left half of the 1dp line clipped. Offset by strokePx/2 so the entire border is visible. * The icon contentDescription was set to the tab label, duplicating the visible label below for TalkBack users. Null it out so the tab is announced once via the Text content. No behavior change; the same callback fires on tab clicks and the visibility rule (PrimaryTab.routePaths) is untouched. --- .../ui/components/StackBottomBar.kt | 149 ++++++++++++++---- app/src/main/res/drawable/ic_tab_history.xml | 20 +++ app/src/main/res/drawable/ic_tab_lobby.xml | 18 +++ app/src/main/res/drawable/ic_tab_news.xml | 19 +++ app/src/main/res/drawable/ic_tab_profile.xml | 20 +++ app/src/main/res/drawable/ic_tab_wallet.xml | 19 +++ 6 files changed, 218 insertions(+), 27 deletions(-) create mode 100644 app/src/main/res/drawable/ic_tab_history.xml create mode 100644 app/src/main/res/drawable/ic_tab_lobby.xml create mode 100644 app/src/main/res/drawable/ic_tab_news.xml create mode 100644 app/src/main/res/drawable/ic_tab_profile.xml create mode 100644 app/src/main/res/drawable/ic_tab_wallet.xml diff --git a/app/src/main/java/com/plainstudio/stackcasino/ui/components/StackBottomBar.kt b/app/src/main/java/com/plainstudio/stackcasino/ui/components/StackBottomBar.kt index d16b52e..83971f7 100644 --- a/app/src/main/java/com/plainstudio/stackcasino/ui/components/StackBottomBar.kt +++ b/app/src/main/java/com/plainstudio/stackcasino/ui/components/StackBottomBar.kt @@ -1,48 +1,143 @@ package com.plainstudio.stackcasino.ui.components -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.outlined.AccountBalanceWallet -import androidx.compose.material.icons.outlined.History -import androidx.compose.material.icons.outlined.Home -import androidx.compose.material.icons.outlined.Newspaper -import androidx.compose.material.icons.outlined.Person +import androidx.annotation.DrawableRes +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.size import androidx.compose.material3.Icon -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.drawBehind +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import com.plainstudio.stackcasino.R import com.plainstudio.stackcasino.navigation.PrimaryTab +import com.plainstudio.stackcasino.ui.theme.AccentViolet +import com.plainstudio.stackcasino.ui.theme.SurfaceBase +import com.plainstudio.stackcasino.ui.theme.SurfaceOutline +import com.plainstudio.stackcasino.ui.theme.TextMedium /** - * Material 3 bottom navigation bar bound to the five top-level - * destinations defined in [PrimaryTab]. Tab visibility is owned by the - * caller: render this only when the current destination is one of - * [PrimaryTab.route]. + * Bottom navigation bar mirroring the mockup spec + * (mockup/js/components.js, `bottomNav`): + * + * nav: h-16, border-t border-line, bg-[#0B0B12], grid 5 cols + * button: stacked icon + 9px tracked label, violet when active, + * muted otherwise + * icon: 18dp, stroked (no fill), stroke-width 2 + * + * Drawn as a custom [Row] instead of `androidx.compose.material3.NavigationBar` + * because the Material 3 default surface, indicator pill and icon + * tinting would all need overrides; a plain row matches the mockup + * exactly with less ceremony. + * + * Tab visibility is owned by the caller: render this only when the + * current destination is one of [PrimaryTab.route]. */ @Composable fun StackBottomBar( currentRoute: String?, onTabSelected: (PrimaryTab) -> Unit, + modifier: Modifier = Modifier, ) { - NavigationBar { - PrimaryTab.entries.forEach { tab -> - NavigationBarItem( - selected = currentRoute == tab.route.path, - onClick = { onTabSelected(tab) }, - icon = { Icon(imageVector = tab.icon, contentDescription = tab.label) }, - label = { Text(tab.label) }, - ) + Surface( + modifier = + modifier + .fillMaxWidth() + .height(BarHeight), + color = SurfaceBase, + ) { + Row( + modifier = + Modifier + .fillMaxSize() + .drawBehind { + val strokePx = TopBorderWidth.toPx() + // drawLine centers the stroke on the given coordinate; + // offsetting by half its width keeps the entire border + // visible inside the row instead of being clipped at + // the top edge. + val centerY = strokePx / 2f + drawLine( + color = SurfaceOutline, + start = Offset(0f, centerY), + end = Offset(size.width, centerY), + strokeWidth = strokePx, + ) + }, + ) { + PrimaryTab.entries.forEach { tab -> + BottomNavTab( + tab = tab, + isActive = currentRoute == tab.route.path, + onClick = { onTabSelected(tab) }, + modifier = Modifier.weight(1f), + ) + } } } } -private val PrimaryTab.icon: ImageVector +@Composable +private fun BottomNavTab( + tab: PrimaryTab, + isActive: Boolean, + onClick: () -> Unit, + modifier: Modifier = Modifier, +) { + val tint = if (isActive) AccentViolet else TextMedium + Column( + modifier = + modifier + .fillMaxHeight() + .clickable(onClick = onClick), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + painter = painterResource(tab.iconRes), + // The label below already announces the tab to TalkBack; a + // contentDescription on the icon would cause it to read twice. + contentDescription = null, + tint = tint, + modifier = Modifier.size(IconSize), + ) + Spacer(modifier = Modifier.height(IconLabelGap)) + Text( + text = tab.label.uppercase(), + color = tint, + fontSize = LabelFontSize, + letterSpacing = LabelLetterSpacing, + ) + } +} + +@get:DrawableRes +private val PrimaryTab.iconRes: Int get() = when (this) { - PrimaryTab.Lobby -> Icons.Outlined.Home - PrimaryTab.Wallet -> Icons.Outlined.AccountBalanceWallet - PrimaryTab.History -> Icons.Outlined.History - PrimaryTab.News -> Icons.Outlined.Newspaper - PrimaryTab.Profile -> Icons.Outlined.Person + PrimaryTab.Lobby -> R.drawable.ic_tab_lobby + PrimaryTab.Wallet -> R.drawable.ic_tab_wallet + PrimaryTab.History -> R.drawable.ic_tab_history + PrimaryTab.News -> R.drawable.ic_tab_news + PrimaryTab.Profile -> R.drawable.ic_tab_profile } + +private val BarHeight = 64.dp +private val TopBorderWidth = 1.dp +private val IconSize = 18.dp +private val IconLabelGap = 4.dp +private val LabelFontSize = 9.sp +private val LabelLetterSpacing = 1.2.sp diff --git a/app/src/main/res/drawable/ic_tab_history.xml b/app/src/main/res/drawable/ic_tab_history.xml new file mode 100644 index 0000000..aaa4b0a --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_history.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tab_lobby.xml b/app/src/main/res/drawable/ic_tab_lobby.xml new file mode 100644 index 0000000..6a3c89e --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_lobby.xml @@ -0,0 +1,18 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tab_news.xml b/app/src/main/res/drawable/ic_tab_news.xml new file mode 100644 index 0000000..4765a3f --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_news.xml @@ -0,0 +1,19 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tab_profile.xml b/app/src/main/res/drawable/ic_tab_profile.xml new file mode 100644 index 0000000..533dd7b --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_profile.xml @@ -0,0 +1,20 @@ + + + + diff --git a/app/src/main/res/drawable/ic_tab_wallet.xml b/app/src/main/res/drawable/ic_tab_wallet.xml new file mode 100644 index 0000000..5cc30c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_tab_wallet.xml @@ -0,0 +1,19 @@ + + + +