From 83887ee25ab22513518194a050871ebf00dfed1b Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sat, 4 Jul 2026 15:53:40 +0300 Subject: [PATCH 1/8] Carry per-screen bar behavior through the chrome contract --- .../app/navigation/NativeNavChrome.ios.kt | 8 +- nativecomposekit/api/nativecomposekit.api | 22 +++++- .../api/nativecomposekit.klib.api | 27 ++++++- .../nativecomposekit/chrome/NativeChrome.kt | 60 +++++++++++++-- .../chrome/NativeBarConfigTest.kt | 76 +++++++++++++++++++ 5 files changed, 184 insertions(+), 9 deletions(-) create mode 100644 nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt diff --git a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavChrome.ios.kt b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavChrome.ios.kt index 3e03c49..00d340c 100644 --- a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavChrome.ios.kt +++ b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavChrome.ios.kt @@ -5,6 +5,7 @@ import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.safeDrawing import androidx.compose.runtime.CompositionLocalProvider import io.github.apdelrahman1911.nativecomposekit.app.LocalNativeContentBottomInset +import io.github.apdelrahman1911.nativecomposekit.chrome.NativeBarConfig import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeAction import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeCancellable import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeEntry @@ -32,6 +33,9 @@ class NativeNavChrome( private val tabs: List, private val actionsForTab: (String) -> List, private val onAction: (String) -> Unit, + // Per-screen chrome behavior (bar visibility, large title, this screen's own actions) — the + // platform-neutral half of customization; appearance stays in each platform's own styling surface. + private val barConfigForRoute: (NativeRoute) -> NativeBarConfig = { NativeBarConfig.Default }, ) : NativeChromeSource { override fun currentState(): NativeChromeState { @@ -46,7 +50,9 @@ class NativeNavChrome( sheetId = state.sheet?.id, backTitle = stack.getOrNull(stack.size - 2)?.let(titleForRoute), backStacksByTab = state.tabs.associate { tab -> - tab.id to state.stackFor(tab).map { NativeChromeEntry(it.id, titleForRoute(it)) } + tab.id to state.stackFor(tab).map { + NativeChromeEntry(it.id, titleForRoute(it), barConfigForRoute(it)) + } }, ) } diff --git a/nativecomposekit/api/nativecomposekit.api b/nativecomposekit/api/nativecomposekit.api index 8950549..99f0dc6 100644 --- a/nativecomposekit/api/nativecomposekit.api +++ b/nativecomposekit/api/nativecomposekit.api @@ -1,3 +1,21 @@ +public final class io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig { + public static final field $stable I + public static final field Companion Lio/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig$Companion; + public fun ()V + public fun (ZZZLjava/util/List;)V + public synthetic fun (ZZZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun equals (Ljava/lang/Object;)Z + public final fun getActions ()Ljava/util/List; + public final fun getHidesTabBar ()Z + public final fun getHidesTopBar ()Z + public final fun getPrefersLargeTitle ()Z + public fun hashCode ()I +} + +public final class io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig$Companion { + public final fun getDefault ()Lio/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig; +} + public final class io/github/apdelrahman1911/nativecomposekit/chrome/NativeChromeAction { public static final field $stable I public fun (Ljava/lang/String;Ljava/lang/String;)V @@ -13,8 +31,10 @@ public abstract interface class io/github/apdelrahman1911/nativecomposekit/chrom public final class io/github/apdelrahman1911/nativecomposekit/chrome/NativeChromeEntry { public static final field $stable I - public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Lio/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig;)V + public synthetic fun (Ljava/lang/String;Ljava/lang/String;Lio/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z + public final fun getBar ()Lio/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig; public final fun getId ()Ljava/lang/String; public final fun getTitle ()Ljava/lang/String; public fun hashCode ()I diff --git a/nativecomposekit/api/nativecomposekit.klib.api b/nativecomposekit/api/nativecomposekit.klib.api index c6139b6..3691e4b 100644 --- a/nativecomposekit/api/nativecomposekit.klib.api +++ b/nativecomposekit/api/nativecomposekit.klib.api @@ -433,6 +433,27 @@ sealed interface <#A: out kotlin/Any?> io.github.apdelrahman1911.nativecomposeki } } +final class io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig { // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig|null[0] + constructor (kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin.collections/List = ...) // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.|(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.collections.List){}[0] + + final val actions // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.actions|{}actions[0] + final fun (): kotlin.collections/List // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.actions.|(){}[0] + final val hidesTabBar // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTabBar|{}hidesTabBar[0] + final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTabBar.|(){}[0] + final val hidesTopBar // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTopBar|{}hidesTopBar[0] + final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTopBar.|(){}[0] + final val prefersLargeTitle // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.prefersLargeTitle|{}prefersLargeTitle[0] + final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.prefersLargeTitle.|(){}[0] + + final fun equals(kotlin/Any?): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hashCode|hashCode(){}[0] + + final object Companion { // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.Companion|null[0] + final val Default // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.Companion.Default|{}Default[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.Companion.Default.|(){}[0] + } +} + final class io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeAction { // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeAction|null[0] constructor (kotlin/String, kotlin/String) // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeAction.|(kotlin.String;kotlin.String){}[0] @@ -446,8 +467,10 @@ final class io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeAction } final class io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry { // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry|null[0] - constructor (kotlin/String, kotlin/String) // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.|(kotlin.String;kotlin.String){}[0] + constructor (kotlin/String, kotlin/String, io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig = ...) // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.|(kotlin.String;kotlin.String;io.github.apdelrahman1911.nativecomposekit.chrome.NativeBarConfig){}[0] + final val bar // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.bar|{}bar[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.bar.|(){}[0] final val id // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.id|{}id[0] final fun (): kotlin/String // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.id.|(){}[0] final val title // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeEntry.title|{}title[0] @@ -1000,6 +1023,7 @@ final object io.github.apdelrahman1911.nativecomposekit.theme/NativeTheme { // i final fun (androidx.compose.runtime/Composer?, kotlin/Int): io.github.apdelrahman1911.nativecomposekit.theme/NativeTokens // io.github.apdelrahman1911.nativecomposekit.theme/NativeTheme.tokens.|(androidx.compose.runtime.Composer?;kotlin.Int){}[0] } +final val io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeBarConfig$stableprop // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeBarConfig$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_chrome_NativeBarConfig$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeAction$stableprop // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeAction$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeAction$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeEntry$stableprop // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeEntry$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeEntry$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeState$stableprop // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeState$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeState$stableprop[0] @@ -1068,6 +1092,7 @@ final fun (androidx.compose.ui/Modifier).io.github.apdelrahman1911.nativecompose final fun <#A: kotlin/Any?> io.github.apdelrahman1911.nativecomposekit.components/NativeContentState(io.github.apdelrahman1911.nativecomposekit.components/NativeLoadState<#A>, androidx.compose.ui/Modifier?, kotlin/Function0?, kotlin/String?, kotlin/String?, androidx.compose.ui.graphics.vector/ImageVector?, androidx.compose.ui.graphics.vector/ImageVector?, kotlin/String?, kotlin/String?, kotlin/Boolean, kotlin/Function2?, kotlin/Function2?, kotlin/Function3?, kotlin/Function3<#A, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // io.github.apdelrahman1911.nativecomposekit.components/NativeContentState|NativeContentState(io.github.apdelrahman1911.nativecomposekit.components.NativeLoadState<0:0>;androidx.compose.ui.Modifier?;kotlin.Function0?;kotlin.String?;kotlin.String?;androidx.compose.ui.graphics.vector.ImageVector?;androidx.compose.ui.graphics.vector.ImageVector?;kotlin.String?;kotlin.String?;kotlin.Boolean;kotlin.Function2?;kotlin.Function2?;kotlin.Function3?;kotlin.Function3<0:0,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){0§}[0] final fun <#A: kotlin/Any?> io.github.apdelrahman1911.nativecomposekit.components/NativeListSection(kotlin.collections/List<#A>, kotlin/Function1<#A, kotlin/Any>, androidx.compose.ui/Modifier?, kotlin/String?, kotlin/String?, io.github.apdelrahman1911.nativecomposekit.components/NativeListSectionStyle?, kotlin/Boolean, androidx.compose.ui.unit/Dp, kotlin/String?, kotlin/String?, kotlin/Function3<#A, androidx.compose.runtime/Composer, kotlin/Int, kotlin/Unit>, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int, kotlin/Int) // io.github.apdelrahman1911.nativecomposekit.components/NativeListSection|NativeListSection(kotlin.collections.List<0:0>;kotlin.Function1<0:0,kotlin.Any>;androidx.compose.ui.Modifier?;kotlin.String?;kotlin.String?;io.github.apdelrahman1911.nativecomposekit.components.NativeListSectionStyle?;kotlin.Boolean;androidx.compose.ui.unit.Dp;kotlin.String?;kotlin.String?;kotlin.Function3<0:0,androidx.compose.runtime.Composer,kotlin.Int,kotlin.Unit>;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int;kotlin.Int){0§}[0] final fun <#A: kotlin/Any?> io.github.apdelrahman1911.nativecomposekit.components/NativeRadioGroup(kotlin.collections/List<#A>, #A?, kotlin/Function1<#A, kotlin/Unit>, androidx.compose.ui/Modifier?, kotlin/Function1<#A, kotlin/String>?, kotlin/Boolean, io.github.apdelrahman1911.nativecomposekit.components/NativeSelectionStyle?, kotlin/String?, kotlin/String?, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.github.apdelrahman1911.nativecomposekit.components/NativeRadioGroup|NativeRadioGroup(kotlin.collections.List<0:0>;0:0?;kotlin.Function1<0:0,kotlin.Unit>;androidx.compose.ui.Modifier?;kotlin.Function1<0:0,kotlin.String>?;kotlin.Boolean;io.github.apdelrahman1911.nativecomposekit.components.NativeSelectionStyle?;kotlin.String?;kotlin.String?;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){0§}[0] +final fun io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeBarConfig$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeBarConfig$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_chrome_NativeBarConfig$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeAction$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeAction$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeAction$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeEntry$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeEntry$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeEntry$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeState$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.chrome/io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeState$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_chrome_NativeChromeState$stableprop_getter(){}[0] diff --git a/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt b/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt index d12f63c..839fedd 100644 --- a/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt +++ b/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt @@ -37,23 +37,71 @@ public class NativeChromeAction( } /** - * One back-stack entry as chrome may render it: a stable [id] (unique within its stack — renderers key on it) - * and the [title] shown while that entry is on screen. This is still **display data, not a navigable stack**: - * a shell may build one screen/bar item per entry and animate between them, but the only way it can change - * navigation is the intent methods — it never mutates these lists. Compares by value. + * Per-screen chrome BEHAVIOR for one back-stack entry — the platform-neutral knobs that must travel with + * the entry itself (appearance stays per-platform: Compose parameters/slots on a Material host, the iOS + * shell style registry on UIKit). Everything defaults to today's behavior; a config is pure display data + * like the rest of the projection. Compares by value; not a `data class` so fields stay appendable + * binary-compatibly. + * + * - [hidesTopBar] / [hidesTabBar] — hide the navigation/tab bar while this entry is on top (immersive + * readers, full-bleed media). Renderers animate the change with their platform's own transition. + * - [prefersLargeTitle] — opt this entry into a large navigation title where the platform supports it + * (the iOS shell; requires the shell style's large-title opt-in). NOTE: with Compose content there is no + * `UIScrollView` under the bar, so a large title does NOT collapse on scroll like native UIKit lists — + * it stays large while the entry is on top. Platforms without the concept ignore it. + * - [actions] — this entry's OWN top-bar actions (rendered as native bar button items by the iOS shell), + * replacing the tab-scoped actions while the entry is on top. Empty = no per-screen actions. + */ +@Immutable +public class NativeBarConfig( + public val hidesTopBar: Boolean = false, + public val hidesTabBar: Boolean = false, + public val prefersLargeTitle: Boolean = false, + public val actions: List = emptyList(), +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is NativeBarConfig) return false + return hidesTopBar == other.hidesTopBar && + hidesTabBar == other.hidesTabBar && + prefersLargeTitle == other.prefersLargeTitle && + actions == other.actions + } + + override fun hashCode(): Int { + var result = hidesTopBar.hashCode() + result = 31 * result + hidesTabBar.hashCode() + result = 31 * result + prefersLargeTitle.hashCode() + result = 31 * result + actions.hashCode() + return result + } + + public companion object { + /** Today's behavior: bars visible, compact title, no per-screen actions. */ + public val Default: NativeBarConfig = NativeBarConfig() + } +} + +/** + * One back-stack entry as chrome may render it: a stable [id] (unique within its stack — renderers key on it), + * the [title] shown while that entry is on screen, and the entry's per-screen [bar] behavior. This is still + * **display data, not a navigable stack**: a shell may build one screen/bar item per entry and animate between + * them, but the only way it can change navigation is the intent methods — it never mutates these lists. + * Compares by value. */ @Immutable public class NativeChromeEntry( public val id: String, public val title: String, + public val bar: NativeBarConfig = NativeBarConfig.Default, ) { override fun equals(other: Any?): Boolean { if (this === other) return true if (other !is NativeChromeEntry) return false - return id == other.id && title == other.title + return id == other.id && title == other.title && bar == other.bar } - override fun hashCode(): Int = id.hashCode() * 31 + title.hashCode() + override fun hashCode(): Int = (id.hashCode() * 31 + title.hashCode()) * 31 + bar.hashCode() } /** diff --git a/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt b/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt new file mode 100644 index 0000000..924bbc2 --- /dev/null +++ b/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt @@ -0,0 +1,76 @@ +package io.github.apdelrahman1911.nativecomposekit.chrome + +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +/** + * Pins the per-screen chrome config contract: value semantics (shells de-duplicate emissions by equality), + * the do-nothing default, and that a [NativeChromeEntry] carries its config without disturbing entries + * built the pre-config way (the appended-field ABI pattern). + */ +class NativeBarConfigTest { + + @Test + fun default_config_changes_nothing() { + val d = NativeBarConfig.Default + assertEquals(false, d.hidesTopBar) + assertEquals(false, d.hidesTabBar) + assertEquals(false, d.prefersLargeTitle) + assertTrue(d.actions.isEmpty()) + // A freshly constructed no-arg config is the same value as Default. + assertEquals(NativeBarConfig(), d) + assertEquals(NativeBarConfig().hashCode(), d.hashCode()) + } + + @Test + fun compares_by_value_across_every_field() { + val action = NativeChromeAction("share", "square.and.arrow.up") + val a = NativeBarConfig(hidesTopBar = true, actions = listOf(action)) + val same = NativeBarConfig(hidesTopBar = true, actions = listOf(NativeChromeAction("share", "square.and.arrow.up"))) + assertEquals(a, same) + assertEquals(a.hashCode(), same.hashCode()) + + assertNotEquals(a, NativeBarConfig(hidesTopBar = false, actions = listOf(action))) + assertNotEquals(a, NativeBarConfig(hidesTopBar = true)) // actions differ + assertNotEquals(NativeBarConfig(hidesTabBar = true), NativeBarConfig(prefersLargeTitle = true)) + } + + @Test + fun entries_carry_their_config_and_default_it_when_omitted() { + val immersive = NativeBarConfig(hidesTopBar = true, hidesTabBar = true) + val reader = NativeChromeEntry("reader/1", "Reader", immersive) + assertEquals(immersive, reader.bar) + + // Pre-config construction stays valid and equals an explicit-Default entry (appended-field pattern). + val legacy = NativeChromeEntry("home", "Home") + assertEquals(NativeBarConfig.Default, legacy.bar) + assertEquals(NativeChromeEntry("home", "Home", NativeBarConfig.Default), legacy) + + // The config participates in entry equality — a shell may skip re-rendering only on true equality. + assertNotEquals(reader, NativeChromeEntry("reader/1", "Reader")) + } + + @Test + fun state_projects_per_entry_configs_through_the_contract() { + // A minimal stack-projecting source — proves the config travels the nav-agnostic contract per entry. + val entries = listOf( + NativeChromeEntry("home", "Home"), + NativeChromeEntry("reader/1", "Reader", NativeBarConfig(hidesTopBar = true, hidesTabBar = true)), + ) + val state = NativeChromeState( + title = "Reader", + canGoBack = true, + selectedTabId = "library", + tabs = listOf(NativeChromeTab("library", "Library", "books.vertical")), + actions = emptyList(), + sheetId = null, + backStacksByTab = mapOf("library" to entries), + ) + val projected = state.backStacksByTab.getValue("library") + assertEquals(NativeBarConfig.Default, projected[0].bar) + assertTrue(projected[1].bar.hidesTopBar) + assertTrue(projected[1].bar.hidesTabBar) + } +} From 5beb97492d41b8d17c39032bd4751554ec3d605b Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sat, 4 Jul 2026 16:12:19 +0300 Subject: [PATCH 2/8] Give the Material host bar slots, defaults, and per-screen visibility --- composeApp/build.gradle.kts | 19 +++ .../app/navigation/NativeNavHostBarsTest.kt | 120 +++++++++++++++++ .../app/navigation/NativeNavBars.kt | 121 ++++++++++++++++++ .../app/navigation/NativeNavHost.kt | 76 +++++------ 4 files changed, 298 insertions(+), 38 deletions(-) create mode 100644 composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHostBarsTest.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts index f9820fa..375666e 100644 --- a/composeApp/build.gradle.kts +++ b/composeApp/build.gradle.kts @@ -38,6 +38,14 @@ kotlin { applyDefaultHierarchyTemplate() sourceSets { + // Compose UI tests for the sample's navigation host on the JVM via Robolectric (no emulator) — + // mirrors :nativecomposekit's androidUnitTest setup. Test-only, never shipped. + @OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class) + androidUnitTest.dependencies { + implementation(compose.uiTest) + implementation(libs.robolectric) + implementation(libs.junit) + } commonMain.dependencies { // The design-system kit, extracted to its own module. `api` (not implementation) is required so // the iOS framework's `export(project(":nativecomposekit"))` can re-export its public ObjC symbols. @@ -100,4 +108,15 @@ android { buildConfig = true // BuildConfig.DEBUG gates the demo diagnostics (nav/keyboard tracing) to debug builds } + testOptions { + unitTests { + isIncludeAndroidResources = true // Robolectric loads themes/manifest for the Compose UI tests + isReturnDefaultValues = true + } + } +} + +dependencies { + // The empty ComponentActivity + manifest the Robolectric Compose UI tests host into (debug variant). + debugImplementation(libs.androidx.compose.ui.test.manifest) } diff --git a/composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHostBarsTest.kt b/composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHostBarsTest.kt new file mode 100644 index 0000000..b4a6a9c --- /dev/null +++ b/composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHostBarsTest.kt @@ -0,0 +1,120 @@ +package io.github.apdelrahman1911.nativecomposekit.app.navigation + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Home +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material3.Text +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithContentDescription +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.runComposeUiTest +import io.github.apdelrahman1911.nativecomposekit.chrome.NativeBarConfig +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Pins the Material host's chrome contract: the DEFAULT bars keep today's exact structure (title, back + * arrow when pushed, tab items), [NativeBarConfig] hides bars per route, and the [NativeNavHost] slots + * replace or restyle the bars without touching navigation. + */ +@OptIn(ExperimentalTestApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) // Robolectric's bundled runtime doesn't cover compileSdk 36 yet +class NativeNavHostBarsTest { + + private enum class Tab(override val id: String) : NativeTab { Home("home"), Settings("settings") } + private data class Route(override val id: String) : NativeRoute + + private fun navigator() = createNativeNavigator( + tabs = listOf(Tab.Home, Tab.Settings), + initialTab = Tab.Home, + rootRoutes = { tab -> Route("${tab.id}-root") }, + ) + + private val graph = nativeNavGraph { + screen { route -> Text("content:${route.id}") } + } + + private val tabs = listOf( + NativeNavBarItem(Tab.Home, "Home", Icons.Filled.Home), + NativeNavBarItem(Tab.Settings, "Settings", Icons.Filled.Settings), + ) + + @Test + fun default_bars_keep_todays_structure() = runComposeUiTest { + val nav = navigator() + setContent { + NativeNavHost(nav, graph, tabs, title = { "Title:${it.id}" }) + } + // Root: title + both tab items, no back arrow. + onNodeWithText("Title:home-root").assertIsDisplayed() + onNodeWithText("Home").assertIsDisplayed() + onNodeWithText("Settings").assertIsDisplayed() + assertEquals(0, onAllNodes(androidx.compose.ui.test.hasContentDescription("Back")).fetchSemanticsNodes().size) + + // Pushed: back arrow appears and pops on click. + nav.push(Route("detail")) + waitForIdle() + onNodeWithText("Title:detail").assertIsDisplayed() + onNodeWithContentDescription("Back").assertIsDisplayed().performClick() + waitForIdle() + onNodeWithText("Title:home-root").assertIsDisplayed() + } + + @Test + fun bar_config_hides_bars_for_an_immersive_route() = runComposeUiTest { + val nav = navigator() + setContent { + NativeNavHost( + nav, graph, tabs, + title = { "Title:${it.id}" }, + barConfig = { route -> + if (route.id == "reader") NativeBarConfig(hidesTopBar = true, hidesTabBar = true) + else NativeBarConfig.Default + }, + ) + } + onNodeWithText("Title:home-root").assertIsDisplayed() + + nav.push(Route("reader")) + waitForIdle() + // Both bars gone, content still there. + assertEquals(0, onAllNodes(androidx.compose.ui.test.hasText("Title:reader")).fetchSemanticsNodes().size) + assertEquals(0, onAllNodes(androidx.compose.ui.test.hasText("Home")).fetchSemanticsNodes().size) + onNodeWithText("content:reader").assertIsDisplayed() + + // Popping restores both bars. + nav.pop() + waitForIdle() + onNodeWithText("Title:home-root").assertIsDisplayed() + onNodeWithText("Home").assertIsDisplayed() + } + + @Test + fun top_bar_slot_replaces_the_default_and_restyled_default_still_works() = runComposeUiTest { + val nav = navigator() + setContent { + NativeNavHost( + nav, graph, tabs, + title = { "Title:${it.id}" }, + topBar = { state -> + if (state.route.id == "home-root") Text("custom-bar:${state.title}") + else NativeNavDefaults.TopBar(state, centeredTitle = true) // restyled default + }, + ) + } + // Custom slot rendered; the default bar's title node is the custom one now. + onNodeWithText("custom-bar:Title:home-root").assertIsDisplayed() + + // A pushed route falls back to the (restyled) default bar with the working back arrow. + nav.push(Route("detail")) + waitForIdle() + onNodeWithText("Title:detail").assertIsDisplayed() + onNodeWithContentDescription("Back").assertIsDisplayed() + } +} diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt new file mode 100644 index 0000000..20884a3 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt @@ -0,0 +1,121 @@ +package io.github.apdelrahman1911.nativecomposekit.app.navigation + +import androidx.compose.foundation.layout.RowScope +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBar +import androidx.compose.material3.NavigationBarDefaults +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.NavigationBarItemColors +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBar +import androidx.compose.material3.TopAppBarColors +import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.contentColorFor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** + * Everything [NativeNavHost]'s **top-bar slot** receives — enough to rebuild the default bar or replace it + * wholesale (the slot gets the current [route], so a custom bar can vary per screen without any extra API). + * Pure display data + intents; replacing the bar never touches the stack. + */ +@Immutable +class NativeNavTopBarState( + val route: NativeRoute, + val title: String, + val canPop: Boolean, + val onBack: () -> Unit, + val actions: @Composable RowScope.() -> Unit, +) + +/** Everything [NativeNavHost]'s **bottom-bar slot** receives; same contract as [NativeNavTopBarState]. */ +@Immutable +class NativeNavBottomBarState( + val tabs: List, + val selectedTabId: String, + val onSelectTab: (NativeTab) -> Unit, +) + +/** + * The default Material bars [NativeNavHost] renders — public so a consumer can *re-configure* them from a + * slot (colors, centered title, item colors) without rebuilding them, keeping the simple path simple: + * omit the slots and nothing changes; pass `topBar = { NativeNavDefaults.TopBar(it, colors = …) }` to + * restyle; pass a fully custom composable to replace. Android stays Compose-native here on purpose — the + * iOS shell has its own UIKit styling surface (`NativeShellStyle`), and the two are meant to diverge. + */ +object NativeNavDefaults { + + /** + * The stock top bar: compact, back arrow when [NativeNavTopBarState.canPop], the host's actions on the + * right. [colors] and [centeredTitle] restyle it; defaults are exactly the pre-slot appearance. + */ + @OptIn(ExperimentalMaterial3Api::class) + @Composable + fun TopBar( + state: NativeNavTopBarState, + modifier: Modifier = Modifier, + colors: TopAppBarColors? = null, + centeredTitle: Boolean = false, + ) { + val barColors = colors ?: TopAppBarDefaults.topAppBarColors() + val navigationIcon: @Composable () -> Unit = { + if (state.canPop) { + IconButton(onClick = state.onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") + } + } + } + if (centeredTitle) { + CenterAlignedTopAppBar( + title = { Text(state.title) }, + modifier = modifier, + navigationIcon = navigationIcon, + actions = state.actions, + colors = barColors, + ) + } else { + TopAppBar( + title = { Text(state.title) }, + modifier = modifier, + navigationIcon = navigationIcon, + actions = state.actions, + colors = barColors, + ) + } + } + + /** + * The stock bottom bar: one Material [NavigationBarItem] per tab. [containerColor]/[contentColor]/ + * [itemColors] restyle it; defaults are exactly the pre-slot appearance. + */ + @Composable + fun NavigationBar( + state: NativeNavBottomBarState, + modifier: Modifier = Modifier, + containerColor: Color = NavigationBarDefaults.containerColor, + contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), + itemColors: NavigationBarItemColors? = null, + ) { + val resolvedItemColors = itemColors ?: NavigationBarItemDefaults.colors() + NavigationBar(modifier = modifier, containerColor = containerColor, contentColor = contentColor) { + state.tabs.forEach { item -> + NavigationBarItem( + selected = item.tab.id == state.selectedTabId, + onClick = { state.onSelectTab(item.tab) }, + icon = { Icon(item.icon, contentDescription = item.label) }, + label = { Text(item.label) }, + colors = resolvedItemColors, + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHost.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHost.kt index 98a4255..fda0f76 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHost.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavHost.kt @@ -3,30 +3,19 @@ package io.github.apdelrahman1911.nativecomposekit.app.navigation import androidx.compose.animation.AnimatedContentTransitionScope import androidx.compose.animation.ContentTransform import androidx.compose.animation.core.tween -import androidx.compose.runtime.key -import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.ui.NavDisplay import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.ModalBottomSheet -import androidx.compose.material3.NavigationBar -import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold -import androidx.compose.material3.Text -import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue +import androidx.compose.runtime.key import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.ui.NavDisplay +import io.github.apdelrahman1911.nativecomposekit.chrome.NativeBarConfig /** Push/pop slide duration (predictive-back settle uses it too). */ private const val NAV_TRANSITION_MS = 320 @@ -41,14 +30,16 @@ data class NativeNavBarItem(val tab: NativeTab, val label: String, val icon: Ima * entry — see docs/navigation.md for that shell's ratified-projection protocol). Kotlin owns the stack either * way; renderers only report user actions back as intents. * - * A compact `TopAppBar` sits at the top (with a back arrow once pushed) so the content fills the screen directly - * beneath it — no tall header eating vertical space. Renders the selected tab's **top** route inside an + * By default a compact `TopAppBar` sits at the top (with a back arrow once pushed) and a Material + * `NavigationBar` at the bottom — both are [NativeNavDefaults] and both are replaceable through the + * [topBar]/[bottomBar] slots (restyle by calling the defaults with parameters, or pass any composable); + * [barConfig] hides either bar per route (immersive screens). Omitting all three keeps today's exact look. + * Renders the selected tab's **top** route inside an * `AnimatedContent` (push slides forward, pop backward; tab switches swap instantly — the native tab * convention, and required so iOS overlay-placed native controls never linger over the incoming tab); * system back → [NativeNavigator.pop]; the * `NavigationBar` → [NativeNavigator.selectTab]; a non-null [NativeNavigationState.sheet] → `ModalBottomSheet`. */ -@OptIn(ExperimentalMaterial3Api::class) @Composable fun NativeNavHost( navigator: NativeNavigator, @@ -57,37 +48,46 @@ fun NativeNavHost( modifier: Modifier = Modifier, title: (NativeRoute) -> String = { "" }, actions: @Composable RowScope.() -> Unit = {}, + // Per-screen chrome BEHAVIOR (hide either bar while a route is on top). Shares the kit's + // platform-neutral vocabulary with the iOS shell; the Android default bars ignore the + // iOS-oriented fields (prefersLargeTitle, actions-as-data) — Compose actions are slots. + barConfig: (NativeRoute) -> NativeBarConfig = { NativeBarConfig.Default }, + // Bar APPEARANCE/STRUCTURE: omit for today's exact Material bars; call the public defaults with + // parameters to restyle (NativeNavDefaults.TopBar(it, colors = …, centeredTitle = true)); or pass + // any composable to replace a bar outright. Replacing chrome never touches the stack. + topBar: @Composable (NativeNavTopBarState) -> Unit = { NativeNavDefaults.TopBar(it) }, + bottomBar: @Composable (NativeNavBottomBarState) -> Unit = { NativeNavDefaults.NavigationBar(it) }, ) { val state = navigator.state val selectedTab = state.selectedTab val top = state.currentStack().last() val canPop = state.currentStack().size > 1 + val topConfig = barConfig(top) Scaffold( modifier = modifier.fillMaxSize(), topBar = { - TopAppBar( - title = { Text(title(top)) }, - navigationIcon = { - if (canPop) { - IconButton(onClick = { navigator.pop() }) { - Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back") - } - } - }, - actions = actions, - ) + if (!topConfig.hidesTopBar) { + topBar( + NativeNavTopBarState( + route = top, + title = title(top), + canPop = canPop, + onBack = { navigator.pop() }, + actions = actions, + ), + ) + } }, bottomBar = { - NavigationBar { - tabs.forEach { item -> - NavigationBarItem( - selected = item.tab.id == selectedTab.id, - onClick = { navigator.selectTab(item.tab) }, - icon = { Icon(item.icon, contentDescription = item.label) }, - label = { Text(item.label) }, - ) - } + if (!topConfig.hidesTabBar) { + bottomBar( + NativeNavBottomBarState( + tabs = tabs, + selectedTabId = selectedTab.id, + onSelectTab = { navigator.selectTab(it) }, + ), + ) } }, ) { inner -> From f39439e561d4ba90d7b91853347e9d8a7a0f5d0f Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sat, 4 Jul 2026 16:40:04 +0300 Subject: [PATCH 3/8] Style the iOS shell chrome through a kit registry and per-entry config --- WhatsApp Image 2026-07-03 at 15.00.40.jpeg | Bin 0 -> 133056 bytes iosApp/iosApp/Native/NativeNavShell.swift | 127 ++++++++++++------ .../api/nativecomposekit.klib.api | 57 ++++++++ .../theme/NativeShellStyle.kt | 118 ++++++++++++++++ .../theme/NativeShellStyleTest.kt | 68 ++++++++++ 5 files changed, 330 insertions(+), 40 deletions(-) create mode 100644 WhatsApp Image 2026-07-03 at 15.00.40.jpeg create mode 100644 nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt create mode 100644 nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt diff --git a/WhatsApp Image 2026-07-03 at 15.00.40.jpeg b/WhatsApp Image 2026-07-03 at 15.00.40.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..add1170763d81ec4cc75a0033ded1eeacb9f4ba1 GIT binary patch literal 133056 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 // io.github.apdelrahman1911.nativecomposekit.components/NativeSheetDetent.values|values#static(){}[0] } +final enum class io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground : kotlin/Enum { // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground|null[0] + enum entry Custom // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.Custom|null[0] + enum entry SystemMaterial // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.SystemMaterial|null[0] + enum entry Themed // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.Themed|null[0] + + final val entries // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground.values|values#static(){}[0] +} + abstract fun interface io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeCancellable { // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeCancellable|null[0] abstract fun cancel() // io.github.apdelrahman1911.nativecomposekit.chrome/NativeChromeCancellable.cancel|cancel(){}[0] } @@ -849,6 +861,41 @@ final class io.github.apdelrahman1911.nativecomposekit.theme/NativeCapabilities final fun hashCode(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/NativeCapabilities.hashCode|hashCode(){}[0] } +final class io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor { // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor|null[0] + constructor (androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor.|(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] + + final val dark // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor.dark|{}dark[0] + final fun (): androidx.compose.ui.graphics/Color // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor.dark.|(){}[0] + final val light // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor.light|{}light[0] + final fun (): androidx.compose.ui.graphics/Color // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor.light.|(){}[0] +} + +final class io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle { // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle|null[0] + constructor (io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., platform.UIKit/UIFont? = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.|(io.github.apdelrahman1911.nativecomposekit.theme.NativeShellBarBackground;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;platform.UIKit.UIFont?;kotlin.Boolean;kotlin.Boolean){}[0] + + final val barBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.barBackground|{}barBackground[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.barBackground.|(){}[0] + final val customBarBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.customBarBackground|{}customBarBackground[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.customBarBackground.|(){}[0] + final val largeTitles // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.largeTitles|{}largeTitles[0] + final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.largeTitles.|(){}[0] + final val showsHairline // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.showsHairline|{}showsHairline[0] + final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.showsHairline.|(){}[0] + final val tabItemSelected // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tabItemSelected|{}tabItemSelected[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tabItemSelected.|(){}[0] + final val tabItemUnselected // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tabItemUnselected|{}tabItemUnselected[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tabItemUnselected.|(){}[0] + final val tint // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tint|{}tint[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tint.|(){}[0] + final val titleFont // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.titleFont|{}titleFont[0] + final fun (): platform.UIKit/UIFont? // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.titleFont.|(){}[0] + + final object Companion { // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.Companion|null[0] + final val Default // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.Companion.Default|{}Default[0] + final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.Companion.Default.|(){}[0] + } +} + final class io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors { // io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors|null[0] constructor (androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color, androidx.compose.ui.graphics/Color) // io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors.|(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] @@ -1071,6 +1118,8 @@ final val io.github.apdelrahman1911.nativecomposekit.theme/LocalNativeTokens // final fun (): androidx.compose.runtime/ProvidableCompositionLocal // io.github.apdelrahman1911.nativecomposekit.theme/LocalNativeTokens.|(){}[0] final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeAppearance$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeAppearance$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeAppearance$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeCapabilities$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeCapabilities$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeCapabilities$stableprop[0] +final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellColor$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellColor$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeShellColor$stableprop[0] +final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellStyle$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellStyle$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeShellStyle$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStatusColors$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStatusColors$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeStatusColors$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStrings$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStrings$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeStrings$stableprop[0] final val io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeTheme$stableprop // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeTheme$stableprop|#static{}io_github_apdelrahman1911_nativecomposekit_theme_NativeTheme$stableprop[0] @@ -1176,12 +1225,20 @@ final fun io.github.apdelrahman1911.nativecomposekit.components/rememberNativeFo final fun io.github.apdelrahman1911.nativecomposekit.components/rememberNativeShare(androidx.compose.runtime/Composer?, kotlin/Int): io.github.apdelrahman1911.nativecomposekit.components/NativeShare // io.github.apdelrahman1911.nativecomposekit.components/rememberNativeShare|rememberNativeShare(androidx.compose.runtime.Composer?;kotlin.Int){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/NativeAppearanceScope(kotlin/Boolean, androidx.compose.material3/ColorScheme?, androidx.compose.material3/ColorScheme?, androidx.compose.material3/Typography?, androidx.compose.material3/Shapes?, io.github.apdelrahman1911.nativecomposekit.theme/NativeTokens?, io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors?, io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors?, io.github.apdelrahman1911.nativecomposekit.theme/NativeStrings?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.github.apdelrahman1911.nativecomposekit.theme/NativeAppearanceScope|NativeAppearanceScope(kotlin.Boolean;androidx.compose.material3.ColorScheme?;androidx.compose.material3.ColorScheme?;androidx.compose.material3.Typography?;androidx.compose.material3.Shapes?;io.github.apdelrahman1911.nativecomposekit.theme.NativeTokens?;io.github.apdelrahman1911.nativecomposekit.theme.NativeStatusColors?;io.github.apdelrahman1911.nativecomposekit.theme.NativeStatusColors?;io.github.apdelrahman1911.nativecomposekit.theme.NativeStrings?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/NativeKitTheme(kotlin/Boolean, androidx.compose.material3/ColorScheme?, androidx.compose.material3/ColorScheme?, androidx.compose.material3/Typography?, androidx.compose.material3/Shapes?, io.github.apdelrahman1911.nativecomposekit.theme/NativeTokens?, io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors?, io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors?, io.github.apdelrahman1911.nativecomposekit.theme/NativeStrings?, kotlin/Function2, androidx.compose.runtime/Composer?, kotlin/Int, kotlin/Int) // io.github.apdelrahman1911.nativecomposekit.theme/NativeKitTheme|NativeKitTheme(kotlin.Boolean;androidx.compose.material3.ColorScheme?;androidx.compose.material3.ColorScheme?;androidx.compose.material3.Typography?;androidx.compose.material3.Shapes?;io.github.apdelrahman1911.nativecomposekit.theme.NativeTokens?;io.github.apdelrahman1911.nativecomposekit.theme.NativeStatusColors?;io.github.apdelrahman1911.nativecomposekit.theme.NativeStatusColors?;io.github.apdelrahman1911.nativecomposekit.theme.NativeStrings?;kotlin.Function2;androidx.compose.runtime.Composer?;kotlin.Int;kotlin.Int){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/applyNativeShellStyle(io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle) // io.github.apdelrahman1911.nativecomposekit.theme/applyNativeShellStyle|applyNativeShellStyle(io.github.apdelrahman1911.nativecomposekit.theme.NativeShellStyle){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/darkNativeStatusColors(androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors // io.github.apdelrahman1911.nativecomposekit.theme/darkNativeStatusColors|darkNativeStatusColors(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeAppearance$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeAppearance$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeAppearance$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeCapabilities$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeCapabilities$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeCapabilities$stableprop_getter(){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellColor$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellColor$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeShellColor$stableprop_getter(){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellStyle$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeShellStyle$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeShellStyle$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStatusColors$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStatusColors$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeStatusColors$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStrings$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeStrings$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeStrings$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeTheme$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeTheme$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeTheme$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeTokens$stableprop_getter(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.theme/io_github_apdelrahman1911_nativecomposekit_theme_NativeTokens$stableprop_getter|io_github_apdelrahman1911_nativecomposekit_theme_NativeTokens$stableprop_getter(){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/lightNativeStatusColors(androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ..., androidx.compose.ui.graphics/Color = ...): io.github.apdelrahman1911.nativecomposekit.theme/NativeStatusColors // io.github.apdelrahman1911.nativecomposekit.theme/lightNativeStatusColors|lightNativeStatusColors(androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color;androidx.compose.ui.graphics.Color){}[0] final fun io.github.apdelrahman1911.nativecomposekit.theme/nativeBackgroundUIColor(kotlin/Boolean): platform.UIKit/UIColor // io.github.apdelrahman1911.nativecomposekit.theme/nativeBackgroundUIColor|nativeBackgroundUIColor(kotlin.Boolean){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/nativeShellBarBackgroundUIColor(kotlin/Boolean): platform.UIKit/UIColor // io.github.apdelrahman1911.nativecomposekit.theme/nativeShellBarBackgroundUIColor|nativeShellBarBackgroundUIColor(kotlin.Boolean){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/nativeShellStyle(): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle // io.github.apdelrahman1911.nativecomposekit.theme/nativeShellStyle|nativeShellStyle(){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/nativeShellTabItemSelectedUIColor(kotlin/Boolean): platform.UIKit/UIColor? // io.github.apdelrahman1911.nativecomposekit.theme/nativeShellTabItemSelectedUIColor|nativeShellTabItemSelectedUIColor(kotlin.Boolean){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/nativeShellTabItemUnselectedUIColor(kotlin/Boolean): platform.UIKit/UIColor? // io.github.apdelrahman1911.nativecomposekit.theme/nativeShellTabItemUnselectedUIColor|nativeShellTabItemUnselectedUIColor(kotlin.Boolean){}[0] +final fun io.github.apdelrahman1911.nativecomposekit.theme/nativeShellTintUIColor(kotlin/Boolean): platform.UIKit/UIColor? // io.github.apdelrahman1911.nativecomposekit.theme/nativeShellTintUIColor|nativeShellTintUIColor(kotlin.Boolean){}[0] diff --git a/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt b/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt new file mode 100644 index 0000000..62f24c9 --- /dev/null +++ b/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt @@ -0,0 +1,118 @@ +package io.github.apdelrahman1911.nativecomposekit.theme + +import androidx.compose.runtime.Immutable +import androidx.compose.ui.graphics.Color +import io.github.apdelrahman1911.nativecomposekit.components.toUIColor +import platform.UIKit.UIColor +import platform.UIKit.UIFont + +/** + * A light/dark color pair for the native shell — the shell resolves the right one from the trait + * environment, so styled chrome adapts to appearance changes automatically. + */ +@Immutable +public class NativeShellColor( + public val light: Color, + public val dark: Color, +) + +/** How the shell's navigation bars paint their background. */ +public enum class NativeShellBarBackground { + /** + * The default: OPAQUE, in the theme background color ([nativeBackgroundUIColor] — including a + * consumer-injected palette). Opaque is deliberate: a translucent bar samples whatever is composited + * behind it, and during a navigation transition that is UIKit's dimmed transition container — the bar + * visibly darkens mid-swipe (see docs/native-chrome.md). + */ + Themed, + + /** + * The system bar material (translucent blur / Liquid Glass). Native-looking when content scrolls + * under a bar — but this shell lays content BELOW the top bar, and the material re-exposes the + * documented mid-transition darkening. Opt in only if that trade-off is acceptable. + */ + SystemMaterial, + + /** Opaque, in [NativeShellStyle.customBarBackground] (falls back to [Themed] when that is null). */ + Custom, +} + +/** + * Appearance styling for the iOS native chrome shell — the UIKit-side counterpart of restyling the + * Material host's bars with Compose parameters. Register once with [applyNativeShellStyle] **before the + * shell is created** (next to `createNativeNavRoot()`); the shell reads it while configuring its + * `UINavigationBar`/`UITabBar` appearances. Every field defaults to today's look, so an unstyled app is + * byte-identical. Per-SCREEN behavior (hiding bars, large-title opt-in, per-screen actions) travels + * separately, through `NativeBarConfig` on the chrome contract; this object is appearance only. + * + * - [barBackground]/[customBarBackground] — bar background treatment (see [NativeShellBarBackground]). + * - [tint] — bar control tint: the back chevron, bar button items, and the selected tab item (unless + * overridden by [tabItemSelected]). Null keeps the system tint. + * - [tabItemSelected]/[tabItemUnselected] — tab bar item icon+label colors per state. Null keeps system. + * - [titleFont] — the navigation bar's INLINE title font. Null keeps the system font. (Large titles keep + * the system large-title font.) + * - [showsHairline] — restore the bar's bottom hairline (today's bars draw none). + * - [largeTitles] — master switch for large navigation titles; which screens actually SHOW one is chosen + * per entry via `NativeBarConfig.prefersLargeTitle`. NOTE: with Compose content there is no + * `UIScrollView` under the bar, so large titles do NOT collapse on scroll — they stay large while such + * an entry is on top. + */ +@Immutable +public class NativeShellStyle( + public val barBackground: NativeShellBarBackground = NativeShellBarBackground.Themed, + public val customBarBackground: NativeShellColor? = null, + public val tint: NativeShellColor? = null, + public val tabItemSelected: NativeShellColor? = null, + public val tabItemUnselected: NativeShellColor? = null, + public val titleFont: UIFont? = null, + public val showsHairline: Boolean = false, + public val largeTitles: Boolean = false, +) { + public companion object { + /** Today's exact chrome: opaque themed bars, no hairline, system fonts/tints, compact titles. */ + public val Default: NativeShellStyle = NativeShellStyle() + } +} + +private var registeredStyle: NativeShellStyle = NativeShellStyle.Default + +/** + * Register [style] as the shell chrome appearance. Call from Kotlin startup code BEFORE building the + * shell (the shell configures its bar appearances at creation and does not re-read the style afterwards). + * Main-thread only, like all UIKit interaction here. + */ +public fun applyNativeShellStyle(style: NativeShellStyle) { + registeredStyle = style +} + +/** The registered shell style ([NativeShellStyle.Default] until [applyNativeShellStyle] runs). */ +public fun nativeShellStyle(): NativeShellStyle = registeredStyle + +// ---- Swift-facing resolvers (the shell builds dynamic UIColors over these, one per trait style) ---- + +/** + * The bar background for [dark] under the registered style: the theme background for [Themed] (and as the + * fallback for a [Custom] style missing its colors), the custom pair for [Custom]. Meaningless for + * [SystemMaterial] (the shell uses the system material instead of a color). + */ +public fun nativeShellBarBackgroundUIColor(dark: Boolean): UIColor { + val style = registeredStyle + val custom = style.customBarBackground + return if (style.barBackground == NativeShellBarBackground.Custom && custom != null) { + (if (dark) custom.dark else custom.light).toUIColor() + } else { + nativeBackgroundUIColor(dark) + } +} + +/** The registered control tint for [dark], or null to keep the system tint. */ +public fun nativeShellTintUIColor(dark: Boolean): UIColor? = + registeredStyle.tint?.let { (if (dark) it.dark else it.light).toUIColor() } + +/** The registered selected tab-item color for [dark], or null to keep the system appearance. */ +public fun nativeShellTabItemSelectedUIColor(dark: Boolean): UIColor? = + registeredStyle.tabItemSelected?.let { (if (dark) it.dark else it.light).toUIColor() } + +/** The registered unselected tab-item color for [dark], or null to keep the system appearance. */ +public fun nativeShellTabItemUnselectedUIColor(dark: Boolean): UIColor? = + registeredStyle.tabItemUnselected?.let { (if (dark) it.dark else it.light).toUIColor() } diff --git a/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt b/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt new file mode 100644 index 0000000..44041f4 --- /dev/null +++ b/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt @@ -0,0 +1,68 @@ +package io.github.apdelrahman1911.nativecomposekit.theme + +import androidx.compose.ui.graphics.Color +import io.github.apdelrahman1911.nativecomposekit.components.toComposeColor +import kotlin.test.AfterTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNull + +/** + * Pins the shell style registry the Swift chrome reads: unstyled defaults are today's exact behavior, + * a registered style resolves per trait, and the Custom background falls back safely. + */ +class NativeShellStyleTest { + + @AfterTest + fun restoreDefault() { + applyNativeShellStyle(NativeShellStyle.Default) + } + + @Test + fun unstyled_defaults_keep_todays_chrome() { + val style = nativeShellStyle() + assertEquals(NativeShellBarBackground.Themed, style.barBackground) + assertNull(style.tint) + assertNull(style.tabItemSelected) + assertNull(style.tabItemUnselected) + assertNull(style.titleFont) + assertFalse(style.showsHairline) + assertFalse(style.largeTitles) + // Resolvers: no tint/tab colors registered; bar background = the theme background. + assertNull(nativeShellTintUIColor(dark = false)) + assertNull(nativeShellTabItemSelectedUIColor(dark = true)) + assertEquals( + nativeBackgroundUIColor(dark = false).toComposeColor(), + nativeShellBarBackgroundUIColor(dark = false).toComposeColor(), + ) + } + + @Test + fun registered_style_resolves_per_trait() { + val tintLight = Color(0.10f, 0.60f, 0.50f) + val tintDark = Color(0.20f, 0.80f, 0.70f) + applyNativeShellStyle( + NativeShellStyle( + barBackground = NativeShellBarBackground.Custom, + customBarBackground = NativeShellColor(light = Color.White, dark = Color.Black), + tint = NativeShellColor(light = tintLight, dark = tintDark), + largeTitles = true, + ), + ) + assertEquals(Color.White, nativeShellBarBackgroundUIColor(dark = false).toComposeColor()) + assertEquals(Color.Black, nativeShellBarBackgroundUIColor(dark = true).toComposeColor()) + assertEquals(tintLight, nativeShellTintUIColor(dark = false)?.toComposeColor()) + assertEquals(tintDark, nativeShellTintUIColor(dark = true)?.toComposeColor()) + assertEquals(true, nativeShellStyle().largeTitles) + } + + @Test + fun custom_background_without_colors_falls_back_to_the_theme() { + applyNativeShellStyle(NativeShellStyle(barBackground = NativeShellBarBackground.Custom)) + assertEquals( + nativeBackgroundUIColor(dark = true).toComposeColor(), + nativeShellBarBackgroundUIColor(dark = true).toComposeColor(), + ) + } +} From 0ae083b5dd8f2293087bcef1cf01108a4c899d60 Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sat, 4 Jul 2026 22:42:29 +0300 Subject: [PATCH 4/8] Configure per-entry chrome before transitions and withdraw the large-title opt-in --- .../apdelrahman1911/nativecomposekit/App.kt | 3 + .../nativecomposekit/app/AppNavGraph.kt | 22 ++++ .../nativecomposekit/app/AppRoute.kt | 9 ++ .../nativecomposekit/app/AppScreens.kt | 13 +- .../nativecomposekit/app/ChromeDemoScreen.kt | 112 ++++++++++++++++++ .../nativecomposekit/MainViewController.kt | 54 ++++++++- docs/native-chrome.md | 74 ++++++++++++ docs/navigation.md | 10 +- iosApp/iosApp/Native/NativeNavShell.swift | 28 ++--- nativecomposekit/api/nativecomposekit.api | 5 +- .../api/nativecomposekit.klib.api | 8 +- .../nativecomposekit/chrome/NativeChrome.kt | 13 +- .../chrome/NativeBarConfigTest.kt | 3 +- .../theme/NativeShellStyle.kt | 10 +- .../theme/NativeShellStyleTest.kt | 3 - 15 files changed, 320 insertions(+), 47 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/App.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/App.kt index 0b290da..7d30da5 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/App.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/App.kt @@ -10,6 +10,7 @@ import androidx.compose.runtime.remember import io.github.apdelrahman1911.nativecomposekit.app.AppDevTools import io.github.apdelrahman1911.nativecomposekit.app.AppRoute import io.github.apdelrahman1911.nativecomposekit.app.AppTab +import io.github.apdelrahman1911.nativecomposekit.app.appBarConfig import io.github.apdelrahman1911.nativecomposekit.app.appNavGraph import io.github.apdelrahman1911.nativecomposekit.app.appRootRoute import io.github.apdelrahman1911.nativecomposekit.app.appRouteTitle @@ -51,6 +52,8 @@ fun App() { NativeNavBarItem(AppTab.Settings, "Settings", Icons.Filled.Settings), ), title = ::appRouteTitle, + barConfig = ::appBarConfig, // per-screen chrome behavior, shared with the iOS shell + actions = { // Debug builds only: the "+" on the Library tab presents the glass-interop stress // test as a sheet. Rendered with the kit's own icon button (the sample models kit usage). diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt index 0e58fc1..1feb703 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt @@ -5,6 +5,8 @@ import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeNavigator import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeRoute import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeTab import io.github.apdelrahman1911.nativecomposekit.app.navigation.nativeNavGraph +import io.github.apdelrahman1911.nativecomposekit.chrome.NativeBarConfig +import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeAction import io.github.apdelrahman1911.nativecomposekit.showcase.ShowcaseCategoryScreen import io.github.apdelrahman1911.nativecomposekit.showcase.ShowcaseHomeScreen import io.github.apdelrahman1911.nativecomposekit.showcase.showcaseTitle @@ -32,9 +34,27 @@ fun appRouteTitle(route: NativeRoute): String = when (route) { is AppRoute.GlassInteropTest -> "Interop test" is AppRoute.ComponentMatrix -> "Component matrix" is AppRoute.InteropRepro -> "iOS interop repro" + is AppRoute.ChromeDemo -> "Chrome demo" else -> "" } +/** The id of the chrome demo's per-screen bar action (handled in each platform's shell wiring). */ +const val CHROME_DEMO_ACTION_ID = "chrome-demo-action" + +/** + * Per-screen chrome BEHAVIOR for both hosts — the Material `NativeNavHost` (via its `barConfig` param) and + * the iOS shell (via `NativeNavChrome.barConfigForRoute`). One source of truth, exactly like [appRouteTitle]. + * The demo screen hides the tab bar while pushed and carries its own per-screen bar action (rendered by + * the iOS shell; the Android default bar takes actions as composable slots instead and ignores this list). + */ +fun appBarConfig(route: NativeRoute): NativeBarConfig = when (route) { + is AppRoute.ChromeDemo -> NativeBarConfig( + hidesTabBar = true, + actions = listOf(NativeChromeAction(CHROME_DEMO_ACTION_ID, "sparkles")), + ) + else -> NativeBarConfig.Default +} + /** * The route→screen registry, shared by both platform adapters. Screen callbacks are wired to [navigator] * intents here, keeping the screens themselves navigator-agnostic. @@ -54,9 +74,11 @@ fun appNavGraph(navigator: NativeNavigator): NativeNavGraph = nativeNavGraph { SettingsScreen( onOpenComponentMatrix = { navigator.push(AppRoute.ComponentMatrix) }, onOpenInteropRepro = { navigator.push(AppRoute.InteropRepro) }, + onOpenChromeDemo = { navigator.push(AppRoute.ChromeDemo) }, ) } screen { ComponentMatrixScreen() } + screen { ChromeDemoScreen() } screen { InteropReproScreen() } screen { ShowcaseHomeScreen(onOpenCategory = { key -> navigator.push(AppRoute.Showcase(key)) }) diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt index 5599e11..149f679 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt @@ -52,4 +52,13 @@ sealed interface AppRoute : NativeRoute { data object InteropRepro : AppRoute { override val id = "debug/interop-repro" } + + /** + * Debug screen: the navigation-chrome customization demo. Its `NativeBarConfig` (see `appBarConfig`) + * hides the tab bar while pushed and, on iOS, carries a per-screen bar action; the screen's content + * documents each platform's styling surface. Pushed from Settings. + */ + data object ChromeDemo : AppRoute { + override val id = "debug/chrome-demo" + } } diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt index 0ec756f..75de289 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt @@ -26,7 +26,11 @@ import io.github.apdelrahman1911.nativecomposekit.components.model.NativeTextSty */ @Composable -fun SettingsScreen(onOpenComponentMatrix: () -> Unit = {}, onOpenInteropRepro: () -> Unit = {}) { +fun SettingsScreen( + onOpenComponentMatrix: () -> Unit = {}, + onOpenInteropRepro: () -> Unit = {}, + onOpenChromeDemo: () -> Unit = {}, +) { var notify by remember { mutableStateOf(true) } // Fill to the bottom behind the overlaying tab bar; end content clear of it (0 on Android). val bottomInset = LocalNativeContentBottomInset.current @@ -65,6 +69,13 @@ fun SettingsScreen(onOpenComponentMatrix: () -> Unit = {}, onOpenInteropRepro: ( onClick = onOpenInteropRepro, ) }, + { + NativeListItem( + "Navigation chrome demo", + supporting = "Per-screen NativeBarConfig live (hidden tab bar, per-screen action) + each platform's styling surface", + onClick = onOpenChromeDemo, + ) + }, ), ) } diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt new file mode 100644 index 0000000..4f8bc56 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt @@ -0,0 +1,112 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import io.github.apdelrahman1911.nativecomposekit.components.NativeCard +import io.github.apdelrahman1911.nativecomposekit.components.NativeCardVariant +import io.github.apdelrahman1911.nativecomposekit.components.NativeText +import io.github.apdelrahman1911.nativecomposekit.components.model.NativeTextStyle + +/** + * Debug demo for the navigation-chrome customization surfaces. The LIVE part is this screen's own + * `NativeBarConfig` (see `appBarConfig`): the tab bar is hidden while it is pushed, and on iOS the top bar + * carries this screen's own action (the sparkles button — tap it). The content documents the rest: how each + * platform restyles its chrome INDEPENDENTLY, which is the design — Android stays Compose-native, iOS stays + * UIKit-native, and neither is forced to match the other. + */ +@Composable +fun ChromeDemoScreen() { + val bottomInset = LocalNativeContentBottomInset.current + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 16.dp + bottomInset), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + NativeText( + "Live right now: this screen's NativeBarConfig hides the tab bar while it is on top" + + " (pop back — it returns), and on iOS adds the screen's own bar action. Both hosts read" + + " the SAME per-screen config; everything visual below is per-platform on purpose.", + style = NativeTextStyle.Label, + ) + + NativeText("Shared: per-screen behavior", style = NativeTextStyle.Title) + CodeCard( + """ + // one lambda, used by both hosts (like appRouteTitle) + fun appBarConfig(route: NativeRoute) = when (route) { + is AppRoute.ChromeDemo -> NativeBarConfig( + hidesTabBar = true, + actions = listOf(NativeChromeAction("chrome-demo-action", "sparkles")), + ) + else -> NativeBarConfig.Default + } + """.trimIndent(), + ) + + NativeText("Android: Compose slots + defaults", style = NativeTextStyle.Title) + NativeText( + "The Material host exposes topBar/bottomBar slots. Call NativeNavDefaults with parameters to" + + " restyle the stock bars, or pass any composable to replace one. Actions stay composable" + + " slots on Android — no icon-name lists.", + style = NativeTextStyle.Label, + ) + CodeCard( + """ + NativeNavHost( + navigator, graph, tabs, + barConfig = ::appBarConfig, + topBar = { state -> + NativeNavDefaults.TopBar( + state, + colors = TopAppBarDefaults.topAppBarColors(containerColor = brand), + centeredTitle = true, + ) + }, + bottomBar = { state -> NativeNavDefaults.NavigationBar(state, containerColor = brand) }, + ) + """.trimIndent(), + ) + + NativeText("iOS: the shell style registry", style = NativeTextStyle.Title) + NativeText( + "The UIKit shell reads NativeShellStyle once at startup (register it before createNativeNavRoot)." + + " Defaults are today's chrome. SystemMaterial re-exposes the documented mid-transition" + + " darkening. Large titles are deliberately not offered (see docs/native-chrome.md).", + style = NativeTextStyle.Label, + ) + CodeCard( + """ + applyNativeShellStyle( + NativeShellStyle( + barBackground = NativeShellBarBackground.Custom, + customBarBackground = NativeShellColor(light = brandLight, dark = brandDark), + tint = NativeShellColor(light = accent, dark = accentDark), + tabItemSelected = NativeShellColor(light = accent, dark = accentDark), + ), + ) + """.trimIndent(), + ) + } +} + +@Composable +private fun CodeCard(code: String) { + NativeCard(variant = NativeCardVariant.Filled, modifier = Modifier.fillMaxWidth()) { + Text( + code, + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + ) + } +} diff --git a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt index 120e35f..6ba5e5a 100644 --- a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt @@ -3,6 +3,8 @@ package io.github.apdelrahman1911.nativecomposekit import androidx.compose.ui.window.ComposeUIViewController import io.github.apdelrahman1911.nativecomposekit.app.AppRoute import io.github.apdelrahman1911.nativecomposekit.app.AppTab +import io.github.apdelrahman1911.nativecomposekit.app.CHROME_DEMO_ACTION_ID +import io.github.apdelrahman1911.nativecomposekit.app.appBarConfig import io.github.apdelrahman1911.nativecomposekit.app.appNavGraph import io.github.apdelrahman1911.nativecomposekit.app.appRootRoute import io.github.apdelrahman1911.nativecomposekit.app.appRouteTitle @@ -15,6 +17,7 @@ import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeSource import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeTab import io.github.apdelrahman1911.nativecomposekit.components.NativeImeLog import kotlin.native.Platform +import kotlinx.coroutines.launch import platform.UIKit.UIViewController /** @@ -81,8 +84,57 @@ fun createNativeNavRoot(): NativeNavRoot { emptyList() } }, - onAction = { id -> if (id == "glass-interop") navigator.presentSheet(AppRoute.GlassInteropTest) }, + onAction = { id -> + when (id) { + "glass-interop" -> navigator.presentSheet(AppRoute.GlassInteropTest) + CHROME_DEMO_ACTION_ID -> presentChromeDemoAlert() + } + }, + barConfigForRoute = ::appBarConfig, // per-screen chrome behavior, shared with the Material host + ) + + // TEMP-VERIFY (removed before commit): chrome-customization battery — styled shell (tint + selected + // tab color + large titles enabled) and a scripted ChromeDemo round trip. + io.github.apdelrahman1911.nativecomposekit.theme.applyNativeShellStyle( + io.github.apdelrahman1911.nativecomposekit.theme.NativeShellStyle( + tint = io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor( + light = androidx.compose.ui.graphics.Color(0.75f, 0.20f, 0.30f), + dark = androidx.compose.ui.graphics.Color(0.95f, 0.45f, 0.55f), + ), + tabItemSelected = io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor( + light = androidx.compose.ui.graphics.Color(0.75f, 0.20f, 0.30f), + dark = androidx.compose.ui.graphics.Color(0.95f, 0.45f, 0.55f), + ), + ), ) + kotlinx.coroutines.MainScope().launch { + kotlinx.coroutines.delay(3000) + navigator.selectTab(AppTab.Settings); println("NCK-VERIFY settings selected") + kotlinx.coroutines.delay(1600) + navigator.push(AppRoute.ChromeDemo); println("NCK-VERIFY chrome demo pushed") + kotlinx.coroutines.delay(2600) + navigator.pop(); println("NCK-VERIFY popped back") + kotlinx.coroutines.delay(1600) + println("NCK-VERIFY battery done") + } return NativeNavRoot(chrome = chrome) } + +/** + * The chrome demo's per-screen action: action handlers are plain Kotlin, so the sample does the simplest + * native thing — a real `UIAlertController` presented on the topmost controller. + */ +private fun presentChromeDemoAlert() { + val alert = platform.UIKit.UIAlertController.alertControllerWithTitle( + title = "Per-screen action", + message = "This bar button belongs to the Chrome demo screen only — it came from its NativeBarConfig, not the tab.", + preferredStyle = platform.UIKit.UIAlertControllerStyleAlert, + ) + alert.addAction( + platform.UIKit.UIAlertAction.actionWithTitle("Nice", platform.UIKit.UIAlertActionStyleDefault, handler = null), + ) + var vc = platform.UIKit.UIApplication.sharedApplication.keyWindow?.rootViewController + while (vc?.presentedViewController != null) vc = vc?.presentedViewController + vc?.presentViewController(alert, animated = true, completion = null) +} diff --git a/docs/native-chrome.md b/docs/native-chrome.md index 5fdbd4a..15f1649 100644 --- a/docs/native-chrome.md +++ b/docs/native-chrome.md @@ -130,3 +130,77 @@ recreate the historical dual-ownership loop, is in [`docs/navigation.md`](naviga edge recognizer arms and defeats UIKit's interactive pop. In the native shell, the platform owns back. - **Testing scroll positions on the simulator** — `rememberScrollState()` is saveable and restores across app relaunches, so a relaunched app may not start at the top; that's state restoration, not an inset bug. + +## Customizing the chrome + +Customization is deliberately split in two, and deliberately **per-platform** — Android stays +Compose-native, iOS stays UIKit-native, and a consuming app can (and usually should) style them +differently. Everything defaults to the stock look; all of this is opt-in. The Settings → "Navigation +chrome demo" screen exercises the whole surface live. + +**Per-screen BEHAVIOR — `NativeBarConfig`, shared vocabulary (commonMain).** One lambda per app maps a +route to its chrome behavior, and both hosts consume it (the Material host through its `barConfig` +parameter, the iOS shell through `NativeNavChrome(barConfigForRoute = …)` → carried per entry on +`NativeChromeState.backStacksByTab`): + +```kotlin +fun appBarConfig(route: NativeRoute) = when (route) { + is AppRoute.Reader -> NativeBarConfig(hidesTopBar = true, hidesTabBar = true) // immersive + is AppRoute.Detail -> NativeBarConfig( + actions = listOf(NativeChromeAction("share", "square.and.arrow.up")), // this screen's OWN bar action + ) + else -> NativeBarConfig.Default +} +``` + +Notes: per-screen `actions` are rendered by the iOS shell (a screen without its own falls back to the +tab-scoped `actionsForTab`, the pre-config behavior); the Android default bars take actions as composable +slots instead and ignore the list. On iOS, `hidesTabBar` uses `hidesBottomBarWhenPushed` semantics (it +applies to pushed entries), and bar-visibility changes animate when the transition settles rather than +tracking the swipe finger — a cancelled gesture therefore never needs compensation. + +**Android APPEARANCE — Compose slots + `NativeNavDefaults`.** The Material host's bars are public +defaults behind slots: omit the slots for today's exact look, call the defaults with parameters to +restyle, or pass any composable to replace a bar wholesale (the slot state carries the current route, +title, back intent, tabs, and selection): + +```kotlin +NativeNavHost( + navigator, graph, tabs, + barConfig = ::appBarConfig, + topBar = { state -> NativeNavDefaults.TopBar(state, colors = myColors, centeredTitle = true) }, + bottomBar = { state -> NativeNavDefaults.NavigationBar(state, containerColor = brand) }, +) +``` + +**iOS APPEARANCE — the shell style registry (iosMain).** Register once, before `createNativeNavRoot()`; +the shell reads it while configuring its `UINavigationBar`/`UITabBar` appearances: + +```kotlin +applyNativeShellStyle( + NativeShellStyle( + barBackground = NativeShellBarBackground.Custom, // or Themed (default) / SystemMaterial + customBarBackground = NativeShellColor(light, dark), + tint = NativeShellColor(light, dark), // back chevron, bar buttons, selected tab + tabItemSelected = NativeShellColor(light, dark), + tabItemUnselected = NativeShellColor(light, dark), + titleFont = UIFont.boldSystemFontOfSize(17.0), + showsHairline = false, + ), +) +``` + +One documented trade-off: `SystemMaterial` re-exposes the mid-transition darkening (the translucent bar +samples UIKit's dimmed transition container — the reason the default is opaque). Restyling beyond the +registry (custom bar views, search controllers, …) is a fork of `NativeNavShell.swift` — it is reference +code, and the ratified-projection contract underneath is the stable boundary. + +### Deferred: large titles + +A per-screen large-title opt-in was built, verified at rest, and **withdrawn**: UIKit can only animate the +mixed large↔compact bar-height change of an *interactive pop* over content that UNDERLAPS the navigation +bar (stock apps put scroll views under it). This shell deliberately lays screens *below* the bar, so during +a slow swipe-back from a large-title screen UIKit snaps the bar height, drops the outgoing large title, and +reveals the incoming one through a clipped re-layout — visibly broken. Proper support means top-underlapping +hosts plus a top content inset consumed by every screen (the same contract the tab bar already has at the +bottom) — a self-contained follow-up track; the compact-title chrome ships until then. diff --git a/docs/navigation.md b/docs/navigation.md index f5ddb36..9d0fc4d 100644 --- a/docs/navigation.md +++ b/docs/navigation.md @@ -61,8 +61,11 @@ projection in shared code** — and only the iOS-specific pieces live in `iosMai - **`NativeChromeState(title, canGoBack, selectedTabId, tabs, actions, sheetId, backTitle, backStacksByTab)`** *(commonMain)* — an immutable projection. `backStacksByTab` (tabId → root-first `NativeChromeEntry(id, - title)` list) is what a **stack-rendering** shell renders — one native screen per entry; it is still display - data, not a navigable stack. Flat shells (a bare title bar) can ignore it and use `title`/`backTitle`. + title, bar)` list) is what a **stack-rendering** shell renders — one native screen per entry; it is still + display data, not a navigable stack. Each entry's `bar` is its **`NativeBarConfig`** — the per-screen + chrome behavior (hide the top/tab bar while on top, this screen's own bar actions); see + docs/native-chrome.md § Customizing the chrome. Flat shells (a bare title bar) can ignore + all of it and use `title`/`backTitle`. `NativeChromeTab(id, title, sfSymbol)` / `NativeChromeAction(id, sfSymbol)` describe tabs + top-bar actions; `sheetId` only says whether a sheet should be up. - **`NativeChromeStateSource`** *(commonMain)* — the nav-agnostic core: state out + intents in. @@ -139,7 +142,8 @@ wiring, not library API.** A real consumer swaps it for its own library and writ without a running composition (the iOS case). `push` is idempotent at the top and **fails fast on a duplicate id deeper in the stack** — ids are the entry identity every renderer keys on. - **`NativeNavChrome`** — adapts `NativeNavigator` into `NativeChromeSource` (projection, ratification via - `popTo`, per-entry content hosts). This is the file to read before writing your own adapter. + `popTo`, per-entry content hosts, and per-screen chrome behavior via `barConfigForRoute` — the same + lambda the Material host takes as `barConfig`). This is the file to read before writing your own adapter. - **`nativeNavGraph { screen { route -> … } }`** — route → `@Composable`, matched by reified route type. - **`NativeNavHost` / `NativeNavContent`** — the **Compose-canvas** renderer used where the native shell isn't: all of Android, and the iOS pure-Compose fallback (`MainViewController()`). diff --git a/iosApp/iosApp/Native/NativeNavShell.swift b/iosApp/iosApp/Native/NativeNavShell.swift index 9f3ff66..c47763c 100644 --- a/iosApp/iosApp/Native/NativeNavShell.swift +++ b/iosApp/iosApp/Native/NativeNavShell.swift @@ -130,18 +130,24 @@ final class NativeShellViewController: UITabBarController, UINavigationControlle // Entry ids are unique per STACK, not per app — the same route id may live on two tabs, and each // needs its own controller (one UIViewController can never sit in two navigation stacks). let hostKey = "\(tabId)|\(entry.id)" + let host: RouteHostController if let existing = hostsByEntry[hostKey] { - existing.navigationItem.title = entry.title - target.append(existing) + host = existing } else { guard let content = root.chrome.contentViewController(entryId: entry.id) else { NSLog("NCK-Shell: no content for entry '%@' (stale projection) — deferring tab '%@'", entry.id, tabId) return } - let host = RouteHostController(entryId: entry.id, title: entry.title, content: content) + host = RouteHostController(entryId: entry.id, title: entry.title, content: content) hostsByEntry[hostKey] = host - target.append(host) } + // Per-entry chrome must land BEFORE the mutations below: UIKit reads `hidesBottomBarWhenPushed` + // AT PUSH TIME (setting it after the push means the tab bar never hides), and navigationItem + // changes once a transition has started re-lay the bar mid-animation. + host.navigationItem.title = entry.title + host.hidesBottomBarWhenPushed = entry.bar.hidesTabBar + applyActions(entry, to: host, state: state, tabId: tabId) + target.append(host) } let current = nav.viewControllers.compactMap { $0 as? RouteHostController } @@ -164,13 +170,6 @@ final class NativeShellViewController: UITabBarController, UINavigationControlle expectedEntries[tabId] = targetIds // in lockstep with the mutation, never from didShow isApplying = false - // Per-entry chrome: this screen's own actions, tab-bar visibility while it is pushed, and its - // large-title preference (effective only under the style's `largeTitles` master switch). - for (entry, host) in zip(entries, target) { - applyActions(entry, to: host, state: state, tabId: tabId) - host.hidesBottomBarWhenPushed = entry.bar.hidesTabBar - host.navigationItem.largeTitleDisplayMode = entry.bar.prefersLargeTitle ? .always : .never - } // Top-bar visibility follows the TOP entry. Applied here (at apply/settle time, animated when // visible) rather than mid-gesture: a cancelled interactive pop then needs no compensation — // no state change, no sync, no bar flicker. @@ -323,7 +322,6 @@ final class NativeShellViewController: UITabBarController, UINavigationControlle var titleAttributes: [NSAttributedString.Key: Any] = [.foregroundColor: UIColor.label] if let font = style.titleFont { titleAttributes[.font] = font } nav.titleTextAttributes = titleAttributes - nav.largeTitleTextAttributes = [.foregroundColor: UIColor.label] // large titles keep the system font navBar.standardAppearance = nav navBar.compactAppearance = nav navBar.scrollEdgeAppearance = nav @@ -333,8 +331,10 @@ final class NativeShellViewController: UITabBarController, UINavigationControlle NativeShellStyleKt.nativeShellTintUIColor(dark: traits.userInterfaceStyle == .dark) ?? .tintColor } } - // Master switch only — WHICH screens show a large title is per entry (largeTitleDisplayMode below). - navBar.prefersLargeTitles = style.largeTitles + // Compact titles only: large titles need content that UNDERLAPS the bar for UIKit to animate the + // mixed large<->compact bar-height change of an interactive pop, and this shell lays screens below + // the bar (see docs/native-chrome.md § Deferred: large titles). + navBar.prefersLargeTitles = false } private func configureTabBarAppearance() { diff --git a/nativecomposekit/api/nativecomposekit.api b/nativecomposekit/api/nativecomposekit.api index 99f0dc6..5e2a9ad 100644 --- a/nativecomposekit/api/nativecomposekit.api +++ b/nativecomposekit/api/nativecomposekit.api @@ -2,13 +2,12 @@ public final class io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarCo public static final field $stable I public static final field Companion Lio/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfig$Companion; public fun ()V - public fun (ZZZLjava/util/List;)V - public synthetic fun (ZZZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun (ZZLjava/util/List;)V + public synthetic fun (ZZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V public fun equals (Ljava/lang/Object;)Z public final fun getActions ()Ljava/util/List; public final fun getHidesTabBar ()Z public final fun getHidesTopBar ()Z - public final fun getPrefersLargeTitle ()Z public fun hashCode ()I } diff --git a/nativecomposekit/api/nativecomposekit.klib.api b/nativecomposekit/api/nativecomposekit.klib.api index 965fe24..780f111 100644 --- a/nativecomposekit/api/nativecomposekit.klib.api +++ b/nativecomposekit/api/nativecomposekit.klib.api @@ -446,7 +446,7 @@ sealed interface <#A: out kotlin/Any?> io.github.apdelrahman1911.nativecomposeki } final class io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig { // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig|null[0] - constructor (kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin.collections/List = ...) // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.|(kotlin.Boolean;kotlin.Boolean;kotlin.Boolean;kotlin.collections.List){}[0] + constructor (kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin.collections/List = ...) // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.|(kotlin.Boolean;kotlin.Boolean;kotlin.collections.List){}[0] final val actions // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.actions|{}actions[0] final fun (): kotlin.collections/List // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.actions.|(){}[0] @@ -454,8 +454,6 @@ final class io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig { final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTabBar.|(){}[0] final val hidesTopBar // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTopBar|{}hidesTopBar[0] final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hidesTopBar.|(){}[0] - final val prefersLargeTitle // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.prefersLargeTitle|{}prefersLargeTitle[0] - final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.prefersLargeTitle.|(){}[0] final fun equals(kotlin/Any?): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.equals|equals(kotlin.Any?){}[0] final fun hashCode(): kotlin/Int // io.github.apdelrahman1911.nativecomposekit.chrome/NativeBarConfig.hashCode|hashCode(){}[0] @@ -871,14 +869,12 @@ final class io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor { } final class io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle { // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle|null[0] - constructor (io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., platform.UIKit/UIFont? = ..., kotlin/Boolean = ..., kotlin/Boolean = ...) // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.|(io.github.apdelrahman1911.nativecomposekit.theme.NativeShellBarBackground;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;platform.UIKit.UIFont?;kotlin.Boolean;kotlin.Boolean){}[0] + constructor (io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? = ..., platform.UIKit/UIFont? = ..., kotlin/Boolean = ...) // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.|(io.github.apdelrahman1911.nativecomposekit.theme.NativeShellBarBackground;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor?;platform.UIKit.UIFont?;kotlin.Boolean){}[0] final val barBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.barBackground|{}barBackground[0] final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellBarBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.barBackground.|(){}[0] final val customBarBackground // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.customBarBackground|{}customBarBackground[0] final fun (): io.github.apdelrahman1911.nativecomposekit.theme/NativeShellColor? // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.customBarBackground.|(){}[0] - final val largeTitles // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.largeTitles|{}largeTitles[0] - final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.largeTitles.|(){}[0] final val showsHairline // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.showsHairline|{}showsHairline[0] final fun (): kotlin/Boolean // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.showsHairline.|(){}[0] final val tabItemSelected // io.github.apdelrahman1911.nativecomposekit.theme/NativeShellStyle.tabItemSelected|{}tabItemSelected[0] diff --git a/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt b/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt index 839fedd..967935a 100644 --- a/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt +++ b/nativecomposekit/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeChrome.kt @@ -45,18 +45,17 @@ public class NativeChromeAction( * * - [hidesTopBar] / [hidesTabBar] — hide the navigation/tab bar while this entry is on top (immersive * readers, full-bleed media). Renderers animate the change with their platform's own transition. - * - [prefersLargeTitle] — opt this entry into a large navigation title where the platform supports it - * (the iOS shell; requires the shell style's large-title opt-in). NOTE: with Compose content there is no - * `UIScrollView` under the bar, so a large title does NOT collapse on scroll like native UIKit lists — - * it stays large while the entry is on top. Platforms without the concept ignore it. * - [actions] — this entry's OWN top-bar actions (rendered as native bar button items by the iOS shell), * replacing the tab-scoped actions while the entry is on top. Empty = no per-screen actions. + * + * (A per-screen large-title opt-in was evaluated and deliberately NOT shipped: UIKit can only animate the + * mixed large↔compact bar-height change of an interactive pop over content that underlaps the bar, and the + * shell lays screens below it — see docs/native-chrome.md § Deferred: large titles.) */ @Immutable public class NativeBarConfig( public val hidesTopBar: Boolean = false, public val hidesTabBar: Boolean = false, - public val prefersLargeTitle: Boolean = false, public val actions: List = emptyList(), ) { override fun equals(other: Any?): Boolean { @@ -64,20 +63,18 @@ public class NativeBarConfig( if (other !is NativeBarConfig) return false return hidesTopBar == other.hidesTopBar && hidesTabBar == other.hidesTabBar && - prefersLargeTitle == other.prefersLargeTitle && actions == other.actions } override fun hashCode(): Int { var result = hidesTopBar.hashCode() result = 31 * result + hidesTabBar.hashCode() - result = 31 * result + prefersLargeTitle.hashCode() result = 31 * result + actions.hashCode() return result } public companion object { - /** Today's behavior: bars visible, compact title, no per-screen actions. */ + /** Today's behavior: bars visible, no per-screen actions. */ public val Default: NativeBarConfig = NativeBarConfig() } } diff --git a/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt b/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt index 924bbc2..7ec6c91 100644 --- a/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt +++ b/nativecomposekit/src/commonTest/kotlin/io/github/apdelrahman1911/nativecomposekit/chrome/NativeBarConfigTest.kt @@ -17,7 +17,6 @@ class NativeBarConfigTest { val d = NativeBarConfig.Default assertEquals(false, d.hidesTopBar) assertEquals(false, d.hidesTabBar) - assertEquals(false, d.prefersLargeTitle) assertTrue(d.actions.isEmpty()) // A freshly constructed no-arg config is the same value as Default. assertEquals(NativeBarConfig(), d) @@ -34,7 +33,7 @@ class NativeBarConfigTest { assertNotEquals(a, NativeBarConfig(hidesTopBar = false, actions = listOf(action))) assertNotEquals(a, NativeBarConfig(hidesTopBar = true)) // actions differ - assertNotEquals(NativeBarConfig(hidesTabBar = true), NativeBarConfig(prefersLargeTitle = true)) + assertNotEquals(NativeBarConfig(hidesTabBar = true), NativeBarConfig(hidesTopBar = true)) } @Test diff --git a/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt b/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt index 62f24c9..7bd903f 100644 --- a/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt +++ b/nativecomposekit/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyle.kt @@ -52,10 +52,9 @@ public enum class NativeShellBarBackground { * - [titleFont] — the navigation bar's INLINE title font. Null keeps the system font. (Large titles keep * the system large-title font.) * - [showsHairline] — restore the bar's bottom hairline (today's bars draw none). - * - [largeTitles] — master switch for large navigation titles; which screens actually SHOW one is chosen - * per entry via `NativeBarConfig.prefersLargeTitle`. NOTE: with Compose content there is no - * `UIScrollView` under the bar, so large titles do NOT collapse on scroll — they stay large while such - * an entry is on top. + * + * (Large-title support was evaluated and deliberately NOT shipped — see docs/native-chrome.md + * § Deferred: large titles.) */ @Immutable public class NativeShellStyle( @@ -66,10 +65,9 @@ public class NativeShellStyle( public val tabItemUnselected: NativeShellColor? = null, public val titleFont: UIFont? = null, public val showsHairline: Boolean = false, - public val largeTitles: Boolean = false, ) { public companion object { - /** Today's exact chrome: opaque themed bars, no hairline, system fonts/tints, compact titles. */ + /** Today's exact chrome: opaque themed bars, no hairline, system fonts/tints. */ public val Default: NativeShellStyle = NativeShellStyle() } } diff --git a/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt b/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt index 44041f4..a8f2429 100644 --- a/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt +++ b/nativecomposekit/src/iosTest/kotlin/io/github/apdelrahman1911/nativecomposekit/theme/NativeShellStyleTest.kt @@ -28,7 +28,6 @@ class NativeShellStyleTest { assertNull(style.tabItemUnselected) assertNull(style.titleFont) assertFalse(style.showsHairline) - assertFalse(style.largeTitles) // Resolvers: no tint/tab colors registered; bar background = the theme background. assertNull(nativeShellTintUIColor(dark = false)) assertNull(nativeShellTabItemSelectedUIColor(dark = true)) @@ -47,14 +46,12 @@ class NativeShellStyleTest { barBackground = NativeShellBarBackground.Custom, customBarBackground = NativeShellColor(light = Color.White, dark = Color.Black), tint = NativeShellColor(light = tintLight, dark = tintDark), - largeTitles = true, ), ) assertEquals(Color.White, nativeShellBarBackgroundUIColor(dark = false).toComposeColor()) assertEquals(Color.Black, nativeShellBarBackgroundUIColor(dark = true).toComposeColor()) assertEquals(tintLight, nativeShellTintUIColor(dark = false)?.toComposeColor()) assertEquals(tintDark, nativeShellTintUIColor(dark = true)?.toComposeColor()) - assertEquals(true, nativeShellStyle().largeTitles) } @Test From f91c4de2ffce58a1bd05e577a9d80bf74ca60936 Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sun, 5 Jul 2026 02:55:05 +0300 Subject: [PATCH 5/8] Add a toolbar styles catalog with live Material and native iOS bar exhibits --- .../app/IosBarPreviews.android.kt | 52 +++ .../app/ToolbarStylesScreenTest.kt | 51 +++ .../nativecomposekit/app/AppNavGraph.kt | 9 + .../nativecomposekit/app/AppRoute.kt | 10 + .../nativecomposekit/app/AppScreens.kt | 8 + .../nativecomposekit/app/ChromeDemoScreen.kt | 17 - .../nativecomposekit/app/DemoUi.kt | 37 ++ .../nativecomposekit/app/IosBarPreviews.kt | 35 ++ .../app/ToolbarStylesScreen.kt | 360 ++++++++++++++++++ .../app/navigation/NativeNavBars.kt | 15 +- .../nativecomposekit/MainViewController.kt | 26 -- .../app/IosBarPreviews.ios.kt | 142 +++++++ 12 files changed, 718 insertions(+), 44 deletions(-) create mode 100644 composeApp/src/androidMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.android.kt create mode 100644 composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreenTest.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/DemoUi.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.kt create mode 100644 composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreen.kt create mode 100644 composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt diff --git a/composeApp/src/androidMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.android.kt b/composeApp/src/androidMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.android.kt new file mode 100644 index 0000000..6ca5b63 --- /dev/null +++ b/composeApp/src/androidMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.android.kt @@ -0,0 +1,52 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp + +// The native bar previews are genuinely iOS-only (they embed real UIKit bars); Android shows an honest +// placeholder so the catalog reads the same on both platforms. + +@Composable +private fun IosOnlyPlaceholder(label: String, modifier: Modifier, heightDp: Int) { + Box( + modifier = modifier.fillMaxWidth().height(heightDp.dp) + .border(1.dp, MaterialTheme.colorScheme.outlineVariant, RoundedCornerShape(8.dp)), + contentAlignment = Alignment.Center, + ) { + Text(label, style = MaterialTheme.typography.labelMedium) + } +} + +@Composable +actual fun IosNavBarPreview( + title: String, + modifier: Modifier, + background: IosPreviewBackground, + customBackground: Color?, + tint: Color?, + actionSymbols: List, + showsBack: Boolean, + hairline: Boolean, +) { + IosOnlyPlaceholder("iOS-only preview (real UINavigationBar) — run the iOS app", modifier, heightDp = 44) +} + +@Composable +actual fun IosTabBarPreview( + modifier: Modifier, + selectedColor: Color?, + unselectedColor: Color?, + tint: Color?, +) { + IosOnlyPlaceholder("iOS-only preview (real UITabBar) — run the iOS app", modifier, heightDp = 49) +} diff --git a/composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreenTest.kt b/composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreenTest.kt new file mode 100644 index 0000000..f2469e5 --- /dev/null +++ b/composeApp/src/androidUnitTest/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreenTest.kt @@ -0,0 +1,51 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.ui.test.ExperimentalTestApi +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.compose.ui.test.performScrollTo +import androidx.compose.ui.test.runComposeUiTest +import org.junit.Assert.assertTrue +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config + +/** + * Smoke-pins the Android path of the toolbar-styles catalog: the live Material exhibits actually render + * (default, tinted, actions incl. a TEXT action, centered title, tab bars, the fully custom slot bar), + * the iOS-only exhibits show their honest placeholder, and the immersive demo wires its back intent. + */ +@OptIn(ExperimentalTestApi::class) +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [34]) // Robolectric's bundled runtime doesn't cover compileSdk 36 yet +class ToolbarStylesScreenTest { + + @Test + fun catalog_renders_the_android_exhibits_live() = runComposeUiTest { + setContent { ToolbarStylesScreen() } + + onNodeWithText("1. Default toolbar").assertIsDisplayed() + onNodeWithText("Default title").assertIsDisplayed() // live default Material bar + onNodeWithText("Tinted title").performScrollTo().assertIsDisplayed() // live tinted bar + onNodeWithText("Edit").performScrollTo().assertIsDisplayed() // TEXT action in the slot + onNodeWithText("Centered").performScrollTo().assertIsDisplayed() // centered-title variant + onNodeWithText("Custom slot").performScrollTo().assertIsDisplayed() // fully custom slot bar + onNodeWithText("Subtitle — anything goes in a slot").performScrollTo().assertIsDisplayed() + // iOS-only exhibits are honest placeholders on Android (several across the sections). + val placeholders = onAllNodes( + androidx.compose.ui.test.hasText("iOS-only preview (real UINavigationBar) — run the iOS app"), + ).fetchSemanticsNodes() + assertTrue(placeholders.size >= 3) + } + + @Test + fun immersive_demo_renders_and_pops_via_its_button() = runComposeUiTest { + var popped = false + setContent { ImmersiveDemoScreen(onBack = { popped = true }) } + onNodeWithText("Immersive").assertIsDisplayed() + onNodeWithText("Go back").performClick() + assertTrue(popped) + } +} diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt index 1feb703..3039799 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppNavGraph.kt @@ -35,6 +35,8 @@ fun appRouteTitle(route: NativeRoute): String = when (route) { is AppRoute.ComponentMatrix -> "Component matrix" is AppRoute.InteropRepro -> "iOS interop repro" is AppRoute.ChromeDemo -> "Chrome demo" + is AppRoute.ToolbarStyles -> "Toolbar styles" + is AppRoute.ImmersiveDemo -> "Immersive" else -> "" } @@ -52,6 +54,8 @@ fun appBarConfig(route: NativeRoute): NativeBarConfig = when (route) { hidesTabBar = true, actions = listOf(NativeChromeAction(CHROME_DEMO_ACTION_ID, "sparkles")), ) + // The immersive demo hides BOTH bars — pop via swipe-back / system back / the screen's own button. + is AppRoute.ImmersiveDemo -> NativeBarConfig(hidesTopBar = true, hidesTabBar = true) else -> NativeBarConfig.Default } @@ -75,10 +79,15 @@ fun appNavGraph(navigator: NativeNavigator): NativeNavGraph = nativeNavGraph { onOpenComponentMatrix = { navigator.push(AppRoute.ComponentMatrix) }, onOpenInteropRepro = { navigator.push(AppRoute.InteropRepro) }, onOpenChromeDemo = { navigator.push(AppRoute.ChromeDemo) }, + onOpenToolbarStyles = { navigator.push(AppRoute.ToolbarStyles) }, ) } screen { ComponentMatrixScreen() } screen { ChromeDemoScreen() } + screen { + ToolbarStylesScreen(onOpenImmersive = { navigator.push(AppRoute.ImmersiveDemo) }) + } + screen { ImmersiveDemoScreen(onBack = { navigator.pop() }) } screen { InteropReproScreen() } screen { ShowcaseHomeScreen(onOpenCategory = { key -> navigator.push(AppRoute.Showcase(key)) }) diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt index 149f679..332f1a3 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppRoute.kt @@ -61,4 +61,14 @@ sealed interface AppRoute : NativeRoute { data object ChromeDemo : AppRoute { override val id = "debug/chrome-demo" } + + /** Debug screen: the toolbar/navigation-bar styles catalog (Material variants live, native iOS bar previews). */ + data object ToolbarStyles : AppRoute { + override val id = "debug/toolbar-styles" + } + + /** Debug screen: the immersive per-screen config demo — BOTH bars hidden while on top. */ + data object ImmersiveDemo : AppRoute { + override val id = "debug/immersive-demo" + } } diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt index 75de289..9052a40 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/AppScreens.kt @@ -30,6 +30,7 @@ fun SettingsScreen( onOpenComponentMatrix: () -> Unit = {}, onOpenInteropRepro: () -> Unit = {}, onOpenChromeDemo: () -> Unit = {}, + onOpenToolbarStyles: () -> Unit = {}, ) { var notify by remember { mutableStateOf(true) } // Fill to the bottom behind the overlaying tab bar; end content clear of it (0 on Android). @@ -76,6 +77,13 @@ fun SettingsScreen( onClick = onOpenChromeDemo, ) }, + { + NativeListItem( + "Navigation toolbar styles", + supporting = "Catalog of toolbar/tab-bar variants: Material live previews, native iOS bar previews, per-screen demos", + onClick = onOpenToolbarStyles, + ) + }, ), ) } diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt index 4f8bc56..3d27423 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ChromeDemoScreen.kt @@ -3,18 +3,12 @@ package io.github.apdelrahman1911.nativecomposekit.app import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.unit.dp -import io.github.apdelrahman1911.nativecomposekit.components.NativeCard -import io.github.apdelrahman1911.nativecomposekit.components.NativeCardVariant import io.github.apdelrahman1911.nativecomposekit.components.NativeText import io.github.apdelrahman1911.nativecomposekit.components.model.NativeTextStyle @@ -99,14 +93,3 @@ fun ChromeDemoScreen() { ) } } - -@Composable -private fun CodeCard(code: String) { - NativeCard(variant = NativeCardVariant.Filled, modifier = Modifier.fillMaxWidth()) { - Text( - code, - modifier = Modifier.padding(12.dp), - style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), - ) - } -} diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/DemoUi.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/DemoUi.kt new file mode 100644 index 0000000..bc39e9f --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/DemoUi.kt @@ -0,0 +1,37 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.unit.dp +import io.github.apdelrahman1911.nativecomposekit.components.NativeCard +import io.github.apdelrahman1911.nativecomposekit.components.NativeCardVariant +import io.github.apdelrahman1911.nativecomposekit.components.NativeText +import io.github.apdelrahman1911.nativecomposekit.components.model.NativeTextStyle + +/** Shared bits for the debug/demo screens (chrome demo, toolbar styles): monospace code cards + labels. */ + +@Composable +internal fun CodeCard(code: String) { + NativeCard(variant = NativeCardVariant.Filled, modifier = Modifier.fillMaxWidth()) { + Text( + code, + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall.copy(fontFamily = FontFamily.Monospace), + ) + } +} + +@Composable +internal fun DemoSectionTitle(text: String) { + NativeText(text, style = NativeTextStyle.Title) +} + +@Composable +internal fun DemoNote(text: String) { + NativeText(text, style = NativeTextStyle.Label) +} diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.kt new file mode 100644 index 0000000..198c66c --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.kt @@ -0,0 +1,35 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color + +/** Background treatment for an embedded iOS bar preview (mirrors `NativeShellBarBackground`). */ +enum class IosPreviewBackground { Themed, Material, Custom } + +/** + * A REAL, non-interactive `UINavigationBar` rendered inline (iOS) so the toolbar-styles catalog can show + * genuine native bar variants side by side — the same `UINavigationBarAppearance` knobs the shell style + * registry maps to. On Android this renders a labeled placeholder (the variants are iOS-only by design). + * Preview-only: the app's actual chrome is styled once via `applyNativeShellStyle`. + */ +@Composable +expect fun IosNavBarPreview( + title: String, + modifier: Modifier = Modifier, + background: IosPreviewBackground = IosPreviewBackground.Themed, + customBackground: Color? = null, + tint: Color? = null, + actionSymbols: List = emptyList(), + showsBack: Boolean = false, + hairline: Boolean = false, +) + +/** A REAL, non-interactive `UITabBar` preview (iOS); a labeled placeholder on Android. */ +@Composable +expect fun IosTabBarPreview( + modifier: Modifier = Modifier, + selectedColor: Color? = null, + unselectedColor: Color? = null, + tint: Color? = null, +) diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreen.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreen.kt new file mode 100644 index 0000000..c61687b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/ToolbarStylesScreen.kt @@ -0,0 +1,360 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +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.rememberScrollState +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.automirrored.filled.ArrowBack +import androidx.compose.material.icons.automirrored.filled.List +import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.Favorite +import androidx.compose.material.icons.filled.GridView +import androidx.compose.material.icons.filled.Settings +import androidx.compose.material.icons.filled.Share +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.NavigationBarItemDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.material3.TopAppBarDefaults +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.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeNavBarItem +import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeNavBottomBarState +import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeNavDefaults +import io.github.apdelrahman1911.nativecomposekit.app.navigation.NativeNavTopBarState + +/** + * Debug catalog: every toolbar/navigation-bar style a consumer can reach TODAY, with honest "not + * supported" labels where a knob doesn't exist. Two kinds of exhibits: + * - **Material bars render live inline** — they are ordinary composables ([NativeNavDefaults] + the + * [NativeNavHost] slot states), so each variant below is the real thing, built with the exact public API. + * - **iOS bars render as real embedded `UINavigationBar`/`UITabBar` previews** (non-interactive) showing + * what the shell style registry maps to; on Android those exhibits show a labeled placeholder. + * Nothing here changes the app's shipped chrome — the live app bars around this screen stay default. + */ +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun ToolbarStylesScreen(onOpenImmersive: () -> Unit = {}) { + val bottomInset = LocalNativeContentBottomInset.current + val crimson = Color(0.75f, 0.20f, 0.30f) + + Column( + modifier = Modifier.fillMaxSize().verticalScroll(rememberScrollState()) + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 16.dp + bottomInset), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + DemoNote( + "Every exhibit uses the public customization surface. Material bars are LIVE composables;" + + " iOS exhibits are real embedded UIKit bars (previews — the app's actual chrome is styled" + + " once at startup). The app's own bars around this screen stay default on purpose.", + ) + + // 1 — Default ------------------------------------------------------------------------------ + DemoSectionTitle("1. Default toolbar") + DemoNote("The shipped look on both platforms: compact title, back arrow when pushed, no restyle.") + Exhibit("Android host default (live)") { + NativeNavDefaults.TopBar(topState("Default title"), windowInsets = WindowInsets(0)) + } + Exhibit("iOS shell default (native preview)") { + IosNavBarPreview(title = "Default title", showsBack = true) + } + + // 2 — Tinted ------------------------------------------------------------------------------- + DemoSectionTitle("2. Tinted toolbar") + Exhibit("Android: NativeNavDefaults.TopBar(colors = …) (live)") { + NativeNavDefaults.TopBar( + topState("Tinted title"), + windowInsets = WindowInsets(0), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = MaterialTheme.colorScheme.primary, + titleContentColor = MaterialTheme.colorScheme.onPrimary, + navigationIconContentColor = MaterialTheme.colorScheme.onPrimary, + actionIconContentColor = MaterialTheme.colorScheme.onPrimary, + ), + ) + } + Exhibit("iOS: NativeShellStyle(tint, customBarBackground) (native preview)") { + IosNavBarPreview( + title = "Tinted title", + background = IosPreviewBackground.Custom, + customBackground = Color(0.98f, 0.92f, 0.93f), + tint = crimson, + showsBack = true, + actionSymbols = listOf("square.and.arrow.up"), + ) + } + + // 3 — Actions ------------------------------------------------------------------------------ + DemoSectionTitle("3. Toolbar with actions") + Exhibit("Android: actions are a free composable slot (live: icon, text, disabled, destructive)") { + NativeNavDefaults.TopBar( + windowInsets = WindowInsets(0), + state = topState("Actions") { + IconButton(onClick = {}) { Icon(Icons.Filled.Share, contentDescription = "Share") } + TextButton(onClick = {}) { Text("Edit") } + IconButton(onClick = {}, enabled = false) { + Icon(Icons.Filled.Favorite, contentDescription = "Disabled") + } + IconButton(onClick = {}) { + Icon(Icons.Filled.Delete, contentDescription = "Delete", tint = MaterialTheme.colorScheme.error) + } + }, + ) + } + Exhibit("iOS: per-screen/tab actions via NativeChromeAction (native preview: one + many)") { + Column(verticalArrangement = Arrangement.spacedBy(8.dp)) { + IosNavBarPreview(title = "One action", actionSymbols = listOf("square.and.arrow.up")) + IosNavBarPreview(title = "Many actions", actionSymbols = listOf("square.and.arrow.up", "trash", "star")) + } + } + DemoNote( + "Not supported on iOS today (documented limitation): NativeChromeAction is icon-only" + + " (SF Symbol name) — text actions, disabled state, and destructive styling are not in the" + + " contract. On Android all of those work because actions are composables.", + ) + + // 4 — Per-screen config --------------------------------------------------------------------- + DemoSectionTitle("4. Per-screen toolbar config (live)") + DemoNote( + "NativeBarConfig travels per screen through the shared contract: per-screen title" + + " (appRouteTitle), per-screen actions, and bar visibility — and it resets automatically on" + + " pop because the config belongs to the entry, not the bar. The 'Navigation chrome demo'" + + " screen (Settings → Developer) shows a hidden TAB bar + a per-screen action; the button" + + " below pushes a screen that hides BOTH bars.", + ) + Button(onClick = onOpenImmersive) { Text("Open immersive demo (hides both bars)") } + + // 5 — Transparent / glass ------------------------------------------------------------------- + DemoSectionTitle("5. Transparent / glass backgrounds") + Exhibit("Android: containerColor = Transparent over content (live)") { + Box( + Modifier.fillMaxWidth() + .background(Brush.horizontalGradient(listOf(Color(0xFF7E57C2), Color(0xFF26A69A)))), + ) { + NativeNavDefaults.TopBar( + topState("Transparent"), + windowInsets = WindowInsets(0), + colors = TopAppBarDefaults.topAppBarColors( + containerColor = Color.Transparent, + titleContentColor = Color.White, + navigationIconContentColor = Color.White, + ), + ) + } + } + Exhibit("iOS: NativeShellBarBackground.SystemMaterial (native preview — real blur)") { + Box( + Modifier.fillMaxWidth() + .background(Brush.horizontalGradient(listOf(Color(0xFF7E57C2), Color(0xFF26A69A)))), + ) { + IosNavBarPreview(title = "System material", background = IosPreviewBackground.Material) + } + } + DemoNote( + "iOS trade-off (documented): the app's REAL bars default to opaque because a translucent bar" + + " samples UIKit's dimmed transition container mid-swipe. SystemMaterial is an explicit opt-in.", + ) + + // 6 — Elevation / hairline ------------------------------------------------------------------ + DemoSectionTitle("6. Elevation / shadow / hairline") + DemoNote( + "Android: Material 3 top bars are FLAT by design; scrolledContainerColor gives the tonal" + + " scroll change, and a drop shadow is possible through a custom slot (section 9). iOS: UIKit" + + " bars have no elevation concept — the knob is the bottom hairline (off by default).", + ) + Exhibit("iOS: showsHairline = true (native preview)") { + IosNavBarPreview(title = "With hairline", hairline = true) + } + + // 7 — Title alignment ----------------------------------------------------------------------- + DemoSectionTitle("7. Title alignment and style") + Exhibit("Android: start-aligned (default, live)") { + NativeNavDefaults.TopBar(topState("Start-aligned"), windowInsets = WindowInsets(0)) + } + Exhibit("Android: centeredTitle = true (live)") { + NativeNavDefaults.TopBar(topState("Centered"), centeredTitle = true, windowInsets = WindowInsets(0)) + } + DemoNote( + "Subtitles: not offered by the defaults on either platform — on Android use a custom slot" + + " (section 9 shows one with a subtitle); on iOS a native subtitle is not supported. iOS titles" + + " are system-managed (centered, compact); a custom titleFont is supported via NativeShellStyle." + + " Large titles are deliberately deferred (interactive swipe-back constraint — see" + + " docs/native-chrome.md).", + ) + + // 8 — Tab bars ------------------------------------------------------------------------------ + DemoSectionTitle("8. Bottom / tab bar variants") + Exhibit("Android: default NavigationBar (live)") { + NativeNavDefaults.NavigationBar(bottomState(), windowInsets = WindowInsets(0)) + } + Exhibit("Android: tinted container + item colors (live)") { + NativeNavDefaults.NavigationBar( + bottomState(), + windowInsets = WindowInsets(0), + containerColor = MaterialTheme.colorScheme.primaryContainer, + itemColors = NavigationBarItemDefaults.colors( + selectedIconColor = MaterialTheme.colorScheme.onPrimary, + indicatorColor = MaterialTheme.colorScheme.primary, + unselectedIconColor = MaterialTheme.colorScheme.onPrimaryContainer, + selectedTextColor = MaterialTheme.colorScheme.primary, + unselectedTextColor = MaterialTheme.colorScheme.onPrimaryContainer, + ), + ) + } + Exhibit("iOS: default UITabBar (native preview)") { + IosTabBarPreview() + } + Exhibit("iOS: tabItemSelected / tabItemUnselected (native preview)") { + IosTabBarPreview(selectedColor = crimson, unselectedColor = Color(0.45f, 0.45f, 0.50f)) + } + DemoNote( + "Hiding the tab bar per screen: section 4 (both platforms). Custom tab-bar SHAPE/background:" + + " Android = replace the bottomBar slot with any composable; iOS = not supported (the system" + + " bar's shape is owned by UIKit).", + ) + + // 9 — Fully custom Android slot ------------------------------------------------------------- + DemoSectionTitle("9. Fully custom Android toolbar (slot replacement, live)") + Exhibit("topBar = { state -> MyBrandBar(state) } — any composable, same state object") { + FancyDemoBar(topState("Custom slot")) + } + CodeCard( + """ + NativeNavHost( + navigator, graph, tabs, + topBar = { state -> MyBrandBar(state) }, // full replacement + ) + """.trimIndent(), + ) + + // 10 — iOS shell style: the whole surface --------------------------------------------------- + DemoSectionTitle("10. iOS shell style — the full registry") + DemoNote( + "Everything the iOS shell reads today (applied once at startup, before createNativeNavRoot):" + + " bar background (Themed / SystemMaterial / Custom), control tint, tab item colors, inline" + + " title font, hairline. Per-screen behavior (visibility + actions) comes from NativeBarConfig" + + " instead. Large titles: deferred.", + ) + CodeCard( + """ + applyNativeShellStyle( + NativeShellStyle( + barBackground = NativeShellBarBackground.Custom, + customBarBackground = NativeShellColor(light, dark), + tint = NativeShellColor(light, dark), + tabItemSelected = NativeShellColor(light, dark), + tabItemUnselected = NativeShellColor(light, dark), + titleFont = UIFont.boldSystemFontOfSize(17.0), + showsHairline = false, + ), + ) + """.trimIndent(), + ) + } +} + +/** The immersive per-screen demo: BOTH bars hidden by this screen's [appBarConfig] entry. */ +@Composable +fun ImmersiveDemoScreen(onBack: () -> Unit = {}) { + Box( + modifier = Modifier.fillMaxSize().background( + Brush.verticalGradient(listOf(Color(0xFF1A1A2E), Color(0xFF16213E), Color(0xFF0F3460))), + ), + contentAlignment = Alignment.Center, + ) { + Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(16.dp)) { + Text("Immersive", color = Color.White, style = MaterialTheme.typography.headlineLarge) + Text( + "NativeBarConfig(hidesTopBar = true, hidesTabBar = true)\n" + + "Both bars are hidden while this entry is on top and return on pop.\n" + + "Swipe back / system back still work — or:", + color = Color(0xFFB0BEC5), + style = MaterialTheme.typography.bodyMedium, + ) + Button(onClick = onBack) { Text("Go back") } + } + } +} + +// ---- exhibit plumbing ------------------------------------------------------------------------- + +/** A labeled, clipped frame around one live bar exhibit. */ +@Composable +private fun Exhibit(label: String, content: @Composable () -> Unit) { + Column(verticalArrangement = Arrangement.spacedBy(4.dp)) { + Text(label, style = MaterialTheme.typography.labelMedium) + Box(Modifier.fillMaxWidth().clip(RoundedCornerShape(12.dp))) { content() } + } +} + +private fun topState( + title: String, + actions: @Composable RowScope.() -> Unit = {}, +) = NativeNavTopBarState( + route = AppRoute.ToolbarStyles, + title = title, + canPop = true, + onBack = {}, + actions = actions, +) + +private fun bottomState() = NativeNavBottomBarState( + tabs = listOf( + NativeNavBarItem(AppTab.Catalog, "Components", Icons.Filled.GridView), + NativeNavBarItem(AppTab.Library, "Library", Icons.AutoMirrored.Filled.List), + NativeNavBarItem(AppTab.Settings, "Settings", Icons.Filled.Settings), + ), + selectedTabId = AppTab.Catalog.id, + onSelectTab = {}, +) + +/** Section 9's "deep replacement" example: gradient brand bar with avatar, subtitle, and a pill action. */ +@Composable +private fun FancyDemoBar(state: NativeNavTopBarState) { + Row( + modifier = Modifier.fillMaxWidth().height(64.dp) + .background(Brush.horizontalGradient(listOf(Color(0xFF512DA8), Color(0xFF00897B)))) + .padding(horizontal = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + if (state.canPop) { + IconButton(onClick = state.onBack) { + Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = Color.White) + } + } + Box(Modifier.size(32.dp).clip(CircleShape).background(Color.White.copy(alpha = 0.9f))) + Spacer(Modifier.size(10.dp)) + Column(Modifier.weight(1f)) { + Text(state.title, color = Color.White, fontWeight = FontWeight.Bold) + Text("Subtitle — anything goes in a slot", color = Color.White.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall) + } + Box( + Modifier.clip(RoundedCornerShape(50)).background(Color.White.copy(alpha = 0.2f)) + .padding(horizontal = 12.dp, vertical = 6.dp), + ) { Text("Action", color = Color.White, style = MaterialTheme.typography.labelMedium) } + } +} diff --git a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt index 20884a3..bcc23a9 100644 --- a/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt +++ b/composeApp/src/commonMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/navigation/NativeNavBars.kt @@ -1,6 +1,7 @@ package io.github.apdelrahman1911.nativecomposekit.app.navigation import androidx.compose.foundation.layout.RowScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material3.CenterAlignedTopAppBar @@ -65,6 +66,9 @@ object NativeNavDefaults { modifier: Modifier = Modifier, colors: TopAppBarColors? = null, centeredTitle: Boolean = false, + // M3 default (status-bar aware) for real host placement; pass WindowInsets(0) when embedding the + // bar as an inline preview/exhibit. + windowInsets: WindowInsets = TopAppBarDefaults.windowInsets, ) { val barColors = colors ?: TopAppBarDefaults.topAppBarColors() val navigationIcon: @Composable () -> Unit = { @@ -80,6 +84,7 @@ object NativeNavDefaults { modifier = modifier, navigationIcon = navigationIcon, actions = state.actions, + windowInsets = windowInsets, colors = barColors, ) } else { @@ -88,6 +93,7 @@ object NativeNavDefaults { modifier = modifier, navigationIcon = navigationIcon, actions = state.actions, + windowInsets = windowInsets, colors = barColors, ) } @@ -104,9 +110,16 @@ object NativeNavDefaults { containerColor: Color = NavigationBarDefaults.containerColor, contentColor: Color = MaterialTheme.colorScheme.contentColorFor(containerColor), itemColors: NavigationBarItemColors? = null, + // M3 default (navigation-bar aware) for real host placement; WindowInsets(0) for inline exhibits. + windowInsets: WindowInsets = NavigationBarDefaults.windowInsets, ) { val resolvedItemColors = itemColors ?: NavigationBarItemDefaults.colors() - NavigationBar(modifier = modifier, containerColor = containerColor, contentColor = contentColor) { + NavigationBar( + modifier = modifier, + containerColor = containerColor, + contentColor = contentColor, + windowInsets = windowInsets, + ) { state.tabs.forEach { item -> NavigationBarItem( selected = item.tab.id == state.selectedTabId, diff --git a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt index 6ba5e5a..8ef6788 100644 --- a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt +++ b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/MainViewController.kt @@ -17,7 +17,6 @@ import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeSource import io.github.apdelrahman1911.nativecomposekit.chrome.NativeChromeTab import io.github.apdelrahman1911.nativecomposekit.components.NativeImeLog import kotlin.native.Platform -import kotlinx.coroutines.launch import platform.UIKit.UIViewController /** @@ -93,31 +92,6 @@ fun createNativeNavRoot(): NativeNavRoot { barConfigForRoute = ::appBarConfig, // per-screen chrome behavior, shared with the Material host ) - // TEMP-VERIFY (removed before commit): chrome-customization battery — styled shell (tint + selected - // tab color + large titles enabled) and a scripted ChromeDemo round trip. - io.github.apdelrahman1911.nativecomposekit.theme.applyNativeShellStyle( - io.github.apdelrahman1911.nativecomposekit.theme.NativeShellStyle( - tint = io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor( - light = androidx.compose.ui.graphics.Color(0.75f, 0.20f, 0.30f), - dark = androidx.compose.ui.graphics.Color(0.95f, 0.45f, 0.55f), - ), - tabItemSelected = io.github.apdelrahman1911.nativecomposekit.theme.NativeShellColor( - light = androidx.compose.ui.graphics.Color(0.75f, 0.20f, 0.30f), - dark = androidx.compose.ui.graphics.Color(0.95f, 0.45f, 0.55f), - ), - ), - ) - kotlinx.coroutines.MainScope().launch { - kotlinx.coroutines.delay(3000) - navigator.selectTab(AppTab.Settings); println("NCK-VERIFY settings selected") - kotlinx.coroutines.delay(1600) - navigator.push(AppRoute.ChromeDemo); println("NCK-VERIFY chrome demo pushed") - kotlinx.coroutines.delay(2600) - navigator.pop(); println("NCK-VERIFY popped back") - kotlinx.coroutines.delay(1600) - println("NCK-VERIFY battery done") - } - return NativeNavRoot(chrome = chrome) } diff --git a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt new file mode 100644 index 0000000..3cd14ce --- /dev/null +++ b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt @@ -0,0 +1,142 @@ +package io.github.apdelrahman1911.nativecomposekit.app + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.ExperimentalComposeUiApi +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.UIKitInteropProperties +import androidx.compose.ui.viewinterop.UIKitView +import io.github.apdelrahman1911.nativecomposekit.theme.nativeBackgroundUIColor +import kotlinx.cinterop.ExperimentalForeignApi +import platform.UIKit.NSForegroundColorAttributeName +import platform.UIKit.UIBarButtonItem +import platform.UIKit.UIBarButtonItemStyle +import platform.UIKit.UIColor +import platform.UIKit.UIImage +import platform.UIKit.UINavigationBar +import platform.UIKit.UINavigationBarAppearance +import platform.UIKit.UINavigationItem +import platform.UIKit.UITabBar +import platform.UIKit.UITabBarAppearance +import platform.UIKit.UITabBarItem +import platform.UIKit.UIUserInterfaceStyle +import platform.UIKit.colorWithDynamicProvider +import platform.UIKit.labelColor + +// Preview-local Color→UIColor bridge (the kit's converter is internal by design). +private fun Color.toPreviewUIColor(): UIColor = + UIColor(red = red.toDouble(), green = green.toDouble(), blue = blue.toDouble(), alpha = alpha.toDouble()) + +/** + * A real `UINavigationBar` as an inline, non-interactive preview. Raw `UIKitView` on purpose (display-only + * demo; the kit's scroll-safe backing machinery is for its own components) — a slight scroll lag on this + * debug page is acceptable and documented behavior for raw interop. + */ +@OptIn(ExperimentalComposeUiApi::class, ExperimentalForeignApi::class) +@Composable +actual fun IosNavBarPreview( + title: String, + modifier: Modifier, + background: IosPreviewBackground, + customBackground: Color?, + tint: Color?, + actionSymbols: List, + showsBack: Boolean, + hairline: Boolean, +) { + val bar = remember { UINavigationBar() } + UIKitView( + factory = { + bar.userInteractionEnabled = false + bar + }, + modifier = modifier.fillMaxWidth().height(44.dp), + properties = UIKitInteropProperties(interactionMode = null, placedAsOverlay = true), + update = { _ -> + val appearance = UINavigationBarAppearance() + if (background == IosPreviewBackground.Material) { + appearance.configureWithDefaultBackground() + } else { + appearance.configureWithOpaqueBackground() + appearance.backgroundColor = + if (background == IosPreviewBackground.Custom && customBackground != null) { + customBackground.toPreviewUIColor() + } else { + UIColor.colorWithDynamicProvider { traits -> + nativeBackgroundUIColor( + dark = traits?.userInterfaceStyle == UIUserInterfaceStyle.UIUserInterfaceStyleDark, + ) + } + } + } + if (!hairline) appearance.shadowColor = UIColor.clearColor() + appearance.titleTextAttributes = mapOf(NSForegroundColorAttributeName to UIColor.labelColor()) + bar.standardAppearance = appearance + bar.compactAppearance = appearance + bar.scrollEdgeAppearance = appearance + tint?.let { bar.tintColor = it.toPreviewUIColor() } + + val item = UINavigationItem(title = title) + item.rightBarButtonItems = actionSymbols.map { symbol -> + UIBarButtonItem( + image = UIImage.systemImageNamed(symbol), + style = UIBarButtonItemStyle.UIBarButtonItemStylePlain, + target = null, + action = null, + ) + } + if (showsBack) { + bar.setItems(listOf(UINavigationItem(title = "Back"), item), animated = false) + } else { + bar.setItems(listOf(item), animated = false) + } + }, + ) +} + +/** A real `UITabBar` as an inline, non-interactive preview. */ +@OptIn(ExperimentalComposeUiApi::class) +@Composable +actual fun IosTabBarPreview( + modifier: Modifier, + selectedColor: Color?, + unselectedColor: Color?, + tint: Color?, +) { + val bar = remember { UITabBar() } + UIKitView( + factory = { + bar.userInteractionEnabled = false + val items = listOf( + UITabBarItem(title = "Components", image = UIImage.systemImageNamed("square.grid.2x2"), tag = 0), + UITabBarItem(title = "Library", image = UIImage.systemImageNamed("books.vertical"), tag = 1), + UITabBarItem(title = "Settings", image = UIImage.systemImageNamed("gearshape"), tag = 2), + ) + bar.setItems(items, animated = false) + bar.selectedItem = items.first() + bar + }, + modifier = modifier.fillMaxWidth().height(50.dp), + properties = UIKitInteropProperties(interactionMode = null, placedAsOverlay = true), + update = { _ -> + val appearance = UITabBarAppearance() + appearance.configureWithDefaultBackground() + selectedColor?.toPreviewUIColor()?.let { sel -> + appearance.stackedLayoutAppearance.selected.iconColor = sel + appearance.stackedLayoutAppearance.selected.titleTextAttributes = + mapOf(NSForegroundColorAttributeName to sel) + } + unselectedColor?.toPreviewUIColor()?.let { unsel -> + appearance.stackedLayoutAppearance.normal.iconColor = unsel + appearance.stackedLayoutAppearance.normal.titleTextAttributes = + mapOf(NSForegroundColorAttributeName to unsel) + } + bar.standardAppearance = appearance + tint?.let { bar.tintColor = it.toPreviewUIColor() } + }, + ) +} From 906e05ea955bb9391a8e537a0b0052fc70c8740a Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sun, 5 Jul 2026 03:22:51 +0300 Subject: [PATCH 6/8] Clip the embedded iOS bar previews and size the tab-bar exhibit correctly --- .../nativecomposekit/app/IosBarPreviews.ios.kt | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt index 3cd14ce..099a23c 100644 --- a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt +++ b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt @@ -52,6 +52,9 @@ actual fun IosNavBarPreview( UIKitView( factory = { bar.userInteractionEnabled = false + // A standalone UINavigationBar paints its background UPWARD past its bounds (the status-bar + // extension); unclipped that bleeds above the preview slot as fake top padding. + bar.clipsToBounds = true bar }, modifier = modifier.fillMaxWidth().height(44.dp), @@ -111,6 +114,7 @@ actual fun IosTabBarPreview( UIKitView( factory = { bar.userInteractionEnabled = false + bar.clipsToBounds = true // same out-of-bounds background painting as the nav bar val items = listOf( UITabBarItem(title = "Components", image = UIImage.systemImageNamed("square.grid.2x2"), tag = 0), UITabBarItem(title = "Library", image = UIImage.systemImageNamed("books.vertical"), tag = 1), @@ -120,7 +124,9 @@ actual fun IosTabBarPreview( bar.selectedItem = items.first() bar }, - modifier = modifier.fillMaxWidth().height(50.dp), + // A bare UITabBar needs room for the iOS 26 pill's icon-above-label layout — squeezed below + // ~84pt the labels collapse onto the icons. + modifier = modifier.fillMaxWidth().height(84.dp), properties = UIKitInteropProperties(interactionMode = null, placedAsOverlay = true), update = { _ -> val appearance = UITabBarAppearance() From 9b4971545bab8487d3da0ec8a92c7bff690a11e9 Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sun, 5 Jul 2026 08:44:18 +0300 Subject: [PATCH 7/8] Pad the iOS bar previews evenly and flatten their buttons --- .../app/IosBarPreviews.ios.kt | 56 ++++++++++++++----- 1 file changed, 42 insertions(+), 14 deletions(-) diff --git a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt index 099a23c..12cf038 100644 --- a/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt +++ b/composeApp/src/iosMain/kotlin/io/github/apdelrahman1911/nativecomposekit/app/IosBarPreviews.ios.kt @@ -1,7 +1,10 @@ package io.github.apdelrahman1911.nativecomposekit.app +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.ExperimentalComposeUiApi @@ -16,6 +19,10 @@ import platform.UIKit.NSForegroundColorAttributeName import platform.UIKit.UIBarButtonItem import platform.UIKit.UIBarButtonItemStyle import platform.UIKit.UIColor +import platform.Foundation.NSNumber +import platform.Foundation.NSSelectorFromString +import platform.Foundation.numberWithBool +import platform.Foundation.setValue import platform.UIKit.UIImage import platform.UIKit.UINavigationBar import platform.UIKit.UINavigationBarAppearance @@ -31,6 +38,25 @@ import platform.UIKit.labelColor private fun Color.toPreviewUIColor(): UIColor = UIColor(red = red.toDouble(), green = green.toDouble(), blue = blue.toDouble(), alpha = alpha.toDouble()) +// iOS 26 wraps every UIBarButtonItem (even custom-view ones) in a Liquid Glass capsule; a standalone +// clipped preview bar gives the capsule's backdrop sampling nothing to sample, so it degrades into a +// washed-out disc. UIKit's per-item opt-out is `hidesSharedBackground` — set via KVC behind a selector +// check so this still compiles/runs against pre-26 SDK bindings. Previews therefore show the classic +// flat icons; the app's real bars and the live pushed demos show the true glass treatment. +@OptIn(ExperimentalForeignApi::class) +private fun flatPreviewItem(symbol: String): UIBarButtonItem { + val item = UIBarButtonItem( + image = UIImage.systemImageNamed(symbol), + style = UIBarButtonItemStyle.UIBarButtonItemStylePlain, + target = null, + action = null, + ) + if (item.respondsToSelector(NSSelectorFromString("setHidesSharedBackground:"))) { + item.setValue(NSNumber.numberWithBool(true), forKey = "hidesSharedBackground") + } + return item +} + /** * A real `UINavigationBar` as an inline, non-interactive preview. Raw `UIKitView` on purpose (display-only * demo; the kit's scroll-safe backing machinery is for its own components) — a slight scroll lag on this @@ -49,7 +75,16 @@ actual fun IosNavBarPreview( hairline: Boolean, ) { val bar = remember { UINavigationBar() } - UIKitView( + // The 44pt bar sits centered in a padded frame; for a Custom background the frame is painted in the + // same color, so the preview block has even breathing room above AND below the bar content. + val frameModifier = + if (background == IosPreviewBackground.Custom && customBackground != null) { + modifier.fillMaxWidth().background(customBackground) + } else { + modifier.fillMaxWidth() + } + Box(frameModifier.padding(vertical = 10.dp)) { + UIKitView( factory = { bar.userInteractionEnabled = false // A standalone UINavigationBar paints its background UPWARD past its bounds (the status-bar @@ -57,7 +92,7 @@ actual fun IosNavBarPreview( bar.clipsToBounds = true bar }, - modifier = modifier.fillMaxWidth().height(44.dp), + modifier = Modifier.fillMaxWidth().height(44.dp), properties = UIKitInteropProperties(interactionMode = null, placedAsOverlay = true), update = { _ -> val appearance = UINavigationBarAppearance() @@ -84,21 +119,14 @@ actual fun IosNavBarPreview( tint?.let { bar.tintColor = it.toPreviewUIColor() } val item = UINavigationItem(title = title) - item.rightBarButtonItems = actionSymbols.map { symbol -> - UIBarButtonItem( - image = UIImage.systemImageNamed(symbol), - style = UIBarButtonItemStyle.UIBarButtonItemStylePlain, - target = null, - action = null, - ) - } + item.rightBarButtonItems = actionSymbols.map { symbol -> flatPreviewItem(symbol) } if (showsBack) { - bar.setItems(listOf(UINavigationItem(title = "Back"), item), animated = false) - } else { - bar.setItems(listOf(item), animated = false) + item.leftBarButtonItems = listOf(flatPreviewItem("chevron.left")) } + bar.setItems(listOf(item), animated = false) }, - ) + ) + } } /** A real `UITabBar` as an inline, non-interactive preview. */ From 021f62226da1f579034b0302c6f2fe45ccf9e4fb Mon Sep 17 00:00:00 2001 From: Apdelrahman1911 Date: Sun, 5 Jul 2026 08:57:33 +0300 Subject: [PATCH 8/8] Ignore local screenshot folders and document the chrome API in the changelog --- .gitignore | 6 ++++++ CHANGELOG.md | 13 +++++++++++++ 2 files changed, 19 insertions(+) diff --git a/.gitignore b/.gitignore index 6271197..85bed57 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,9 @@ iosApp/iosApp.xcodeproj/ screenshots/ docs/*.jpeg docs/*.jpg + +# Local review screenshots (user-provided, never commit) +p1/ +p2/ +p3/ +p4/ diff --git a/CHANGELOG.md b/CHANGELOG.md index ce97fef..d8a7391 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ changes; the public surface is ABI-locked per release via binary-compatibility-v ## [Unreleased] +### Added + +- **Per-screen chrome behavior** — `NativeBarConfig` (`hidesTopBar`, `hidesTabBar`, per-screen + `actions`) carried on `NativeChromeEntry.bar`, so any host can drive per-screen bar visibility and + native toolbar actions through the existing chrome projection. Defaults unchanged; fully opt-in. +- **iOS shell style registry** — `NativeShellStyle` + `applyNativeShellStyle()` (iosMain): themed / + system-material / custom bar backgrounds, global tint, tab-item selected/unselected colors, title + font, and hairline visibility for the native `UINavigationBar`/`UITabBar` chrome, with + `nativeShell…UIColor` resolvers for Swift shells. +- The sample app gained Compose-side bar slots + restylable defaults (`NativeNavDefaults`) and a + "Navigation toolbar styles" catalog (Settings → Developer) demonstrating the full customization + surface on both platforms, including documented limitations. + ## [0.1.0] — 2026-07-04 First public release.