feat: adaptive navigation based on window size class (Closes #19)
Compact uses ModalNavigationDrawer with hamburger button, Medium uses NavigationRail, Expanded uses PermanentNavigationDrawer. Hamburger icon is hidden when navigation is permanently visible. Extracted shared DrawerContent and MainScaffold composables for reuse across navigation modes. Added NAV_RAIL and NAV_PERMANENT_DRAWER test tags. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -40,18 +40,29 @@ fun App(
|
||||
val windowSizeClass = WindowSizeClass.fromWidth(maxWidth)
|
||||
|
||||
CompositionLocalProvider(LocalWindowSizeClass provides windowSizeClass) {
|
||||
AppContent(onThemeChanged = {})
|
||||
AppContent()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AppContent(
|
||||
onThemeChanged: @Composable (isDark: Boolean) -> Unit = {}
|
||||
) {
|
||||
val character = EXAMPLE_CHARACTER
|
||||
var isDark by LocalThemeIsDark.current
|
||||
private fun AppContent() {
|
||||
val windowSizeClass = LocalWindowSizeClass.current
|
||||
|
||||
when (windowSizeClass) {
|
||||
WindowSizeClass.Compact -> CompactNavigation()
|
||||
WindowSizeClass.Medium -> MediumNavigation()
|
||||
WindowSizeClass.Expanded -> ExpandedNavigation()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compact layout: ModalNavigationDrawer with hamburger menu button.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun CompactNavigation() {
|
||||
val drawerState = rememberDrawerState(initialValue = DrawerValue.Closed)
|
||||
val scope = rememberCoroutineScope()
|
||||
|
||||
@@ -59,6 +70,80 @@ private fun AppContent(
|
||||
drawerState = drawerState,
|
||||
drawerContent = {
|
||||
ModalDrawerSheet {
|
||||
DrawerContent()
|
||||
}
|
||||
}
|
||||
) {
|
||||
MainScaffold(
|
||||
showMenuButton = true,
|
||||
onMenuClick = {
|
||||
scope.launch {
|
||||
if (drawerState.isClosed) drawerState.open() else drawerState.close()
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Medium layout: NavigationRail on the left side with main content.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MediumNavigation() {
|
||||
Row(modifier = Modifier.fillMaxSize()) {
|
||||
NavigationRail(
|
||||
modifier = Modifier.testTag(TestTags.NAV_RAIL)
|
||||
) {
|
||||
Spacer(Modifier.height(8.dp))
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Person, contentDescription = null) },
|
||||
label = { Text("Character") },
|
||||
selected = true,
|
||||
onClick = { /* TODO: Navigate */ },
|
||||
modifier = Modifier.testTag(TestTags.NAV_CHARACTER_SHEET)
|
||||
)
|
||||
NavigationRailItem(
|
||||
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
||||
label = { Text("Settings") },
|
||||
selected = false,
|
||||
onClick = { /* TODO: Navigate to settings */ },
|
||||
modifier = Modifier.testTag(TestTags.NAV_SETTINGS)
|
||||
)
|
||||
}
|
||||
MainScaffold(
|
||||
showMenuButton = false,
|
||||
onMenuClick = {},
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Expanded layout: PermanentNavigationDrawer always visible.
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun ExpandedNavigation() {
|
||||
PermanentNavigationDrawer(
|
||||
drawerContent = {
|
||||
PermanentDrawerSheet(modifier = Modifier.testTag(TestTags.NAV_PERMANENT_DRAWER)) {
|
||||
DrawerContent()
|
||||
}
|
||||
}
|
||||
) {
|
||||
MainScaffold(
|
||||
showMenuButton = false,
|
||||
onMenuClick = {}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared drawer content used by Compact (ModalDrawerSheet) and Expanded (PermanentDrawerSheet).
|
||||
*/
|
||||
@Composable
|
||||
private fun DrawerContent() {
|
||||
Spacer(Modifier.height(16.dp))
|
||||
Text(
|
||||
"Menu",
|
||||
@@ -72,9 +157,7 @@ private fun AppContent(
|
||||
icon = { Icon(Icons.Default.Person, contentDescription = null) },
|
||||
label = { Text("Character Sheet") },
|
||||
selected = true,
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
},
|
||||
onClick = { /* TODO: Navigate */ },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.testTag(TestTags.NAV_CHARACTER_SHEET)
|
||||
@@ -84,37 +167,45 @@ private fun AppContent(
|
||||
icon = { Icon(Icons.Default.Settings, contentDescription = null) },
|
||||
label = { Text("Settings") },
|
||||
selected = false,
|
||||
onClick = {
|
||||
scope.launch { drawerState.close() }
|
||||
// TODO: Navigate to settings
|
||||
},
|
||||
onClick = { /* TODO: Navigate to settings */ },
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 12.dp, vertical = 4.dp)
|
||||
.testTag(TestTags.NAV_SETTINGS)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main scaffold with top app bar and content area.
|
||||
*
|
||||
* @param showMenuButton whether to show the hamburger menu icon (hidden when nav is persistent)
|
||||
* @param onMenuClick callback for hamburger menu button click
|
||||
* @param modifier optional modifier (e.g., weight in Row for Medium layout)
|
||||
*/
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun MainScaffold(
|
||||
showMenuButton: Boolean,
|
||||
onMenuClick: () -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val character = EXAMPLE_CHARACTER
|
||||
var isDark by LocalThemeIsDark.current
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
TopAppBar(
|
||||
title = { Text("Shadowrun Character Sheet") },
|
||||
modifier = Modifier.testTag(TestTags.TOP_APP_BAR),
|
||||
navigationIcon = {
|
||||
if (showMenuButton) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
scope.launch {
|
||||
if (drawerState.isClosed) {
|
||||
drawerState.open()
|
||||
} else {
|
||||
drawerState.close()
|
||||
}
|
||||
}
|
||||
},
|
||||
onClick = onMenuClick,
|
||||
modifier = Modifier.testTag(TestTags.MENU_BUTTON)
|
||||
) {
|
||||
Icon(Icons.Default.Menu, contentDescription = "Menu")
|
||||
}
|
||||
}
|
||||
},
|
||||
actions = {
|
||||
IconButton(
|
||||
@@ -156,4 +247,3 @@ private fun AppContent(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,8 @@ object TestTags {
|
||||
// --- Navigation items ---
|
||||
const val NAV_CHARACTER_SHEET = "nav_character_sheet"
|
||||
const val NAV_SETTINGS = "nav_settings"
|
||||
const val NAV_RAIL = "nav_rail"
|
||||
const val NAV_PERMANENT_DRAWER = "nav_permanent_drawer"
|
||||
|
||||
// --- Dice / roll buttons ---
|
||||
fun rollButton(name: String): String = "roll_button_${name.lowercase().replace(" ", "_")}"
|
||||
|
||||
@@ -30,6 +30,8 @@ class TestTagsTest {
|
||||
fun navTagsAreDefined() {
|
||||
assertEquals("nav_character_sheet", TestTags.NAV_CHARACTER_SHEET)
|
||||
assertEquals("nav_settings", TestTags.NAV_SETTINGS)
|
||||
assertEquals("nav_rail", TestTags.NAV_RAIL)
|
||||
assertEquals("nav_permanent_drawer", TestTags.NAV_PERMANENT_DRAWER)
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -49,6 +51,8 @@ class TestTagsTest {
|
||||
TestTags.PANEL_DAMAGE_MONITOR,
|
||||
TestTags.NAV_CHARACTER_SHEET,
|
||||
TestTags.NAV_SETTINGS,
|
||||
TestTags.NAV_RAIL,
|
||||
TestTags.NAV_PERMANENT_DRAWER,
|
||||
TestTags.TOP_APP_BAR,
|
||||
TestTags.THEME_TOGGLE,
|
||||
TestTags.MENU_BUTTON
|
||||
|
||||
Reference in New Issue
Block a user