From 9dc21c126bc0ac5638e034c30bb4bcc3bfe1c57e Mon Sep 17 00:00:00 2001 From: SiteRelEnby <125829806+SiteRelEnby@users.noreply.github.com> Date: Sun, 26 Jul 2026 18:56:00 -0400 Subject: [PATCH 1/3] feat(ui): adaptive top-level navigation for large screens Replaces the fixed bottom NavigationBar with Material3's NavigationSuiteScaffold, which renders the top-level destinations as a bottom bar on compact widths (phones) and a navigation rail on wider ones (tablets, landscape, foldables, large/free-form windows). Non-top-level (full-screen) destinations show no nav chrome, as before. Breakpoint is the standard 600dp compact/medium width. First step of large-screen support: a tablet in portrait is already wide enough to get the rail, so the app stops looking like a stretched phone without any per-screen changes. Content max-width and list/detail two-pane layouts are follow-ups. --- sheaf/app/build.gradle.kts | 2 + .../java/systems/lupine/sheaf/ui/SheafApp.kt | 67 +++++++++++-------- sheaf/gradle/libs.versions.toml | 1 + 3 files changed, 41 insertions(+), 29 deletions(-) diff --git a/sheaf/app/build.gradle.kts b/sheaf/app/build.gradle.kts index 3e23b4b..d0df11a 100644 --- a/sheaf/app/build.gradle.kts +++ b/sheaf/app/build.gradle.kts @@ -141,6 +141,8 @@ dependencies { implementation(libs.androidx.ui.graphics) implementation(libs.androidx.ui.tooling.preview) implementation(libs.androidx.material3) + // Adaptive top-level navigation: bottom bar on phones, rail on wide screens. + implementation(libs.androidx.material3.adaptive.navigation.suite) implementation(libs.androidx.material.icons.extended) implementation(libs.androidx.navigation.compose) diff --git a/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt b/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt index 2396216..2d418e2 100644 --- a/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt +++ b/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt @@ -9,7 +9,10 @@ import androidx.compose.material.icons.automirrored.outlined.MenuBook import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.outlined.* import androidx.compose.material3.* +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffold +import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteType import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector import android.net.Uri @@ -204,41 +207,47 @@ fun SheafApp( LocalFileCdnBase provides fileCdnBase, LocalDisplayTimeZone provides displayZone, ) { - Scaffold( - bottomBar = { - if (showBottomBar) { - NavigationBar { - val currentDest = navBackStack?.destination - topLevelDestinations.forEach { dest -> - val selected = currentDest?.hierarchy?.any { it.route == dest.route } == true - NavigationBarItem( - selected = selected, - onClick = { - navController.navigate(dest.route) { - popUpTo(navController.graph.findStartDestination().id) { - saveState = true - } - launchSingleTop = true - restoreState = true - } - }, - icon = { - Icon( - if (selected) dest.selectedIcon else dest.unselectedIcon, - contentDescription = dest.label, - ) - }, - label = { Text(dest.label) }, + // Adaptive top-level chrome: a bottom bar on compact widths (phones), a + // navigation rail on wider ones (tablets, landscape, foldables, large + // windows), and nothing at all on non-top-level (full-screen) destinations. + // 600dp is the standard compact/medium width breakpoint. + val currentDest = navBackStack?.destination + val wide = LocalConfiguration.current.screenWidthDp >= 600 + val navSuiteType = when { + !showBottomBar -> NavigationSuiteType.None + wide -> NavigationSuiteType.NavigationRail + else -> NavigationSuiteType.NavigationBar + } + NavigationSuiteScaffold( + layoutType = navSuiteType, + navigationSuiteItems = { + topLevelDestinations.forEach { dest -> + val selected = currentDest?.hierarchy?.any { it.route == dest.route } == true + item( + selected = selected, + onClick = { + navController.navigate(dest.route) { + popUpTo(navController.graph.findStartDestination().id) { + saveState = true + } + launchSingleTop = true + restoreState = true + } + }, + icon = { + Icon( + if (selected) dest.selectedIcon else dest.unselectedIcon, + contentDescription = dest.label, ) - } - } + }, + label = { Text(dest.label) }, + ) } }, - ) { innerPadding -> + ) { NavHost( navController = navController, startDestination = Routes.LOGIN, - modifier = Modifier.padding(innerPadding), enterTransition = { fadeIn() }, exitTransition = { fadeOut() }, popEnterTransition = { fadeIn() }, diff --git a/sheaf/gradle/libs.versions.toml b/sheaf/gradle/libs.versions.toml index 2a1e711..402b432 100644 --- a/sheaf/gradle/libs.versions.toml +++ b/sheaf/gradle/libs.versions.toml @@ -49,6 +49,7 @@ androidx-ui-graphics = { group = "androidx.compose.ui", name = "ui-graphics" } androidx-ui-tooling = { group = "androidx.compose.ui", name = "ui-tooling" } androidx-ui-tooling-preview = { group = "androidx.compose.ui", name = "ui-tooling-preview" } androidx-material3 = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } +androidx-material3-adaptive-navigation-suite = { group = "androidx.compose.material3", name = "material3-adaptive-navigation-suite", version.ref = "material3" } androidx-material-icons-extended = { group = "androidx.compose.material", name = "material-icons-extended" } androidx-navigation-compose = { group = "androidx.navigation", name = "navigation-compose", version.ref = "navigationCompose" } hilt-android = { group = "com.google.dagger", name = "hilt-android", version.ref = "hilt" } From 6a0218def683f6b81f6535db633245b2e91445e9 Mon Sep 17 00:00:00 2001 From: SiteRelEnby <125829806+SiteRelEnby@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:01:30 -0400 Subject: [PATCH 2/3] feat(ui): cap content width on wide windows Wraps the NavHost content so a single pane doesn't stretch across a whole tablet: content is capped at 840dp and centred, with gutters beyond that. widthIn is a no-op below the cap, so phones and split-screen are unaffected. The relationship graph opts out (FULL_BLEED_ROUTES) since the pan/zoom canvas wants the whole area. Second step of large-screen support, on top of the adaptive navigation. --- .../java/systems/lupine/sheaf/ui/SheafApp.kt | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt b/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt index 2d418e2..2dc0ae2 100644 --- a/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt +++ b/sheaf/app/src/main/java/systems/lupine/sheaf/ui/SheafApp.kt @@ -2,7 +2,13 @@ package systems.lupine.sheaf.ui import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.ui.Alignment +import androidx.compose.ui.unit.dp import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.MenuBook import androidx.compose.material.icons.automirrored.outlined.MenuBook @@ -142,6 +148,15 @@ data class TopLevelDest( val unselectedIcon: ImageVector, ) +// Widest a single content pane grows to; beyond this, centre it with gutters +// rather than stretching forms and lists across a whole tablet. A no-op on +// anything narrower (phones, split-screen). +private val MAX_CONTENT_WIDTH = 840.dp + +// Destinations that should use the full window width rather than the capped +// content pane (a pan/zoom canvas wants all the room it can get). +private val FULL_BLEED_ROUTES = setOf(Routes.RELATIONSHIP_GRAPH) + val topLevelDestinations = listOf( TopLevelDest(Routes.HOME, "Home", Icons.Filled.Home, Icons.Outlined.Home), TopLevelDest(Routes.PEOPLE, "Members", Icons.Filled.People, Icons.Outlined.People), @@ -245,9 +260,18 @@ fun SheafApp( } }, ) { + // Cap content width on wide windows so forms and lists don't stretch + // across a whole tablet; centre what remains. widthIn is a no-op below + // the cap, so phones are unaffected. Full-bleed screens (the pan/zoom + // relationship graph) opt out and use the whole area. + val fullBleed = currentRoute in FULL_BLEED_ROUTES + val contentModifier = if (fullBleed) Modifier.fillMaxSize() + else Modifier.fillMaxHeight().widthIn(max = MAX_CONTENT_WIDTH) + Box(Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { NavHost( navController = navController, startDestination = Routes.LOGIN, + modifier = contentModifier, enterTransition = { fadeIn() }, exitTransition = { fadeOut() }, popEnterTransition = { fadeIn() }, @@ -684,6 +708,7 @@ fun SheafApp( ) } } + } } } } From fa5e11ff7a86a15ee2c4a7c9c8df593f79a35350 Mon Sep 17 00:00:00 2001 From: SiteRelEnby <125829806+SiteRelEnby@users.noreply.github.com> Date: Sun, 26 Jul 2026 21:24:30 -0400 Subject: [PATCH 3/3] fix(ui): stop pillar-boxing the app on landscape / large screens MainActivity was locked to portrait, so on a landscape tablet or foldable the system pillar-boxed the whole window - black bars either side of the app, including the navigation rail. Dropping the orientation lock lets the app fill the window and rotate, which the adaptive navigation and content max-width already handle. Android 16 forces this on large screens anyway once we bump targetSdk, so this gets us there ahead of the deadline. --- sheaf/app/src/main/AndroidManifest.xml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sheaf/app/src/main/AndroidManifest.xml b/sheaf/app/src/main/AndroidManifest.xml index 75ed739..3ecd471 100644 --- a/sheaf/app/src/main/AndroidManifest.xml +++ b/sheaf/app/src/main/AndroidManifest.xml @@ -112,8 +112,7 @@ android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" - android:windowSoftInputMode="adjustResize" - android:screenOrientation="portrait"> + android:windowSoftInputMode="adjustResize">