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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,377 @@
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.animateFloatAsState
6
+ import androidx.compose.animation.core.spring
7
+ import androidx.compose.foundation.background
8
+ import androidx.compose.foundation.clickable
9
+ import androidx.compose.foundation.interaction.MutableInteractionSource
10
+ import androidx.compose.foundation.interaction.collectIsPressedAsState
11
+ import androidx.compose.foundation.border
12
+ import androidx.compose.foundation.layout.Arrangement
13
+ import androidx.compose.foundation.layout.Box
14
+ import androidx.compose.foundation.layout.BoxScope
15
+ import androidx.compose.foundation.layout.BoxWithConstraints
16
+ import androidx.compose.foundation.layout.Column
17
+ import androidx.compose.foundation.layout.Row
18
+ import androidx.compose.foundation.layout.RowScope
19
+ import androidx.compose.foundation.layout.absolutePadding
20
+ import androidx.compose.foundation.layout.fillMaxHeight
21
+ import androidx.compose.foundation.layout.fillMaxSize
22
+ import androidx.compose.foundation.layout.fillMaxWidth
23
+ import androidx.compose.foundation.layout.height
24
+ import androidx.compose.foundation.layout.offset
25
+ import androidx.compose.foundation.layout.padding
26
+ import androidx.compose.foundation.layout.size
27
+ import androidx.compose.foundation.layout.width
28
+ import androidx.compose.foundation.shape.CircleShape
29
+ import androidx.compose.foundation.shape.RoundedCornerShape
30
+ import androidx.compose.runtime.Composable
31
+ import androidx.compose.runtime.CompositionLocalProvider
32
+ import androidx.compose.runtime.LaunchedEffect
33
+ import androidx.compose.runtime.MutableState
34
+ import androidx.compose.runtime.compositionLocalOf
35
+ import androidx.compose.runtime.getValue
36
+ import androidx.compose.runtime.mutableStateOf
37
+ import androidx.compose.runtime.remember
38
+ import androidx.compose.ui.AbsoluteAlignment
39
+ import androidx.compose.ui.Alignment
40
+ import androidx.compose.ui.Modifier
41
+ import androidx.compose.ui.draw.clip
42
+ import androidx.compose.ui.draw.shadow
43
+ import androidx.compose.ui.graphics.graphicsLayer
44
+ import androidx.compose.ui.graphics.Color
45
+ import androidx.compose.ui.graphics.Shape
46
+ import androidx.compose.ui.semantics.Role
47
+ import androidx.compose.ui.text.style.TextOverflow
48
+ import androidx.compose.ui.unit.Dp
49
+ import androidx.compose.ui.unit.dp
50
+ import vn.momo.kits.application.LocalContext
51
+ import vn.momo.kits.application.MiniAppContext
52
+ import vn.momo.kits.components.Badge
53
+ import vn.momo.kits.components.BadgeDot
54
+ import vn.momo.kits.components.DotSize
55
+ import vn.momo.kits.components.Icon
56
+ import vn.momo.kits.components.Text
57
+ import vn.momo.kits.const.AppTheme
58
+ import vn.momo.kits.const.Spacing
59
+ import vn.momo.kits.const.Typography
60
+ import vn.momo.kits.layout.DuoMetrics
61
+ import vn.momo.kits.layout.ScreenLayout
62
+ import vn.momo.kits.layout.SideBarEdge
63
+ import vn.momo.kits.modifier.noFeedbackClickable
64
+ import vn.momo.kits.modifier.setAutomationId
65
+
66
+ // Figma Fold-Design-System MT1/MT2, mirrored from RN DuoTabBar.tsx.
67
+ private val ITEM_SIZE = DuoMetrics.controlSize
68
+ private val ICON_SIZE = 28.dp
69
+ private val QR_ICON_SIZE = 24.dp
70
+ private val RAIL_PADDING = 6.dp
71
+ private val RAIL_GAP = Spacing.XXS
72
+ // selection bubble overflows the 48 rail cell by 5 above and below
73
+ private val RAIL_BUBBLE_WIDTH = 44.dp
74
+ private val RAIL_BUBBLE_HEIGHT = 58.dp
75
+ private val FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight
76
+ private val FLOATING_PADDING = Spacing.XS
77
+ // the system bar sizes its pill to the tabs instead of stretching it across the display
78
+ private val FLOATING_ITEM_WIDTH = 80.dp
79
+ // while pressed the selection bubble grows into a lens past the pill's edges
80
+ private const val LENS_SCALE_X = 1.2f
81
+ private const val LENS_SCALE_Y = 1.3f
82
+ // Figma Home-2026 tab bar "Selection": brand pink at 8%
83
+ private const val SELECTION_TINT_ALPHA = 0x14 / 255f
84
+ // RN reanimated spring { damping: 18, stiffness: 220, mass: 0.8 }
85
+ private val BUBBLE_SPRING = spring<Dp>(dampingRatio = 0.68f, stiffness = 275f, visibilityThreshold = Dp(0.5f))
86
+ private val LENS_SPRING = spring<Float>(dampingRatio = 0.68f, stiffness = 275f)
87
+
88
+ // Space from the window bottom the rail stack and its gap take, so the header side bar stays above it.
89
+ internal val LocalDuoRailReserve = compositionLocalOf { 0.dp }
90
+
91
+ // Rail plus the QR button under it; compressed, the rail is a single button.
92
+ internal fun duoRailHeight(itemCount: Int, hasQr: Boolean, compressed: Boolean = false): Dp {
93
+ val rail = if (compressed) ITEM_SIZE
94
+ else RAIL_PADDING * 2 + ITEM_SIZE * itemCount + RAIL_GAP * (itemCount - 1).coerceAtLeast(0)
95
+ return rail + if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
96
+ }
97
+
98
+ // Overlaps the home indicator inset like the system tab bar (21pt above the bottom with a 34pt inset).
99
+ private fun floatingTabBarBottom(insetBottom: Dp): Dp =
100
+ maxOf(insetBottom - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S)
101
+
102
+ // Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
103
+ internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
104
+ if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + floatingTabBarBottom(layout.insetBottom) else 0.dp
105
+
106
+ // iPhone Duo tab bar: rail in the side bar, or a floating bar when unfolded portrait.
107
+ @Composable
108
+ internal fun BoxScope.DuoTabBar(
109
+ layout: ScreenLayout,
110
+ items: List<BottomTabItem>,
111
+ floatingButton: BottomTabFloatingButton?,
112
+ selectedIndex: Int,
113
+ onTabSelected: (Int) -> Unit,
114
+ compression: DuoBarCompression = DuoBarCompression(),
115
+ ) {
116
+ if (layout.sideBar != null) {
117
+ DuoRailTabBar(layout, items, floatingButton, selectedIndex, onTabSelected, compression)
118
+ } else {
119
+ DuoFloatingTabBar(layout, items, floatingButton, selectedIndex, onTabSelected)
120
+ }
121
+ }
122
+
123
+ // Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E). When the
124
+ // side bar is short it can shrink to one button showing the current tab (HIG "Tab bar compressed");
125
+ // tapping it expands the rail and compresses the toolbar.
126
+ @Composable
127
+ private fun BoxScope.DuoRailTabBar(
128
+ layout: ScreenLayout,
129
+ items: List<BottomTabItem>,
130
+ floatingButton: BottomTabFloatingButton?,
131
+ selectedIndex: Int,
132
+ onTabSelected: (Int) -> Unit,
133
+ compression: DuoBarCompression,
134
+ ) {
135
+ val sideBar = layout.sideBar ?: return
136
+ val isRight = sideBar.edge == SideBarEdge.RIGHT
137
+
138
+ Column(
139
+ modifier = Modifier
140
+ .align(if (isRight) AbsoluteAlignment.BottomRight else AbsoluteAlignment.BottomLeft)
141
+ .width(sideBar.width)
142
+ .padding(bottom = sideBar.bottom)
143
+ .absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
144
+ horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
145
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
146
+ ) {
147
+ val current = items.getOrNull(selectedIndex)
148
+ if (compression.tabBarCompressed && current != null) {
149
+ Box(
150
+ modifier = Modifier
151
+ .size(ITEM_SIZE)
152
+ .tabBarMaterial(CircleShape)
153
+ .noFeedbackClickable(onClick = compression.expandTabBar)
154
+ .setAutomationId("bottom_tab_bar"),
155
+ contentAlignment = Alignment.Center,
156
+ ) {
157
+ DuoTabIcon(current, selected = true)
158
+ }
159
+ } else {
160
+ Rail(items, selectedIndex) {
161
+ onTabSelected(it)
162
+ compression.onTabSelected()
163
+ }
164
+ }
165
+ if (floatingButton != null) {
166
+ QrButton(floatingButton, size = ITEM_SIZE)
167
+ }
168
+ }
169
+ }
170
+
171
+ // Column of icon-only tab items with the sliding selection bubble.
172
+ @Composable
173
+ private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (Int) -> Unit) {
174
+ val pressedIndex = remember { mutableStateOf<Int?>(null) }
175
+ val bubbleOffset by animateDpAsState(
176
+ targetValue = (ITEM_SIZE + RAIL_GAP) * (pressedIndex.value ?: selectedIndex).coerceAtLeast(0),
177
+ animationSpec = BUBBLE_SPRING,
178
+ label = "DuoRailBubble",
179
+ )
180
+ Box(
181
+ Modifier
182
+ .width(ITEM_SIZE)
183
+ .tabBarMaterial(CircleShape)
184
+ .setAutomationId("bottom_tab_bar")
185
+ ) {
186
+ Box(
187
+ Modifier
188
+ .offset(
189
+ x = (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
190
+ y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2 + bubbleOffset,
191
+ )
192
+ .size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
193
+ .selectionLens(pressedIndex.value != null)
194
+ )
195
+ Column(
196
+ modifier = Modifier.padding(vertical = RAIL_PADDING),
197
+ verticalArrangement = Arrangement.spacedBy(RAIL_GAP),
198
+ ) {
199
+ items.forEachIndexed { index, item ->
200
+ Box(
201
+ modifier = Modifier
202
+ .size(ITEM_SIZE)
203
+ .tabClickable(index, pressedIndex) { onItemClick(index) },
204
+ contentAlignment = Alignment.Center,
205
+ ) {
206
+ DuoTabIcon(item, selected = index == selectedIndex)
207
+ }
208
+ }
209
+ }
210
+ }
211
+ }
212
+
213
+ // Floating labelled bar for the unfolded portrait display, QR pinned in its own platter beside it.
214
+ @Composable
215
+ private fun BoxScope.DuoFloatingTabBar(
216
+ layout: ScreenLayout,
217
+ items: List<BottomTabItem>,
218
+ floatingButton: BottomTabFloatingButton?,
219
+ selectedIndex: Int,
220
+ onTabSelected: (Int) -> Unit,
221
+ ) {
222
+ val pressedIndex = remember { mutableStateOf<Int?>(null) }
223
+
224
+ Row(
225
+ modifier = Modifier
226
+ .align(Alignment.BottomCenter)
227
+ .fillMaxWidth()
228
+ .absolutePadding(
229
+ left = layout.insetLeft + DuoMetrics.floatingTabBarMargin,
230
+ right = layout.insetRight + DuoMetrics.floatingTabBarMargin,
231
+ )
232
+ .padding(bottom = floatingTabBarBottom(layout.insetBottom)),
233
+ horizontalArrangement = Arrangement.spacedBy(Spacing.S, Alignment.CenterHorizontally),
234
+ ) {
235
+ Box(
236
+ modifier = Modifier
237
+ .weight(1f, fill = false)
238
+ .width(FLOATING_ITEM_WIDTH * items.size + FLOATING_PADDING * 2)
239
+ .height(FLOATING_HEIGHT)
240
+ .tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
241
+ .padding(FLOATING_PADDING)
242
+ .setAutomationId("bottom_tab_bar")
243
+ ) {
244
+ BoxWithConstraints(Modifier.fillMaxSize()) {
245
+ val cellWidth = maxWidth / items.size.coerceAtLeast(1)
246
+ val bubbleOffset by animateDpAsState(
247
+ targetValue = cellWidth * (pressedIndex.value ?: selectedIndex).coerceAtLeast(0),
248
+ animationSpec = BUBBLE_SPRING,
249
+ label = "DuoFloatingBubble",
250
+ )
251
+ Box(
252
+ Modifier
253
+ .offset(x = bubbleOffset)
254
+ .width(cellWidth)
255
+ .fillMaxHeight()
256
+ .selectionLens(pressedIndex.value != null)
257
+ )
258
+ Row(Modifier.fillMaxSize()) {
259
+ items.forEachIndexed { index, item ->
260
+ FloatingCell(item, selected = index == selectedIndex, index, pressedIndex) {
261
+ onTabSelected(index)
262
+ }
263
+ }
264
+ }
265
+ }
266
+ }
267
+ if (floatingButton != null) {
268
+ QrButton(floatingButton, size = FLOATING_HEIGHT, Modifier.shadow(3.dp, CircleShape))
269
+ }
270
+ }
271
+ }
272
+
273
+ @Composable
274
+ private fun RowScope.FloatingCell(
275
+ item: BottomTabItem,
276
+ selected: Boolean,
277
+ index: Int,
278
+ pressedIndex: MutableState<Int?>,
279
+ onClick: () -> Unit,
280
+ ) {
281
+ val colors = AppTheme.current.colors
282
+ Column(
283
+ modifier = Modifier
284
+ .weight(1f)
285
+ .fillMaxHeight()
286
+ .tabClickable(index, pressedIndex, onClick),
287
+ horizontalAlignment = Alignment.CenterHorizontally,
288
+ verticalArrangement = Arrangement.spacedBy(Spacing.XXS, Alignment.CenterVertically),
289
+ ) {
290
+ DuoTabIcon(item, selected)
291
+ // like the system tab bar the label keeps its design size; Text grows it up to 5pt on the wide Duo displays
292
+ val context = LocalContext.current
293
+ val fixedScale = remember(context) {
294
+ (context ?: MiniAppContext()).copy(userScaleRate = 1f, useOSScaleRate = false)
295
+ }
296
+ CompositionLocalProvider(LocalContext provides fixedScale) {
297
+ Text(
298
+ text = item.label,
299
+ style = Typography.labelXsMedium,
300
+ color = if (selected) colors.primary else colors.text.hint,
301
+ maxLines = 1,
302
+ overflow = TextOverflow.Ellipsis,
303
+ )
304
+ }
305
+ }
306
+ }
307
+
308
+ // Tracks the finger so the selection lens jumps to the pressed tab before it is selected.
309
+ @Composable
310
+ private fun Modifier.tabClickable(index: Int, pressedIndex: MutableState<Int?>, onClick: () -> Unit): Modifier {
311
+ val interactionSource = remember { MutableInteractionSource() }
312
+ val pressed by interactionSource.collectIsPressedAsState()
313
+ LaunchedEffect(pressed) {
314
+ if (pressed) pressedIndex.value = index
315
+ else if (pressedIndex.value == index) pressedIndex.value = null
316
+ }
317
+ return clickable(interactionSource = interactionSource, indication = null, role = Role.Tab, onClick = onClick)
318
+ }
319
+
320
+ // Selection bubble; while pressed it grows into a lens (80x54 to about 96x70).
321
+ @Composable
322
+ private fun Modifier.selectionLens(pressed: Boolean): Modifier {
323
+ val lens by animateFloatAsState(if (pressed) 1f else 0f, LENS_SPRING, label = "DuoSelectionLens")
324
+ val tint = AppTheme.current.colors.primary.copy(alpha = SELECTION_TINT_ALPHA)
325
+ return graphicsLayer {
326
+ scaleX = 1 + lens * (LENS_SCALE_X - 1)
327
+ scaleY = 1 + lens * (LENS_SCALE_Y - 1)
328
+ }.background(tint, CircleShape)
329
+ }
330
+
331
+ @Composable
332
+ private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
333
+ val colors = AppTheme.current.colors
334
+ val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
335
+ Box {
336
+ Icon(
337
+ source = item.icon,
338
+ size = ICON_SIZE,
339
+ color = if (selected) colors.primary else colors.text.hint,
340
+ )
341
+ when {
342
+ badgeLabel == null && !item.showDot -> Unit
343
+ badgeLabel.isNullOrEmpty() -> BadgeDot(
344
+ size = DotSize.Small,
345
+ modifier = Modifier.align(Alignment.TopEnd),
346
+ )
347
+ else -> Badge(
348
+ badgeLabel,
349
+ modifier = Modifier.align(Alignment.TopStart).offset(x = ICON_SIZE / 2, y = -Spacing.XS),
350
+ )
351
+ }
352
+ }
353
+ }
354
+
355
+ @Composable
356
+ private fun QrButton(data: BottomTabFloatingButton, size: Dp, modifier: Modifier = Modifier) {
357
+ Box(
358
+ modifier = modifier
359
+ .size(size)
360
+ .clip(CircleShape)
361
+ .background(AppTheme.current.colors.primary)
362
+ .noFeedbackClickable(onClick = data.onPress)
363
+ .setAutomationId(data.label),
364
+ contentAlignment = Alignment.Center,
365
+ ) {
366
+ Icon(source = data.icon, color = Color.White, size = QR_ICON_SIZE)
367
+ }
368
+ }
369
+
370
+ // RN BarMaterial: surface with a 1dp border.
371
+ @Composable
372
+ private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
373
+ val colors = AppTheme.current.colors
374
+ return shadow(3.dp, shape)
375
+ .background(colors.background.surface)
376
+ .border(1.dp, colors.border.default, shape)
377
+ }
@@ -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,21 +52,24 @@ 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
@@ -73,6 +81,54 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
73
81
  else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
74
82
  }
75
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: 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()
76
132
 
77
133
  val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
78
134
 
@@ -91,13 +147,15 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
91
147
  // Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
92
148
  val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
93
149
 
150
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
94
151
  val headerImage = AppTheme.current.assets.headerBackground
95
- .takeUnless { options.headerType is HeaderType.Transparent }
152
+ .takeUnless { options.headerType is HeaderType.Transparent || useDuoGlass }
96
153
 
97
154
  Box(
98
- Modifier.height(AppStatusBar.current + HEADER_HEIGHT.dp)
155
+ Modifier.height(layout.headerHeight)
99
156
  .fillMaxWidth()
100
- .conditional(headerImage.isNullOrEmpty()) {
157
+ .then(if (useDuoGlass) Modifier.duoGlassBar() else Modifier)
158
+ .conditional(headerImage.isNullOrEmpty() && !useDuoGlass) {
101
159
  background(background.copy(alpha = animatedAlpha))
102
160
  }
103
161
  .conditional(IsShowBaseLineDebug) {
@@ -124,21 +182,34 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
124
182
  }
125
183
  }
126
184
  Row(
127
- modifier = Modifier.height(HEADER_HEIGHT.dp)
185
+ modifier = Modifier.height(layout.headerBarHeight)
128
186
  .fillMaxWidth()
129
- .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
+ ),
130
192
  verticalAlignment = Alignment.CenterVertically,
131
193
  horizontalArrangement = Arrangement.SpaceBetween
132
194
  ) {
133
- if(!options.hiddenBack) {
134
- BackButton(
135
- borderColor = headerColor.borderColor,
136
- backgroundButton = headerColor.backgroundButton,
137
- tintIconColor = headerColor.tintIconColor,
138
- onBackHandler = {
139
- navigator.onBackSafe { }
140
- }
141
- )
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
+ }
142
213
  Spacer(Modifier.width(Spacing.S))
143
214
  }
144
215
 
@@ -155,20 +226,42 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
155
226
  Box(Modifier.onGloballyPositioned {
156
227
  if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
157
228
  }){
158
- 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
+ }
159
242
  }
160
243
  }
161
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
+ }
162
253
  Box(
163
- modifier = Modifier.height(HEADER_HEIGHT.dp)
254
+ modifier = Modifier.height(layout.headerBarHeight)
164
255
  .fillMaxWidth()
165
- .padding(horizontal = Spacing.M),
256
+ .horizontalSafeAreaPadding()
257
+ .padding(horizontal = Spacing.M + centerInset),
166
258
  contentAlignment = Alignment.Center
167
259
  ) {
168
260
  HeaderContent(
169
261
  headerTitle = options.headerTitle,
170
262
  tintIconColor = titleColor,
171
263
  titlePosition = titlePosition,
264
+ centerWidthFraction = if (clearance != null) 1f else 0.5f,
172
265
  modifier = Modifier.fillMaxWidth()
173
266
  )
174
267
  }
@@ -202,6 +295,7 @@ internal fun HeaderContent(
202
295
  headerTitle: HeaderTitle,
203
296
  tintIconColor: Color,
204
297
  titlePosition: TitlePosition = TitlePosition.LEFT,
298
+ centerWidthFraction: Float = 0.5f,
205
299
  modifier: Modifier = Modifier,
206
300
  ){
207
301
  Box(
@@ -213,7 +307,7 @@ internal fun HeaderContent(
213
307
  HeaderTitle(
214
308
  title = headerTitle.title,
215
309
  color = tintIconColor,
216
- modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) 0.5f else 1f),
310
+ modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) centerWidthFraction else 1f),
217
311
  textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
218
312
  )
219
313
  }
@@ -325,6 +419,10 @@ data class HeaderBackProps(
325
419
 
326
420
  @InternalApi
327
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
+
328
426
  internal fun getHeaderColor(headerType: HeaderType, opacity: Float, tintColor: Color?, defaultColor: Color): HeaderColor{
329
427
  return if(headerType is HeaderType.Animated) {
330
428
  if (opacity == 1f || !headerType.isSurface)