@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 (194) hide show
  1. package/CLAUDE.md +36 -8
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +15 -1
  4. package/compose/build.gradle.kts.backup +14 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle.properties +1 -1
  61. package/ios/Application/ApplicationEnvironment.swift +17 -0
  62. package/ios/Application/MaxApi.swift +12 -3
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  75. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  76. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  77. package/ios/Divider/Divider.swift +37 -4
  78. package/ios/Extensions/Color++.swift +38 -9
  79. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  80. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  81. package/ios/Icon/Icon.swift +21 -23
  82. package/ios/Icon/Icons.swift +1329 -0
  83. package/ios/IconButton/IconButton.swift +15 -7
  84. package/ios/Image/Image.swift +7 -23
  85. package/ios/Information/Information.swift +27 -9
  86. package/ios/Input/ErrorView.swift +2 -2
  87. package/ios/Input/Input.swift +160 -147
  88. package/ios/Input/InputPhoneNumber.swift +12 -9
  89. package/ios/Input/InputSearch.swift +102 -75
  90. package/ios/Input/InputTextArea.swift +128 -145
  91. package/ios/InputDropDown/InputDropDown.swift +10 -7
  92. package/ios/InputMoney/InputMoney.swift +33 -11
  93. package/ios/InputOTP/InputOTP.swift +22 -12
  94. package/ios/Layout/Card.swift +50 -0
  95. package/ios/Layout/GridContext.swift +187 -3
  96. package/ios/Layout/Item.swift +5 -4
  97. package/ios/Layout/ScreenLayout.swift +380 -0
  98. package/ios/Layout/Section.swift +4 -100
  99. package/ios/Loader/Loader.swift +5 -9
  100. package/ios/Lottie/LottieView.swift +14 -2
  101. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
  102. package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
  103. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  104. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  105. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  106. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  107. package/ios/Navigation/Component/Header.swift +531 -0
  108. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  109. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  110. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  111. package/ios/Navigation/Component/HeaderRight.swift +377 -0
  112. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  113. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  114. package/ios/Navigation/Component/SideBar.swift +183 -0
  115. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
  116. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  117. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
  118. package/ios/Navigation/Navigation.swift +204 -0
  119. package/ios/Navigation/NavigationContainer.swift +194 -0
  120. package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
  121. package/ios/Navigation/StackScreen.swift +820 -0
  122. package/ios/Navigation/StackTransition.swift +400 -0
  123. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  124. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  125. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  126. package/ios/Pagination/PaginationScroll.swift +2 -2
  127. package/ios/Popup/PopupDisplay.swift +72 -136
  128. package/ios/Popup/PopupInput.swift +10 -13
  129. package/ios/Popup/PopupLayout.swift +140 -0
  130. package/ios/Popup/PopupNotify.swift +56 -73
  131. package/ios/Popup/PopupPromotion.swift +31 -47
  132. package/ios/PopupView/FullscreenPopup.swift +13 -19
  133. package/ios/PopupView/PopupView.swift +1 -1
  134. package/ios/PopupView/Utils++.swift +0 -8
  135. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  136. package/ios/Radio/Radio.swift +2 -1
  137. package/ios/Rating/Rating.swift +9 -1
  138. package/ios/Resources/dot_loading.json +558 -0
  139. package/ios/Resources/lottie_circle_loader.json +1 -0
  140. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  141. package/ios/Skeleton/Skeleton.swift +22 -27
  142. package/ios/Slider/Slider.swift +16 -18
  143. package/ios/Stepper/Stepper.swift +6 -1
  144. package/ios/Steps/Steps.swift +8 -6
  145. package/ios/SuggestAction/SuggestAction.swift +5 -0
  146. package/ios/Swipeable/SwipeCell.swift +10 -6
  147. package/ios/Switch/Switch.swift +22 -20
  148. package/ios/TabView/TabView.swift +102 -21
  149. package/ios/Tag/Tag.swift +15 -4
  150. package/ios/Template/Logo/Logo.swift +2 -1
  151. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  152. package/ios/Title/Title.swift +6 -17
  153. package/ios/Tooltip/Tooltip.swift +29 -14
  154. package/ios/Typography/Text.swift +61 -19
  155. package/ios/Uploader/Uploader.swift +3 -1
  156. package/ios/native-kits.podspec +2 -1
  157. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  163. package/package.json +1 -1
  164. package/scripts/gen-ios-icons.mjs +36 -0
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  177. package/ios/Application/Components.swift +0 -270
  178. package/ios/Application/ComposeApi.swift +0 -22
  179. package/ios/Application/HeaderRight.swift +0 -306
  180. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  181. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  182. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  183. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  184. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  185. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  186. package/ios/Application/Navigation/component/Header.swift +0 -178
  187. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  188. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  189. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  190. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  191. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  192. package/ios/Application/Screen.swift +0 -250
  193. package/ios/Application/StackScreen.swift +0 -492
  194. /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
@@ -65,6 +69,8 @@ import vn.momo.kits.navigation.component.Header
65
69
  import vn.momo.kits.navigation.component.HeaderBackground
66
70
  import vn.momo.kits.navigation.component.HeaderRight
67
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
70
76
  import vn.momo.kits.platform.BackHandler
@@ -75,7 +81,9 @@ import kotlinx.coroutines.delay
75
81
  import vn.momo.kits.application.LocalContext
76
82
  import vn.momo.kits.utils.horizontalSafeAreaPadding
77
83
  import vn.momo.kits.layout.LocalScreenLayout
84
+ import vn.momo.kits.layout.ScreenLayout
78
85
  import vn.momo.kits.layout.consumeHorizontalInsets
86
+ import vn.momo.kits.layout.DuoMetrics
79
87
  import vn.momo.kits.layout.getScreenLayout
80
88
  import kotlin.time.Clock
81
89
  import kotlin.time.Duration.Companion.milliseconds
@@ -84,11 +92,19 @@ import kotlin.time.ExperimentalTime
84
92
  internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
85
93
  internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
86
94
 
95
+ // Space a tab's content leaves for the tab bar and home indicator. On iPhone Duo the floating bar
96
+ // overlays the content and the rail lives in the side strip, so only their own inset applies.
87
97
  @Composable
88
- private fun bottomTabBarInset(): Dp {
89
- if (LocalNavigation.current.bottomTabIndex == -1) return 0.dp
98
+ private fun bottomTabChildInset(): Dp {
90
99
  val keyboard = keyboardSizeState().value
91
- return (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp)
100
+ val navigationBar = AppNavigationBar.current
101
+ val layout = getScreenLayout()
102
+ return when {
103
+ layout.isIPhoneDuo && layout.sideBar == null -> (duoFloatingTabBarInset(layout) - keyboard).coerceAtLeast(0.dp)
104
+ layout.isIPhoneDuo -> (navigationBar - keyboard).coerceAtLeast(0.dp)
105
+ else -> (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp) +
106
+ (navigationBar - keyboard).coerceAtLeast(0.dp)
107
+ }
92
108
  }
93
109
 
94
110
  @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
@@ -169,6 +185,11 @@ internal fun StackScreen(
169
185
  else
170
186
  (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 1)
171
187
  val snapState: ScrollableState = lazyListState ?: scrollState
188
+ val headerHide = rememberQuickReturnHide(
189
+ enabled = options.usesDuoQuickReturnHeader(screenLayout),
190
+ scrollState = scrollState,
191
+ heightPx = with(density) { screenLayout.headerHeight.toPx() },
192
+ )
172
193
 
173
194
  val footerHeightPx = remember { mutableIntStateOf(0) }
174
195
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
@@ -193,42 +214,62 @@ internal fun StackScreen(
193
214
  LocalFooterHeightPx provides footerHeightPx,
194
215
  LocalHeaderRightWidthPx provides headerRightWidthPx
195
216
  ) {
196
- Box(
197
- Modifier
198
- .fillMaxSize()
199
- .background(options.backgroundColor ?: AppTheme.current.colors.background.default)
200
- .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
201
- hideKeyboardOnTap()
217
+ // AI-GENERATED START: custom screen background, full window and outside the keyboard padding
218
+ Box(Modifier.fillMaxSize()) {
219
+ options.backgroundComponent?.let { background ->
220
+ Box(Modifier.fillMaxSize()) { background() }
221
+ }
222
+ // AI-GENERATED END: custom screen background, full window and outside the keyboard padding
223
+ Box(
224
+ Modifier
225
+ .fillMaxSize()
226
+ // AI-GENERATED START: custom background replaces the colour
227
+ .background(
228
+ if (options.backgroundComponent != null) Color.Transparent
229
+ else options.backgroundColor ?: AppTheme.current.colors.background.default
230
+ )
231
+ // AI-GENERATED END: custom background replaces the colour
232
+ .conditional(options.keyboardOptions.keyboardShouldPersistTaps) {
233
+ hideKeyboardOnTap()
234
+ }
235
+ .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
236
+ imePadding()
237
+ }
238
+ ) {
239
+ Box(Modifier.zIndex(1f)) {
240
+ HeaderBackground()
202
241
  }
203
- .conditional(options.keyboardOptions.useAvoidKeyboard && supportsImePadding()) {
204
- imePadding()
242
+
243
+ Box(Modifier.zIndex(4f).graphicsLayer { translationY = -headerHide.value }) {
244
+ Header(onBackHandler)
205
245
  }
206
- ) {
207
- Box(Modifier.zIndex(1f)) {
208
- HeaderBackground()
209
- }
210
246
 
211
- Box(Modifier.zIndex(4f)) {
212
- Header(onBackHandler)
213
- }
247
+ HeaderSideBar(
248
+ bottomInset = maxOf(
249
+ bottomBarsHeight(isBottomTabChild) + Spacing.XL,
250
+ if (isBottomTabChild) LocalDuoRailReserve.current else 0.dp,
251
+ ),
252
+ modifier = Modifier.zIndex(4f),
253
+ )
214
254
 
215
- HeaderSideBar(bottomInset = bottomBarsHeight(isBottomTabChild), modifier = Modifier.zIndex(4f))
255
+ Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
256
+ SearchAnimated(snapState = snapState)
257
+ }
216
258
 
217
- Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
218
- SearchAnimated(snapState = snapState)
219
- }
259
+ Column(Modifier.zIndex(2f).fillMaxSize()) {
260
+ MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
261
+ FooterContent()
262
+ }
220
263
 
221
- Column(Modifier.zIndex(2f).fillMaxSize()) {
222
- MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
223
- FooterContent()
224
- }
264
+ Box(Modifier.zIndex(6f).horizontalSafeAreaPadding()) {
265
+ FloatingContent(isBottomTabChild = isBottomTabChild)
266
+ }
225
267
 
226
- Box(Modifier.zIndex(6f).horizontalSafeAreaPadding()) {
227
- FloatingContent(isBottomTabChild = isBottomTabChild)
268
+ OverplayView(isBottomTabChild = isBottomTabChild, id = id)
228
269
  }
229
-
230
- OverplayView(isBottomTabChild = isBottomTabChild, id = id)
270
+ // AI-GENERATED START: custom screen background, full window and outside the keyboard padding
231
271
  }
272
+ // AI-GENERATED END: custom screen background, full window and outside the keyboard padding
232
273
  }
233
274
  }
234
275
 
@@ -239,7 +280,7 @@ private fun bottomBarsHeight(isBottomTabChild: Boolean): Dp {
239
280
  val navigationBar = AppNavigationBar.current
240
281
  return when {
241
282
  options.hasFooter() -> with(LocalDensity.current) { LocalFooterHeightPx.current.intValue.toDp() }
242
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
283
+ isBottomTabChild -> bottomTabChildInset()
243
284
  else -> navigationBar
244
285
  }
245
286
  }
@@ -274,10 +315,11 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
274
315
  val scrollState = LocalScrollState.current
275
316
  val navigationBar = AppNavigationBar.current
276
317
  val screenLayout = getScreenLayout()
318
+ val useDuoGlass = options.usesDuoGlassHeader(screenLayout)
277
319
 
278
320
  Spacer(
279
321
  Modifier.height(
280
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
322
+ if (!useDuoGlass && (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent)))
281
323
  screenLayout.headerHeight else 0.dp
282
324
  )
283
325
  )
@@ -287,38 +329,45 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
287
329
  Spacer(Modifier.height((HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp)))
288
330
  }
289
331
  // a bottom tab root stays full width: the tab bar and each tab's own StackScreen handle the insets
332
+ val insetContent = !isBottomTabRoot
290
333
  Column(
291
334
  Modifier
292
335
  .fillMaxWidth()
293
336
  .weight(1f)
294
- .conditional(!isBottomTabRoot) {
337
+ .conditional(insetContent && options.headerType !is HeaderType.Animated) {
295
338
  horizontalSafeAreaPadding()
296
339
  }
297
340
  .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
298
341
  verticalScroll(scrollState)
299
342
  }
343
+ .conditional(useDuoGlass) {
344
+ padding(top = screenLayout.headerHeight)
345
+ }
300
346
  .padding(
301
347
  bottom = when {
302
348
  options.hasFooter() || isBottomTabRoot -> 0.dp
303
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
349
+ isBottomTabChild -> bottomTabChildInset()
304
350
  !options.scrollData.scrollable && options.scrollData.scrollState == null -> 0.dp
305
351
  else -> navigationBar
306
352
  }
307
353
  )
308
354
  ) {
309
- CompositionLocalProvider(
310
- LocalScreenLayout provides if (isBottomTabRoot) screenLayout else screenLayout.consumeHorizontalInsets()
311
- ) {
312
- ScreenContent(content = content)
313
- }
355
+ ScreenContent(content = content, insetContent = insetContent)
314
356
  }
315
357
 
316
358
  }
317
359
 
318
360
  @Composable
319
- internal fun ScreenContent(content: @Composable () -> Unit) {
361
+ internal fun ScreenContent(content: @Composable () -> Unit, insetContent: Boolean) {
320
362
  val scrollState = LocalScrollState.current
321
363
  val options = LocalOptions.current
364
+ val screenLayout = getScreenLayout()
365
+ val insetBody: @Composable () -> Unit = {
366
+ CompositionLocalProvider(
367
+ LocalScreenLayout provides if (insetContent) screenLayout.consumeHorizontalInsets() else screenLayout,
368
+ content = content,
369
+ )
370
+ }
322
371
 
323
372
  if (options.headerType is HeaderType.Animated) {
324
373
  val animatedHeader = options.headerType
@@ -329,12 +378,16 @@ internal fun ScreenContent(content: @Composable () -> Unit) {
329
378
  Box(bannerModifier) {
330
379
  animatedHeader.composable.invoke(scrollState.value)
331
380
  }
332
- Box(Modifier.padding(top = getScreenLayout().headerHeight + animatedHeader.layoutOffSet)) {
333
- content()
381
+ Box(
382
+ Modifier
383
+ .padding(top = screenLayout.headerHeight + animatedHeader.layoutOffSet)
384
+ .conditional(insetContent) { horizontalSafeAreaPadding() }
385
+ ) {
386
+ insetBody()
334
387
  }
335
388
  }
336
389
  } else {
337
- content()
390
+ insetBody()
338
391
  }
339
392
  }
340
393
 
@@ -343,7 +396,9 @@ internal fun FooterContent() {
343
396
  val options = LocalOptions.current
344
397
  if (options.hasFooter()) {
345
398
  val keyboardSize = keyboardSizeState()
346
- val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
399
+ // the Duo floating tab bar overlays tab content, so a tab's footer sits above it
400
+ val duoTabInset = if (LocalNavigation.current.bottomTabIndex != -1) duoFloatingTabBarInset(getScreenLayout()) else 0.dp
401
+ val bottomPadding = (maxOf(AppNavigationBar.current, duoTabInset) - keyboardSize.value).coerceAtLeast(0.dp)
347
402
  Footer(
348
403
  footerComponent = options.footerComponent,
349
404
  bottomPadding = bottomPadding,
@@ -391,9 +446,10 @@ fun Footer(
391
446
 
392
447
  val backgroundColor = if (transparent) Color.Transparent else AppTheme.current.colors.background.surface
393
448
 
449
+ // inset as a whole, not padded inside, so the surface and its shadow stay off the iPhone Duo side bar
394
450
  Box(Modifier.onGloballyPositioned {
395
451
  if (footerHeightPx.intValue != it.size.height) footerHeightPx.intValue = it.size.height
396
- }) {
452
+ }.horizontalSafeAreaPadding()) {
397
453
  Box(
398
454
  Modifier
399
455
  .fillMaxWidth()
@@ -401,7 +457,6 @@ fun Footer(
401
457
  .conditional(IsShowBaseLineDebug) {
402
458
  border(1.dp, Colors.blue_03)
403
459
  }
404
- .horizontalSafeAreaPadding()
405
460
  .padding(
406
461
  top = if (useMargin) Spacing.S else 0.dp,
407
462
  start = if (useMargin) Spacing.M else 0.dp,
@@ -454,10 +509,14 @@ internal fun SearchAnimated(
454
509
  // collapsed, the search is centered in the bar row
455
510
  val minTopPadding = screenLayout.headerStatusBarHeight + (screenLayout.headerBarHeight - HEADER_HEIGHT.dp) / 2
456
511
  val maxTopPadding = screenLayout.headerHeight
512
+ // collapsed on the iPhone Duo status bar row, the search stays between its clusters (48pt back there)
513
+ val clearance = screenLayout.statusBarClearance
514
+ val backWidth = if (clearance != null && !useSideBar) DuoMetrics.controlSize else 28.dp
457
515
  val minStartPadding = Spacing.M
458
- val maxStartPadding = if (options.hiddenBack || useSideBar) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
516
+ val maxStartPadding = (clearance?.left ?: 0.dp) +
517
+ if (options.hiddenBack || useSideBar) Spacing.M else backWidth + Spacing.M + Spacing.S
459
518
  val minEndPadding = Spacing.M
460
- val maxEndPadding = when {
519
+ val maxEndPadding = (clearance?.right ?: 0.dp) + when {
461
520
  options.headerRight is HeaderRight.None -> Spacing.M
462
521
  useSideBar && options.headerRight is HeaderRight.Toolkit -> Spacing.M
463
522
  else -> headerRightWidthDp + Spacing.M * 2
@@ -608,6 +667,52 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
608
667
 
609
668
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
610
669
 
670
+ // iPhone Duo side bar: the header leaves with the content and slides back in on a reverse scroll
671
+ // (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
672
+ @Composable
673
+ private fun rememberQuickReturnHide(
674
+ enabled: Boolean,
675
+ scrollState: ScrollState,
676
+ heightPx: Float,
677
+ ): Animatable<Float, AnimationVector1D> {
678
+ val hide = remember { Animatable(0f) }
679
+ LaunchedEffect(enabled, scrollState, heightPx) {
680
+ if (!enabled) {
681
+ hide.snapTo(0f)
682
+ return@LaunchedEffect
683
+ }
684
+ var last = scrollState.value
685
+ snapshotFlow { scrollState.value }.collect { y ->
686
+ val delta = y - last
687
+ last = y
688
+ hide.snapTo(if (y <= 0) 0f else (hide.value + delta).coerceIn(0f, heightPx))
689
+ }
690
+ }
691
+ // a half-shown header settles fully in or out once the scroll stops
692
+ LaunchedEffect(enabled, scrollState, heightPx) {
693
+ if (!enabled) return@LaunchedEffect
694
+ snapshotFlow { scrollState.isScrollInProgress }
695
+ .filter { !it }
696
+ .collect {
697
+ val value = hide.value
698
+ // within the first header height the header still sits on the content
699
+ if (scrollState.value < heightPx || value <= 0f || value >= heightPx) return@collect
700
+ launch { hide.animateTo(if (value > heightPx / 2) heightPx else 0f, tween(200)) }
701
+ }
702
+ }
703
+ return hide
704
+ }
705
+
706
+ internal fun NavigationOptions.usesDuoQuickReturnHeader(layout: ScreenLayout): Boolean =
707
+ usesDuoGlassHeader(layout) || (layout.sideBar != null && headerType is HeaderType.Animated &&
708
+ scrollData.scrollable && scrollData.scrollState is ScrollState)
709
+
710
+ // iPhone Duo side bar: content scrolls under a glass header instead of starting below it (RN Screen).
711
+ internal fun NavigationOptions.usesDuoGlassHeader(layout: ScreenLayout): Boolean =
712
+ layout.sideBar != null && headerType is HeaderType.DefaultOrExtended &&
713
+ getInputSearchType(this) == InputSearchType.None &&
714
+ scrollData.scrollable && scrollData.scrollState is ScrollState
715
+
611
716
  internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
612
717
  return when (val headerType = options.headerType) {
613
718
  is HeaderType.DefaultOrExtended -> when {
@@ -8,8 +8,10 @@ import androidx.compose.animation.scaleOut
8
8
  import androidx.compose.foundation.background
9
9
  import androidx.compose.foundation.layout.*
10
10
  import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.CompositionLocalProvider
11
12
  import androidx.compose.runtime.DisposableEffect
12
13
  import androidx.compose.runtime.LaunchedEffect
14
+ import androidx.compose.runtime.remember
13
15
  import androidx.compose.ui.Alignment
14
16
  import androidx.compose.ui.Modifier
15
17
  import androidx.compose.ui.unit.dp
@@ -26,7 +28,9 @@ import vn.momo.kits.navigation.LocalNavigator
26
28
  import vn.momo.kits.navigation.NavigationOptions
27
29
  import vn.momo.kits.navigation.StackScreen
28
30
  import vn.momo.kits.navigation.component.HeaderType
31
+ import vn.momo.kits.platform.BackHandler
29
32
  import vn.momo.kits.platform.reportFloatingArrowBack
33
+ import vn.momo.kits.layout.getScreenLayout
30
34
 
31
35
  private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
32
36
  internal fun setBottomTabOption(index: Int, options: NavigationOptions){
@@ -43,16 +47,25 @@ internal fun getBottomTabOption(index: Int): NavigationOptions? {
43
47
  @Composable
44
48
  fun BottomTab(
45
49
  items: List<BottomTabItem>,
46
- floatingButton: BottomTabFloatingButton? = null
50
+ floatingButton: BottomTabFloatingButton? = null,
51
+ // iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
52
+ // tapping the compressed one swaps them
53
+ compressionBehavior: DuoBarCompressionBehavior = DuoBarCompressionBehavior.TOOLBAR,
54
+ initialRouteName: String? = null,
55
+ onTabPress: ((index: Int, switchTab: () -> Unit) -> Unit)? = null,
56
+ backToInitialTab: Boolean = true,
47
57
  ) {
48
58
  val navigation = LocalNavigation.current
49
59
  val navigator = LocalNavigator.current
50
60
  val navController = rememberNavController()
61
+ val startRoute = remember {
62
+ "option${items.indexOfFirst { it.name == initialRouteName }.coerceAtLeast(0)}"
63
+ }
51
64
 
52
- bottomTabOptionItems = items.mapIndexed { index, item ->
53
- item.options ?: NavigationOptions()
54
- }.toMutableList()
55
-
65
+ // Only on new items: this recomposes on every tab switch and would drop options a tab set at runtime.
66
+ remember(items) {
67
+ bottomTabOptionItems = items.map { it.options ?: NavigationOptions() }.toMutableList()
68
+ }
56
69
 
57
70
  LaunchedEffect(Unit){
58
71
  navigation.markAsBottomTabRoot()
@@ -62,15 +75,14 @@ fun BottomTab(
62
75
  )
63
76
  }
64
77
 
65
- // Tab 0 keeps the seeked transition; other tabs go to the host's floating arrow. Matches RN.
66
78
  val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
67
- val isFirstTab = currentRoute == null || currentRoute == "option0"
79
+ val onStartTab = currentRoute == null || currentRoute == startRoute
80
+ val isFirstTab = onStartTab || !backToInitialTab
81
+ val backToStartTab = { navController.popBackStack(startRoute, inclusive = false) }
68
82
 
69
83
  DisposableEffect(isFirstTab) {
70
84
  reportFloatingArrowBack(!isFirstTab)
71
- navigator.tabBackHandler = if (isFirstTab) null else ({
72
- navController.popBackStack(navController.graph.startDestinationRoute!!, false)
73
- })
85
+ navigator.tabBackHandler = if (isFirstTab) null else ({ backToStartTab() })
74
86
  // both live outside this composition
75
87
  onDispose {
76
88
  reportFloatingArrowBack(false)
@@ -78,81 +90,112 @@ fun BottomTab(
78
90
  }
79
91
  }
80
92
 
93
+ val layout = getScreenLayout()
94
+ val hasQr = floatingButton != null
95
+ val compression = rememberDuoBarCompression(compressionBehavior, items.size, hasQr, layout)
96
+ val railReserve = layout.sideBar?.takeIf { layout.isIPhoneDuo }?.let {
97
+ it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
98
+ } ?: 0.dp
99
+ val onTabSelected: (Int) -> Unit = { index ->
100
+ val switchTab = {
101
+ val targetRoute = "option$index"
102
+ if (navController.currentBackStackEntry?.destination?.route != targetRoute) {
103
+ // one entry per tab above the start tab, so switching never grows the back stack
104
+ navController.navigate(targetRoute) {
105
+ popUpTo(startRoute) { saveState = true }
106
+ launchSingleTop = true
107
+ restoreState = true
108
+ }
109
+ }
110
+ }
111
+ onTabPress?.invoke(index, switchTab) ?: switchTab()
112
+ }
113
+
81
114
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
82
115
  Box(modifier = Modifier
83
116
  .fillMaxSize()
84
117
  ) {
85
- NavHost(
86
- navController = navController,
87
- startDestination = "option0"
118
+ CompositionLocalProvider(
119
+ LocalDuoRailReserve provides railReserve,
120
+ LocalDuoBarCompression provides compression,
88
121
  ) {
89
- items.forEachIndexed { index, item ->
90
- composable(
91
- route = "option$index",
92
- enterTransition = {
93
- fadeIn(animationSpec = tween(200)) +
94
- scaleIn(
95
- initialScale = 0.97f,
96
- animationSpec = tween(200)
97
- )
98
- },
99
- exitTransition = {
100
- fadeOut(animationSpec = tween(200)) +
101
- scaleOut(
102
- targetScale = 0.97f,
103
- animationSpec = tween(200)
104
- )
105
- },
106
- popEnterTransition = {
107
- fadeIn(animationSpec = tween(200)) +
108
- scaleIn(
109
- initialScale = 0.97f,
110
- animationSpec = tween(200)
111
- )
112
- },
113
- popExitTransition = {
114
- fadeOut(animationSpec = tween(200)) +
115
- scaleOut(
116
- targetScale = 0.97f,
117
- animationSpec = tween(200)
118
- )
119
- }
120
- ) {
121
- val option = getBottomTabOption(index)
122
+ NavHost(
123
+ navController = navController,
124
+ startDestination = startRoute
125
+ ) {
126
+ items.forEachIndexed { index, item ->
127
+ composable(
128
+ route = "option$index",
129
+ enterTransition = {
130
+ fadeIn(animationSpec = tween(200)) +
131
+ scaleIn(
132
+ initialScale = 0.97f,
133
+ animationSpec = tween(200)
134
+ )
135
+ },
136
+ exitTransition = {
137
+ fadeOut(animationSpec = tween(200)) +
138
+ scaleOut(
139
+ targetScale = 0.97f,
140
+ animationSpec = tween(200)
141
+ )
142
+ },
143
+ popEnterTransition = {
144
+ fadeIn(animationSpec = tween(200)) +
145
+ scaleIn(
146
+ initialScale = 0.97f,
147
+ animationSpec = tween(200)
148
+ )
149
+ },
150
+ popExitTransition = {
151
+ fadeOut(animationSpec = tween(200)) +
152
+ scaleOut(
153
+ targetScale = 0.97f,
154
+ animationSpec = tween(200)
155
+ )
156
+ }
157
+ ) {
158
+ val option = getBottomTabOption(index)
122
159
 
123
- StackScreen(
124
- content = item.screen,
125
- navigationOptions = option,
126
- bottomTabIndex = index,
127
- onBackHandler = {
128
- if (index != 0) {
129
- navController.popBackStack(
130
- navController.graph.startDestinationRoute!!,
131
- inclusive = false
132
- )
133
- } else {
134
- navigator.pop()
160
+ StackScreen(
161
+ content = item.screen,
162
+ navigationOptions = option,
163
+ bottomTabIndex = index,
164
+ onBackHandler = {
165
+ if (backToInitialTab && "option$index" != startRoute) {
166
+ backToStartTab()
167
+ } else {
168
+ navigator.pop()
169
+ }
135
170
  }
136
- }
137
- )
171
+ )
172
+ }
138
173
  }
139
174
  }
140
175
  }
141
176
  }
142
- Column {
143
- BottomTabBar(
177
+ BackHandler(enabled = !onStartTab) {
178
+ if (backToInitialTab) backToStartTab() else navigator.pop()
179
+ }
180
+ if (layout.isIPhoneDuo) {
181
+ DuoTabBar(
182
+ layout = layout,
144
183
  items = items,
145
184
  floatingButton = floatingButton,
146
- navController = navController,
147
- onTabSelected = {
148
- val currentRoute = navController.currentBackStackEntry?.destination?.route
149
- val targetRoute = "option$it"
150
- if (currentRoute != targetRoute) {
151
- navController.navigate(targetRoute)
152
- }
153
- }
185
+ selectedIndex = items.indices.firstOrNull { "option$it" == currentRoute } ?: 0,
186
+ onTabSelected = onTabSelected,
187
+ compression = compression,
154
188
  )
155
- Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
189
+ } else {
190
+ Column {
191
+ BottomTabBar(
192
+ items = items,
193
+ floatingButton = floatingButton,
194
+ navController = navController,
195
+ onTabSelected = onTabSelected,
196
+ )
197
+ Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
198
+ }
156
199
  }
157
200
  }
158
201
  }
@@ -41,7 +41,7 @@ internal fun BottomTabBar(
41
41
  onTabSelected: (Int) -> Unit,
42
42
  ) {
43
43
  val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
44
- val selectedIndex = items.indexOfFirst { "option${items.indexOf(it)}" == currentRoute }
44
+ val selectedIndex = items.indices.firstOrNull { "option$it" == currentRoute } ?: 0
45
45
 
46
46
  val layout = getScreenLayout()
47
47
  val mid = items.size / 2
@@ -179,14 +179,13 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
179
179
  overflow = TextOverflow.Ellipsis
180
180
  )
181
181
  }
182
- if (badgeLabel != null) {
183
- if (badgeLabel.isEmpty())
184
- BadgeDot(
185
- size = DotSize.Small,
186
- modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
187
- )
188
- else
189
- Badge(badgeLabel, modifier = Modifier.offset(x = 44.dp, y = (-42).dp))
182
+ if (badgeLabel != null && badgeLabel.isNotEmpty()) {
183
+ Badge(badgeLabel, modifier = Modifier.offset(x = 44.dp, y = (-42).dp))
184
+ } else if (badgeLabel != null || item.showDot) {
185
+ BadgeDot(
186
+ size = DotSize.Small,
187
+ modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
188
+ )
190
189
  }
191
190
  }
192
191
  }