@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
@@ -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,29 @@ 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
+
62
82
  @Composable
63
83
  internal fun Toolkit(
64
84
  headerRight: HeaderRight.Toolkit,
65
85
  tintColor: Color? = null,
66
- headerColor: HeaderColor
86
+ headerColor: HeaderColor,
87
+ vertical: Boolean = false,
67
88
  ) {
68
89
  val api = LocalMaxApi.current
69
90
  val context = LocalContext.current
@@ -103,6 +124,26 @@ internal fun Toolkit(
103
124
  }
104
125
  }
105
126
 
127
+ fun onPressHelpCenter() {
128
+ if (context == null) return
129
+ api?.showHelpCenter(
130
+ mapOf(
131
+ "appId" to context.appId,
132
+ "code" to context.appCode,
133
+ "name" to context.appName,
134
+ "icon" to context.appIcon,
135
+ "description" to context.description
136
+ )
137
+ ) {}
138
+ }
139
+
140
+ fun onPressClose() {
141
+ if (headerRight.useCloseIcon) api?.dismiss { } else api?.dismissAll { }
142
+ }
143
+
144
+ val useHelpCenter = usesHelpCenter(context)
145
+ val closeIcon = if (headerRight.useCloseIcon) "16_navigation_close_circle" else "16_basic_home"
146
+
106
147
  val navButtonConfig = getNavigationButtonConfig(
107
148
  headerRight,
108
149
  ::onPressShortcut,
@@ -113,12 +154,7 @@ internal fun Toolkit(
113
154
  group.items.any { it.showBadge }
114
155
  }
115
156
 
116
- val isShowShortcut by remember(headerRight.useShortcut, headerRight.useMore, context) {
117
- derivedStateOf {
118
- (headerRight.useShortcut == true) &&
119
- !(headerRight.useMore == true && context == null)
120
- }
121
- }
157
+ val isShowShortcut = showsShortcut(headerRight, context)
122
158
 
123
159
  val icon by remember(navButtonConfig.icon, isFavorite) {
124
160
  derivedStateOf {
@@ -127,6 +163,37 @@ internal fun Toolkit(
127
163
  }
128
164
  }
129
165
 
166
+ if (vertical) {
167
+ Column(verticalArrangement = Arrangement.spacedBy(Spacing.M)) {
168
+ if (isShowShortcut) {
169
+ SideBarButton(
170
+ icon = icon,
171
+ headerColor = headerColor,
172
+ enabled = !isLoading,
173
+ showBadge = showBadge,
174
+ onClick = navButtonConfig.onPress,
175
+ )
176
+ }
177
+ SideBarGroup(headerColor = headerColor, itemCount = if (useHelpCenter) 2 else 1) {
178
+ if (useHelpCenter) {
179
+ SideBarGroupItem(
180
+ icon = "help_center",
181
+ headerColor = headerColor,
182
+ automationId = "btn_navigation_help_center",
183
+ onClick = ::onPressHelpCenter,
184
+ )
185
+ }
186
+ SideBarGroupItem(
187
+ icon = closeIcon,
188
+ headerColor = headerColor,
189
+ automationId = "btn_navigation_close",
190
+ onClick = ::onPressClose,
191
+ )
192
+ }
193
+ }
194
+ return
195
+ }
196
+
130
197
  Row(
131
198
  verticalAlignment = Alignment.CenterVertically
132
199
  ) {
@@ -150,22 +217,12 @@ internal fun Toolkit(
150
217
  .clip(shape = RoundedCornerShape(14.dp))
151
218
  .background(headerColor.backgroundButton)
152
219
  ) {
153
- if (context != null) {
220
+ if (useHelpCenter) {
154
221
  Icon(
155
222
  source = "help_center",
156
223
  size = 20.dp,
157
224
  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
- }
225
+ modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressHelpCenter)
169
226
  )
170
227
  Box(
171
228
  modifier = Modifier
@@ -175,12 +232,10 @@ internal fun Toolkit(
175
232
  )
176
233
  }
177
234
  Icon(
178
- source = "16_basic_home",
235
+ source = closeIcon,
179
236
  size = 20.dp,
180
237
  color = headerColor.tintIconColor,
181
- modifier = Modifier.padding(4.dp).activeOpacityClickable {
182
- api?.dismissAll { }
183
- }
238
+ modifier = Modifier.padding(4.dp).activeOpacityClickable(onClick = ::onPressClose)
184
239
  )
185
240
  }
186
241
  }
@@ -270,7 +325,9 @@ sealed interface HeaderRight {
270
325
  val useMore: Boolean = false,
271
326
  val useSystemTools: Boolean = true,
272
327
  val tools: List<ToolGroup> = emptyList(),
273
- val toolCallback: ((String) -> Unit)? = { _ -> }
328
+ val toolCallback: ((String) -> Unit)? = { _ -> },
329
+ // Close dismisses only this mini app instead of returning home.
330
+ val useCloseIcon: Boolean = false,
274
331
  ) : HeaderRight
275
332
  }
276
333
 
@@ -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,185 @@
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.ColumnScope
10
+ import androidx.compose.foundation.layout.Spacer
11
+ import androidx.compose.foundation.layout.absolutePadding
12
+ import androidx.compose.foundation.layout.fillMaxWidth
13
+ import androidx.compose.foundation.layout.height
14
+ import androidx.compose.foundation.layout.offset
15
+ import androidx.compose.foundation.layout.size
16
+ import androidx.compose.foundation.layout.width
17
+ import androidx.compose.foundation.shape.CircleShape
18
+ import androidx.compose.foundation.shape.RoundedCornerShape
19
+ import androidx.compose.runtime.Composable
20
+ import androidx.compose.runtime.LaunchedEffect
21
+ import androidx.compose.ui.AbsoluteAlignment
22
+ import androidx.compose.ui.Alignment
23
+ import androidx.compose.ui.Modifier
24
+ import androidx.compose.ui.draw.clip
25
+ import androidx.compose.ui.draw.shadow
26
+ import androidx.compose.ui.graphics.Shape
27
+ import androidx.compose.ui.layout.layout
28
+ import androidx.compose.ui.unit.Dp
29
+ import androidx.compose.ui.unit.constrainHeight
30
+ import androidx.compose.ui.unit.dp
31
+ import vn.momo.kits.components.BadgeDot
32
+ import vn.momo.kits.components.DotSize
33
+ import vn.momo.kits.components.Icon
34
+ import vn.momo.kits.const.Colors
35
+ import vn.momo.kits.const.Radius
36
+ import vn.momo.kits.const.Spacing
37
+ import vn.momo.kits.layout.DuoMetrics
38
+ import vn.momo.kits.layout.SideBarEdge
39
+ import vn.momo.kits.layout.getScreenLayout
40
+ import vn.momo.kits.modifier.activeOpacityClickable
41
+ import vn.momo.kits.modifier.conditional
42
+ import vn.momo.kits.modifier.setAutomationId
43
+ import vn.momo.kits.navigation.LocalNavigator
44
+ import vn.momo.kits.navigation.LocalOptions
45
+ import vn.momo.kits.navigation.bottomtab.LocalDuoBarCompression
46
+
47
+ private val SIDE_BAR_ICON_SIZE = 24.dp
48
+
49
+ // iPhone Duo: back and toolkit leave the bar for a column in the side safe area, as the system bars do.
50
+ // bottomInset is what footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
51
+ @Composable
52
+ internal fun BoxScope.HeaderSideBar(bottomInset: Dp, modifier: Modifier = Modifier) {
53
+ val options = LocalOptions.current
54
+ val navigator = LocalNavigator.current
55
+ val sideBar = getScreenLayout().sideBar ?: return
56
+ val toolkit = options.headerRight as? HeaderRight.Toolkit
57
+ if (options.headerType == HeaderType.None || (options.hiddenBack && toolkit == null)) return
58
+
59
+ val headerColor = getHeaderColor(options.headerType, headerOpacity(), options.tintColor, Colors.black_17)
60
+ val isRight = sideBar.edge == SideBarEdge.RIGHT
61
+ val compression = LocalDuoBarCompression.current
62
+
63
+ // full height, so the tab bar compression decision matches this screen's toolkit
64
+ val fullHeight = DuoMetrics.controlSize + (toolkit?.let { Spacing.M + toolkitColumnHeight(it) } ?: 0.dp)
65
+ LaunchedEffect(fullHeight, compression.setToolbarHeight) { compression.setToolbarHeight(fullHeight) }
66
+
67
+ Column(
68
+ modifier = modifier
69
+ .align(if (isRight) AbsoluteAlignment.TopRight else AbsoluteAlignment.TopLeft)
70
+ .width(sideBar.width)
71
+ .layout { measurable, constraints ->
72
+ val placeable = measurable.measure(constraints.copy(minHeight = 0))
73
+ val maxY = constraints.maxHeight - bottomInset.roundToPx() - placeable.height
74
+ val y = minOf(sideBar.top.roundToPx(), maxY).coerceAtLeast(0)
75
+ layout(placeable.width, constraints.constrainHeight(y + placeable.height)) {
76
+ placeable.place(0, y)
77
+ }
78
+ }
79
+ .absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
80
+ horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
81
+ verticalArrangement = Arrangement.spacedBy(Spacing.M),
82
+ ) {
83
+ if (options.hiddenBack) {
84
+ // keep the slot so the toolkit does not jump between root and pushed screens
85
+ Spacer(Modifier.size(DuoMetrics.controlSize))
86
+ } else {
87
+ SideBarButton(
88
+ icon = "arrow-back",
89
+ headerColor = headerColor,
90
+ automationId = "btn_navigation_back",
91
+ onClick = { navigator.onBackSafe { } },
92
+ )
93
+ }
94
+ when {
95
+ toolkit == null || compression.toolbarHidden -> Unit
96
+ // HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
97
+ compression.toolbarCompressed -> SideBarButton(
98
+ icon = "navigation_menu",
99
+ headerColor = headerColor,
100
+ showBadge = toolkit.tools.any { group -> group.items.any { it.showBadge } },
101
+ automationId = "btn_navigation_toolbar",
102
+ onClick = compression.expandToolbar,
103
+ )
104
+ else -> Toolkit(headerRight = toolkit, tintColor = options.tintColor, headerColor = headerColor, vertical = true)
105
+ }
106
+ }
107
+ }
108
+
109
+ @Composable
110
+ internal fun SideBarButton(
111
+ icon: String,
112
+ headerColor: HeaderColor,
113
+ onClick: () -> Unit,
114
+ enabled: Boolean = true,
115
+ showBadge: Boolean = false,
116
+ automationId: String? = null,
117
+ ) {
118
+ Box {
119
+ Box(
120
+ modifier = Modifier
121
+ .size(DuoMetrics.controlSize)
122
+ .sideBarGlass(headerColor, CircleShape)
123
+ .activeOpacityClickable(enabled = enabled, onClick = onClick)
124
+ .conditional(automationId != null) { setAutomationId(automationId.orEmpty()) },
125
+ contentAlignment = Alignment.Center,
126
+ ) {
127
+ Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
128
+ }
129
+ if (showBadge) {
130
+ BadgeDot(
131
+ size = DotSize.Small,
132
+ modifier = Modifier.align(Alignment.TopEnd).offset(x = -Spacing.XXS, y = Spacing.XXS),
133
+ )
134
+ }
135
+ }
136
+ }
137
+
138
+ // Capsule grouping related controls, one item per 50pt.
139
+ @Composable
140
+ internal fun SideBarGroup(
141
+ headerColor: HeaderColor,
142
+ itemCount: Int,
143
+ content: @Composable ColumnScope.() -> Unit,
144
+ ) {
145
+ val height = if (itemCount > 1) DuoMetrics.itemPitch * itemCount else DuoMetrics.controlSize
146
+ Column(
147
+ modifier = Modifier
148
+ .width(DuoMetrics.controlSize)
149
+ .height(height)
150
+ .sideBarGlass(headerColor, RoundedCornerShape(Radius.XL)),
151
+ content = content,
152
+ )
153
+ }
154
+
155
+ @Composable
156
+ internal fun ColumnScope.SideBarGroupItem(
157
+ icon: String,
158
+ headerColor: HeaderColor,
159
+ automationId: String,
160
+ onClick: () -> Unit,
161
+ ) {
162
+ Box(
163
+ modifier = Modifier
164
+ .weight(1f)
165
+ .fillMaxWidth()
166
+ .activeOpacityClickable(onClick = onClick)
167
+ .setAutomationId(automationId),
168
+ contentAlignment = Alignment.Center,
169
+ ) {
170
+ Icon(source = icon, size = SIDE_BAR_ICON_SIZE, color = headerColor.tintIconColor)
171
+ }
172
+ }
173
+
174
+ // Dark glass under light icons (banner headers), light glass otherwise. The light one is opaque:
175
+ // Compose has no backdrop blur and a translucent fill shows its own shadow through (RN uses 85% white).
176
+ private fun Modifier.sideBarGlass(headerColor: HeaderColor, shape: Shape): Modifier =
177
+ if (headerColor.tintIconColor == Colors.black_01) {
178
+ clip(shape)
179
+ .background(Colors.black_20.copy(alpha = 0.5f))
180
+ .border(0.5.dp, Colors.black_01.copy(alpha = 0.2f), shape)
181
+ } else {
182
+ shadow(3.dp, shape)
183
+ .background(Colors.black_01)
184
+ .border(0.5.dp, Colors.black_20.copy(alpha = 0.08f), shape)
185
+ }
@@ -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>
@@ -95,9 +95,12 @@ fun getAppStatusBarHeight(): Dp {
95
95
  }
96
96
  }
97
97
 
98
+ internal val horizontalSafeAreaInsets: WindowInsets
99
+ @Composable get() = WindowInsets.systemBars.union(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal)
100
+
98
101
  // Left/right safe area like SwiftUI (landscape camera/notch); consumed, so nested StackScreens don't pad twice.
99
102
  internal fun Modifier.horizontalSafeAreaPadding(): Modifier = composed {
100
- windowInsetsPadding(WindowInsets.systemBars.union(WindowInsets.displayCutout).only(WindowInsetsSides.Horizontal))
103
+ windowInsetsPadding(horizontalSafeAreaInsets)
101
104
  }
102
105
 
103
106
  @Composable
@@ -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 {
@@ -0,0 +1,86 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import androidx.compose.runtime.getValue
6
+ import androidx.compose.runtime.mutableStateOf
7
+ import androidx.compose.runtime.remember
8
+ import androidx.compose.runtime.setValue
9
+ import androidx.compose.ui.uikit.LocalUIViewController
10
+ import androidx.compose.ui.unit.dp
11
+ import kotlinx.cinterop.BetaInteropApi
12
+ import kotlinx.cinterop.ExperimentalForeignApi
13
+ import kotlinx.cinterop.readValue
14
+ import kotlinx.cinterop.useContents
15
+ import platform.CoreGraphics.CGRectZero
16
+ import platform.Foundation.NSSelectorFromString
17
+ import platform.Foundation.NSValue
18
+ import platform.Foundation.valueForKey
19
+ import platform.UIKit.CGRectValue
20
+ import platform.UIKit.UIView
21
+ import platform.UIKit.UIViewAutoresizingFlexibleHeight
22
+ import platform.UIKit.UIViewAutoresizingFlexibleWidth
23
+ import platform.darwin.NSObject
24
+ import platform.darwin.NSObjectProtocol
25
+ import platform.Foundation.NSClassFromString
26
+
27
+ // The API is newer than the SDK Kotlin/Native binds, so it is reached through the runtime.
28
+ @OptIn(ExperimentalForeignApi::class)
29
+ private val regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
30
+
31
+ @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
32
+ private fun UIView.occlusionRegions(target: UIView): List<ReservedRegion> {
33
+ if (window == null || !respondsToSelector(regionsOfKind)) return emptyList()
34
+ val kindClass = NSClassFromString("UIViewReservedRegionKind") as? NSObjectProtocol ?: return emptyList()
35
+ val kind = kindClass.performSelector(NSSelectorFromString("occlusionRegionKind")) ?: return emptyList()
36
+ val regions = performSelector(regionsOfKind, withObject = kind) as? List<*> ?: return emptyList()
37
+ return regions.mapNotNull { region ->
38
+ val frame = ((region as? NSObject)?.valueForKey("frame") as? NSValue)?.CGRectValue ?: return@mapNotNull null
39
+ convertRect(frame, toView = target).useContents {
40
+ ReservedRegion(origin.x.dp, origin.y.dp, size.width.dp, size.height.dp)
41
+ }
42
+ }
43
+ }
44
+
45
+ // Invisible full-size probe: reading the regions in layoutSubviews makes UIKit run it again
46
+ // when they change, so nothing polls.
47
+ @OptIn(ExperimentalForeignApi::class)
48
+ private class ReservedRegionsProbe(
49
+ private val onChange: (List<ReservedRegion>) -> Unit,
50
+ ) : UIView(frame = CGRectZero.readValue()) {
51
+ private var last: List<ReservedRegion>? = null
52
+
53
+ init {
54
+ userInteractionEnabled = false
55
+ autoresizingMask = UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight
56
+ }
57
+
58
+ override fun layoutSubviews() {
59
+ super.layoutSubviews()
60
+ val regions = superview?.let { occlusionRegions(it) } ?: emptyList()
61
+ if (regions != last) {
62
+ last = regions
63
+ onChange(regions)
64
+ }
65
+ }
66
+
67
+ override fun didMoveToWindow() {
68
+ super.didMoveToWindow()
69
+ setNeedsLayout()
70
+ }
71
+ }
72
+
73
+ @OptIn(ExperimentalForeignApi::class)
74
+ @Composable
75
+ internal actual fun rememberReservedRegions(): List<ReservedRegion> {
76
+ if (!isPhoneIdiom()) return emptyList()
77
+ val host = LocalUIViewController.current.view
78
+ var regions by remember { mutableStateOf(emptyList<ReservedRegion>()) }
79
+ DisposableEffect(host) {
80
+ val probe = ReservedRegionsProbe { regions = it }
81
+ probe.setFrame(host.bounds)
82
+ host.insertSubview(probe, atIndex = 0)
83
+ onDispose { probe.removeFromSuperview() }
84
+ }
85
+ return regions
86
+ }
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.167.1-duo.1
21
+ version=0.167.1-beta.4
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -41,6 +41,12 @@ public class KitConfig {
41
41
  public var trustBanner: TrustBannerData? = nil
42
42
  public var headerBar: String? = nil
43
43
  public var headerGradient: String? = nil
44
+
45
+ public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
46
+ self.trustBanner = trustBanner
47
+ self.headerBar = headerBar
48
+ self.headerGradient = headerGradient
49
+ }
44
50
  }
45
51
 
46
52
  public class ApplicationEnvironment: ObservableObject {
@@ -59,6 +65,17 @@ public class ApplicationEnvironment: ObservableObject {
59
65
  }
60
66
  }
61
67
 
68
+ extension ApplicationEnvironment {
69
+ /// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
70
+ func requestDismiss() {
71
+ if let composeApi {
72
+ composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
73
+ } else {
74
+ maxApi?.dismiss(completion: nil)
75
+ }
76
+ }
77
+ }
78
+
62
79
  private struct ApplicationEnvironmentKey: EnvironmentKey {
63
80
  static let defaultValue = ApplicationEnvironment()
64
81
  }
@@ -1,11 +1,12 @@
1
1
  import Foundation
2
2
 
3
+ @available(*, deprecated, message: "Use KitComposeApi or MaxApi")
3
4
  public protocol ComposeApi {
4
5
  func request(funcName: String, params: Any, _ completion: ((String) -> Void)?)
5
6
  func request(funcName: String, params: Any) -> String
6
7
  }
7
8
 
8
- // Default implementation
9
+ @available(*, deprecated, message: "Use KitComposeApi or MaxApi")
9
10
  public class ComposeApiImpl: ComposeApi {
10
11
  public static let shared = ComposeApiImpl()
11
12