@momo-kits/native-kits 0.162.44-debug → 0.162.45-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 (199) 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 +2 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +2 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +314 -0
  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/Navigation.kt +10 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +127 -83
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +116 -26
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle/libs.versions.toml +1 -1
  61. package/gradle.properties +1 -1
  62. package/ios/Application/ApplicationEnvironment.swift +17 -0
  63. package/ios/Application/MaxApi.swift +30 -3
  64. package/ios/Avatar/Avatar.swift +4 -3
  65. package/ios/Badge/Badge.swift +6 -4
  66. package/ios/Badge/BadgeDot.swift +1 -1
  67. package/ios/Badge/BadgeRibbon.swift +0 -1
  68. package/ios/Button/Button.swift +15 -12
  69. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  70. package/ios/Carousel/Carousel.swift +29 -12
  71. package/ios/Checkbox/Checkbox.swift +7 -3
  72. package/ios/Chip/Chip.swift +5 -4
  73. package/ios/Collapse/Collapse.swift +70 -19
  74. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  75. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  76. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  77. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  78. package/ios/Divider/Divider.swift +37 -4
  79. package/ios/Extensions/Color++.swift +38 -9
  80. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  81. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  82. package/ios/Icon/Icon.swift +21 -23
  83. package/ios/Icon/Icons.swift +1329 -0
  84. package/ios/IconButton/IconButton.swift +15 -7
  85. package/ios/Image/Image.swift +7 -23
  86. package/ios/Information/Information.swift +27 -9
  87. package/ios/Input/ErrorView.swift +2 -2
  88. package/ios/Input/Input.swift +160 -147
  89. package/ios/Input/InputPhoneNumber.swift +12 -9
  90. package/ios/Input/InputSearch.swift +107 -77
  91. package/ios/Input/InputTextArea.swift +128 -145
  92. package/ios/InputDropDown/InputDropDown.swift +10 -7
  93. package/ios/InputMoney/InputMoney.swift +33 -11
  94. package/ios/InputOTP/InputOTP.swift +22 -12
  95. package/ios/Layout/Card.swift +50 -0
  96. package/ios/Layout/GridContext.swift +187 -3
  97. package/ios/Layout/Item.swift +5 -4
  98. package/ios/Layout/ScreenLayout.swift +385 -0
  99. package/ios/Layout/Section.swift +4 -100
  100. package/ios/Loader/Loader.swift +5 -9
  101. package/ios/Lottie/LottieView.swift +14 -2
  102. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  103. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  104. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  105. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  106. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  107. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  108. package/ios/Navigation/Component/Header.swift +545 -0
  109. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  110. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  111. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  112. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  113. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  114. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  115. package/ios/Navigation/Component/SideBar.swift +183 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  117. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  118. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  119. package/ios/Navigation/Navigation.swift +204 -0
  120. package/ios/Navigation/NavigationContainer.swift +195 -0
  121. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  122. package/ios/Navigation/StackScreen.swift +930 -0
  123. package/ios/Navigation/StackTransition.swift +400 -0
  124. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  125. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  126. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  127. package/ios/Pagination/PaginationScroll.swift +2 -2
  128. package/ios/Popup/PopupDisplay.swift +72 -135
  129. package/ios/Popup/PopupInput.swift +10 -13
  130. package/ios/Popup/PopupLayout.swift +140 -0
  131. package/ios/Popup/PopupNotify.swift +59 -69
  132. package/ios/Popup/PopupPromotion.swift +31 -47
  133. package/ios/PopupView/FullscreenPopup.swift +13 -19
  134. package/ios/PopupView/PopupView.swift +1 -1
  135. package/ios/PopupView/Utils++.swift +0 -8
  136. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  137. package/ios/Radio/Radio.swift +2 -1
  138. package/ios/Rating/Rating.swift +9 -1
  139. package/ios/Resources/dot_loading.json +558 -0
  140. package/ios/Resources/lottie_circle_loader.json +1 -0
  141. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  142. package/ios/Skeleton/Skeleton.swift +22 -27
  143. package/ios/Slider/Slider.swift +16 -18
  144. package/ios/Stepper/Stepper.swift +6 -1
  145. package/ios/Steps/Steps.swift +8 -6
  146. package/ios/SuggestAction/SuggestAction.swift +5 -0
  147. package/ios/Swipeable/SwipeCell.swift +10 -6
  148. package/ios/Switch/Switch.swift +22 -20
  149. package/ios/TabView/TabView.swift +102 -21
  150. package/ios/Tag/Tag.swift +15 -4
  151. package/ios/Template/Logo/Logo.swift +2 -1
  152. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  153. package/ios/Title/Title.swift +6 -17
  154. package/ios/Tooltip/Tooltip.swift +29 -14
  155. package/ios/Typography/FontScale.swift +3 -3
  156. package/ios/Typography/Text.swift +81 -19
  157. package/ios/Uploader/Uploader.swift +3 -1
  158. package/ios/native-kits.podspec +2 -1
  159. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  163. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  164. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  165. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  166. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  167. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  168. package/package.json +1 -1
  169. package/scripts/gen-ios-icons.mjs +36 -0
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  177. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  178. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  182. package/ios/Application/Components.swift +0 -270
  183. package/ios/Application/ComposeApi.swift +0 -22
  184. package/ios/Application/HeaderRight.swift +0 -306
  185. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  186. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  187. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  188. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  189. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  190. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  191. package/ios/Application/Navigation/component/Header.swift +0 -178
  192. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  193. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  194. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  195. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  196. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  197. package/ios/Application/Screen.swift +0 -250
  198. package/ios/Application/StackScreen.swift +0 -492
  199. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -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
@@ -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
@@ -22,7 +22,6 @@ import vn.momo.kits.components.Text
22
22
  import vn.momo.kits.const.Spacing
23
23
  import vn.momo.kits.const.Typography
24
24
  import vn.momo.kits.modifier.activeOpacityClickable
25
- import vn.momo.kits.platform.getScreenDimensions
26
25
 
27
26
  @Composable
28
27
  internal fun HeaderLocation(
@@ -32,7 +31,7 @@ internal fun HeaderLocation(
32
31
  if (data !is HeaderTitle.Location) return
33
32
  if (data.isLoading) return TitleLocationShimmer()
34
33
 
35
- val maxWidth = getScreenDimensions().width.dp - 172.dp
34
+ val maxWidth = headerTitleMaxWidth()
36
35
  val tint = parseHeaderTint(data.tintColor) ?: tintIconColor
37
36
 
38
37
  Column(
@@ -6,6 +6,7 @@ import androidx.compose.foundation.background
6
6
  import androidx.compose.foundation.border
7
7
  import androidx.compose.foundation.layout.Arrangement
8
8
  import androidx.compose.foundation.layout.Box
9
+ import androidx.compose.foundation.layout.Column
9
10
  import androidx.compose.foundation.layout.Row
10
11
  import androidx.compose.foundation.layout.height
11
12
  import androidx.compose.foundation.layout.offset
@@ -25,7 +26,9 @@ import androidx.compose.ui.Alignment
25
26
  import androidx.compose.ui.Modifier
26
27
  import androidx.compose.ui.draw.clip
27
28
  import androidx.compose.ui.graphics.Color
29
+ import androidx.compose.ui.unit.Dp
28
30
  import androidx.compose.ui.unit.dp
31
+ import vn.momo.kits.layout.DuoMetrics
29
32
  import vn.momo.kits.application.LocalContext
30
33
  import vn.momo.kits.application.MiniAppContext
31
34
  import vn.momo.kits.components.BadgeDot
@@ -59,11 +62,32 @@ fun HeaderRight(
59
62
  }
60
63
  }
61
64
 
65
+ private const val HELP_CENTER_APP_ID = "vn.momo.helpcenter"
66
+
67
+ private fun usesHelpCenter(context: MiniAppContext?): Boolean =
68
+ context != null && context.appId != HELP_CENTER_APP_ID
69
+
70
+ private fun showsShortcut(headerRight: HeaderRight.Toolkit, context: MiniAppContext?): Boolean =
71
+ headerRight.useShortcut && !(headerRight.useMore && context == null)
72
+
73
+ // Height of the vertical toolkit: the optional shortcut, then the help/home capsule.
74
+ @Composable
75
+ internal fun toolkitColumnHeight(headerRight: HeaderRight.Toolkit): Dp {
76
+ val context = LocalContext.current
77
+ val shortcut = if (showsShortcut(headerRight, context)) DuoMetrics.controlSize + Spacing.M else 0.dp
78
+ val groupCount = if (usesHelpCenter(context)) 2 else 1
79
+ return shortcut + if (groupCount > 1) DuoMetrics.itemPitch * groupCount else DuoMetrics.controlSize
80
+ }
81
+
82
+ // SideBarColumn: the iPhone Duo side bar; SideBarRow: the Duo header row on the status bar, same 48pt controls.
83
+ internal enum class ToolkitLayout { Bar, SideBarColumn, SideBarRow }
84
+
62
85
  @Composable
63
86
  internal fun Toolkit(
64
87
  headerRight: HeaderRight.Toolkit,
65
88
  tintColor: Color? = null,
66
- headerColor: HeaderColor
89
+ headerColor: HeaderColor,
90
+ layout: ToolkitLayout = ToolkitLayout.Bar,
67
91
  ) {
68
92
  val api = LocalMaxApi.current
69
93
  val context = LocalContext.current
@@ -103,6 +127,26 @@ internal fun Toolkit(
103
127
  }
104
128
  }
105
129
 
130
+ fun onPressHelpCenter() {
131
+ if (context == null) return
132
+ api?.showHelpCenter(
133
+ mapOf(
134
+ "appId" to context.appId,
135
+ "code" to context.appCode,
136
+ "name" to context.appName,
137
+ "icon" to context.appIcon,
138
+ "description" to context.description
139
+ )
140
+ ) {}
141
+ }
142
+
143
+ fun onPressClose() {
144
+ if (headerRight.useCloseIcon) api?.dismiss { } else api?.dismissAll { }
145
+ }
146
+
147
+ val useHelpCenter = usesHelpCenter(context)
148
+ val closeIcon = if (headerRight.useCloseIcon) "16_navigation_close_circle" else "16_basic_home"
149
+
106
150
  val navButtonConfig = getNavigationButtonConfig(
107
151
  headerRight,
108
152
  ::onPressShortcut,
@@ -113,12 +157,7 @@ internal fun Toolkit(
113
157
  group.items.any { it.showBadge }
114
158
  }
115
159
 
116
- val isShowShortcut by remember(headerRight.useShortcut, headerRight.useMore, context) {
117
- derivedStateOf {
118
- (headerRight.useShortcut == true) &&
119
- !(headerRight.useMore == true && context == null)
120
- }
121
- }
160
+ val isShowShortcut = showsShortcut(headerRight, context)
122
161
 
123
162
  val icon by remember(navButtonConfig.icon, isFavorite) {
124
163
  derivedStateOf {
@@ -127,6 +166,47 @@ internal fun Toolkit(
127
166
  }
128
167
  }
129
168
 
169
+ if (layout != ToolkitLayout.Bar) {
170
+ val horizontal = layout == ToolkitLayout.SideBarRow
171
+ val controls = @Composable {
172
+ if (isShowShortcut) {
173
+ SideBarButton(
174
+ icon = icon,
175
+ headerColor = headerColor,
176
+ enabled = !isLoading,
177
+ showBadge = showBadge,
178
+ onClick = navButtonConfig.onPress,
179
+ )
180
+ }
181
+ SideBarGroup(headerColor = headerColor, itemCount = if (useHelpCenter) 2 else 1, horizontal = horizontal) { item ->
182
+ if (useHelpCenter) {
183
+ SideBarGroupItem(
184
+ icon = "help_center",
185
+ headerColor = headerColor,
186
+ automationId = "btn_navigation_help_center",
187
+ onClick = ::onPressHelpCenter,
188
+ modifier = item,
189
+ )
190
+ }
191
+ SideBarGroupItem(
192
+ icon = closeIcon,
193
+ headerColor = headerColor,
194
+ automationId = "btn_navigation_close",
195
+ onClick = ::onPressClose,
196
+ modifier = item,
197
+ )
198
+ }
199
+ }
200
+ if (horizontal) {
201
+ Row(horizontalArrangement = Arrangement.spacedBy(Spacing.M), verticalAlignment = Alignment.CenterVertically) {
202
+ controls()
203
+ }
204
+ } else {
205
+ Column(verticalArrangement = Arrangement.spacedBy(Spacing.M)) { controls() }
206
+ }
207
+ return
208
+ }
209
+
130
210
  Row(
131
211
  verticalAlignment = Alignment.CenterVertically
132
212
  ) {
@@ -150,22 +230,12 @@ internal fun Toolkit(
150
230
  .clip(shape = RoundedCornerShape(14.dp))
151
231
  .background(headerColor.backgroundButton)
152
232
  ) {
153
- if (context != null) {
233
+ if (useHelpCenter) {
154
234
  Icon(
155
235
  source = "help_center",
156
236
  size = 20.dp,
157
237
  color = headerColor.tintIconColor,
158
- modifier = Modifier.padding(4.dp).activeOpacityClickable {
159
- api?.showHelpCenter(
160
- mapOf(
161
- "appId" to context.appId,
162
- "code" to context.appCode,
163
- "name" to context.appName,
164
- "icon" to context.appIcon,
165
- "description" to context.description
166
- )
167
- ) {}
168
- }
238
+ modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressHelpCenter)
169
239
  )
170
240
  Box(
171
241
  modifier = Modifier
@@ -175,12 +245,10 @@ internal fun Toolkit(
175
245
  )
176
246
  }
177
247
  Icon(
178
- source = "16_basic_home",
248
+ source = closeIcon,
179
249
  size = 20.dp,
180
250
  color = headerColor.tintIconColor,
181
- modifier = Modifier.padding(4.dp).activeOpacityClickable {
182
- api?.dismissAll { }
183
- }
251
+ modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressClose)
184
252
  )
185
253
  }
186
254
  }
@@ -188,16 +256,36 @@ internal fun Toolkit(
188
256
 
189
257
 
190
258
 
259
+ // Colors follow the enclosing header, so it fits a `HeaderRight.Custom` next to the toolkit.
260
+ @Composable
261
+ fun NavigationButton(
262
+ icon: String,
263
+ onClick: () -> Unit,
264
+ showBadge: Boolean = false,
265
+ disabled: Boolean = false,
266
+ modifier: Modifier = Modifier,
267
+ ) {
268
+ NavigationButton(
269
+ disabled = disabled,
270
+ icon = icon,
271
+ showBadge = showBadge,
272
+ onClick = onClick,
273
+ headerColor = LocalHeaderColor.current ?: getHeaderColor(HeaderType.Default(), 1f, null, Colors.black_17),
274
+ modifier = modifier,
275
+ )
276
+ }
277
+
191
278
  @Composable
192
279
  internal fun NavigationButton(
193
280
  disabled: Boolean,
194
281
  icon: String,
195
282
  showBadge: Boolean? = false,
196
283
  onClick: () -> Unit,
197
- headerColor: HeaderColor
284
+ headerColor: HeaderColor,
285
+ modifier: Modifier = Modifier,
198
286
  ) {
199
287
  Box(
200
- modifier = Modifier
288
+ modifier = modifier
201
289
  .size(28.dp)
202
290
  .activeOpacityClickable(enabled = !disabled, onClick = onClick)
203
291
  ) {
@@ -270,7 +358,9 @@ sealed interface HeaderRight {
270
358
  val useMore: Boolean = false,
271
359
  val useSystemTools: Boolean = true,
272
360
  val tools: List<ToolGroup> = emptyList(),
273
- val toolCallback: ((String) -> Unit)? = { _ -> }
361
+ val toolCallback: ((String) -> Unit)? = { _ -> },
362
+ // Close dismisses only this mini app instead of returning home.
363
+ val useCloseIcon: Boolean = false,
274
364
  ) : HeaderRight
275
365
  }
276
366
 
@@ -32,7 +32,6 @@ import vn.momo.kits.const.Typography
32
32
  import vn.momo.kits.const.getFontFamily
33
33
  import vn.momo.kits.modifier.activeOpacityClickable
34
34
  import vn.momo.kits.navigation.component.HeaderTitle.User
35
- import vn.momo.kits.platform.getScreenDimensions
36
35
 
37
36
  @Composable
38
37
  internal fun HeaderUser(
@@ -44,7 +43,7 @@ internal fun HeaderUser(
44
43
  return TitleUserShimmer()
45
44
  }
46
45
 
47
- val maxWidth = getScreenDimensions().width.dp - 172.dp
46
+ val maxWidth = headerTitleMaxWidth()
48
47
 
49
48
  Row(
50
49
  modifier = Modifier
@@ -0,0 +1,192 @@
1
+ package vn.momo.kits.navigation.component
2
+
3
+ import androidx.compose.foundation.background
4
+ import androidx.compose.foundation.border
5
+ import androidx.compose.foundation.layout.Arrangement
6
+ import androidx.compose.foundation.layout.Box
7
+ import androidx.compose.foundation.layout.BoxScope
8
+ import androidx.compose.foundation.layout.Column
9
+ import androidx.compose.foundation.layout.Row
10
+ import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.absolutePadding
12
+ import androidx.compose.foundation.layout.offset
13
+ import androidx.compose.foundation.layout.size
14
+ import androidx.compose.foundation.layout.width
15
+ import androidx.compose.foundation.shape.CircleShape
16
+ import androidx.compose.foundation.shape.RoundedCornerShape
17
+ import androidx.compose.runtime.Composable
18
+ import androidx.compose.runtime.LaunchedEffect
19
+ import androidx.compose.ui.AbsoluteAlignment
20
+ import androidx.compose.ui.Alignment
21
+ import androidx.compose.ui.Modifier
22
+ import androidx.compose.ui.draw.clip
23
+ import androidx.compose.ui.draw.shadow
24
+ import androidx.compose.ui.graphics.Shape
25
+ import androidx.compose.ui.layout.layout
26
+ import androidx.compose.ui.unit.Dp
27
+ import androidx.compose.ui.unit.constrainHeight
28
+ import androidx.compose.ui.unit.dp
29
+ import vn.momo.kits.components.BadgeDot
30
+ import vn.momo.kits.components.DotSize
31
+ import vn.momo.kits.components.Icon
32
+ import vn.momo.kits.const.Colors
33
+ import vn.momo.kits.const.Radius
34
+ import vn.momo.kits.const.Spacing
35
+ import vn.momo.kits.layout.DuoMetrics
36
+ import vn.momo.kits.layout.SideBarEdge
37
+ import vn.momo.kits.layout.getScreenLayout
38
+ import vn.momo.kits.modifier.activeOpacityClickable
39
+ import vn.momo.kits.modifier.conditional
40
+ import vn.momo.kits.modifier.setAutomationId
41
+ import vn.momo.kits.navigation.LocalNavigator
42
+ import vn.momo.kits.navigation.LocalOptions
43
+ import vn.momo.kits.navigation.bottomtab.LocalDuoBarCompression
44
+
45
+ private val SIDE_BAR_ICON_SIZE = 24.dp
46
+
47
+ // iPhone Duo: back and toolkit leave the bar for a column in the side safe area, as the system bars do.
48
+ // bottomInset is what footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
49
+ @Composable
50
+ internal fun BoxScope.HeaderSideBar(bottomInset: Dp, modifier: Modifier = Modifier) {
51
+ val options = LocalOptions.current
52
+ val navigator = LocalNavigator.current
53
+ val screenLayout = getScreenLayout()
54
+ val sideBar = screenLayout.sideBar ?: return
55
+ val toolkit = options.headerRight as? HeaderRight.Toolkit
56
+ val empty = options.headerType == HeaderType.None || (options.hiddenBack && toolkit == null)
57
+ val compression = LocalDuoBarCompression.current
58
+ val fullHeight = if (empty) 0.dp else DuoMetrics.controlSize + (toolkit?.let { Spacing.M + toolkitColumnHeight(it) } ?: 0.dp)
59
+ LaunchedEffect(fullHeight, compression.setToolbarHeight) { compression.setToolbarHeight(fullHeight) }
60
+ if (empty) return
61
+
62
+ val headerColor = getHeaderColor(options.headerType, headerOpacity(), options.tintColor, Colors.black_17)
63
+ val isRight = sideBar.edge == SideBarEdge.RIGHT
64
+
65
+ Column(
66
+ modifier = modifier
67
+ .align(if (isRight) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft)
68
+ .width(sideBar.width)
69
+ .layout { measurable, constraints ->
70
+ val placeable = measurable.measure(constraints.copy(minHeight = 0))
71
+ val maxY = screenLayout.height.roundToPx() - bottomInset.roundToPx() - placeable.height
72
+ val y = minOf(sideBar.top.roundToPx(), maxY).coerceAtLeast(0)
73
+ layout(placeable.width, constraints.constrainHeight(y + placeable.height)) {
74
+ placeable.place(0, y)
75
+ }
76
+ }
77
+ .absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
78
+ horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
79
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
80
+ ) {
81
+ if (options.hiddenBack) {
82
+ // keep the slot so the toolkit does not jump between root and pushed screens
83
+ Spacer(Modifier.size(DuoMetrics.controlSize))
84
+ } else {
85
+ SideBarButton(
86
+ icon = "arrow-back",
87
+ headerColor = headerColor,
88
+ automationId = "btn_navigation_back",
89
+ onClick = { navigator.onBackSafe { } },
90
+ )
91
+ }
92
+ when {
93
+ toolkit == null || compression.toolbarHidden -> Unit
94
+ // HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
95
+ compression.toolbarCompressed -> SideBarButton(
96
+ icon = "navigation_menu",
97
+ headerColor = headerColor,
98
+ showBadge = toolkit.tools.any { group -> group.items.any { it.showBadge } },
99
+ automationId = "btn_navigation_toolbar",
100
+ onClick = compression.expandToolbar,
101
+ )
102
+ else -> Toolkit(
103
+ headerRight = toolkit,
104
+ tintColor = options.tintColor,
105
+ headerColor = headerColor,
106
+ layout = ToolkitLayout.SideBarColumn,
107
+ )
108
+ }
109
+ }
110
+ }
111
+
112
+ @Composable
113
+ internal fun SideBarButton(
114
+ icon: String,
115
+ headerColor: HeaderColor,
116
+ onClick: () -> Unit,
117
+ enabled: Boolean = true,
118
+ showBadge: Boolean = false,
119
+ automationId: String? = null,
120
+ ) {
121
+ Box {
122
+ Box(
123
+ modifier = Modifier
124
+ .size(DuoMetrics.controlSize)
125
+ .sideBarGlass(headerColor, CircleShape)
126
+ .activeOpacityClickable(enabled = enabled, onClick = onClick)
127
+ .conditional(automationId != null) { setAutomationId(automationId.orEmpty()) },
128
+ contentAlignment = Alignment.Center,
129
+ ) {
130
+ Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
131
+ }
132
+ if (showBadge) {
133
+ BadgeDot(
134
+ size = DotSize.Small,
135
+ modifier = Modifier.align(Alignment.TopEnd).offset(x = -Spacing.XXS, y = Spacing.XXS),
136
+ )
137
+ }
138
+ }
139
+ }
140
+
141
+ // Capsule grouping related controls, one item per 50pt: vertical in the side bar, horizontal on a header row.
142
+ @Composable
143
+ internal fun SideBarGroup(
144
+ headerColor: HeaderColor,
145
+ itemCount: Int,
146
+ horizontal: Boolean = false,
147
+ content: @Composable (itemModifier: Modifier) -> Unit,
148
+ ) {
149
+ val length = if (itemCount > 1) DuoMetrics.itemPitch * itemCount else DuoMetrics.controlSize
150
+ val item = length / itemCount.coerceAtLeast(1)
151
+ val glass = Modifier.sideBarGlass(headerColor, RoundedCornerShape(Radius.XL))
152
+ if (horizontal) {
153
+ Row(Modifier.size(width = length, height = DuoMetrics.controlSize).then(glass)) {
154
+ content(Modifier.size(width = item, height = DuoMetrics.controlSize))
155
+ }
156
+ } else {
157
+ Column(Modifier.size(width = DuoMetrics.controlSize, height = length).then(glass)) {
158
+ content(Modifier.size(width = DuoMetrics.controlSize, height = item))
159
+ }
160
+ }
161
+ }
162
+
163
+ @Composable
164
+ internal fun SideBarGroupItem(
165
+ icon: String,
166
+ headerColor: HeaderColor,
167
+ automationId: String,
168
+ onClick: () -> Unit,
169
+ modifier: Modifier = Modifier,
170
+ ) {
171
+ Box(
172
+ modifier = modifier
173
+ .activeOpacityClickable(onClick = onClick)
174
+ .setAutomationId(automationId),
175
+ contentAlignment = Alignment.Center,
176
+ ) {
177
+ Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
178
+ }
179
+ }
180
+
181
+ // Dark glass under light icons (banner headers), light glass otherwise. The light one is opaque:
182
+ // Compose has no backdrop blur and a translucent fill shows its own shadow through (RN uses 85% white).
183
+ private fun Modifier.sideBarGlass(headerColor: HeaderColor, shape: Shape): Modifier =
184
+ if (headerColor.tintIconColor == Colors.black_01) {
185
+ clip(shape)
186
+ .background(Colors.black_20.copy(alpha = 0.5f))
187
+ .border(0.5.dp, Colors.black_01.copy(alpha = 0.2f), shape)
188
+ } else {
189
+ shadow(3.dp, shape)
190
+ .background(Colors.black_01)
191
+ .border(0.5.dp, Colors.black_20.copy(alpha = 0.08f), shape)
192
+ }
@@ -83,7 +83,8 @@ internal fun SnackBar(
83
83
  durationMillis = 200,
84
84
  )
85
85
  )
86
- navigator.hideSnackBar()
86
+ // hideSnackBar() would call clear() -> this close again while still bound
87
+ OverplayComponentRegistry.hardClearAfterDismiss()
87
88
  }
88
89
  }
89
90
 
@@ -14,6 +14,9 @@ data class ScreenDimension(
14
14
 
15
15
  expect fun getPlatformName(): String
16
16
 
17
+ // iPhone idiom only; iPad windows and Android never get the iPhone / iPhone Duo layout.
18
+ expect fun isPhoneIdiom(): Boolean
19
+
17
20
  @Composable
18
21
  expect fun getScreenDimensions(): ScreenDimension
19
22
 
@@ -0,0 +1,13 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.Immutable
5
+ import androidx.compose.ui.unit.Dp
6
+
7
+ @Immutable
8
+ internal data class ReservedRegion(val x: Dp, val y: Dp, val width: Dp, val height: Dp)
9
+
10
+ // Regions the system occludes (iOS 27.1+), in the coordinates of the Compose root view: the
11
+ // iPhone Duo camera, and the status bar cluster as it grows for a call or a Live Activity.
12
+ @Composable
13
+ internal expect fun rememberReservedRegions(): List<ReservedRegion>
@@ -1,6 +1,11 @@
1
1
  package vn.momo.kits.utils
2
2
 
3
3
  import androidx.compose.foundation.layout.WindowInsets
4
+ import androidx.compose.foundation.layout.WindowInsetsSides
5
+ import androidx.compose.foundation.layout.displayCutout
6
+ import androidx.compose.foundation.layout.only
7
+ import androidx.compose.foundation.layout.union
8
+ import androidx.compose.foundation.layout.windowInsetsPadding
4
9
  import androidx.compose.foundation.layout.asPaddingValues
5
10
  import androidx.compose.foundation.layout.systemBars
6
11
  import androidx.compose.runtime.Composable
@@ -90,6 +95,14 @@ fun getAppStatusBarHeight(): Dp {
90
95
  }
91
96
  }
92
97
 
98
+ internal val horizontalSafeAreaInsets: WindowInsets
99
+ @Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)
100
+
101
+ // Left/right safe area like SwiftUI (landscape camera/notch); consumed, so nested StackScreens don't pad twice.
102
+ internal fun Modifier.horizontalSafeAreaPadding(): Modifier = composed {
103
+ windowInsetsPadding(horizontalSafeAreaInsets)
104
+ }
105
+
93
106
  @Composable
94
107
  internal fun getNavigationBarHeight(): Dp {
95
108
  return if (AppNavigationBar.current == 0.dp) {
@@ -39,11 +39,15 @@ import platform.Foundation.NSBundle
39
39
  import platform.UIKit.UIColor
40
40
  import platform.UIKit.UIDevice
41
41
  import platform.UIKit.UIScreen
42
+ import platform.UIKit.UIUserInterfaceIdiomPhone
42
43
  import vn.momo.kits.navigation.AttachHostBackGesture
43
44
  import vn.momo.kits.navigation.Navigator
44
45
 
45
46
  actual fun getPlatformName(): String = "iOS"
46
47
 
48
+ actual fun isPhoneIdiom(): Boolean =
49
+ UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPhone
50
+
47
51
  @OptIn(ExperimentalForeignApi::class)
48
52
  @Composable
49
53
  actual fun getScreenDimensions(): ScreenDimension = memScoped {