@momo-kits/native-kits 0.300.3-debug → 0.300.4-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -1,6 +1,9 @@
1
1
  package vn.momo.kits.navigation
2
2
 
3
3
  import androidx.compose.animation.animateColorAsState
4
+ import androidx.compose.animation.core.Animatable
5
+ import androidx.compose.animation.core.AnimationVector1D
6
+ import androidx.compose.animation.core.tween
4
7
  import androidx.compose.foundation.ScrollState
5
8
  import androidx.compose.foundation.background
6
9
  import androidx.compose.foundation.border
@@ -39,6 +42,7 @@ import androidx.compose.ui.Alignment
39
42
  import androidx.compose.ui.Modifier
40
43
  import androidx.compose.ui.graphics.Brush
41
44
  import androidx.compose.ui.graphics.Color
45
+ import androidx.compose.ui.graphics.graphicsLayer
42
46
  import androidx.compose.ui.graphics.lerp
43
47
  import androidx.compose.ui.layout.onGloballyPositioned
44
48
  import androidx.compose.ui.platform.LocalDensity
@@ -51,7 +55,6 @@ import kotlinx.coroutines.flow.filter
51
55
  import kotlinx.coroutines.launch
52
56
  import vn.momo.kits.components.InputSearch
53
57
  import vn.momo.kits.const.AppNavigationBar
54
- import vn.momo.kits.const.AppStatusBar
55
58
  import vn.momo.kits.const.AppTheme
56
59
  import vn.momo.kits.application.IsShowBaseLineDebug
57
60
  import vn.momo.kits.const.Colors
@@ -65,14 +68,24 @@ import vn.momo.kits.navigation.component.HEADER_HEIGHT
65
68
  import vn.momo.kits.navigation.component.Header
66
69
  import vn.momo.kits.navigation.component.HeaderBackground
67
70
  import vn.momo.kits.navigation.component.HeaderRight
71
+ import vn.momo.kits.navigation.component.HeaderSideBar
72
+ import vn.momo.kits.navigation.bottomtab.LocalDuoRailReserve
73
+ import vn.momo.kits.navigation.bottomtab.duoFloatingTabBarInset
68
74
  import vn.momo.kits.navigation.component.HeaderType
69
75
  import vn.momo.kits.navigation.component.InputSearchType
76
+ import vn.momo.kits.navigation.bottomtab.LocalBottomTabFocused
70
77
  import vn.momo.kits.platform.BackHandler
71
78
  import vn.momo.kits.platform.supportsImePadding
72
79
  import vn.momo.kits.navigation.tracking.ScreenTracker
73
80
  import vn.momo.kits.navigation.tracking.ScreenTrackingState
74
81
  import kotlinx.coroutines.delay
75
82
  import vn.momo.kits.application.LocalContext
83
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
84
+ import vn.momo.kits.layout.LocalScreenLayout
85
+ import vn.momo.kits.layout.ScreenLayout
86
+ import vn.momo.kits.layout.consumeHorizontalInsets
87
+ import vn.momo.kits.layout.DuoMetrics
88
+ import vn.momo.kits.layout.getScreenLayout
76
89
  import kotlin.time.Clock
77
90
  import kotlin.time.Duration.Companion.milliseconds
78
91
  import kotlin.time.ExperimentalTime
@@ -80,11 +93,19 @@ import kotlin.time.ExperimentalTime
80
93
  internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
81
94
  internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
82
95
 
96
+ // Space a tab's content leaves for the tab bar and home indicator. On iPhone Duo the floating bar
97
+ // overlays the content and the rail lives in the side strip, so only their own inset applies.
83
98
  @Composable
84
- private fun bottomTabBarInset(): Dp {
85
- if (LocalNavigation.current.bottomTabIndex == -1) return 0.dp
99
+ private fun bottomTabChildInset(): Dp {
86
100
  val keyboard = keyboardSizeState().value
87
- return (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp)
101
+ val navigationBar = AppNavigationBar.current
102
+ val layout = getScreenLayout()
103
+ return when {
104
+ layout.isIPhoneDuo && layout.sideBar == null -> (duoFloatingTabBarInset(layout) - keyboard).coerceAtLeast(0.dp)
105
+ layout.isIPhoneDuo -> (navigationBar - keyboard).coerceAtLeast(0.dp)
106
+ else -> (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp) +
107
+ (navigationBar - keyboard).coerceAtLeast(0.dp)
108
+ }
88
109
  }
89
110
 
90
111
  @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
@@ -100,7 +121,7 @@ internal fun StackScreen(
100
121
  val navigator = LocalNavigator.current
101
122
  val maxApi = LocalMaxApi.current
102
123
  val context = LocalContext.current
103
- val statusBar = AppStatusBar.current
124
+ val screenLayout = getScreenLayout()
104
125
  val density = LocalDensity.current
105
126
  val screenRegistry = LocalDynamicScreenRegistry.current
106
127
  val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
@@ -109,20 +130,24 @@ internal fun StackScreen(
109
130
  val isBottomTabChild = bottomTabIndex != -1
110
131
  val isBottomTabRoot = navigation.isBottomTabRoot
111
132
 
133
+ // A mounted-but-hidden bottom tab tracks again each time it is focused (RN useFocusEffect).
134
+ val tabFocused = LocalBottomTabFocused.current
135
+
112
136
  // Auto tracking state
113
- val trackingState = remember {
137
+ val trackingState = remember(tabFocused) {
114
138
  ScreenTrackingState().apply {
115
139
  time = Clock.System.now().toEpochMilliseconds()
116
140
  }
117
141
  }
118
142
 
119
143
  // Determine action: push or back
120
- val action = remember {
144
+ val action = remember(tabFocused) {
121
145
  if (ScreenTracker.getLastScreenName() != null) "push" else "back"
122
146
  }
123
147
 
124
148
  // Auto tracking effects
125
- LaunchedEffect(name) {
149
+ LaunchedEffect(name, tabFocused) {
150
+ if (!tabFocused) return@LaunchedEffect
126
151
  // Track screen navigated immediately
127
152
  ScreenTracker.trackScreenNavigated(
128
153
  maxApi = maxApi,
@@ -156,7 +181,7 @@ internal fun StackScreen(
156
181
  }
157
182
 
158
183
  val limit = with(density) {
159
- (statusBar + HEADER_HEIGHT.dp).toPx()
184
+ screenLayout.headerHeight.toPx()
160
185
  }.toInt()
161
186
 
162
187
  val lazyListState = options.scrollData.scrollState as? LazyListState
@@ -165,11 +190,16 @@ internal fun StackScreen(
165
190
  else
166
191
  (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 1)
167
192
  val snapState: ScrollableState = lazyListState ?: scrollState
193
+ val headerHide = rememberQuickReturnHide(
194
+ enabled = options.usesDuoQuickReturnHeader(screenLayout),
195
+ scrollState = scrollState,
196
+ heightPx = with(density) { screenLayout.headerHeight.toPx() },
197
+ )
168
198
 
169
199
  val footerHeightPx = remember { mutableIntStateOf(0) }
170
200
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
171
201
 
172
- if (navigation.options.scrollData.scrollToTopCallback != null) {
202
+ if (tabFocused && navigation.options.scrollData.scrollToTopCallback != null) {
173
203
  RegisterScrollToTop(navigation.options.scrollData.scrollToTopCallback)
174
204
  }
175
205
 
@@ -189,40 +219,74 @@ internal fun StackScreen(
189
219
  LocalFooterHeightPx provides footerHeightPx,
190
220
  LocalHeaderRightWidthPx provides headerRightWidthPx
191
221
  ) {
192
- Box(
193
- Modifier
194
- .fillMaxSize()
195
- .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
196
- .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
197
- hideKeyboardOnTap()
222
+ // AI-GENERATED START: custom screen background, full window and outside the keyboard padding
223
+ Box(Modifier.fillMaxSize()) {
224
+ options.backgroundComponent?.let { background ->
225
+ Box(Modifier.fillMaxSize()) { background() }
226
+ }
227
+ // AI-GENERATED END: custom screen background, full window and outside the keyboard padding
228
+ Box(
229
+ Modifier
230
+ .fillMaxSize()
231
+ // AI-GENERATED START: custom background replaces the colour
232
+ .background(
233
+ if (options.backgroundComponent != null) Color.Transparent
234
+ else options.backgroundColor ?: AppTheme.current.colors.background.default
235
+ )
236
+ // AI-GENERATED END: custom background replaces the colour
237
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
238
+ hideKeyboardOnTap()
239
+ }
240
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
241
+ imePadding()
242
+ }
243
+ ) {
244
+ Box(Modifier.zIndex(1f)) {
245
+ HeaderBackground()
198
246
  }
199
- .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
200
- imePadding()
247
+
248
+ Box(Modifier.zIndex(4f).graphicsLayer { translationY = -headerHide.value }) {
249
+ Header(onBackHandler)
201
250
  }
202
- ) {
203
- Box(Modifier.zIndex(1f)) {
204
- HeaderBackground()
205
- }
206
251
 
207
- Box(Modifier.zIndex(4f)) {
208
- Header(onBackHandler)
209
- }
252
+ HeaderSideBar(
253
+ bottomInset = maxOf(
254
+ bottomBarsHeight(isBottomTabChild) + Spacing.XL,
255
+ if (isBottomTabChild) LocalDuoRailReserve.current else 0.dp,
256
+ ),
257
+ modifier = Modifier.zIndex(4f),
258
+ )
210
259
 
211
- Column(Modifier.zIndex(5f)) {
212
- SearchAnimated(snapState = snapState)
213
- }
260
+ Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
261
+ SearchAnimated(snapState = snapState)
262
+ }
214
263
 
215
- Column(Modifier.zIndex(2f).fillMaxSize()) {
216
- MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
217
- FooterContent()
218
- }
264
+ Column(Modifier.zIndex(2f).fillMaxSize()) {
265
+ MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
266
+ FooterContent()
267
+ }
219
268
 
220
- Box(Modifier.zIndex(6f)) {
221
- FloatingContent(isBottomTabChild = isBottomTabChild)
222
- }
269
+ Box(Modifier.zIndex(6f).horizontalSafeAreaPadding()) {
270
+ FloatingContent(isBottomTabChild = isBottomTabChild)
271
+ }
223
272
 
224
- OverplayView(isBottomTabChild = isBottomTabChild, id = id)
273
+ OverplayView(isBottomTabChild = isBottomTabChild, id = id)
274
+ }
275
+ // AI-GENERATED START: custom screen background, full window and outside the keyboard padding
225
276
  }
277
+ // AI-GENERATED END: custom screen background, full window and outside the keyboard padding
278
+ }
279
+ }
280
+
281
+ // Height the footer, tab bar or home indicator takes from the bottom of the screen.
282
+ @Composable
283
+ private fun bottomBarsHeight(isBottomTabChild: Boolean): Dp {
284
+ val options = LocalOptions.current
285
+ val navigationBar = AppNavigationBar.current
286
+ return when {
287
+ options.hasFooter() -> with(LocalDensity.current) { LocalFooterHeightPx.current.intValue.toDp() }
288
+ isBottomTabChild -> bottomTabChildInset()
289
+ else -> navigationBar
226
290
  }
227
291
  }
228
292
 
@@ -230,18 +294,10 @@ internal fun StackScreen(
230
294
  @Composable
231
295
  internal fun FloatingContent(isBottomTabChild: Boolean) {
232
296
  val options = LocalOptions.current
233
- val density = LocalDensity.current
234
- val footerHeightPx = LocalFooterHeightPx.current
235
297
  val scrollState = LocalScrollState.current
236
- val navigationBar = AppNavigationBar.current
237
298
 
238
299
  val fabProps = options.floatingButtonProps ?: return
239
- val bottomPadding = fabProps.bottom
240
- ?: (Spacing.M + when {
241
- options.hasFooter() -> with(density) { footerHeightPx.intValue.toDp() }
242
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
243
- else -> navigationBar
244
- })
300
+ val bottomPadding = fabProps.bottom ?: (Spacing.M + bottomBarsHeight(isBottomTabChild))
245
301
 
246
302
  FloatingButton(
247
303
  scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
@@ -263,11 +319,13 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
263
319
  val density = LocalDensity.current
264
320
  val scrollState = LocalScrollState.current
265
321
  val navigationBar = AppNavigationBar.current
322
+ val screenLayout = getScreenLayout()
323
+ val useDuoGlass = options.usesDuoGlassHeader(screenLayout)
266
324
 
267
325
  Spacer(
268
326
  Modifier.height(
269
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
270
- AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
327
+ if (!useDuoGlass && (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent)))
328
+ screenLayout.headerHeight else 0.dp
271
329
  )
272
330
  )
273
331
  if (inputSearchType == InputSearchType.Animated) {
@@ -275,31 +333,46 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
275
333
 
276
334
  Spacer(Modifier.height((HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp)))
277
335
  }
336
+ // a bottom tab root stays full width: the tab bar and each tab's own StackScreen handle the insets
337
+ val insetContent = !isBottomTabRoot
278
338
  Column(
279
339
  Modifier
280
340
  .fillMaxWidth()
281
341
  .weight(1f)
342
+ .conditional(insetContent && options.headerType !is HeaderType.Animated) {
343
+ horizontalSafeAreaPadding()
344
+ }
282
345
  .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
283
346
  verticalScroll(scrollState)
284
347
  }
348
+ .conditional(useDuoGlass) {
349
+ padding(top = screenLayout.headerHeight)
350
+ }
285
351
  .padding(
286
352
  bottom = when {
287
353
  options.hasFooter() || isBottomTabRoot -> 0.dp
288
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
354
+ isBottomTabChild -> bottomTabChildInset()
289
355
  !options.scrollData.scrollable && options.scrollData.scrollState == null -> 0.dp
290
356
  else -> navigationBar
291
357
  }
292
358
  )
293
359
  ) {
294
- ScreenContent(content = content)
360
+ ScreenContent(content = content, insetContent = insetContent)
295
361
  }
296
362
 
297
363
  }
298
364
 
299
365
  @Composable
300
- internal fun ScreenContent(content: @Composable () -> Unit) {
366
+ internal fun ScreenContent(content: @Composable () -> Unit, insetContent: Boolean) {
301
367
  val scrollState = LocalScrollState.current
302
368
  val options = LocalOptions.current
369
+ val screenLayout = getScreenLayout()
370
+ val insetBody: @Composable () -> Unit = {
371
+ CompositionLocalProvider(
372
+ LocalScreenLayout provides if (insetContent) screenLayout.consumeHorizontalInsets() else screenLayout,
373
+ content = content,
374
+ )
375
+ }
303
376
 
304
377
  if (options.headerType is HeaderType.Animated) {
305
378
  val animatedHeader = options.headerType
@@ -310,12 +383,16 @@ internal fun ScreenContent(content: @Composable () -> Unit) {
310
383
  Box(bannerModifier) {
311
384
  animatedHeader.composable.invoke(scrollState.value)
312
385
  }
313
- Box(Modifier.padding(top = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
314
- content()
386
+ Box(
387
+ Modifier
388
+ .padding(top = screenLayout.headerHeight + animatedHeader.layoutOffSet)
389
+ .conditional(insetContent) { horizontalSafeAreaPadding() }
390
+ ) {
391
+ insetBody()
315
392
  }
316
393
  }
317
394
  } else {
318
- content()
395
+ insetBody()
319
396
  }
320
397
  }
321
398
 
@@ -324,7 +401,9 @@ internal fun FooterContent() {
324
401
  val options = LocalOptions.current
325
402
  if (options.hasFooter()) {
326
403
  val keyboardSize = keyboardSizeState()
327
- val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
404
+ // the Duo floating tab bar overlays tab content, so a tab's footer sits above it
405
+ val duoTabInset = if (LocalNavigation.current.bottomTabIndex != -1) duoFloatingTabBarInset(getScreenLayout()) else 0.dp
406
+ val bottomPadding = (maxOf(AppNavigationBar.current, duoTabInset) - keyboardSize.value).coerceAtLeast(0.dp)
328
407
  Footer(
329
408
  footerComponent = options.footerComponent,
330
409
  bottomPadding = bottomPadding,
@@ -372,9 +451,10 @@ fun Footer(
372
451
 
373
452
  val backgroundColor = if (transparent) Color.Transparent else AppTheme.current.colors.background.surface
374
453
 
454
+ // inset as a whole, not padded inside, so the surface and its shadow stay off the iPhone Duo side bar
375
455
  Box(Modifier.onGloballyPositioned {
376
456
  if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
377
- }) {
457
+ }.horizontalSafeAreaPadding()) {
378
458
  Box(
379
459
  Modifier
380
460
  .fillMaxWidth()
@@ -429,17 +509,28 @@ internal fun SearchAnimated(
429
509
  if (inputSearchType == InputSearchType.None) return
430
510
  val inputSearchProps = (options.headerType as? HeaderType.DefaultOrExtended)?.inputSearchProps ?: return
431
511
 
432
- val minTopPadding = AppStatusBar.current
433
- val maxTopPadding = AppStatusBar.current + HEADER_HEIGHT.dp
512
+ val screenLayout = getScreenLayout()
513
+ val useSideBar = screenLayout.sideBar != null
514
+ // collapsed, the search is centered in the bar row
515
+ val minTopPadding = screenLayout.headerStatusBarHeight + (screenLayout.headerBarHeight - HEADER_HEIGHT.dp) / 2
516
+ val maxTopPadding = screenLayout.headerHeight
517
+ // collapsed on the iPhone Duo status bar row, the search stays between its clusters (48pt back there)
518
+ val clearance = screenLayout.statusBarClearance
519
+ val backWidth = if (clearance != null && !useSideBar) DuoMetrics.controlSize else 28.dp
434
520
  val minStartPadding = Spacing.M
435
- val maxStartPadding = if (options.hiddenBack) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
521
+ val maxStartPadding = (clearance?.left ?: 0.dp) +
522
+ if (options.hiddenBack || useSideBar) Spacing.M else backWidth + Spacing.M + Spacing.S
436
523
  val minEndPadding = Spacing.M
437
- val maxEndPadding = headerRightWidthDp + if (options.headerRight is HeaderRight.None) Spacing.M else Spacing.M * 2
524
+ val maxEndPadding = (clearance?.right ?: 0.dp) + when {
525
+ options.headerRight is HeaderRight.None -> Spacing.M
526
+ useSideBar && options.headerRight is HeaderRight.Toolkit -> Spacing.M
527
+ else -> headerRightWidthDp + Spacing.M * 2
528
+ }
438
529
 
439
530
  val targetColor = lerp(
440
531
  AppTheme.current.colors.background.surface,
441
532
  AppTheme.current.colors.background.default,
442
- (scrollDp / minTopPadding).coerceIn(0f, 1f)
533
+ (scrollDp / minTopPadding.coerceAtLeast(1.dp)).coerceIn(0f, 1f)
443
534
  )
444
535
  val animatedColor by animateColorAsState(targetValue = targetColor, label = "BackgroundColor")
445
536
 
@@ -581,6 +672,52 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
581
672
 
582
673
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
583
674
 
675
+ // iPhone Duo side bar: the header leaves with the content and slides back in on a reverse scroll
676
+ // (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
677
+ @Composable
678
+ private fun rememberQuickReturnHide(
679
+ enabled: Boolean,
680
+ scrollState: ScrollState,
681
+ heightPx: Float,
682
+ ): Animatable<Float, AnimationVector1D> {
683
+ val hide = remember { Animatable(0f) }
684
+ LaunchedEffect(enabled, scrollState, heightPx) {
685
+ if (!enabled) {
686
+ hide.snapTo(0f)
687
+ return@LaunchedEffect
688
+ }
689
+ var last = scrollState.value
690
+ snapshotFlow { scrollState.value }.collect { y ->
691
+ val delta = y - last
692
+ last = y
693
+ hide.snapTo(if (y <= 0) 0f else (hide.value + delta).coerceIn(0f, heightPx))
694
+ }
695
+ }
696
+ // a half-shown header settles fully in or out once the scroll stops
697
+ LaunchedEffect(enabled, scrollState, heightPx) {
698
+ if (!enabled) return@LaunchedEffect
699
+ snapshotFlow { scrollState.isScrollInProgress }
700
+ .filter { !it }
701
+ .collect {
702
+ val value = hide.value
703
+ // within the first header height the header still sits on the content
704
+ if (scrollState.value < heightPx || value <= 0f || value >= heightPx) return@collect
705
+ launch { hide.animateTo(if (value > heightPx / 2) heightPx else 0f, tween(200)) }
706
+ }
707
+ }
708
+ return hide
709
+ }
710
+
711
+ internal fun NavigationOptions.usesDuoQuickReturnHeader(layout: ScreenLayout): Boolean =
712
+ usesDuoGlassHeader(layout) || (layout.sideBar != null && headerType is HeaderType.Animated &&
713
+ scrollData.scrollable && scrollData.scrollState is ScrollState)
714
+
715
+ // iPhone Duo side bar: content scrolls under a glass header instead of starting below it (RN Screen).
716
+ internal fun NavigationOptions.usesDuoGlassHeader(layout: ScreenLayout): Boolean =
717
+ layout.sideBar != null && headerType is HeaderType.DefaultOrExtended &&
718
+ getInputSearchType(this) == InputSearchType.None &&
719
+ scrollData.scrollable && scrollData.scrollState is ScrollState
720
+
584
721
  internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
585
722
  return when (val headerType = options.headerType) {
586
723
  is HeaderType.DefaultOrExtended -> when {