@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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 (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  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/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /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,6 +68,9 @@ 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
70
76
  import vn.momo.kits.platform.BackHandler
@@ -74,6 +80,10 @@ import vn.momo.kits.navigation.tracking.ScreenTrackingState
74
80
  import kotlinx.coroutines.delay
75
81
  import vn.momo.kits.application.LocalContext
76
82
  import vn.momo.kits.utils.horizontalSafeAreaPadding
83
+ import vn.momo.kits.layout.LocalScreenLayout
84
+ import vn.momo.kits.layout.ScreenLayout
85
+ import vn.momo.kits.layout.consumeHorizontalInsets
86
+ import vn.momo.kits.layout.getScreenLayout
77
87
  import kotlin.time.Clock
78
88
  import kotlin.time.Duration.Companion.milliseconds
79
89
  import kotlin.time.ExperimentalTime
@@ -81,11 +91,19 @@ import kotlin.time.ExperimentalTime
81
91
  internal val LocalFooterHeightPx = staticCompositionLocalOf { mutableIntStateOf(0) }
82
92
  internal val LocalHeaderRightWidthPx = staticCompositionLocalOf { mutableIntStateOf(0) }
83
93
 
94
+ // Space a tab's content leaves for the tab bar and home indicator. On iPhone Duo the floating bar
95
+ // overlays the content and the rail lives in the side strip, so only their own inset applies.
84
96
  @Composable
85
- private fun bottomTabBarInset(): Dp {
86
- if (LocalNavigation.current.bottomTabIndex == -1) return 0.dp
97
+ private fun bottomTabChildInset(): Dp {
87
98
  val keyboard = keyboardSizeState().value
88
- return (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp)
99
+ val navigationBar = AppNavigationBar.current
100
+ val layout = getScreenLayout()
101
+ return when {
102
+ layout.isIPhoneDuo && layout.sideBar == null -> (duoFloatingTabBarInset(layout) - keyboard).coerceAtLeast(0.dp)
103
+ layout.isIPhoneDuo -> (navigationBar - keyboard).coerceAtLeast(0.dp)
104
+ else -> (BOTTOM_TAB_BAR_HEIGHT.dp + Spacing.S - keyboard).coerceAtLeast(0.dp) +
105
+ (navigationBar - keyboard).coerceAtLeast(0.dp)
106
+ }
89
107
  }
90
108
 
91
109
  @OptIn(ExperimentalMaterialApi::class, ExperimentalTime::class)
@@ -101,7 +119,7 @@ internal fun StackScreen(
101
119
  val navigator = LocalNavigator.current
102
120
  val maxApi = LocalMaxApi.current
103
121
  val context = LocalContext.current
104
- val statusBar = AppStatusBar.current
122
+ val screenLayout = getScreenLayout()
105
123
  val density = LocalDensity.current
106
124
  val screenRegistry = LocalDynamicScreenRegistry.current
107
125
  val navigation = remember { Navigation(id = id, bottomTabIndex = bottomTabIndex, initOptions = navigationOptions, registry = screenRegistry) }
@@ -157,7 +175,7 @@ internal fun StackScreen(
157
175
  }
158
176
 
159
177
  val limit = with(density) {
160
- (statusBar + HEADER_HEIGHT.dp).toPx()
178
+ screenLayout.headerHeight.toPx()
161
179
  }.toInt()
162
180
 
163
181
  val lazyListState = options.scrollData.scrollState as? LazyListState
@@ -166,6 +184,11 @@ internal fun StackScreen(
166
184
  else
167
185
  (options.scrollData.scrollState as? ScrollState ?: rememberScrollState()).proxyLimitedScrollState(limit, 1)
168
186
  val snapState: ScrollableState = lazyListState ?: scrollState
187
+ val headerHide = rememberQuickReturnHide(
188
+ enabled = options.usesDuoQuickReturnHeader(screenLayout),
189
+ scrollState = scrollState,
190
+ heightPx = with(density) { screenLayout.headerHeight.toPx() },
191
+ )
169
192
 
170
193
  val footerHeightPx = remember { mutableIntStateOf(0) }
171
194
  val headerRightWidthPx = remember { mutableIntStateOf(0) }
@@ -205,15 +228,23 @@ internal fun StackScreen(
205
228
  HeaderBackground()
206
229
  }
207
230
 
208
- Box(Modifier.zIndex(4f)) {
231
+ Box(Modifier.zIndex(4f).graphicsLayer { translationY = -headerHide.value }) {
209
232
  Header(onBackHandler)
210
233
  }
211
234
 
235
+ HeaderSideBar(
236
+ bottomInset = maxOf(
237
+ bottomBarsHeight(isBottomTabChild) + Spacing.XL,
238
+ if (isBottomTabChild) LocalDuoRailReserve.current else 0.dp,
239
+ ),
240
+ modifier = Modifier.zIndex(4f),
241
+ )
242
+
212
243
  Column(Modifier.zIndex(5f).horizontalSafeAreaPadding()) {
213
244
  SearchAnimated(snapState = snapState)
214
245
  }
215
246
 
216
- Column(Modifier.zIndex(2f).fillMaxSize().horizontalSafeAreaPadding()) {
247
+ Column(Modifier.zIndex(2f).fillMaxSize()) {
217
248
  MainContent(content = content, isBottomTabRoot = isBottomTabRoot, isBottomTabChild = isBottomTabChild)
218
249
  FooterContent()
219
250
  }
@@ -227,22 +258,26 @@ internal fun StackScreen(
227
258
  }
228
259
  }
229
260
 
261
+ // Height the footer, tab bar or home indicator takes from the bottom of the screen.
262
+ @Composable
263
+ private fun bottomBarsHeight(isBottomTabChild: Boolean): Dp {
264
+ val options = LocalOptions.current
265
+ val navigationBar = AppNavigationBar.current
266
+ return when {
267
+ options.hasFooter() -> with(LocalDensity.current) { LocalFooterHeightPx.current.intValue.toDp() }
268
+ isBottomTabChild -> bottomTabChildInset()
269
+ else -> navigationBar
270
+ }
271
+ }
272
+
230
273
  @Suppress("FrequentlyChangingValue")
231
274
  @Composable
232
275
  internal fun FloatingContent(isBottomTabChild: Boolean) {
233
276
  val options = LocalOptions.current
234
- val density = LocalDensity.current
235
- val footerHeightPx = LocalFooterHeightPx.current
236
277
  val scrollState = LocalScrollState.current
237
- val navigationBar = AppNavigationBar.current
238
278
 
239
279
  val fabProps = options.floatingButtonProps ?: return
240
- val bottomPadding = fabProps.bottom
241
- ?: (Spacing.M + when {
242
- options.hasFooter() -> with(density) { footerHeightPx.intValue.toDp() }
243
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
244
- else -> navigationBar
245
- })
280
+ val bottomPadding = fabProps.bottom ?: (Spacing.M + bottomBarsHeight(isBottomTabChild))
246
281
 
247
282
  FloatingButton(
248
283
  scrollPosition = fabProps.scrollState?.value ?: scrollState.value,
@@ -264,11 +299,13 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
264
299
  val density = LocalDensity.current
265
300
  val scrollState = LocalScrollState.current
266
301
  val navigationBar = AppNavigationBar.current
302
+ val screenLayout = getScreenLayout()
303
+ val useDuoGlass = options.usesDuoGlassHeader(screenLayout)
267
304
 
268
305
  Spacer(
269
306
  Modifier.height(
270
- if (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent))
271
- AppStatusBar.current + HEADER_HEIGHT.dp else 0.dp
307
+ if (!useDuoGlass && (options.headerType is HeaderType.DefaultOrExtended || (options.headerType is HeaderType.Transparent && !options.headerType.isFullScreenContent)))
308
+ screenLayout.headerHeight else 0.dp
272
309
  )
273
310
  )
274
311
  if (inputSearchType == InputSearchType.Animated) {
@@ -276,31 +313,46 @@ internal fun ColumnScope.MainContent(content: @Composable () -> Unit, isBottomTa
276
313
 
277
314
  Spacer(Modifier.height((HEADER_HEIGHT.dp - scrollDp).coerceIn(0.dp, HEADER_HEIGHT.dp)))
278
315
  }
316
+ // a bottom tab root stays full width: the tab bar and each tab's own StackScreen handle the insets
317
+ val insetContent = !isBottomTabRoot
279
318
  Column(
280
319
  Modifier
281
320
  .fillMaxWidth()
282
321
  .weight(1f)
322
+ .conditional(insetContent && options.headerType !is HeaderType.Animated) {
323
+ horizontalSafeAreaPadding()
324
+ }
283
325
  .conditional(options.scrollData.scrollable && options.scrollData.scrollState is ScrollState) {
284
326
  verticalScroll(scrollState)
285
327
  }
328
+ .conditional(useDuoGlass) {
329
+ padding(top = screenLayout.headerHeight)
330
+ }
286
331
  .padding(
287
332
  bottom = when {
288
333
  options.hasFooter() || isBottomTabRoot -> 0.dp
289
- isBottomTabChild -> bottomTabBarInset() + (navigationBar - keyboardSizeState().value).coerceAtLeast(0.dp)
334
+ isBottomTabChild -> bottomTabChildInset()
290
335
  !options.scrollData.scrollable && options.scrollData.scrollState == null -> 0.dp
291
336
  else -> navigationBar
292
337
  }
293
338
  )
294
339
  ) {
295
- ScreenContent(content = content)
340
+ ScreenContent(content = content, insetContent = insetContent)
296
341
  }
297
342
 
298
343
  }
299
344
 
300
345
  @Composable
301
- internal fun ScreenContent(content: @Composable () -> Unit) {
346
+ internal fun ScreenContent(content: @Composable () -> Unit, insetContent: Boolean) {
302
347
  val scrollState = LocalScrollState.current
303
348
  val options = LocalOptions.current
349
+ val screenLayout = getScreenLayout()
350
+ val insetBody: @Composable () -> Unit = {
351
+ CompositionLocalProvider(
352
+ LocalScreenLayout provides if (insetContent) screenLayout.consumeHorizontalInsets() else screenLayout,
353
+ content = content,
354
+ )
355
+ }
304
356
 
305
357
  if (options.headerType is HeaderType.Animated) {
306
358
  val animatedHeader = options.headerType
@@ -311,12 +363,16 @@ internal fun ScreenContent(content: @Composable () -> Unit) {
311
363
  Box(bannerModifier) {
312
364
  animatedHeader.composable.invoke(scrollState.value)
313
365
  }
314
- Box(Modifier.padding(top = AppStatusBar.current + HEADER_HEIGHT.dp + animatedHeader.layoutOffSet)) {
315
- content()
366
+ Box(
367
+ Modifier
368
+ .padding(top = screenLayout.headerHeight + animatedHeader.layoutOffSet)
369
+ .conditional(insetContent) { horizontalSafeAreaPadding() }
370
+ ) {
371
+ insetBody()
316
372
  }
317
373
  }
318
374
  } else {
319
- content()
375
+ insetBody()
320
376
  }
321
377
  }
322
378
 
@@ -325,7 +381,9 @@ internal fun FooterContent() {
325
381
  val options = LocalOptions.current
326
382
  if (options.hasFooter()) {
327
383
  val keyboardSize = keyboardSizeState()
328
- val bottomPadding = (AppNavigationBar.current - keyboardSize.value).coerceAtLeast(0.dp)
384
+ // the Duo floating tab bar overlays tab content, so a tab's footer sits above it
385
+ val duoTabInset = if (LocalNavigation.current.bottomTabIndex != -1) duoFloatingTabBarInset(getScreenLayout()) else 0.dp
386
+ val bottomPadding = (maxOf(AppNavigationBar.current, duoTabInset) - keyboardSize.value).coerceAtLeast(0.dp)
329
387
  Footer(
330
388
  footerComponent = options.footerComponent,
331
389
  bottomPadding = bottomPadding,
@@ -383,6 +441,7 @@ fun Footer(
383
441
  .conditional(IsShowBaseLineDebug) {
384
442
  border(1.dp, Colors.blue_03)
385
443
  }
444
+ .horizontalSafeAreaPadding()
386
445
  .padding(
387
446
  top = if (useMargin) Spacing.S else 0.dp,
388
447
  start = if (useMargin) Spacing.M else 0.dp,
@@ -430,17 +489,24 @@ internal fun SearchAnimated(
430
489
  if (inputSearchType == InputSearchType.None) return
431
490
  val inputSearchProps = (options.headerType as? HeaderType.DefaultOrExtended)?.inputSearchProps ?: return
432
491
 
433
- val minTopPadding = AppStatusBar.current
434
- val maxTopPadding = AppStatusBar.current + HEADER_HEIGHT.dp
492
+ val screenLayout = getScreenLayout()
493
+ val useSideBar = screenLayout.sideBar != null
494
+ // collapsed, the search is centered in the bar row
495
+ val minTopPadding = screenLayout.headerStatusBarHeight + (screenLayout.headerBarHeight - HEADER_HEIGHT.dp) / 2
496
+ val maxTopPadding = screenLayout.headerHeight
435
497
  val minStartPadding = Spacing.M
436
- val maxStartPadding = if (options.hiddenBack) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
498
+ val maxStartPadding = if (options.hiddenBack || useSideBar) Spacing.M else 48.dp // button back: size + left +right = 28 + 12 + 8 = 48
437
499
  val minEndPadding = Spacing.M
438
- val maxEndPadding = headerRightWidthDp + if (options.headerRight is HeaderRight.None) Spacing.M else Spacing.M * 2
500
+ val maxEndPadding = when {
501
+ options.headerRight is HeaderRight.None -> Spacing.M
502
+ useSideBar && options.headerRight is HeaderRight.Toolkit -> Spacing.M
503
+ else -> headerRightWidthDp + Spacing.M * 2
504
+ }
439
505
 
440
506
  val targetColor = lerp(
441
507
  AppTheme.current.colors.background.surface,
442
508
  AppTheme.current.colors.background.default,
443
- (scrollDp / minTopPadding).coerceIn(0f, 1f)
509
+ (scrollDp / minTopPadding.coerceAtLeast(1.dp)).coerceIn(0f, 1f)
444
510
  )
445
511
  val animatedColor by animateColorAsState(targetValue = targetColor, label = "BackgroundColor")
446
512
 
@@ -582,6 +648,52 @@ internal val LocalOptions = staticCompositionLocalOf<NavigationOptions> { error(
582
648
 
583
649
  val StackScreenScrollableState = staticCompositionLocalOf<ScrollableState?> { null }
584
650
 
651
+ // iPhone Duo side bar: the header leaves with the content and slides back in on a reverse scroll
652
+ // (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
653
+ @Composable
654
+ private fun rememberQuickReturnHide(
655
+ enabled: Boolean,
656
+ scrollState: ScrollState,
657
+ heightPx: Float,
658
+ ): Animatable<Float, AnimationVector1D> {
659
+ val hide = remember { Animatable(0f) }
660
+ LaunchedEffect(enabled, scrollState, heightPx) {
661
+ if (!enabled) {
662
+ hide.snapTo(0f)
663
+ return@LaunchedEffect
664
+ }
665
+ var last = scrollState.value
666
+ snapshotFlow { scrollState.value }.collect { y ->
667
+ val delta = y - last
668
+ last = y
669
+ hide.snapTo(if (y <= 0) 0f else (hide.value + delta).coerceIn(0f, heightPx))
670
+ }
671
+ }
672
+ // a half-shown header settles fully in or out once the scroll stops
673
+ LaunchedEffect(enabled, scrollState, heightPx) {
674
+ if (!enabled) return@LaunchedEffect
675
+ snapshotFlow { scrollState.isScrollInProgress }
676
+ .filter { !it }
677
+ .collect {
678
+ val value = hide.value
679
+ // within the first header height the header still sits on the content
680
+ if (scrollState.value < heightPx || value <= 0f || value >= heightPx) return@collect
681
+ launch { hide.animateTo(if (value > heightPx / 2) heightPx else 0f, tween(200)) }
682
+ }
683
+ }
684
+ return hide
685
+ }
686
+
687
+ internal fun NavigationOptions.usesDuoQuickReturnHeader(layout: ScreenLayout): Boolean =
688
+ usesDuoGlassHeader(layout) || (layout.sideBar != null && headerType is HeaderType.Animated &&
689
+ scrollData.scrollable && scrollData.scrollState is ScrollState)
690
+
691
+ // iPhone Duo side bar: content scrolls under a glass header instead of starting below it (RN Screen).
692
+ internal fun NavigationOptions.usesDuoGlassHeader(layout: ScreenLayout): Boolean =
693
+ layout.sideBar != null && headerType is HeaderType.DefaultOrExtended &&
694
+ getInputSearchType(this) == InputSearchType.None &&
695
+ scrollData.scrollable && scrollData.scrollState is ScrollState
696
+
585
697
  internal fun getInputSearchType(options: NavigationOptions): InputSearchType {
586
698
  return when (val headerType = options.headerType) {
587
699
  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
@@ -27,6 +29,7 @@ import vn.momo.kits.navigation.NavigationOptions
27
29
  import vn.momo.kits.navigation.StackScreen
28
30
  import vn.momo.kits.navigation.component.HeaderType
29
31
  import vn.momo.kits.platform.reportFloatingArrowBack
32
+ import vn.momo.kits.layout.getScreenLayout
30
33
 
31
34
  private var bottomTabOptionItems : MutableList<NavigationOptions?> = mutableListOf()
32
35
  internal fun setBottomTabOption(index: Int, options: NavigationOptions){
@@ -43,16 +46,19 @@ internal fun getBottomTabOption(index: Int): NavigationOptions? {
43
46
  @Composable
44
47
  fun BottomTab(
45
48
  items: List<BottomTabItem>,
46
- floatingButton: BottomTabFloatingButton? = null
49
+ floatingButton: BottomTabFloatingButton? = null,
50
+ // iPhone Duo: when the side bar can't hold both bars, which one starts compressed;
51
+ // tapping the compressed one swaps them
52
+ compressionBehavior: DuoBarCompressionBehavior = DuoBarCompressionBehavior.TOOLBAR,
47
53
  ) {
48
54
  val navigation = LocalNavigation.current
49
55
  val navigator = LocalNavigator.current
50
56
  val navController = rememberNavController()
51
57
 
52
- bottomTabOptionItems = items.mapIndexed { index, item ->
53
- item.options ?: NavigationOptions()
54
- }.toMutableList()
55
-
58
+ // Only on new items: this recomposes on every tab switch and would drop options a tab set at runtime.
59
+ remember(items) {
60
+ bottomTabOptionItems = items.map { it.options ?: NavigationOptions() }.toMutableList()
61
+ }
56
62
 
57
63
  LaunchedEffect(Unit){
58
64
  navigation.markAsBottomTabRoot()
@@ -78,81 +84,109 @@ fun BottomTab(
78
84
  }
79
85
  }
80
86
 
87
+ val layout = getScreenLayout()
88
+ val hasQr = floatingButton != null
89
+ val compression = rememberDuoBarCompression(compressionBehavior, items.size, hasQr, layout)
90
+ val railReserve = layout.sideBar?.takeIf { layout.isIPhoneDuo }?.let {
91
+ it.bottom + duoRailHeight(items.size, hasQr, compressed = compression.tabBarCompressed) + DUO_GROUP_GAP
92
+ } ?: 0.dp
93
+ val onTabSelected: (Int) -> Unit = {
94
+ val targetRoute = "option$it"
95
+ if (currentRoute != targetRoute) {
96
+ // one entry per tab above the first, so switching never grows the back stack
97
+ navController.navigate(targetRoute) {
98
+ popUpTo("option0") { saveState = true }
99
+ launchSingleTop = true
100
+ restoreState = true
101
+ }
102
+ }
103
+ }
104
+
81
105
  Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
82
106
  Box(modifier = Modifier
83
107
  .fillMaxSize()
84
108
  ) {
85
- NavHost(
86
- navController = navController,
87
- startDestination = "option0"
109
+ CompositionLocalProvider(
110
+ LocalDuoRailReserve provides railReserve,
111
+ LocalDuoBarCompression provides compression,
88
112
  ) {
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)
113
+ NavHost(
114
+ navController = navController,
115
+ startDestination = "option0"
116
+ ) {
117
+ items.forEachIndexed { index, item ->
118
+ composable(
119
+ route = "option$index",
120
+ enterTransition = {
121
+ fadeIn(animationSpec = tween(200)) +
122
+ scaleIn(
123
+ initialScale = 0.97f,
124
+ animationSpec = tween(200)
125
+ )
126
+ },
127
+ exitTransition = {
128
+ fadeOut(animationSpec = tween(200)) +
129
+ scaleOut(
130
+ targetScale = 0.97f,
131
+ animationSpec = tween(200)
132
+ )
133
+ },
134
+ popEnterTransition = {
135
+ fadeIn(animationSpec = tween(200)) +
136
+ scaleIn(
137
+ initialScale = 0.97f,
138
+ animationSpec = tween(200)
139
+ )
140
+ },
141
+ popExitTransition = {
142
+ fadeOut(animationSpec = tween(200)) +
143
+ scaleOut(
144
+ targetScale = 0.97f,
145
+ animationSpec = tween(200)
146
+ )
147
+ }
148
+ ) {
149
+ val option = getBottomTabOption(index)
122
150
 
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()
151
+ StackScreen(
152
+ content = item.screen,
153
+ navigationOptions = option,
154
+ bottomTabIndex = index,
155
+ onBackHandler = {
156
+ if (index != 0) {
157
+ navController.popBackStack(
158
+ navController.graph.startDestinationRoute!!,
159
+ inclusive = false
160
+ )
161
+ } else {
162
+ navigator.pop()
163
+ }
135
164
  }
136
- }
137
- )
165
+ )
166
+ }
138
167
  }
139
168
  }
140
169
  }
141
170
  }
142
- Column {
143
- BottomTabBar(
171
+ if (layout.isIPhoneDuo) {
172
+ DuoTabBar(
173
+ layout = layout,
144
174
  items = items,
145
175
  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
- }
176
+ selectedIndex = items.indices.firstOrNull { "option$it" == currentRoute } ?: 0,
177
+ onTabSelected = onTabSelected,
178
+ compression = compression,
154
179
  )
155
- Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
180
+ } else {
181
+ Column {
182
+ BottomTabBar(
183
+ items = items,
184
+ floatingButton = floatingButton,
185
+ navController = navController,
186
+ onTabSelected = onTabSelected,
187
+ )
188
+ Spacer(modifier = Modifier.fillMaxWidth().height(AppNavigationBar.current + Spacing.S).background(AppTheme.current.colors.background.surface))
189
+ }
156
190
  }
157
191
  }
158
192
  }
@@ -26,7 +26,8 @@ import vn.momo.kits.const.*
26
26
  import vn.momo.kits.modifier.InternalApi
27
27
  import vn.momo.kits.modifier.conditional
28
28
  import vn.momo.kits.modifier.noFeedbackClickable
29
- import vn.momo.kits.platform.getScreenDimensions
29
+ import vn.momo.kits.layout.getScreenLayout
30
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
30
31
 
31
32
  internal val floatingButtonWidth = 75.dp
32
33
  @InternalApi
@@ -40,18 +41,19 @@ internal fun BottomTabBar(
40
41
  onTabSelected: (Int) -> Unit,
41
42
  ) {
42
43
  val currentRoute = navController.currentBackStackEntryAsState().value?.destination?.route
43
- val selectedIndex = items.indexOfFirst { "option${items.indexOf(it)}" == currentRoute }
44
+ val selectedIndex = items.indices.firstOrNull { "option$it" == currentRoute } ?: 0
44
45
 
45
- val screenWidth = getScreenDimensions().width.dp
46
+ val layout = getScreenLayout()
46
47
  val mid = items.size / 2
47
48
 
48
49
  val tabWidth = if (floatingButton != null) {
49
- (screenWidth - floatingButtonWidth) / items.size
50
+ (layout.contentWidth - floatingButtonWidth) / items.size
50
51
  } else {
51
- screenWidth / items.size
52
+ layout.contentWidth / items.size
52
53
  }
53
54
  val adjustSize = if(floatingButton != null && selectedIndex >= mid) floatingButtonWidth else 0.dp
54
- val targetOffset = tabWidth * selectedIndex + tabWidth / 2 - screenWidth / 2 + adjustSize
55
+ // offsets are from the bar center; tabs live between the side safe areas
56
+ val targetOffset = layout.insetLeft + tabWidth * selectedIndex + tabWidth / 2 - layout.width / 2 + adjustSize
55
57
 
56
58
  val indicatorOffsetX by animateDpAsState(
57
59
  targetValue = targetOffset,
@@ -69,7 +71,8 @@ internal fun BottomTabBar(
69
71
  .background(AppTheme.current.colors.background.surface)
70
72
  .conditional(IsShowBaseLineDebug) {
71
73
  border(1.dp, Colors.blue_03)
72
- },
74
+ }
75
+ .horizontalSafeAreaPadding(),
73
76
  horizontalArrangement = Arrangement.SpaceAround,
74
77
  ) {
75
78
  if (floatingButton == null) {
@@ -101,7 +104,11 @@ internal fun BottomTabBar(
101
104
  )
102
105
  )
103
106
  )
104
- floatingButton?.let { FloatingButton(it) }
107
+ floatingButton?.let {
108
+ Box(Modifier.offset(x = (layout.insetLeft - layout.insetRight) / 2)) {
109
+ FloatingButton(it)
110
+ }
111
+ }
105
112
  }
106
113
  }
107
114
 
@@ -172,14 +179,13 @@ internal fun TabBarItem(item: BottomTabItem, selected: Boolean, onClick: () -> U
172
179
  overflow = TextOverflow.Ellipsis
173
180
  )
174
181
  }
175
- if (badgeLabel != null) {
176
- if (badgeLabel.isEmpty())
177
- BadgeDot(
178
- size = DotSize.Small,
179
- modifier = Modifier.offset(x = 50.dp, y = (-48).dp)
180
- )
181
- else
182
- 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
+ )
183
189
  }
184
190
  }
185
191
  }