feat(app): add authenticated app shell with top bar and bottom navigation
This commit is contained in:
@@ -34,5 +34,6 @@ dependencies {
|
||||
implementation(libs.compose.ui)
|
||||
implementation(libs.compose.ui.tooling.preview)
|
||||
implementation(libs.compose.material3)
|
||||
implementation(libs.compose.material.icons.extended)
|
||||
debugImplementation(libs.compose.ui.tooling)
|
||||
}
|
||||
|
||||
+190
@@ -1,12 +1,43 @@
|
||||
package com.flagship.abox.manager.designsystem
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.LocationOn
|
||||
import androidx.compose.material.icons.filled.Person
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.Dp
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun ABoxSurface(
|
||||
@@ -36,3 +67,162 @@ fun ABoxButton(
|
||||
fun ABoxLoadingIndicator(modifier: Modifier = Modifier) {
|
||||
CircularProgressIndicator(modifier = modifier)
|
||||
}
|
||||
|
||||
/**
|
||||
* Circular user avatar displaying the initial character of username or a default person icon.
|
||||
* Styled with an accented profile ring.
|
||||
*/
|
||||
@Composable
|
||||
fun ABoxUserAvatar(
|
||||
name: String?,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
size: Dp = 36.dp,
|
||||
contentDescription: String? = null,
|
||||
) {
|
||||
val initial = name?.trim()?.takeIf { it.isNotEmpty() }?.take(1)?.uppercase()
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.primaryContainer)
|
||||
.border(
|
||||
width = 2.dp,
|
||||
color = MaterialTheme.colorScheme.primary.copy(alpha = 0.8f),
|
||||
shape = CircleShape,
|
||||
)
|
||||
.clickable(onClickLabel = contentDescription, onClick = onClick),
|
||||
contentAlignment = Alignment.Center,
|
||||
) {
|
||||
if (initial != null) {
|
||||
Text(
|
||||
text = initial,
|
||||
style = MaterialTheme.typography.titleMedium.copy(
|
||||
fontWeight = FontWeight.Bold,
|
||||
color = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Person,
|
||||
contentDescription = contentDescription,
|
||||
tint = MaterialTheme.colorScheme.onPrimaryContainer,
|
||||
modifier = Modifier.size(size * 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Material 3 pill top bar containing leading icon, title/provider label,
|
||||
* an optional '+' action button, and user avatar.
|
||||
*/
|
||||
@Composable
|
||||
fun ABoxTopPillBar(
|
||||
title: String,
|
||||
onAvatarClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
userName: String? = null,
|
||||
leadingIcon: ImageVector = Icons.Default.LocationOn,
|
||||
onAddClick: (() -> Unit)? = null,
|
||||
addActionContentDescription: String? = "Add",
|
||||
avatarContentDescription: String? = "Account",
|
||||
) {
|
||||
Surface(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(56.dp),
|
||||
shape = RoundedCornerShape(28.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f),
|
||||
tonalElevation = 2.dp,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = leadingIcon,
|
||||
contentDescription = null,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.size(22.dp),
|
||||
)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
style = MaterialTheme.typography.titleMedium.copy(fontWeight = FontWeight.SemiBold),
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
if (onAddClick != null) {
|
||||
IconButton(
|
||||
onClick = onAddClick,
|
||||
modifier = Modifier.size(36.dp),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Add,
|
||||
contentDescription = addActionContentDescription,
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ABoxUserAvatar(
|
||||
name = userName,
|
||||
onClick = onAvatarClick,
|
||||
size = 36.dp,
|
||||
contentDescription = avatarContentDescription,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ABox standard Material 3 Navigation Bar with pill indicator styling.
|
||||
*/
|
||||
@Composable
|
||||
fun ABoxNavigationBar(
|
||||
modifier: Modifier = Modifier,
|
||||
containerColor: Color = MaterialTheme.colorScheme.surface,
|
||||
content: @Composable RowScope.() -> Unit,
|
||||
) {
|
||||
NavigationBar(
|
||||
modifier = modifier,
|
||||
containerColor = containerColor,
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ABox standard Navigation Bar Item.
|
||||
*/
|
||||
@Composable
|
||||
fun RowScope.ABoxNavigationBarItem(
|
||||
selected: Boolean,
|
||||
onClick: () -> Unit,
|
||||
icon: @Composable () -> Unit,
|
||||
label: @Composable (() -> Unit)? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
enabled: Boolean = true,
|
||||
) {
|
||||
NavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
icon = icon,
|
||||
label = label,
|
||||
modifier = modifier,
|
||||
enabled = enabled,
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
selectedTextColor = MaterialTheme.colorScheme.onSurface,
|
||||
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user