@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
@@ -0,0 +1,98 @@
1
+ package vn.momo.kits.navigation.bottomtab
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.Stable
5
+ import androidx.compose.runtime.compositionLocalOf
6
+ import androidx.compose.runtime.getValue
7
+ import androidx.compose.runtime.mutableStateOf
8
+ import androidx.compose.runtime.remember
9
+ import androidx.compose.runtime.setValue
10
+ import androidx.compose.ui.unit.Dp
11
+ import vn.momo.kits.const.Spacing
12
+ import vn.momo.kits.layout.DuoMetrics
13
+ import vn.momo.kits.layout.ScreenLayout
14
+
15
+ // iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
16
+ enum class DuoBarCompressionBehavior { TOOLBAR, TAB_BAR }
17
+
18
+ private enum class DuoBar { TOOLBAR, TAB_BAR }
19
+
20
+ @Stable
21
+ internal class DuoBarCompression(
22
+ val toolbarCompressed: Boolean = false,
23
+ val tabBarCompressed: Boolean = false,
24
+ // the open rail needs the toolbar button slot too: only Back stays
25
+ val toolbarHidden: Boolean = false,
26
+ val expandToolbar: () -> Unit = {},
27
+ val expandTabBar: () -> Unit = {},
28
+ // a rail opened from the compressed state closes once a tab is picked
29
+ val onTabSelected: () -> Unit = {},
30
+ // side bar columns report their full height so the tab bar knows what is left
31
+ val setToolbarHeight: (Dp) -> Unit = {},
32
+ )
33
+
34
+ internal val LocalDuoBarCompression = compositionLocalOf { DuoBarCompression() }
35
+
36
+ // space kept between the top and the bottom groups (HIG Vertical controls)
37
+ internal val DUO_GROUP_GAP = Spacing.M
38
+ // Back, then the right toolkit: help + home capsule
39
+ private val DEFAULT_TOOLBAR_HEIGHT = DuoMetrics.controlSize + Spacing.M + DuoMetrics.itemPitch * 2
40
+ // Back, then the single button the toolbar folds into
41
+ private val COMPRESSED_TOOLBAR_HEIGHT = DuoMetrics.controlSize * 2 + Spacing.M
42
+ // Back alone, while a rail opened from the compressed state takes the toolbar slot
43
+ private val BACK_ONLY_HEIGHT = DuoMetrics.controlSize
44
+
45
+ // Port of RN DuoBarCompression (HIG, Designing for iPhone Duo: Vertical controls):
46
+ // - both fit: both stay whole
47
+ // - one fits whole beside the other compressed: one is compressed, tapping it swaps them
48
+ // - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
49
+ // again once a tab is picked)
50
+ @Composable
51
+ internal fun rememberDuoBarCompression(
52
+ behavior: DuoBarCompressionBehavior,
53
+ tabCount: Int,
54
+ hasQr: Boolean,
55
+ layout: ScreenLayout,
56
+ ): DuoBarCompression {
57
+ var toolbarHeight by remember { mutableStateOf(DEFAULT_TOOLBAR_HEIGHT) }
58
+ var expanded by remember(behavior) {
59
+ mutableStateOf(if (behavior == DuoBarCompressionBehavior.TOOLBAR) DuoBar.TAB_BAR else DuoBar.TOOLBAR)
60
+ }
61
+ var opened by remember { mutableStateOf<DuoBar?>(null) }
62
+ val setToolbarHeight = remember { { height: Dp -> toolbarHeight = height } }
63
+ val sideBar = layout.sideBar
64
+
65
+ return remember(sideBar, layout.height, tabCount, hasQr, toolbarHeight, expanded, opened) {
66
+ if (sideBar == null) return@remember DuoBarCompression()
67
+ val room = layout.height - sideBar.bottom - sideBar.top - DUO_GROUP_GAP
68
+ val tabBar = duoRailHeight(tabCount, hasQr)
69
+ val compressedTabBar = duoRailHeight(1, hasQr, compressed = true)
70
+ fun fits(toolbar: Dp, tabs: Dp) = toolbar + tabs <= room
71
+
72
+ when {
73
+ fits(toolbarHeight, tabBar) -> DuoBarCompression(setToolbarHeight = setToolbarHeight)
74
+ fits(COMPRESSED_TOOLBAR_HEIGHT, tabBar) && fits(toolbarHeight, compressedTabBar) -> DuoBarCompression(
75
+ toolbarCompressed = expanded == DuoBar.TAB_BAR,
76
+ tabBarCompressed = expanded == DuoBar.TOOLBAR,
77
+ expandToolbar = { expanded = DuoBar.TOOLBAR },
78
+ expandTabBar = { expanded = DuoBar.TAB_BAR },
79
+ setToolbarHeight = setToolbarHeight,
80
+ )
81
+ opened == DuoBar.TAB_BAR && fits(BACK_ONLY_HEIGHT, tabBar) -> DuoBarCompression(
82
+ toolbarCompressed = true,
83
+ toolbarHidden = true,
84
+ expandToolbar = { opened = DuoBar.TOOLBAR },
85
+ expandTabBar = { opened = DuoBar.TAB_BAR },
86
+ onTabSelected = { opened = null },
87
+ setToolbarHeight = setToolbarHeight,
88
+ )
89
+ else -> DuoBarCompression(
90
+ toolbarCompressed = opened != DuoBar.TOOLBAR,
91
+ tabBarCompressed = true,
92
+ expandToolbar = { opened = DuoBar.TOOLBAR },
93
+ expandTabBar = { opened = DuoBar.TAB_BAR },
94
+ setToolbarHeight = setToolbarHeight,
95
+ )
96
+ }
97
+ }
98
+ }
@@ -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
+ }
@@ -22,6 +22,8 @@ import androidx.compose.runtime.getValue
22
22
  import androidx.compose.ui.Alignment
23
23
  import androidx.compose.ui.Modifier
24
24
  import androidx.compose.ui.draw.clipToBounds
25
+ import androidx.compose.ui.draw.drawBehind
26
+ import androidx.compose.ui.geometry.Offset
25
27
  import androidx.compose.ui.graphics.graphicsLayer
26
28
  import androidx.compose.ui.graphics.Brush
27
29
  import androidx.compose.ui.graphics.Color
@@ -33,12 +35,12 @@ import androidx.compose.ui.unit.dp
33
35
  import vn.momo.kits.components.Icon
34
36
  import vn.momo.kits.components.Image
35
37
  import vn.momo.kits.components.InputSearchProps
36
- import vn.momo.kits.const.AppStatusBar
37
38
  import vn.momo.kits.const.AppTheme
38
39
  import vn.momo.kits.const.Colors
39
40
  import vn.momo.kits.components.PopupNotifyProps
40
41
  import vn.momo.kits.application.IsShowBaseLineDebug
41
42
  import vn.momo.kits.const.Spacing
43
+ import vn.momo.kits.layout.getScreenLayout
42
44
  import vn.momo.kits.modifier.activeOpacityClickable
43
45
  import vn.momo.kits.modifier.conditional
44
46
  import vn.momo.kits.modifier.setAutomationId
@@ -47,6 +49,7 @@ import vn.momo.kits.navigation.LocalNavigator
47
49
  import vn.momo.kits.navigation.LocalOptions
48
50
  import vn.momo.kits.navigation.LocalScrollState
49
51
  import vn.momo.kits.navigation.getInputSearchType
52
+ import vn.momo.kits.navigation.usesDuoGlassHeader
50
53
  import vn.momo.kits.utils.horizontalSafeAreaPadding
51
54
 
52
55
  @InternalApi
@@ -54,27 +57,67 @@ const val HEADER_HEIGHT = 52
54
57
  internal enum class InputSearchType { None, Header, Animated }
55
58
  enum class TitlePosition { LEFT, CENTER }
56
59
 
60
+ private val HEADER_ACTIONS_WIDTH = 172.dp
61
+
57
62
  @Suppress("FrequentlyChangingValue")
58
63
  @Composable
59
- internal fun Header(onBackHandler: (() -> Unit)? = null) {
64
+ internal fun headerOpacity(): Float {
60
65
  val options = LocalOptions.current
61
- val navigator = LocalNavigator.current
62
66
  val scrollState = LocalScrollState.current
63
- val headerRightWidthPx = LocalHeaderRightWidthPx.current
64
- val inputSearchType = getInputSearchType(options)
65
-
66
67
  val opacityHeight = when (val header = options.headerType) {
67
68
  is HeaderType.Animated -> with(LocalDensity.current) { header.layoutOffSet.roundToPx() }
68
69
  else -> HEADER_HEIGHT
69
70
  }
70
71
  val opacity by animateFloatAsState(
71
- targetValue = if (options.headerType is HeaderType.Transparent) {
72
- 0f
73
- } else {
74
- (scrollState.value / opacityHeight.toFloat())
75
- .coerceIn(0f, 1f)
72
+ targetValue = when {
73
+ options.headerType is HeaderType.Transparent -> 0f
74
+ opacityHeight <= 0 -> if (scrollState.value > 0) 1f else 0f
75
+ else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
76
76
  }
77
77
  )
78
+ return opacity
79
+ }
80
+
81
+ // Back button and toolkit share the bar with the title unless the iPhone Duo side bar holds them.
82
+ @Composable
83
+ internal fun headerTitleMaxWidth(): Dp {
84
+ val layout = getScreenLayout()
85
+ if (layout.sideBar != null) return layout.contentWidth - Spacing.M * 2
86
+ return layout.width - HEADER_ACTIONS_WIDTH
87
+ }
88
+
89
+ // Scroll distance over which the Duo glass bar fades in.
90
+ private val DUO_GLASS_FADE_DISTANCE = 16.dp
91
+
92
+ // RN DuoGlassBackground fallback: translucent surface with a hairline once content scrolls under it.
93
+ @Suppress("FrequentlyChangingValue")
94
+ @Composable
95
+ private fun Modifier.duoGlassBar(): Modifier {
96
+ val scrollState = LocalScrollState.current
97
+ val fadePx = with(LocalDensity.current) { DUO_GLASS_FADE_DISTANCE.toPx() }
98
+ val alpha by animateFloatAsState((scrollState.value / fadePx).coerceIn(0f, 1f), label = "DuoGlassAlpha")
99
+ val colors = AppTheme.current.colors
100
+ return background(colors.background.surface.copy(alpha = 0.8f * alpha))
101
+ .drawBehind {
102
+ val stroke = 0.5.dp.toPx()
103
+ drawLine(
104
+ color = colors.border.default.copy(alpha = alpha),
105
+ start = Offset(0f, size.height - stroke / 2),
106
+ end = Offset(size.width, size.height - stroke / 2),
107
+ strokeWidth = stroke,
108
+ )
109
+ }
110
+ }
111
+
112
+ @Composable
113
+ internal fun Header(onBackHandler: (() -> Unit)? = null) {
114
+ val options = LocalOptions.current
115
+ val navigator = LocalNavigator.current
116
+ val headerRightWidthPx = LocalHeaderRightWidthPx.current
117
+ val inputSearchType = getInputSearchType(options)
118
+ val layout = getScreenLayout()
119
+ val useSideBar = layout.sideBar != null
120
+ val opacity = headerOpacity()
78
121
 
79
122
  val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
80
123
 
@@ -93,13 +136,15 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
93
136
  // Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
94
137
  val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
95
138
 
139
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
96
140
  val headerImage = AppTheme.current.assets.headerBackground
97
- .takeUnless { options.headerType is HeaderType.Transparent }
141
+ .takeUnless { options.headerType is HeaderType.Transparent || useDuoGlass }
98
142
 
99
143
  Box(
100
- Modifier.height(AppStatusBar.current + HEADER_HEIGHT.dp)
144
+ Modifier.height(layout.headerHeight)
101
145
  .fillMaxWidth()
102
- .conditional(headerImage.isNullOrEmpty()) {
146
+ .then(if (useDuoGlass) Modifier.duoGlassBar() else Modifier)
147
+ .conditional(headerImage.isNullOrEmpty() && !useDuoGlass) {
103
148
  background(background.copy(alpha = animatedAlpha))
104
149
  }
105
150
  .conditional(IsShowBaseLineDebug) {
@@ -126,14 +171,14 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
126
171
  }
127
172
  }
128
173
  Row(
129
- modifier = Modifier.height(HEADER_HEIGHT.dp)
174
+ modifier = Modifier.height(layout.headerBarHeight)
130
175
  .fillMaxWidth()
131
176
  .horizontalSafeAreaPadding()
132
177
  .padding(horizontal = Spacing.M),
133
178
  verticalAlignment = Alignment.CenterVertically,
134
179
  horizontalArrangement = Arrangement.SpaceBetween
135
180
  ) {
136
- if(!options.hiddenBack) {
181
+ if (!options.hiddenBack && !useSideBar) {
137
182
  BackButton(
138
183
  borderColor = headerColor.borderColor,
139
184
  backgroundButton = headerColor.backgroundButton,
@@ -158,13 +203,16 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
158
203
  Box(Modifier.onGloballyPositioned {
159
204
  if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
160
205
  }){
161
- HeaderRight(options.headerRight, options.tintColor, headerColor)
206
+ if (!(useSideBar && options.headerRight is HeaderRight.Toolkit)) {
207
+ HeaderRight(options.headerRight, options.tintColor, headerColor)
208
+ }
162
209
  }
163
210
  }
164
211
  if (centerTitle) {
165
212
  Box(
166
- modifier = Modifier.height(HEADER_HEIGHT.dp)
213
+ modifier = Modifier.height(layout.headerBarHeight)
167
214
  .fillMaxWidth()
215
+ .horizontalSafeAreaPadding()
168
216
  .padding(horizontal = Spacing.M),
169
217
  contentAlignment = Alignment.Center
170
218
  ) {
@@ -3,23 +3,26 @@ 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.usesDuoGlassHeader
23
26
 
24
27
  enum class AnimatedHeaderRatio(val value: Float){
25
28
  RATIO_16_9(16f / 9f),
@@ -29,6 +32,9 @@ enum class AnimatedHeaderRatio(val value: Float){
29
32
 
30
33
  internal val HEADER_BACKGROUND_HEIGHT = 154.dp
31
34
 
35
+ // RN DuoScrollGradient: extended gradient runs this far below the header
36
+ private val DUO_GRADIENT_EXTRA = 52.dp
37
+
32
38
  @Suppress("FrequentlyChangingValue")
33
39
  @Composable
34
40
  internal fun HeaderBackground() {
@@ -38,7 +44,8 @@ internal fun HeaderBackground() {
38
44
  val scrollState = LocalScrollState.current
39
45
  val backgroundColor = options.backgroundColor ?: AppTheme.current.colors.background.default
40
46
 
41
- val minHeight = AppStatusBar.current + HEADER_HEIGHT.dp
47
+ val layout = getScreenLayout()
48
+ val minHeight = layout.headerHeight
42
49
  val maxHeight = HEADER_BACKGROUND_HEIGHT
43
50
  val opacity by animateFloatAsState(
44
51
  targetValue = (1 - (scrollState.value * 1f / HEADER_HEIGHT * 1f)).coerceIn(0f, 1f),
@@ -57,20 +64,22 @@ internal fun HeaderBackground() {
57
64
  else -> 0.dp
58
65
  }
59
66
 
60
- Box(Modifier.fillMaxWidth().height(height).background(backgroundColor)) {
67
+ // iPhone Duo glass header: the gradient leaves with the content, as the quick return header does
68
+ val useDuoGlass = options.usesDuoGlassHeader(layout)
69
+ val duoModifier = if (useDuoGlass) {
70
+ val extra = if (options.headerType is HeaderType.Extended) DUO_GRADIENT_EXTRA else 0.dp
71
+ Modifier.height(minHeight + extra).graphicsLayer { translationY = -scrollState.value.toFloat() }
72
+ } else {
73
+ Modifier.height(height)
74
+ }
75
+
76
+ Box(Modifier.fillMaxWidth().then(duoModifier).clipToBounds().background(backgroundColor)) {
77
+ // on Duo the fade spans the gradient box; a banner image keeps its 375:154 shape, clipped
61
78
  Box(Modifier
62
- .height(maxHeight)
63
79
  .fillMaxWidth()
80
+ .then(if (useDuoGlass && !theme.isHeaderImage()) Modifier.fillMaxHeight() else Modifier.height(maxHeight))
64
81
  .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
- )
82
+ background(Brush.verticalGradient(colors = listOf(color, backgroundColor)))
74
83
  }
75
84
  ){
76
85
  if (!theme.isHeaderImage()) return@Box
@@ -23,7 +23,6 @@ import vn.momo.kits.components.Text
23
23
  import vn.momo.kits.const.Spacing
24
24
  import vn.momo.kits.const.Typography
25
25
  import vn.momo.kits.modifier.activeOpacityClickable
26
- import vn.momo.kits.platform.getScreenDimensions
27
26
 
28
27
  @Composable
29
28
  internal fun HeaderJourney(
@@ -33,7 +32,7 @@ internal fun HeaderJourney(
33
32
  if (data !is HeaderTitle.Journey) return
34
33
  if (data.isLoading) return TitleJourneyShimmer()
35
34
 
36
- val maxWidth = getScreenDimensions().width.dp - 172.dp
35
+ val maxWidth = headerTitleMaxWidth()
37
36
  val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
38
37
  val iconTint = parseHeaderTint(data.iconColor) ?: tint
39
38
 
@@ -98,7 +97,8 @@ internal fun parseHeaderTint(value: String?): Color? {
98
97
  val hex = when (raw.length) {
99
98
  3 -> "FF" + raw.map { "$it$it" }.joinToString("")
100
99
  6 -> "FF$raw"
101
- 8 -> raw
100
+ // CSS #RRGGBBAA, as RN passes it; Compose wants AARRGGBB
101
+ 8 -> raw.substring(6) + raw.substring(0, 6)
102
102
  else -> return null
103
103
  }
104
104
  val v = hex.toLongOrNull(16) ?: return null