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