@momo-kits/native-kits 0.167.1-sizescale.2-debug → 0.168.1-beta.1-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 (223) hide show
  1. package/CLAUDE.md +45 -3
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +9 -1
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +3 -4
  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 +353 -113
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +2 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +16 -9
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +6 -3
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +11 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +7 -23
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +309 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +8 -7
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +134 -81
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +126 -29
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +116 -26
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +2 -3
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  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 +13 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +1 -3
  66. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +10 -11
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  68. package/gradle/libs.versions.toml +1 -1
  69. package/gradle.properties +1 -1
  70. package/ios/Application/ApplicationEnvironment.swift +17 -0
  71. package/ios/Application/MaxApi.swift +30 -3
  72. package/ios/Avatar/Avatar.swift +4 -3
  73. package/ios/Badge/Badge.swift +6 -4
  74. package/ios/Badge/BadgeDot.swift +1 -1
  75. package/ios/Badge/BadgeRibbon.swift +0 -1
  76. package/ios/Button/Button.swift +15 -12
  77. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  78. package/ios/Carousel/Carousel.swift +29 -12
  79. package/ios/Checkbox/Checkbox.swift +7 -3
  80. package/ios/Chip/Chip.swift +5 -4
  81. package/ios/Collapse/Collapse.swift +70 -19
  82. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  83. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  84. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  85. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  86. package/ios/Divider/Divider.swift +37 -4
  87. package/ios/Extensions/Color++.swift +38 -9
  88. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  89. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  90. package/ios/Icon/Icon.swift +21 -23
  91. package/ios/Icon/Icons.swift +1329 -0
  92. package/ios/IconButton/IconButton.swift +15 -7
  93. package/ios/Image/Image.swift +7 -23
  94. package/ios/Information/Information.swift +27 -9
  95. package/ios/Input/ErrorView.swift +2 -2
  96. package/ios/Input/Input.swift +160 -147
  97. package/ios/Input/InputPhoneNumber.swift +12 -9
  98. package/ios/Input/InputSearch.swift +107 -77
  99. package/ios/Input/InputTextArea.swift +128 -145
  100. package/ios/InputDropDown/InputDropDown.swift +10 -7
  101. package/ios/InputMoney/InputMoney.swift +33 -11
  102. package/ios/InputOTP/InputOTP.swift +22 -12
  103. package/ios/Layout/Card.swift +50 -0
  104. package/ios/Layout/GridContext.swift +187 -3
  105. package/ios/Layout/Item.swift +5 -4
  106. package/ios/Layout/ScreenLayout.swift +380 -0
  107. package/ios/Layout/Section.swift +4 -100
  108. package/ios/Loader/Loader.swift +5 -9
  109. package/ios/Lottie/LottieView.swift +14 -2
  110. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  111. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  112. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  113. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  114. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  115. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  116. package/ios/Navigation/Component/Header.swift +545 -0
  117. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  118. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  119. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  120. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  121. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  122. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  123. package/ios/Navigation/Component/SideBar.swift +183 -0
  124. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  125. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  126. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  127. package/ios/Navigation/Navigation.swift +204 -0
  128. package/ios/Navigation/NavigationContainer.swift +195 -0
  129. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  130. package/ios/Navigation/StackScreen.swift +934 -0
  131. package/ios/Navigation/StackTransition.swift +400 -0
  132. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  133. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  134. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  135. package/ios/Pagination/PaginationScroll.swift +2 -2
  136. package/ios/Popup/PopupDisplay.swift +72 -135
  137. package/ios/Popup/PopupInput.swift +10 -13
  138. package/ios/Popup/PopupLayout.swift +140 -0
  139. package/ios/Popup/PopupNotify.swift +59 -69
  140. package/ios/Popup/PopupPromotion.swift +31 -47
  141. package/ios/PopupView/FullscreenPopup.swift +13 -19
  142. package/ios/PopupView/PopupView.swift +1 -1
  143. package/ios/PopupView/Utils++.swift +0 -8
  144. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  145. package/ios/Radio/Radio.swift +2 -1
  146. package/ios/Rating/Rating.swift +9 -1
  147. package/ios/Resources/dot_loading.json +558 -0
  148. package/ios/Resources/lottie_circle_loader.json +1 -0
  149. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  150. package/ios/Skeleton/Skeleton.swift +22 -27
  151. package/ios/Slider/Slider.swift +16 -18
  152. package/ios/Stepper/Stepper.swift +6 -1
  153. package/ios/Steps/Steps.swift +8 -6
  154. package/ios/SuggestAction/SuggestAction.swift +5 -0
  155. package/ios/Swipeable/SwipeCell.swift +10 -6
  156. package/ios/Switch/Switch.swift +22 -20
  157. package/ios/TabView/TabView.swift +102 -21
  158. package/ios/Tag/Tag.swift +15 -4
  159. package/ios/Template/Logo/Logo.swift +2 -1
  160. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  161. package/ios/Title/Title.swift +6 -17
  162. package/ios/Tooltip/Tooltip.swift +29 -14
  163. package/ios/Typography/FontScale.swift +3 -3
  164. package/ios/Typography/Text.swift +81 -19
  165. package/ios/Uploader/Uploader.swift +3 -1
  166. package/ios/native-kits.podspec +2 -1
  167. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  168. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  169. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  170. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  171. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  172. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  173. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  176. package/local.properties +2 -2
  177. package/package.json +1 -1
  178. package/scripts/gen-ios-icons.mjs +36 -0
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  182. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  183. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  184. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitSizeScale.kt +0 -56
  192. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
  193. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  194. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  195. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
  196. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  197. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
  198. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
  199. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
  200. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
  201. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
  202. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
  203. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
  204. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
  205. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
  206. package/ios/Application/Components.swift +0 -270
  207. package/ios/Application/ComposeApi.swift +0 -22
  208. package/ios/Application/HeaderRight.swift +0 -306
  209. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  210. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  211. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  212. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  213. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  214. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  215. package/ios/Application/Navigation/component/Header.swift +0 -178
  216. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  217. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  218. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  219. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  220. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  221. package/ios/Application/Screen.swift +0 -250
  222. package/ios/Application/StackScreen.swift +0 -492
  223. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,312 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.animation.core.Spring
4
+ import androidx.compose.animation.core.animateDpAsState
5
+ import androidx.compose.animation.core.spring
6
+ import androidx.compose.foundation.background
7
+ import androidx.compose.foundation.border
8
+ import androidx.compose.foundation.layout.Arrangement
9
+ import androidx.compose.foundation.layout.Box
10
+ import androidx.compose.foundation.layout.BoxScope
11
+ import androidx.compose.foundation.layout.BoxWithConstraints
12
+ import androidx.compose.foundation.layout.Column
13
+ import androidx.compose.foundation.layout.Row
14
+ import androidx.compose.foundation.layout.RowScope
15
+ import androidx.compose.foundation.layout.absolutePadding
16
+ import androidx.compose.foundation.layout.fillMaxHeight
17
+ import androidx.compose.foundation.layout.fillMaxSize
18
+ import androidx.compose.foundation.layout.height
19
+ import androidx.compose.foundation.layout.offset
20
+ import androidx.compose.foundation.layout.padding
21
+ import androidx.compose.foundation.layout.size
22
+ import androidx.compose.foundation.layout.width
23
+ import androidx.compose.foundation.shape.CircleShape
24
+ import androidx.compose.foundation.shape.RoundedCornerShape
25
+ import androidx.compose.runtime.Composable
26
+ import androidx.compose.runtime.compositionLocalOf
27
+ import androidx.compose.runtime.getValue
28
+ import androidx.compose.ui.AbsoluteAlignment
29
+ import androidx.compose.ui.Alignment
30
+ import androidx.compose.ui.Modifier
31
+ import androidx.compose.ui.draw.clip
32
+ import androidx.compose.ui.draw.shadow
33
+ import androidx.compose.ui.graphics.Color
34
+ import androidx.compose.ui.graphics.Shape
35
+ import androidx.compose.ui.text.style.TextOverflow
36
+ import androidx.compose.ui.unit.Dp
37
+ import androidx.compose.ui.unit.dp
38
+ import vn.momo.kits.components.Badge
39
+ import vn.momo.kits.components.BadgeDot
40
+ import vn.momo.kits.components.DotSize
41
+ import vn.momo.kits.components.Icon
42
+ import vn.momo.kits.components.Text
43
+ import vn.momo.kits.const.AppTheme
44
+ import vn.momo.kits.const.Radius
45
+ import vn.momo.kits.const.Spacing
46
+ import vn.momo.kits.const.Typography
47
+ import vn.momo.kits.layout.DuoMetrics
48
+ import vn.momo.kits.layout.ScreenLayout
49
+ import vn.momo.kits.layout.SideBarEdge
50
+ import vn.momo.kits.modifier.noFeedbackClickable
51
+ import vn.momo.kits.modifier.setAutomationId
52
+
53
+ // Figma Fold-Design-System MT1/MT2, mirrored from RN DuoTabBar.tsx.
54
+ private val ITEM_SIZE = DuoMetrics.controlSize
55
+ private val ICON_SIZE = 28.dp
56
+ private val QR_ICON_SIZE = 24.dp
57
+ private val RAIL_PADDING = 6.dp
58
+ private val RAIL_GAP = Spacing.XXS
59
+ // selection bubble overflows the 48 rail cell by 5 above and below
60
+ private val RAIL_BUBBLE_WIDTH = 44.dp
61
+ private val RAIL_BUBBLE_HEIGHT = 58.dp
62
+ private val FLOATING_HEIGHT = 62.dp
63
+ private val FLOATING_PADDING = Spacing.XS
64
+ private val FLOATING_QR_SIZE = 44.dp
65
+ // floating bar spans 6 of the 8 portrait grid columns, like the content
66
+ private const val GRID_COLUMNS = 8
67
+ private const val GRID_SPAN = 6
68
+ // RN reanimated spring { damping: 18, stiffness: 220, mass: 0.8 }
69
+ private val BUBBLE_SPRING = spring<Dp>(dampingRatio = 0.68f, stiffness = 275f, visibilityThreshold = Dp(0.5f))
70
+
71
+ // Space from the window bottom the rail stack and its gap take, so the header side bar stays above it.
72
+ internal val LocalDuoRailReserve = compositionLocalOf { 0.dp }
73
+
74
+ // Rail plus the QR button under it; compressed, the rail is a single button.
75
+ internal fun duoRailHeight(itemCount: Int, hasQr: Boolean, compressed: Boolean = false): Dp {
76
+ val rail = if (compressed) ITEM_SIZE
77
+ else RAIL_PADDING * 2 + ITEM_SIZE * itemCount + RAIL_GAP * (itemCount - 1).coerceAtLeast(0)
78
+ return rail + if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
79
+ }
80
+
81
+ // Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
82
+ internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
83
+ if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + Spacing.M + layout.insetBottom else 0.dp
84
+
85
+ // iPhone Duo tab bar: glass rail in the side bar, or a floating glass bar when unfolded portrait.
86
+ @Composable
87
+ internal fun BoxScope.DuoTabBar(
88
+ layout: ScreenLayout,
89
+ items: List<BottomTabItem>,
90
+ floatingButton: BottomTabFloatingButton?,
91
+ selectedIndex: Int,
92
+ onTabSelected: (Int) -> Unit,
93
+ compression: DuoBarCompression = DuoBarCompression(),
94
+ ) {
95
+ if (layout.sideBar != null) {
96
+ DuoRailTabBar(layout, items, floatingButton, selectedIndex, onTabSelected, compression)
97
+ } else {
98
+ DuoFloatingTabBar(layout, items, floatingButton, selectedIndex, onTabSelected)
99
+ }
100
+ }
101
+
102
+ // Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E). When the
103
+ // side bar is short it can shrink to one button showing the current tab (HIG "Tab bar compressed");
104
+ // tapping it expands the rail and compresses the toolbar.
105
+ @Composable
106
+ private fun BoxScope.DuoRailTabBar(
107
+ layout: ScreenLayout,
108
+ items: List<BottomTabItem>,
109
+ floatingButton: BottomTabFloatingButton?,
110
+ selectedIndex: Int,
111
+ onTabSelected: (Int) -> Unit,
112
+ compression: DuoBarCompression,
113
+ ) {
114
+ val sideBar = layout.sideBar ?: return
115
+ val isRight = sideBar.edge == SideBarEdge.RIGHT
116
+
117
+ Column(
118
+ modifier = Modifier
119
+ .align(if (isRight) AbsoluteAlignment.BottomRight else AbsoluteAlignment.BottomLeft)
120
+ .width(sideBar.width)
121
+ .padding(bottom = sideBar.bottom)
122
+ .absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
123
+ horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
124
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
125
+ ) {
126
+ val current = items.getOrNull(selectedIndex)
127
+ if (compression.tabBarCompressed && current != null) {
128
+ Box(
129
+ modifier = Modifier
130
+ .size(ITEM_SIZE)
131
+ .tabBarMaterial(CircleShape)
132
+ .noFeedbackClickable(onClick = compression.expandTabBar)
133
+ .setAutomationId("bottom_tab_bar"),
134
+ contentAlignment = Alignment.Center,
135
+ ) {
136
+ DuoTabIcon(current, selected = true)
137
+ }
138
+ } else {
139
+ Rail(items, selectedIndex) {
140
+ onTabSelected(it)
141
+ compression.onTabSelected()
142
+ }
143
+ }
144
+ if (floatingButton != null) {
145
+ QrButton(floatingButton, size = ITEM_SIZE)
146
+ }
147
+ }
148
+ }
149
+
150
+ // Glass column of tab items with the sliding selection bubble.
151
+ @Composable
152
+ private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (Int) -> Unit) {
153
+ val bubbleOffset by animateDpAsState(
154
+ targetValue = (ITEM_SIZE + RAIL_GAP) * selectedIndex.coerceAtLeast(0),
155
+ animationSpec = BUBBLE_SPRING,
156
+ label = "DuoRailBubble",
157
+ )
158
+ Box(
159
+ Modifier
160
+ .width(ITEM_SIZE)
161
+ .tabBarMaterial(RoundedCornerShape(Radius.XL))
162
+ .setAutomationId("bottom_tab_bar")
163
+ ) {
164
+ Box(
165
+ Modifier
166
+ .offset(
167
+ x = (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
168
+ y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2 + bubbleOffset,
169
+ )
170
+ .size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
171
+ .background(AppTheme.current.colors.background.selected, CircleShape)
172
+ )
173
+ Column(
174
+ modifier = Modifier.padding(vertical = RAIL_PADDING),
175
+ verticalArrangement = Arrangement.spacedBy(RAIL_GAP),
176
+ ) {
177
+ items.forEachIndexed { index, item ->
178
+ Box(
179
+ modifier = Modifier
180
+ .size(ITEM_SIZE)
181
+ .noFeedbackClickable { onItemClick(index) },
182
+ contentAlignment = Alignment.Center,
183
+ ) {
184
+ DuoTabIcon(item, selected = index == selectedIndex)
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+
191
+ @Composable
192
+ private fun BoxScope.DuoFloatingTabBar(
193
+ layout: ScreenLayout,
194
+ items: List<BottomTabItem>,
195
+ floatingButton: BottomTabFloatingButton?,
196
+ selectedIndex: Int,
197
+ onTabSelected: (Int) -> Unit,
198
+ ) {
199
+ val columnWidth = (layout.width - Spacing.M * 2 - Spacing.M * (GRID_COLUMNS - 1)) / GRID_COLUMNS
200
+ val barWidth = columnWidth * GRID_SPAN + Spacing.M * (GRID_SPAN - 1)
201
+ val useQr = floatingButton != null && items.size % 2 == 0
202
+ val middle = items.size / 2
203
+ val cellCount = items.size + if (useQr) 1 else 0
204
+ val visualIndex = if (useQr && selectedIndex >= middle) selectedIndex + 1 else selectedIndex
205
+
206
+ Box(
207
+ modifier = Modifier
208
+ .align(Alignment.BottomCenter)
209
+ .padding(bottom = layout.insetBottom + Spacing.M)
210
+ .width(barWidth)
211
+ .height(FLOATING_HEIGHT)
212
+ .tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
213
+ .padding(FLOATING_PADDING)
214
+ .setAutomationId("bottom_tab_bar")
215
+ ) {
216
+ BoxWithConstraints(Modifier.fillMaxSize()) {
217
+ val cellWidth = maxWidth / cellCount
218
+ val bubbleOffset by animateDpAsState(
219
+ targetValue = cellWidth * visualIndex.coerceAtLeast(0),
220
+ animationSpec = BUBBLE_SPRING,
221
+ label = "DuoFloatingBubble",
222
+ )
223
+ Box(
224
+ Modifier
225
+ .offset(x = bubbleOffset)
226
+ .width(cellWidth)
227
+ .fillMaxHeight()
228
+ .background(AppTheme.current.colors.background.selected, CircleShape)
229
+ )
230
+ Row(Modifier.fillMaxSize()) {
231
+ items.forEachIndexed { index, item ->
232
+ if (useQr && index == middle) {
233
+ Box(Modifier.weight(1f).fillMaxHeight(), contentAlignment = Alignment.Center) {
234
+ QrButton(floatingButton, size = FLOATING_QR_SIZE)
235
+ }
236
+ }
237
+ FloatingCell(item, selected = index == selectedIndex) { onTabSelected(index) }
238
+ }
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ @Composable
245
+ private fun RowScope.FloatingCell(item: BottomTabItem, selected: Boolean, onClick: () -> Unit) {
246
+ val colors = AppTheme.current.colors
247
+ Column(
248
+ modifier = Modifier
249
+ .weight(1f)
250
+ .fillMaxHeight()
251
+ .noFeedbackClickable(onClick = onClick),
252
+ horizontalAlignment = Alignment.CenterHorizontally,
253
+ verticalArrangement = Arrangement.spacedBy(Spacing.XXS, Alignment.CenterVertically),
254
+ ) {
255
+ DuoTabIcon(item, selected)
256
+ Text(
257
+ text = item.label,
258
+ style = Typography.labelXsMedium,
259
+ color = if (selected) colors.primary else colors.text.default,
260
+ maxLines = 1,
261
+ overflow = TextOverflow.Ellipsis,
262
+ )
263
+ }
264
+ }
265
+
266
+ @Composable
267
+ private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
268
+ val colors = AppTheme.current.colors
269
+ val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
270
+ Box {
271
+ Icon(
272
+ source = item.icon,
273
+ size = ICON_SIZE,
274
+ color = if (selected) colors.primary else colors.text.default,
275
+ )
276
+ when {
277
+ badgeLabel == null && !item.showDot -> Unit
278
+ badgeLabel.isNullOrEmpty() -> BadgeDot(
279
+ size = DotSize.Small,
280
+ modifier = Modifier.align(Alignment.TopEnd),
281
+ )
282
+ else -> Badge(
283
+ badgeLabel,
284
+ modifier = Modifier.align(Alignment.TopStart).offset(x = ICON_SIZE / 2, y = -Spacing.XS),
285
+ )
286
+ }
287
+ }
288
+ }
289
+
290
+ @Composable
291
+ private fun QrButton(data: BottomTabFloatingButton, size: Dp) {
292
+ Box(
293
+ modifier = Modifier
294
+ .size(size)
295
+ .clip(CircleShape)
296
+ .background(AppTheme.current.colors.primary)
297
+ .noFeedbackClickable(onClick = data.onPress)
298
+ .setAutomationId(data.label),
299
+ contentAlignment = Alignment.Center,
300
+ ) {
301
+ Icon(source = data.icon, color = Color.White, size = QR_ICON_SIZE)
302
+ }
303
+ }
304
+
305
+ // RN falls back to this when native glass is missing; Compose has no backdrop blur, so it always does.
306
+ @Composable
307
+ private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
308
+ val colors = AppTheme.current.colors
309
+ return shadow(3.dp, shape)
310
+ .background(colors.background.surface)
311
+ .border(1.dp, colors.border.default, shape)
312
+ }
@@ -18,10 +18,14 @@ import androidx.compose.foundation.layout.width
18
18
  import androidx.compose.foundation.layout.wrapContentHeight
19
19
  import androidx.compose.foundation.shape.RoundedCornerShape
20
20
  import androidx.compose.runtime.Composable
21
+ import androidx.compose.runtime.CompositionLocalProvider
22
+ import androidx.compose.runtime.compositionLocalOf
21
23
  import androidx.compose.runtime.getValue
22
24
  import androidx.compose.ui.Alignment
23
25
  import androidx.compose.ui.Modifier
24
26
  import androidx.compose.ui.draw.clipToBounds
27
+ import androidx.compose.ui.draw.drawBehind
28
+ import androidx.compose.ui.geometry.Offset
25
29
  import androidx.compose.ui.graphics.graphicsLayer
26
30
  import androidx.compose.ui.graphics.Brush
27
31
  import androidx.compose.ui.graphics.Color
@@ -33,12 +37,13 @@ import androidx.compose.ui.unit.dp
33
37
  import vn.momo.kits.components.Icon
34
38
  import vn.momo.kits.components.Image
35
39
  import vn.momo.kits.components.InputSearchProps
36
- import vn.momo.kits.const.AppStatusBar
37
40
  import vn.momo.kits.const.AppTheme
38
41
  import vn.momo.kits.const.Colors
39
42
  import vn.momo.kits.components.PopupNotifyProps
40
43
  import vn.momo.kits.application.IsShowBaseLineDebug
41
44
  import vn.momo.kits.const.Spacing
45
+ import vn.momo.kits.layout.DuoMetrics
46
+ import vn.momo.kits.layout.getScreenLayout
42
47
  import vn.momo.kits.modifier.activeOpacityClickable
43
48
  import vn.momo.kits.modifier.conditional
44
49
  import vn.momo.kits.modifier.setAutomationId
@@ -47,33 +52,83 @@ import vn.momo.kits.navigation.LocalNavigator
47
52
  import vn.momo.kits.navigation.LocalOptions
48
53
  import vn.momo.kits.navigation.LocalScrollState
49
54
  import vn.momo.kits.navigation.getInputSearchType
55
+ import vn.momo.kits.navigation.usesDuoGlassHeader
56
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
50
57
 
51
58
  @InternalApi
52
59
  const val HEADER_HEIGHT = 52
53
60
  internal enum class InputSearchType { None, Header, Animated }
54
61
  enum class TitlePosition { LEFT, CENTER }
55
62
 
63
+ private val HEADER_ACTIONS_WIDTH = 172.dp
64
+
65
+ // Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
66
+ private val STATUS_BAR_ROW_ACTIONS_EXTRA = 55.dp
67
+
56
68
  @Suppress("FrequentlyChangingValue")
57
69
  @Composable
58
- internal fun Header(onBackHandler: (() -> Unit)? = null) {
70
+ internal fun headerOpacity(): Float {
59
71
  val options = LocalOptions.current
60
- val navigator = LocalNavigator.current
61
72
  val scrollState = LocalScrollState.current
62
- val headerRightWidthPx = LocalHeaderRightWidthPx.current
63
- val inputSearchType = getInputSearchType(options)
64
-
65
73
  val opacityHeight = when (val header = options.headerType) {
66
74
  is HeaderType.Animated -> with(LocalDensity.current) { header.layoutOffSet.roundToPx() }
67
75
  else -> HEADER_HEIGHT
68
76
  }
69
77
  val opacity by animateFloatAsState(
70
- targetValue = if (options.headerType is HeaderType.Transparent) {
71
- 0f
72
- } else {
73
- (scrollState.value / opacityHeight.toFloat())
74
- .coerceIn(0f, 1f)
78
+ targetValue = when {
79
+ options.headerType is HeaderType.Transparent -> 0f
80
+ opacityHeight <= 0 -> if (scrollState.value > 0) 1f else 0f
81
+ else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
75
82
  }
76
83
  )
84
+ return opacity
85
+ }
86
+
87
+ // Back button and toolkit share the bar with the title unless the iPhone Duo side bar holds them.
88
+ @Composable
89
+ internal fun headerTitleMaxWidth(): Dp {
90
+ val layout = getScreenLayout()
91
+ val clearance = layout.statusBarClearance
92
+ val clearanceWidth = clearance?.let { it.left + it.right } ?: 0.dp
93
+ if (layout.sideBar != null) return layout.contentWidth - Spacing.M * 2 - clearanceWidth
94
+ return layout.width - HEADER_ACTIONS_WIDTH - (if (clearance != null) STATUS_BAR_ROW_ACTIONS_EXTRA else 0.dp) - clearanceWidth
95
+ }
96
+
97
+ // Scroll distance over which the Duo glass bar fades in.
98
+ private val DUO_GLASS_FADE_DISTANCE = 16.dp
99
+
100
+ // RN DuoGlassBackground fallback: translucent surface with a hairline once content scrolls under it.
101
+ @Suppress("FrequentlyChangingValue")
102
+ @Composable
103
+ private fun Modifier.duoGlassBar(): Modifier {
104
+ val scrollState = LocalScrollState.current
105
+ val fadePx = with(LocalDensity.current) { DUO_GLASS_FADE_DISTANCE.toPx() }
106
+ val alpha by animateFloatAsState((scrollState.value / fadePx).coerceIn(0f, 1f), label = "DuoGlassAlpha")
107
+ val colors = AppTheme.current.colors
108
+ return background(colors.background.surface.copy(alpha = 0.8f * alpha))
109
+ .drawBehind {
110
+ val stroke = 0.5.dp.toPx()
111
+ drawLine(
112
+ color = colors.border.default.copy(alpha = alpha),
113
+ start = Offset(0f, size.height - stroke / 2),
114
+ end = Offset(size.width, size.height - stroke / 2),
115
+ strokeWidth = stroke,
116
+ )
117
+ }
118
+ }
119
+
120
+ @Composable
121
+ internal fun Header(onBackHandler: (() -> Unit)? = null) {
122
+ val options = LocalOptions.current
123
+ val navigator = LocalNavigator.current
124
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
125
+ val inputSearchType = getInputSearchType(options)
126
+ val layout = getScreenLayout()
127
+ val useSideBar = layout.sideBar != null
128
+ // iPhone Duo inner portrait: the row sits on the status bar, between its clusters
129
+ val clearance = layout.statusBarClearance
130
+ val statusBarRow = clearance != null && !useSideBar
131
+ val opacity = headerOpacity()
77
132
 
78
133
  val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
79
134
 
@@ -92,13 +147,15 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
92
147
  // Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
93
148
  val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
94
149
 
150
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
95
151
  val headerImage = AppTheme.current.assets.headerBackground
96
- .takeUnless { options.headerType is HeaderType.Transparent }
152
+ .takeUnless { options.headerType is HeaderType.Transparent || useDuoGlass }
97
153
 
98
154
  Box(
99
- Modifier.height(AppStatusBar.current + HEADER_HEIGHT.dp)
155
+ Modifier.height(layout.headerHeight)
100
156
  .fillMaxWidth()
101
- .conditional(headerImage.isNullOrEmpty()) {
157
+ .then(if (useDuoGlass) Modifier.duoGlassBar() else Modifier)
158
+ .conditional(headerImage.isNullOrEmpty() && !useDuoGlass) {
102
159
  background(background.copy(alpha = animatedAlpha))
103
160
  }
104
161
  .conditional(IsShowBaseLineDebug) {
@@ -125,21 +182,34 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
125
182
  }
126
183
  }
127
184
  Row(
128
- modifier = Modifier.height(HEADER_HEIGHT.dp)
185
+ modifier = Modifier.height(layout.headerBarHeight)
129
186
  .fillMaxWidth()
130
- .padding(horizontal = Spacing.M),
187
+ .horizontalSafeAreaPadding()
188
+ .padding(
189
+ start = Spacing.M + (clearance?.left ?: 0.dp),
190
+ end = Spacing.M + (clearance?.right ?: 0.dp),
191
+ ),
131
192
  verticalAlignment = Alignment.CenterVertically,
132
193
  horizontalArrangement = Arrangement.SpaceBetween
133
194
  ) {
134
- if(!options.hiddenBack) {
135
- BackButton(
136
- borderColor = headerColor.borderColor,
137
- backgroundButton = headerColor.backgroundButton,
138
- tintIconColor = headerColor.tintIconColor,
139
- onBackHandler = {
140
- navigator.onBackSafe { }
141
- }
142
- )
195
+ if (!options.hiddenBack && !useSideBar) {
196
+ if (statusBarRow) {
197
+ SideBarButton(
198
+ icon = "arrow-back",
199
+ headerColor = headerColor,
200
+ automationId = "btn_navigation_back",
201
+ onClick = { navigator.onBackSafe { } },
202
+ )
203
+ } else {
204
+ BackButton(
205
+ borderColor = headerColor.borderColor,
206
+ backgroundButton = headerColor.backgroundButton,
207
+ tintIconColor = headerColor.tintIconColor,
208
+ onBackHandler = {
209
+ navigator.onBackSafe { }
210
+ }
211
+ )
212
+ }
143
213
  Spacer(Modifier.width(Spacing.S))
144
214
  }
145
215
 
@@ -156,20 +226,42 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
156
226
  Box(Modifier.onGloballyPositioned {
157
227
  if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
158
228
  }){
159
- HeaderRight(options.headerRight, options.tintColor, headerColor)
229
+ val headerRight = options.headerRight
230
+ when {
231
+ useSideBar && headerRight is HeaderRight.Toolkit -> Unit
232
+ statusBarRow && headerRight is HeaderRight.Toolkit -> Toolkit(
233
+ headerRight = headerRight,
234
+ tintColor = options.tintColor,
235
+ headerColor = headerColor,
236
+ layout = ToolkitLayout.SideBarRow,
237
+ )
238
+ else -> CompositionLocalProvider(LocalHeaderColor provides headerColor) {
239
+ HeaderRight(headerRight, options.tintColor, headerColor)
240
+ }
241
+ }
160
242
  }
161
243
  }
162
244
  if (centerTitle) {
245
+ // on the status bar row the clusters are lopsided, so both sides keep the wider one's room
246
+ val centerInset = if (clearance != null) {
247
+ val back = if (options.hiddenBack || useSideBar) 0.dp else DuoMetrics.controlSize + Spacing.S
248
+ val right = with(LocalDensity.current) { headerRightWidthPx.intValue.toDp() }
249
+ maxOf(clearance.left + back, clearance.right + right) + Spacing.S
250
+ } else {
251
+ 0.dp
252
+ }
163
253
  Box(
164
- modifier = Modifier.height(HEADER_HEIGHT.dp)
254
+ modifier = Modifier.height(layout.headerBarHeight)
165
255
  .fillMaxWidth()
166
- .padding(horizontal = Spacing.M),
256
+ .horizontalSafeAreaPadding()
257
+ .padding(horizontal = Spacing.M + centerInset),
167
258
  contentAlignment = Alignment.Center
168
259
  ) {
169
260
  HeaderContent(
170
261
  headerTitle = options.headerTitle,
171
262
  tintIconColor = titleColor,
172
263
  titlePosition = titlePosition,
264
+ centerWidthFraction = if (clearance != null) 1f else 0.5f,
173
265
  modifier = Modifier.fillMaxWidth()
174
266
  )
175
267
  }
@@ -203,6 +295,7 @@ internal fun HeaderContent(
203
295
  headerTitle: HeaderTitle,
204
296
  tintIconColor: Color,
205
297
  titlePosition: TitlePosition = TitlePosition.LEFT,
298
+ centerWidthFraction: Float = 0.5f,
206
299
  modifier: Modifier = Modifier,
207
300
  ){
208
301
  Box(
@@ -214,7 +307,7 @@ internal fun HeaderContent(
214
307
  HeaderTitle(
215
308
  title = headerTitle.title,
216
309
  color = tintIconColor,
217
- modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) 0.5f else 1f),
310
+ modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) centerWidthFraction else 1f),
218
311
  textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
219
312
  )
220
313
  }
@@ -326,6 +419,10 @@ data class HeaderBackProps(
326
419
 
327
420
  @InternalApi
328
421
  data class HeaderColor(val tintIconColor: Color, val backgroundButton: Color, val borderColor: Color)
422
+
423
+ // The enclosing header's colors, so a custom header right follows the bar (SwiftUI `\.headerColor`).
424
+ internal val LocalHeaderColor = compositionLocalOf<HeaderColor?> { null }
425
+
329
426
  internal fun getHeaderColor(headerType: HeaderType, opacity: Float, tintColor: Color?, defaultColor: Color): HeaderColor{
330
427
  return if(headerType is HeaderType.Animated) {
331
428
  if (opacity == 1f || !headerType.isSurface)
@@ -3,23 +3,27 @@ package vn.momo.kits.navigation.component
3
3
  import androidx.compose.animation.core.animateFloatAsState
4
4
  import androidx.compose.foundation.background
5
5
  import androidx.compose.foundation.layout.Box
6
+ import androidx.compose.foundation.layout.fillMaxHeight
6
7
  import androidx.compose.foundation.layout.fillMaxSize
7
8
  import androidx.compose.foundation.layout.fillMaxWidth
8
9
  import androidx.compose.foundation.layout.height
9
10
  import androidx.compose.runtime.Composable
10
11
  import androidx.compose.runtime.getValue
11
12
  import androidx.compose.ui.Modifier
12
- import androidx.compose.ui.geometry.Offset
13
+ import androidx.compose.ui.draw.clipToBounds
14
+ import androidx.compose.ui.graphics.graphicsLayer
13
15
  import androidx.compose.ui.graphics.Brush
14
16
  import androidx.compose.ui.graphics.Color
15
17
  import androidx.compose.ui.platform.LocalDensity
16
18
  import androidx.compose.ui.unit.dp
17
19
  import vn.momo.kits.components.Image
18
- import vn.momo.kits.const.AppStatusBar
19
20
  import vn.momo.kits.const.AppTheme
21
+ import vn.momo.kits.layout.getScreenLayout
20
22
  import vn.momo.kits.modifier.conditional
21
23
  import vn.momo.kits.navigation.LocalOptions
22
24
  import vn.momo.kits.navigation.LocalScrollState
25
+ import vn.momo.kits.navigation.getInputSearchType
26
+ import vn.momo.kits.navigation.usesDuoGlassHeader
23
27
 
24
28
  enum class AnimatedHeaderRatio(val value: Float){
25
29
  RATIO_16_9(16f / 9f),
@@ -29,6 +33,9 @@ enum class AnimatedHeaderRatio(val value: Float){
29
33
 
30
34
  internal val HEADER_BACKGROUND_HEIGHT = 154.dp
31
35
 
36
+ // RN DuoScrollGradient: extended gradient runs this far below the header
37
+ private val DUO_GRADIENT_EXTRA = 52.dp
38
+
32
39
  @Suppress("FrequentlyChangingValue")
33
40
  @Composable
34
41
  internal fun HeaderBackground() {
@@ -38,7 +45,8 @@ internal fun HeaderBackground() {
38
45
  val scrollState = LocalScrollState.current
39
46
  val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
40
47
 
41
- val minHeight = AppStatusBar.current + HEADER_HEIGHT.dp
48
+ val layout = getScreenLayout()
49
+ val minHeight = layout.headerHeight
42
50
  val maxHeight = HEADER_BACKGROUND_HEIGHT
43
51
  val opacity by animateFloatAsState(
44
52
  targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
@@ -57,20 +65,32 @@ internal fun HeaderBackground() {
57
65
  else -> 0.dp
58
66
  }
59
67
 
60
- Box(Modifier.fillMaxWidth().height(height).background(backgroundColor)) {
68
+ // iPhone Duo glass header: the gradient leaves with the content, as the quick return header does
69
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
70
+ // RN HeaderExtendHeader with search on the side bar layout: the gradient fills the header box,
71
+ // which for extended shrinks from 52pt below the header over the first 100pt of scroll
72
+ val duoSearch = layout.sideBar != null && getInputSearchType(options) != InputSearchType.None
73
+ val fillsBox = useDuoGlass || duoSearch
74
+ val duoModifier = when {
75
+ useDuoGlass -> {
76
+ val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
77
+ Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
78
+ }
79
+ duoSearch && options.headerType is HeaderType.Extended -> {
80
+ val progress = (with(density) { scrollState.value.toDp() } / 100.dp).coerceIn(0f, 1f)
81
+ Modifier.height(minHeight + DUO_GRADIENT_EXTRA * (1 - progress))
82
+ }
83
+ duoSearch -> Modifier.height(minHeight)
84
+ else -> Modifier.height(height)
85
+ }
86
+
87
+ Box(Modifier.fillMaxWidth().then(duoModifier).clipToBounds().background(backgroundColor)) {
88
+ // on Duo the fade spans the gradient box; a banner image keeps its 375:154 shape, clipped
61
89
  Box(Modifier
62
- .height(maxHeight)
63
90
  .fillMaxWidth()
91
+ .then(if (fillsBox && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
64
92
  .conditional(!theme.isHeaderImage()) {
65
- background(Brush.linearGradient(
66
- colors = listOf(
67
- color,
68
- backgroundColor
69
- ),
70
- start = Offset(0f, 0f),
71
- end = Offset(0f, with(density) { maxHeight.toPx() })
72
- )
73
- )
93
+ background(Brush.verticalGradient(colors = listOf(color, backgroundColor)))
74
94
  }
75
95
  ){
76
96
  if (!theme.isHeaderImage()) return@Box