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 @@
+
+
+
+