@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
@@ -4,6 +4,8 @@ import androidx.compose.foundation.background
4
4
  import androidx.compose.foundation.border
5
5
  import androidx.compose.foundation.clickable
6
6
  import androidx.compose.foundation.interaction.MutableInteractionSource
7
+ import androidx.compose.foundation.rememberScrollState
8
+ import androidx.compose.foundation.verticalScroll
7
9
  import androidx.compose.foundation.layout.*
8
10
  import androidx.compose.foundation.lazy.LazyColumn
9
11
  import androidx.compose.foundation.shape.RoundedCornerShape
@@ -20,7 +22,7 @@ import vn.momo.kits.application.LocalLocalize
20
22
  import vn.momo.kits.const.*
21
23
  import vn.momo.kits.modifier.conditional
22
24
  import vn.momo.kits.modifier.setAutomationId
23
- import vn.momo.kits.platform.getScreenDimensions
25
+ import vn.momo.kits.layout.getScreenLayout
24
26
  import kotlin.math.min
25
27
 
26
28
  data class PopupAction(
@@ -34,9 +36,21 @@ enum class PopupActionDirection {
34
36
  AUTO
35
37
  }
36
38
 
37
- // Portrait width in landscape too, so aspect-ratio images don't push popups past the screen height.
39
+ // Width of a system alert on the iPhone Duo inner display, in both orientations (RN Popup/utils.ts).
40
+ private val DUO_POPUP_MAX_WIDTH = 440.dp
41
+
42
+ // Card width cap plus the popup's own side padding; unbounded off iPhone Duo.
43
+ @Composable
44
+ internal fun popupMaxWidth(horizontalPadding: Dp): Dp =
45
+ if (getScreenLayout().isIPhoneDuoDevice) DUO_POPUP_MAX_WIDTH + horizontalPadding * 2 else Dp.Unspecified
46
+
47
+ // Room left for a popup on iPhone Duo, where landscape is too short for it; null elsewhere.
38
48
  @Composable
39
- internal fun popupMaxWidth(): Dp = getScreenDimensions().let { minOf(it.width, it.height) }.dp
49
+ internal fun popupMaxHeight(): Dp? {
50
+ val layout = getScreenLayout()
51
+ if (!layout.isIPhoneDuoDevice) return null
52
+ return layout.height - layout.duoTopInset - layout.insetBottom - Spacing.XL * 2
53
+ }
40
54
 
41
55
  private fun heightForLines(
42
56
  layout: TextLayoutResult,
@@ -129,12 +143,15 @@ fun PopupNotify(
129
143
  callback?.invoke()
130
144
  }
131
145
 
146
+ val maxHeight = popupMaxHeight()
147
+ val bodyScrollState = rememberScrollState()
148
+
132
149
  Box(
133
150
  Modifier.setAutomationId("popup_notify"), contentAlignment = Alignment.TopEnd
134
151
  ) {
135
152
  Column(
136
153
  modifier = Modifier
137
- .widthIn(max = popupMaxWidth())
154
+ .widthIn(max = popupMaxWidth(horizontalPadding = 12.dp))
138
155
  .fillMaxWidth()
139
156
  .padding(horizontal = 12.dp)
140
157
  .clip(RoundedCornerShape(Radius.L))
@@ -145,31 +162,41 @@ fun PopupNotify(
145
162
  .conditional(IsShowBaseLineDebug) {
146
163
  border(1.dp, Colors.blue_03)
147
164
  }
165
+ .conditional(maxHeight != null) {
166
+ heightIn(max = maxHeight ?: Dp.Unspecified)
167
+ }
148
168
  ) {
149
- if(props.image.isNotEmpty()) {
150
- Image(
151
- source = props.image,
152
- modifier = Modifier.fillMaxWidth().aspectRatio(1.777f),
153
- options = Options(alignment = Alignment.Center)
154
- )
155
- }
169
+ // scrolls when iPhone Duo landscape is too short; actions stay visible
170
+ Column(
171
+ Modifier.conditional(maxHeight != null) {
172
+ weight(1f, fill = false).verticalScroll(bodyScrollState)
173
+ }
174
+ ) {
175
+ if(props.image.isNotEmpty()) {
176
+ Image(
177
+ source = props.image,
178
+ modifier = Modifier.fillMaxWidth().aspectRatio(1.777f),
179
+ options = Options(alignment = Alignment.Center)
180
+ )
181
+ }
156
182
 
157
- Column(modifier = Modifier.padding(Spacing.XL)) {
158
- Text(
159
- style = Typography.headerDefaultBold,
160
- maxLines = 2,
161
- text = props.title,
162
- modifier = Modifier.setAutomationId("title_popup_permission")
163
- )
164
- content(Modifier.padding(top = Spacing.S))
165
- if (props.error.isNotEmpty()) {
183
+ Column(modifier = Modifier.padding(Spacing.XL)) {
166
184
  Text(
167
- text = LocalLocalize.current.translate("errorCode") + props.error,
168
- style = Typography.descriptionXsRegular,
169
- color = AppTheme.current.colors.text.hint,
170
- maxLines = 1,
171
- modifier = Modifier.padding(top = Spacing.S)
185
+ style = Typography.headerDefaultBold,
186
+ maxLines = 2,
187
+ text = props.title,
188
+ modifier = Modifier.setAutomationId("title_popup_permission")
172
189
  )
190
+ content(Modifier.padding(top = Spacing.S))
191
+ if (props.error.isNotEmpty()) {
192
+ Text(
193
+ text = LocalLocalize.current.translate("errorCode") + props.error,
194
+ style = Typography.descriptionXsRegular,
195
+ color = AppTheme.current.colors.text.hint,
196
+ maxLines = 1,
197
+ modifier = Modifier.padding(top = Spacing.S)
198
+ )
199
+ }
173
200
  }
174
201
  }
175
202
 
@@ -227,7 +254,7 @@ internal fun BuildAction(
227
254
  val closeAction = remember { mutableStateOf("button_action") }
228
255
  when (buttonDirection) {
229
256
  PopupActionDirection.AUTO -> {
230
- if (secondary != null && (secondary.title.length > 12 || primary?.title?.length!! > 12)) {
257
+ if (secondary != null && (secondary.title.length > 12 || (primary?.title?.length ?: 0) > 12)) {
231
258
  RenderColumn(
232
259
  secondary = secondary,
233
260
  onAction = onAction,
@@ -22,14 +22,24 @@ import vn.momo.kits.modifier.conditional
22
22
  import vn.momo.kits.modifier.setAutomationId
23
23
  import vn.momo.kits.utils.ifNotNull
24
24
 
25
+ private const val ASPECT_RATIO = 0.72f
26
+ private val CLOSE_ROW_HEIGHT = Spacing.S + 40.dp
27
+
25
28
  @Composable
26
29
  fun PopupPromotion(
27
30
  source: String = "",
28
31
  onPress: (() -> Unit) = {},
29
32
  onClose: (() -> Unit) = {},
30
33
  ) {
34
+ val maxHeight = popupMaxHeight()
35
+ var maxWidth = popupMaxWidth(horizontalPadding = Spacing.M)
36
+ if (maxHeight != null) {
37
+ // fit the portrait image by height when iPhone Duo leaves too little room
38
+ maxWidth = minOf(maxWidth, (maxHeight - CLOSE_ROW_HEIGHT) * ASPECT_RATIO + Spacing.M * 2)
39
+ }
40
+
31
41
  Column(Modifier
32
- .widthIn(max = popupMaxWidth())
42
+ .widthIn(max = maxWidth)
33
43
  .padding(horizontal = Spacing.M)
34
44
  .conditional(IsShowBaseLineDebug) {
35
45
  border(1.dp, Colors.blue_03)
@@ -39,7 +49,7 @@ fun PopupPromotion(
39
49
  Image(
40
50
  source = source,
41
51
  modifier = Modifier.fillMaxWidth()
42
- .aspectRatio(0.72f)
52
+ .aspectRatio(ASPECT_RATIO)
43
53
  .clickable(
44
54
  interactionSource = remember { MutableInteractionSource() },
45
55
  indication = null,
@@ -5,13 +5,10 @@ import androidx.compose.foundation.border
5
5
  import androidx.compose.foundation.layout.*
6
6
  import androidx.compose.foundation.shape.CircleShape
7
7
  import androidx.compose.runtime.Composable
8
- import androidx.compose.runtime.mutableFloatStateOf
9
8
  import androidx.compose.runtime.remember
10
9
  import androidx.compose.ui.Alignment
11
10
  import androidx.compose.ui.Modifier
12
11
  import androidx.compose.ui.draw.clip
13
- import androidx.compose.ui.layout.onGloballyPositioned
14
- import androidx.compose.ui.platform.LocalDensity
15
12
  import androidx.compose.ui.text.style.TextAlign
16
13
  import androidx.compose.ui.unit.dp
17
14
  import vn.momo.kits.application.IsShowBaseLineDebug
@@ -180,7 +177,6 @@ private fun ProgressInfoVertical(
180
177
  ) {
181
178
  val theme = AppTheme.current
182
179
  val lineColor = remember(theme) { theme.colors.primary.copy(alpha = 0.2f) }
183
- val density = LocalDensity.current
184
180
 
185
181
  Column(
186
182
  modifier = modifier
@@ -189,16 +185,18 @@ private fun ProgressInfoVertical(
189
185
  ) {
190
186
  steps.forEachIndexed { index, item ->
191
187
  val isLast = index == steps.lastIndex
192
- val rowHeightPx = remember { mutableFloatStateOf(0f) }
193
- val rowHeightDp = with(density) { rowHeightPx.value.toDp() }
194
188
 
189
+ // Intrinsic height lets the connector fill the row in one pass; measuring the row and
190
+ // feeding it back lagged a frame and never let the line shrink with its text.
195
191
  Row(
196
192
  modifier = Modifier
197
193
  .fillMaxWidth()
198
- .onGloballyPositioned { coords -> rowHeightPx.value = coords.size.height.toFloat() },
194
+ .height(IntrinsicSize.Min),
199
195
  ) {
200
196
  Column(
201
- modifier = Modifier.heightIn(min = if (isLast) 40.dp else 48.dp),
197
+ modifier = Modifier
198
+ .fillMaxHeight()
199
+ .heightIn(min = if (isLast) 40.dp else 48.dp),
202
200
  horizontalAlignment = Alignment.CenterHorizontally,
203
201
  ) {
204
202
  ProgressInfoStepIcon(
@@ -211,7 +209,7 @@ private fun ProgressInfoVertical(
211
209
  Spacer(
212
210
  modifier = Modifier
213
211
  .width(2.dp)
214
- .height(rowHeightDp-if(size == ProgressInfoSize.Large) scaleSize(24f).dp else scaleSize(16f).dp)
212
+ .weight(1f)
215
213
  .background(lineColor),
216
214
  )
217
215
  }
@@ -40,7 +40,7 @@ fun Radio(
40
40
  verticalAlignment = Alignment.CenterVertically,
41
41
  ) {
42
42
  Box(
43
- modifier = modifier
43
+ modifier = Modifier
44
44
  .padding(end = if (label.isEmpty()) 0.dp else Spacing.XS)
45
45
  .border(borderWidth, borderColor, RoundedCornerShape(Radius.M))
46
46
  .conditional(IsShowBaseLineDebug) {
@@ -43,9 +43,11 @@ private fun getValueForPosition(
43
43
  step: Float,
44
44
  ): Float {
45
45
  val availableSpace = containerWidth - thumbWidth
46
- if (availableSpace <= 0f) return min
47
- val relStepUnit = step / (max - min)
46
+ if (availableSpace <= 0f || max <= min) return min
48
47
  var relPosition = (positionInView - thumbWidth / 2f) / availableSpace
48
+ // A zero step would make the snap math NaN, and NaN.roundToInt() throws.
49
+ if (step <= 0f) return clamp(min + relPosition * (max - min), min, max)
50
+ val relStepUnit = step / (max - min)
49
51
  val relOffset = relPosition % relStepUnit
50
52
  relPosition -= relOffset
51
53
  if (relOffset / relStepUnit >= 0.5f) relPosition += relStepUnit
@@ -131,6 +133,11 @@ fun Slider(
131
133
  var draggingLow by remember { mutableStateOf(true) }
132
134
  var isDragging by remember { mutableStateOf(false) }
133
135
 
136
+ // pointerInput is not keyed on the callbacks, so read their latest values.
137
+ val currentOnValueChanged by rememberUpdatedState(onValueChanged)
138
+ val currentOnTouchStart by rememberUpdatedState(onSliderTouchStart)
139
+ val currentOnTouchEnd by rememberUpdatedState(onSliderTouchEnd)
140
+
134
141
  val thumbColor = if (disabled) theme.colors.text.disable else theme.colors.primary
135
142
  val trackActiveColor = if (disabled) theme.colors.text.disable else theme.colors.primary
136
143
  val trackInactiveColor = theme.colors.background.default
@@ -167,7 +174,7 @@ fun Slider(
167
174
 
168
175
  draggingLow = disableRange || isLowCloser(touchX, lowCenterX, highCenterX)
169
176
  isDragging = true
170
- onSliderTouchStart(lowValue, highValue)
177
+ currentOnTouchStart(lowValue, highValue)
171
178
 
172
179
  // Apply initial press
173
180
  applyDrag(touchX, trackWidthPx, thumbSizePx,
@@ -175,7 +182,7 @@ fun Slider(
175
182
  lowValue, highValue) { nl, nh ->
176
183
  lowValue = nl
177
184
  highValue = nh
178
- onValueChanged(nl, nh, true)
185
+ currentOnValueChanged(nl, nh, true)
179
186
  }
180
187
 
181
188
  // Track move / release
@@ -186,7 +193,7 @@ fun Slider(
186
193
 
187
194
  if (!moveChange.pressed) {
188
195
  isDragging = false
189
- onSliderTouchEnd(lowValue, highValue)
196
+ currentOnTouchEnd(lowValue, highValue)
190
197
  break
191
198
  }
192
199
 
@@ -195,7 +202,7 @@ fun Slider(
195
202
  lowValue, highValue) { nl, nh ->
196
203
  lowValue = nl
197
204
  highValue = nh
198
- onValueChanged(nl, nh, true)
205
+ currentOnValueChanged(nl, nh, true)
199
206
  }
200
207
  }
201
208
  }
@@ -8,8 +8,10 @@ import androidx.compose.foundation.gestures.detectHorizontalDragGestures
8
8
  import androidx.compose.foundation.layout.*
9
9
  import androidx.compose.foundation.shape.RoundedCornerShape
10
10
  import androidx.compose.runtime.Composable
11
+ import androidx.compose.runtime.getValue
11
12
  import androidx.compose.runtime.remember
12
13
  import androidx.compose.runtime.rememberCoroutineScope
14
+ import androidx.compose.runtime.rememberUpdatedState
13
15
  import androidx.compose.ui.Alignment
14
16
  import androidx.compose.ui.Modifier
15
17
  import androidx.compose.ui.draw.clip
@@ -62,6 +64,12 @@ fun Swipe(
62
64
 
63
65
  val offsetX = remember { Animatable(0f) }
64
66
 
67
+ // pointerInput is keyed on the action counts only, so read the latest callbacks.
68
+ val currentOnOpen by rememberUpdatedState(onSwipeableOpen)
69
+ val currentOnClose by rememberUpdatedState(onSwipeableClose)
70
+ val currentOnLeftOpen by rememberUpdatedState(onSwipeableLeftOpen)
71
+ val currentOnRightOpen by rememberUpdatedState(onSwipeableRightOpen)
72
+
65
73
  val shape = remember(itemRadius) {
66
74
  if (itemRadius > 0.dp) RoundedCornerShape(itemRadius) else RoundedCornerShape(0.dp)
67
75
  }
@@ -71,17 +79,17 @@ fun Swipe(
71
79
  val current = offsetX.value
72
80
  val target = when {
73
81
  current > leftWidthPx * 0.5f && left.isNotEmpty() -> {
74
- onSwipeableLeftOpen()
75
- onSwipeableOpen()
82
+ currentOnLeftOpen()
83
+ currentOnOpen()
76
84
  leftWidthPx
77
85
  }
78
86
  current < -rightWidthPx * 0.5f && right.isNotEmpty() -> {
79
- onSwipeableRightOpen()
80
- onSwipeableOpen()
87
+ currentOnRightOpen()
88
+ currentOnOpen()
81
89
  -rightWidthPx
82
90
  }
83
91
  else -> {
84
- onSwipeableClose()
92
+ currentOnClose()
85
93
  0f
86
94
  }
87
95
  }
@@ -43,9 +43,10 @@ fun Switch(
43
43
 
44
44
  val (circleBackgroundColor, backgroundColor) = colors
45
45
 
46
- // Memoize automation ID to avoid string concatenation on every render
46
+ // Both params default to "", so blank must fall back too or the id is always empty.
47
47
  val automationIdValue = remember(accessibilityId, title, value) {
48
- accessibilityId ?: "toggle_${title ?: "unknown"}_$value"
48
+ accessibilityId?.takeIf { it.isNotEmpty() }
49
+ ?: "toggle_${title?.takeIf { it.isNotEmpty() } ?: "unknown"}_$value"
49
50
  }
50
51
 
51
52
  // Memoize alignment calculation
@@ -10,9 +10,10 @@ import androidx.compose.foundation.shape.RoundedCornerShape
10
10
  import androidx.compose.runtime.*
11
11
  import androidx.compose.ui.Alignment
12
12
  import androidx.compose.ui.Modifier
13
+ import androidx.compose.ui.draw.clipToBounds
14
+ import androidx.compose.ui.geometry.Offset
13
15
  import androidx.compose.ui.graphics.Color
14
16
  import androidx.compose.ui.graphics.drawscope.withTransform
15
- import androidx.compose.ui.graphics.drawscope.DrawScope
16
17
  import androidx.compose.ui.graphics.Path
17
18
  import androidx.compose.ui.layout.onGloballyPositioned
18
19
  import androidx.compose.ui.layout.positionInParent
@@ -73,6 +74,8 @@ fun TabView(
73
74
  } else null
74
75
 
75
76
  val coroutineScope = rememberCoroutineScope()
77
+ // The collector below outlives recompositions; read the latest callback, not the first one.
78
+ val currentOnPageSelected by rememberUpdatedState(onPageSelected)
76
79
 
77
80
  // Keep selectedIndex in sync when user swipes the pager
78
81
  if (pagerState != null) {
@@ -80,7 +83,7 @@ fun TabView(
80
83
  snapshotFlow { pagerState.currentPage }.collect { page ->
81
84
  if (!lazyRendered.contains(page)) lazyRendered.add(page)
82
85
  selectedIndex = page
83
- onPageSelected(page)
86
+ currentOnPageSelected(page)
84
87
  }
85
88
  }
86
89
  }
@@ -362,7 +365,7 @@ private fun CardTabBar(
362
365
  }
363
366
  }
364
367
 
365
- val active = tabs[selectedIndex]
368
+ val active = tabs.getOrNull(selectedIndex) ?: return@BoxWithConstraints
366
369
  Row(
367
370
  modifier = Modifier
368
371
  // Pinned to the strip's BOTTOM edge so the two always end on the same
@@ -415,7 +418,8 @@ private val CARD_TAB_HEIGHT = 40.dp
415
418
  private val CARD_TAB_ACTIVE_HEIGHT = 44.dp
416
419
  @Composable
417
420
  private fun CardTabEar(width: Dp, mirrored: Boolean, color: Color, nudge: Dp) {
418
- Canvas(modifier = Modifier.offset(x = nudge).width(width).height(CARD_TAB_ACTIVE_HEIGHT)) {
421
+ // Clipped like react-kits' Svg, whose 28-wide path is cut at a 22pt `pathWidth`.
422
+ Canvas(modifier = Modifier.offset(x = nudge).width(width).height(CARD_TAB_ACTIVE_HEIGHT).clipToBounds()) {
419
423
  val path = Path().apply {
420
424
  moveTo(0f, 0f)
421
425
  lineTo(0.60765f, 0f)
@@ -425,8 +429,12 @@ private fun CardTabEar(width: Dp, mirrored: Boolean, color: Color, nudge: Dp) {
425
429
  lineTo(0f, 44f)
426
430
  close()
427
431
  }
428
- val draw: DrawScope.() -> Unit = { drawPath(path = path, color = color) }
429
- if (mirrored) withTransform({ scale(-1f, 1f) }) { draw() } else draw()
432
+ // Path coordinates are dp (react-kits viewBox), but DrawScope draws in px.
433
+ val pxPerDp = density
434
+ withTransform({
435
+ if (mirrored) scale(-1f, 1f)
436
+ scale(pxPerDp, pxPerDp, pivot = Offset.Zero)
437
+ }) { drawPath(path = path, color = color) }
430
438
  }
431
439
  }
432
440
 
@@ -30,6 +30,22 @@ enum class TagColor(val backgroundColor: Color, val textColor: Color) {
30
30
  Grey(Colors.black_04, Colors.black_12),
31
31
  }
32
32
 
33
+ // RN only accepts the `_03` palette as a custom tag color.
34
+ private val tagCustomColors = listOf(
35
+ Colors.black_03,
36
+ Colors.pink_03,
37
+ Colors.violet_03,
38
+ Colors.indigo_03,
39
+ Colors.blue_03,
40
+ Colors.mint_03,
41
+ Colors.green_03,
42
+ Colors.lime_03,
43
+ Colors.yellow_03,
44
+ Colors.gold_03,
45
+ Colors.orange_03,
46
+ Colors.red_03,
47
+ )
48
+
33
49
  @Composable
34
50
  fun Tag(
35
51
  label: String = "Label",
@@ -38,27 +54,9 @@ fun Tag(
38
54
  size: TagSize = TagSize.Large,
39
55
  customColor: Color? = null,
40
56
  ) {
41
- val primaryColors = listOf(
42
- Color(0xFFF0F0F0),
43
- Color(0xFFEB2F96),
44
- Color(0xFF962AF0),
45
- Color(0xFF4E4BFF),
46
- Color(0xFF007AFF),
47
- Color(0xFF13C2C2),
48
- Color(0xFF34C759),
49
- Color(0xFFA0D911),
50
- Color(0xFFFFCC00),
51
- Color(0xFFFA8C16),
52
- Color(0xFFFA541C),
53
- Color(0xFFF5222D)
54
- )
55
- var tagColor = color.backgroundColor
56
- var labelColor = color.textColor
57
-
58
- if (primaryColors.contains(customColor)) {
59
- tagColor = color.backgroundColor
60
- labelColor = color.textColor
61
- }
57
+ val custom = customColor?.takeIf { it in tagCustomColors }
58
+ val tagColor = custom ?: color.backgroundColor
59
+ val labelColor = if (custom != null) Colors.black_01 else color.textColor
62
60
 
63
61
  Box(
64
62
  modifier = Modifier
@@ -87,8 +87,8 @@ fun Text(
87
87
  color ?: theme.colors.text.default
88
88
  }
89
89
 
90
- // Memoize style copy operation
91
- val optimizedStyle = remember(style) {
90
+ // lineHeight is a key: it is scaled, so it changes with the font scale even when style doesn't.
91
+ val optimizedStyle = remember(style, lineHeight) {
92
92
  style.copy(
93
93
  fontWeight = null,
94
94
  lineHeight = if (lineHeight.value.isFinite() && lineHeight.value > 0) lineHeight else style.lineHeight
@@ -21,6 +21,7 @@ import androidx.compose.ui.unit.*
21
21
  import androidx.compose.ui.window.Popup
22
22
  import androidx.compose.ui.window.PopupPositionProvider
23
23
  import androidx.compose.ui.window.PopupProperties
24
+ import kotlinx.coroutines.flow.drop
24
25
  import vn.momo.kits.application.IsShowBaseLineDebug
25
26
  import vn.momo.kits.const.*
26
27
  import vn.momo.kits.modifier.activeOpacityClickable
@@ -147,8 +148,10 @@ fun Tooltip(
147
148
  val density = LocalDensity.current
148
149
  val offsetPx = with(density) { (TOOLTIP_OFFSET + ARROW_SIZE).roundToPx() }
149
150
 
150
- LaunchedEffect(state.isVisible) {
151
- onVisibleChange?.invoke(state.isVisible)
151
+ val currentOnVisibleChange by rememberUpdatedState(onVisibleChange)
152
+ LaunchedEffect(state) {
153
+ // changes only, like RN: the initial value is not reported
154
+ snapshotFlow { state.isVisible }.drop(1).collect { currentOnVisibleChange?.invoke(it) }
152
155
  }
153
156
 
154
157
  Box(modifier = modifier) {
@@ -128,11 +128,13 @@ fun getHours(hourMode: Int): List<String> {
128
128
  * Get a list of minutes
129
129
  */
130
130
  fun getMinutes(interval: Int): List<String> {
131
+ // A non-positive interval would never reach 60.
132
+ val step = interval.coerceAtLeast(1)
131
133
  val minutes = mutableListOf<String>()
132
134
  var min = 0
133
135
  while (min < 60) {
134
136
  minutes.add(paddingNum(min))
135
- min += interval
137
+ min += step
136
138
  }
137
139
  return minutes
138
140
  }
@@ -154,22 +154,14 @@ internal fun WheelPickerItem(
154
154
  .fillMaxWidth()
155
155
  .height(itemHeight)
156
156
  .graphicsLayer {
157
- val firstVisibleIndex = listState.firstVisibleItemIndex
158
- val visibleItemOffset = listState.firstVisibleItemScrollOffset
159
-
160
- val viewportHeight = 5 * itemHeight.value
161
- val itemHeightValue = itemHeight.value
162
-
163
- val absoluteOffset =
164
- (index - firstVisibleIndex) * itemHeightValue - (visibleItemOffset / itemHeightValue)
165
- val relativePosition = absoluteOffset / viewportHeight
166
-
167
- val distance = abs(2 - relativePosition)
168
-
157
+ // Rows away from the centre slot (two below the first visible row); RN fades 1 → 0.8 → 0.4.
158
+ val distance = abs(
159
+ index - listState.firstVisibleItemIndex -
160
+ listState.firstVisibleItemScrollOffset / itemHeight.toPx() - 2
161
+ )
169
162
  alpha = when {
170
- distance <= 1 -> 1f
171
- distance <= 2 -> 0.8f
172
- distance <= 3 -> 0.4f
163
+ distance <= 1f -> 1f - 0.2f * distance
164
+ distance <= 2f -> 0.8f - 0.4f * (distance - 1f)
173
165
  else -> 0.4f
174
166
  }
175
167
  },
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
4
4
  import androidx.compose.foundation.layout.Arrangement
5
5
  import androidx.compose.foundation.layout.Box
6
6
  import androidx.compose.foundation.layout.BoxWithConstraints
7
+ import androidx.compose.foundation.layout.ExperimentalLayoutApi
7
8
  import androidx.compose.foundation.layout.FlowRow
8
9
  import androidx.compose.foundation.layout.PaddingValues
9
10
  import androidx.compose.foundation.layout.fillMaxWidth
@@ -14,6 +15,7 @@ import androidx.compose.runtime.CompositionLocalProvider
14
15
  import androidx.compose.ui.Modifier
15
16
  import androidx.compose.ui.draw.clip
16
17
  import androidx.compose.ui.draw.shadow
18
+ import androidx.compose.ui.graphics.Color
17
19
  import androidx.compose.ui.unit.dp
18
20
  import vn.momo.kits.components.Image
19
21
  import vn.momo.kits.const.AppTheme
@@ -21,30 +23,30 @@ import vn.momo.kits.const.Radius
21
23
  import vn.momo.kits.const.Spacing
22
24
 
23
25
  @Composable
24
- internal fun Card(
26
+ @OptIn(ExperimentalLayoutApi::class)
27
+ fun Card(
25
28
  useShadow: Boolean = false,
26
29
  backgroundImage: String? = null,
30
+ backgroundColor: Color? = null,
27
31
  contentPadding: PaddingValues = PaddingValues(Spacing.M),
28
32
  modifier: Modifier = Modifier,
29
33
  content: @Composable () -> Unit,
30
34
  ) {
31
- val columns = 12
32
- val gutter = 8.dp
35
+ val columns = gridColumns()
36
+ val gap = Spacing.S
33
37
  val shape = RoundedCornerShape(Radius.M)
34
- var surface = modifier.fillMaxWidth().padding(horizontal = 12.dp)
35
- if (useShadow) surface = surface.shadow(4.dp, shape)
36
- surface = surface.clip(shape)
37
- if (backgroundImage == null) surface = surface.background(AppTheme.current.colors.background.surface)
38
+ var surface = modifier.fillMaxWidth().padding(horizontal = Spacing.M)
39
+ if (useShadow) surface = surface.shadow(6.dp, shape)
40
+ surface = surface.clip(shape).background(backgroundColor ?: AppTheme.current.colors.background.surface)
38
41
  Box(modifier = surface) {
39
42
  if (backgroundImage != null) {
40
43
  Image(source = backgroundImage, modifier = Modifier.matchParentSize())
41
44
  }
42
45
  BoxWithConstraints(modifier = Modifier.fillMaxWidth().padding(contentPadding)) {
43
- val sizePerSpan = (maxWidth - gutter * (columns - 1)) / columns - 1.dp / columns
44
- CompositionLocalProvider(LocalGridContext provides GridContext(columns, gutter, sizePerSpan)) {
46
+ CompositionLocalProvider(LocalGridContext provides gridContext(constraints.maxWidth, columns, gap)) {
45
47
  FlowRow(
46
- horizontalArrangement = Arrangement.spacedBy(gutter),
47
- verticalArrangement = Arrangement.spacedBy(gutter),
48
+ horizontalArrangement = Arrangement.spacedBy(gap),
49
+ verticalArrangement = Arrangement.spacedBy(gap),
48
50
  ) { content() }
49
51
  }
50
52
  }
@@ -6,33 +6,45 @@ import androidx.compose.foundation.layout.width
6
6
  import androidx.compose.runtime.Composable
7
7
  import androidx.compose.runtime.compositionLocalOf
8
8
  import androidx.compose.ui.Modifier
9
+ import androidx.compose.ui.platform.LocalDensity
9
10
  import androidx.compose.ui.unit.Dp
10
11
  import androidx.compose.ui.unit.dp
12
+ import kotlin.math.floor
11
13
 
12
14
  internal data class GridContext(
13
15
  val numberOfColumns: Int = 12,
14
- val gutter: Dp = 12.dp,
15
- val sizePerSpan: Dp = 0.dp,
16
+ val gutterPx: Int = 0,
17
+ val columnPx: Float = 0f,
18
+ val density: Float = 1f,
16
19
  ) {
17
- fun sizeForSpan(span: Int): Dp = sizePerSpan * span + gutter * (span - 1)
20
+ fun sizeForSpan(span: Int): Dp = (floor(columnPx * span + gutterPx * (span - 1)) / density).dp
18
21
  }
19
22
 
20
23
  internal val LocalGridContext = compositionLocalOf<GridContext?> { null }
21
24
 
22
25
  @Composable
23
- internal fun Item(
24
- widthSpan: Int = 12,
26
+ internal fun gridColumns(): Int = if (getScreenLayout().contentWidth > 600.dp) 24 else 12
27
+
28
+ @Composable
29
+ internal fun gridContext(maxWidthPx: Int, columns: Int, gap: Dp): GridContext {
30
+ val density = LocalDensity.current
31
+ val gutterPx = with(density) { gap.roundToPx() }
32
+ return GridContext(columns, gutterPx, (maxWidthPx - gutterPx * (columns - 1)).toFloat() / columns, density.density)
33
+ }
34
+
35
+ @Composable
36
+ fun Item(
37
+ widthSpan: Int? = null,
25
38
  heightSpan: Int = 0,
26
39
  modifier: Modifier = Modifier,
40
+ contentModifier: Modifier = Modifier,
27
41
  content: @Composable () -> Unit,
28
42
  ) {
29
43
  val grid = LocalGridContext.current
30
44
  var m = modifier
31
45
  if (grid != null) {
32
- m = m.width(grid.sizeForSpan(widthSpan))
46
+ m = m.width(grid.sizeForSpan(widthSpan ?: grid.numberOfColumns))
33
47
  if (heightSpan > 0) m = m.height(grid.sizeForSpan(heightSpan))
34
48
  }
35
- // A Column (not a Box) so multiple children flow VERTICALLY instead of stacking
36
- // on top of each other — matches the React Item's flex-column content.
37
- Column(modifier = m) { content() }
49
+ Column(modifier = m.then(contentModifier)) { content() }
38
50
  }