@momo-kits/native-kits 0.167.1-duo-search.1-debug → 0.167.1-duo-search.2-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 (141) hide show
  1. package/CLAUDE.md +4 -1
  2. package/compose/build.gradle.kts +1 -1
  3. package/compose/compose.podspec +1 -1
  4. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +134 -51
  5. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  6. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +11 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  33. package/gradle.properties +1 -1
  34. package/ios/Application/ApplicationEnvironment.swift +11 -0
  35. package/ios/Application/ComposeApi.swift +2 -1
  36. package/ios/Application/{Components.swift → LegacyHeader.swift} +6 -62
  37. package/ios/Application/Screen.swift +3 -2
  38. package/ios/Avatar/Avatar.swift +4 -3
  39. package/ios/Badge/Badge.swift +6 -4
  40. package/ios/Badge/BadgeDot.swift +1 -1
  41. package/ios/Badge/BadgeRibbon.swift +0 -1
  42. package/ios/Button/Button.swift +15 -12
  43. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +12 -20
  44. package/ios/Carousel/Carousel.swift +29 -12
  45. package/ios/Checkbox/Checkbox.swift +7 -3
  46. package/ios/Chip/Chip.swift +5 -4
  47. package/ios/Collapse/Collapse.swift +70 -19
  48. package/ios/Colors+Radius+Spacing/Colors.swift +9 -0
  49. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  50. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  51. package/ios/Divider/Divider.swift +37 -4
  52. package/ios/Extensions/Color++.swift +18 -0
  53. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  54. package/ios/Icon/Icon.swift +7 -1
  55. package/ios/IconButton/IconButton.swift +15 -7
  56. package/ios/Image/Image.swift +7 -34
  57. package/ios/Information/Information.swift +27 -9
  58. package/ios/Input/ErrorView.swift +2 -2
  59. package/ios/Input/Input.swift +160 -147
  60. package/ios/Input/InputPhoneNumber.swift +12 -9
  61. package/ios/Input/InputSearch.swift +102 -75
  62. package/ios/Input/InputTextArea.swift +128 -145
  63. package/ios/InputDropDown/InputDropDown.swift +10 -7
  64. package/ios/InputMoney/InputMoney.swift +33 -11
  65. package/ios/InputOTP/InputOTP.swift +22 -12
  66. package/ios/Layout/Card.swift +50 -0
  67. package/ios/Layout/GridContext.swift +123 -3
  68. package/ios/Layout/Item.swift +5 -4
  69. package/ios/Layout/Section.swift +4 -100
  70. package/ios/Loader/Loader.swift +5 -9
  71. package/ios/Lottie/LottieView.swift +10 -0
  72. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +65 -9
  73. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTab.swift +60 -34
  74. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +11 -6
  75. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  76. package/ios/Navigation/Component/Header.swift +493 -0
  77. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  78. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  79. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  80. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  81. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  82. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +20 -23
  83. package/ios/{Application/Navigation/component → Navigation/Component}/SideBar.swift +49 -28
  84. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  85. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  86. package/ios/Navigation/Navigation.swift +190 -0
  87. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +46 -41
  88. package/ios/{Application/Navigation → Navigation}/Navigator.swift +65 -9
  89. package/ios/{Application → Navigation}/StackScreen.swift +236 -103
  90. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  91. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  92. package/ios/Pagination/PaginationScroll.swift +2 -2
  93. package/ios/Popup/PopupDisplay.swift +79 -159
  94. package/ios/Popup/PopupInput.swift +10 -13
  95. package/ios/Popup/PopupLayout.swift +97 -2
  96. package/ios/Popup/PopupNotify.swift +27 -55
  97. package/ios/Popup/PopupPromotion.swift +22 -50
  98. package/ios/PopupView/FullscreenPopup.swift +13 -19
  99. package/ios/PopupView/PopupView.swift +1 -1
  100. package/ios/PopupView/Utils++.swift +0 -8
  101. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  102. package/ios/Radio/Radio.swift +2 -1
  103. package/ios/Rating/Rating.swift +9 -1
  104. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  105. package/ios/Skeleton/Skeleton.swift +22 -27
  106. package/ios/Slider/Slider.swift +16 -18
  107. package/ios/Stepper/Stepper.swift +6 -1
  108. package/ios/Steps/Steps.swift +8 -6
  109. package/ios/SuggestAction/SuggestAction.swift +5 -0
  110. package/ios/Swipeable/SwipeCell.swift +10 -6
  111. package/ios/Switch/Switch.swift +22 -20
  112. package/ios/TabView/TabView.swift +102 -21
  113. package/ios/Tag/Tag.swift +15 -4
  114. package/ios/Template/Logo/Logo.swift +2 -1
  115. package/ios/Template/TrustBanner/TrustBanner.swift +41 -27
  116. package/ios/Title/Title.swift +6 -17
  117. package/ios/Tooltip/Tooltip.swift +29 -14
  118. package/ios/Typography/Text.swift +61 -19
  119. package/ios/Uploader/Uploader.swift +3 -1
  120. package/ios/native-kits.podspec +2 -1
  121. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  122. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  123. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  124. package/package.json +1 -1
  125. package/ios/Application/HeaderRight.swift +0 -324
  126. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  127. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  128. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  129. package/ios/Application/Navigation/NavigatorManager.swift +0 -29
  130. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -54
  131. package/ios/Application/Navigation/component/Header.swift +0 -183
  132. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -84
  133. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  134. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  135. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  136. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -280
  137. /package/ios/{Application/Layout → Layout}/ScreenLayout.swift +0 -0
  138. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
  139. /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoBarCompression.swift +0 -0
  140. /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoTabBar.swift +0 -0
  141. /package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +0 -0
package/CLAUDE.md CHANGED
@@ -56,7 +56,10 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
56
56
 
57
57
  ## SwiftUI kit (`ios/`)
58
58
 
59
- - The pod's minimum is **iOS 15**, but the navigation shell (`NavigationContainer`, `Navigator`, `NavigatorManager`, `BottomTab`, overplay views) is **iOS 16+** (`@available(iOS 16.0, *)`) because it is built on `NavigationStack`. Keep components outside that shell iOS 15-compatible.
59
+ - **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*`, `MaxApi` and the deprecated `Screen` / `LegacyHeader` (Compose `application/`). Change a navigation piece on both sides together.
60
+ - **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders (hence `LegacyHeader.swift` next to `Navigation/Component/Header.swift`). New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
61
+ - Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
62
+ - The pod's minimum is **iOS 15**, but the navigation shell (`NavigationContainer`, `Navigator`, `HostBackGestureBridge`, `BottomTab`, overplay views) is **iOS 16+** (`@available(iOS 16.0, *)`) because it is built on `NavigationStack`. Keep components outside that shell iOS 15-compatible.
60
63
  - `present` opens one `fullScreenCover` that hosts its own `NavigationStack` (`Navigator.presentedPath`): pushes and pops act on the presented flow while it is up, and each overplay (modal / bottom sheet / snackbar) renders in the context it was shown over (`OverplayItem.presentedId`).
61
64
  - Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
62
65
  - Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
@@ -40,7 +40,7 @@ kotlin {
40
40
  }
41
41
 
42
42
  cocoapods {
43
- version = "0.167.1-duo-search.1-debug"
43
+ version = "0.167.1-duo-search.2-debug"
44
44
  summary = "IOS Shared module"
45
45
  homepage = "https://momo.vn"
46
46
  ios.deploymentTarget = "15.0"
@@ -1,6 +1,6 @@
1
1
  Pod::Spec.new do |spec|
2
2
  spec.name = 'compose'
3
- spec.version = '0.167.1-duo-search.1-debug'
3
+ spec.version = '0.167.1-duo-search.2-debug'
4
4
  spec.homepage = 'https://momo.vn'
5
5
  spec.source = { :http=> ''}
6
6
  spec.authors = ''
@@ -1,17 +1,37 @@
1
1
  package vn.momo.kits.application
2
2
 
3
3
  import androidx.annotation.FloatRange
4
- import androidx.compose.animation.animateContentSize
5
- import androidx.compose.foundation.*
4
+ import androidx.compose.foundation.ScrollState
5
+ import androidx.compose.foundation.background
6
+ import androidx.compose.foundation.border
6
7
  import androidx.compose.foundation.gestures.detectTapGestures
7
8
  import androidx.compose.foundation.interaction.FocusInteraction
8
9
  import androidx.compose.foundation.interaction.MutableInteractionSource
9
- import androidx.compose.foundation.layout.*
10
+ import androidx.compose.foundation.layout.Arrangement
11
+ import androidx.compose.foundation.layout.Box
12
+ import androidx.compose.foundation.layout.Row
13
+ import androidx.compose.foundation.layout.fillMaxSize
14
+ import androidx.compose.foundation.layout.padding
15
+ import androidx.compose.foundation.layout.size
16
+ import androidx.compose.foundation.rememberScrollState
10
17
  import androidx.compose.foundation.shape.CircleShape
11
18
  import androidx.compose.foundation.text.BasicTextField
12
19
  import androidx.compose.foundation.text.KeyboardActions
13
20
  import androidx.compose.foundation.text.KeyboardOptions
14
- import androidx.compose.runtime.*
21
+ import androidx.compose.foundation.verticalScroll
22
+ import androidx.compose.runtime.Composable
23
+ import androidx.compose.runtime.LaunchedEffect
24
+ import androidx.compose.runtime.MutableState
25
+ import androidx.compose.runtime.Stable
26
+ import androidx.compose.runtime.State
27
+ import androidx.compose.runtime.derivedStateOf
28
+ import androidx.compose.runtime.getValue
29
+ import androidx.compose.runtime.mutableStateOf
30
+ import androidx.compose.runtime.produceState
31
+ import androidx.compose.runtime.remember
32
+ import androidx.compose.runtime.rememberUpdatedState
33
+ import androidx.compose.runtime.snapshotFlow
34
+ import androidx.compose.runtime.staticCompositionLocalOf
15
35
  import androidx.compose.ui.Alignment
16
36
  import androidx.compose.ui.Modifier
17
37
  import androidx.compose.ui.composed
@@ -25,13 +45,25 @@ import androidx.compose.ui.graphics.Color
25
45
  import androidx.compose.ui.graphics.SolidColor
26
46
  import androidx.compose.ui.graphics.graphicsLayer
27
47
  import androidx.compose.ui.input.pointer.pointerInput
28
- import androidx.compose.ui.layout.*
48
+ import androidx.compose.ui.layout.Layout
49
+ import androidx.compose.ui.layout.Measurable
50
+ import androidx.compose.ui.layout.MeasurePolicy
51
+ import androidx.compose.ui.layout.MeasureResult
52
+ import androidx.compose.ui.layout.MeasureScope
53
+ import androidx.compose.ui.layout.Placeable
54
+ import androidx.compose.ui.layout.layoutId
29
55
  import androidx.compose.ui.platform.LocalDensity
30
56
  import androidx.compose.ui.platform.LocalFocusManager
31
57
  import androidx.compose.ui.platform.LocalSoftwareKeyboardController
32
58
  import androidx.compose.ui.text.TextStyle
33
59
  import androidx.compose.ui.text.style.TextOverflow
34
- import androidx.compose.ui.unit.*
60
+ import androidx.compose.ui.unit.Constraints
61
+ import androidx.compose.ui.unit.Dp
62
+ import androidx.compose.ui.unit.IntOffset
63
+ import androidx.compose.ui.unit.LayoutDirection
64
+ import androidx.compose.ui.unit.dp
65
+ import androidx.compose.ui.unit.lerp
66
+ import androidx.compose.ui.unit.sp
35
67
  import kotlinx.coroutines.flow.collectLatest
36
68
  import kotlinx.coroutines.flow.mapNotNull
37
69
  import vn.momo.kits.components.Icon
@@ -41,15 +73,21 @@ import vn.momo.kits.const.Colors
41
73
  import vn.momo.kits.const.Spacing
42
74
  import vn.momo.kits.const.Typography
43
75
  import vn.momo.kits.const.getFontFamily
76
+ import vn.momo.kits.layout.DuoMetrics
77
+ import vn.momo.kits.layout.ScreenLayout
78
+ import vn.momo.kits.layout.SideBarEdge
79
+ import vn.momo.kits.layout.getScreenLayout
80
+ import vn.momo.kits.utils.horizontalSafeAreaPadding
44
81
  import vn.momo.kits.modifier.kitsAutomationId
45
82
  import vn.momo.kits.modifier.noFeedbackClickable
46
83
  import vn.momo.kits.modifier.setAutomationId
47
84
  import vn.momo.kits.navigation.component.HEADER_FONT_FAMILY
48
85
  import vn.momo.kits.navigation.component.HEADER_HEIGHT
86
+ import vn.momo.kits.navigation.component.SideBarButton
49
87
  import vn.momo.kits.utils.getAppStatusBarHeight
50
- import vn.momo.kits.utils.horizontalSafeAreaPadding
51
88
  import kotlin.math.max
52
89
  import kotlin.math.roundToInt
90
+ import vn.momo.kits.navigation.component.getHeaderColor as getNavigationHeaderColor
53
91
 
54
92
 
55
93
  data class AnimationOption(
@@ -117,8 +155,7 @@ fun LiteScreen(
117
155
  scrollState = finalScrollState,
118
156
  modifier = Modifier
119
157
  .fillMaxSize()
120
- .hideKeyboardOnTap()
121
- .horizontalSafeAreaPadding(),
158
+ .hideKeyboardOnTap(),
122
159
  contentModifier = contentModifier,
123
160
  verticalArrangement = verticalArrangement,
124
161
  horizontalAlignment = horizontalAlignment,
@@ -171,6 +208,8 @@ private fun LiteScreenLayout(
171
208
  content: @Composable () -> Unit,
172
209
  ) {
173
210
  val statusBarHeight = getAppStatusBarHeight()
211
+ val screenLayout = getScreenLayout()
212
+ val applySafeAreaPadding = screenLayout.sideBar != null
174
213
  val isHeaderNone = remember(headerType) {
175
214
  headerType == HeaderType.NONE
176
215
  }
@@ -232,6 +271,12 @@ private fun LiteScreenLayout(
232
271
  val animationOpacityModifier = Modifier.graphicsLayer {
233
272
  alpha = animationOption?.animateOpacity ?: 1f
234
273
  }
274
+ val sideBarColor = getNavigationHeaderColor(
275
+ vn.momo.kits.navigation.component.HeaderType.Default(),
276
+ 1f,
277
+ null,
278
+ Colors.black_17,
279
+ )
235
280
  val titleModifier = Modifier
236
281
  .kitsAutomationId("title_navigation_header")
237
282
  .layoutId(HeaderId.TITLE_ID)
@@ -243,6 +288,8 @@ private fun LiteScreenLayout(
243
288
  }
244
289
 
245
290
  val policy = remember(
291
+ screenLayout,
292
+ applySafeAreaPadding,
246
293
  useAnimationSearch,
247
294
  isHeaderNone,
248
295
  isHeaderExtend,
@@ -255,6 +302,8 @@ private fun LiteScreenLayout(
255
302
  horizontalAlignment,
256
303
  ) {
257
304
  LiteScreenLayoutPolicy(
305
+ screenLayout = screenLayout,
306
+ applySafeAreaPadding = applySafeAreaPadding,
258
307
  useAnimationSearch = useAnimationSearch,
259
308
  isHeaderNone = isHeaderNone,
260
309
  isHeaderExtend = isHeaderExtend,
@@ -269,13 +318,13 @@ private fun LiteScreenLayout(
269
318
  }
270
319
 
271
320
  Layout(
272
- modifier = modifier
273
- .animateContentSize(),
321
+ modifier = modifier,
274
322
  content = {
275
323
  Box(
276
324
  modifier = animationOpacityModifier
277
325
  .layoutId(HeaderId.CONTENT_ID)
278
- .then(contentModifier),
326
+ .then(contentModifier)
327
+ .then(if (applySafeAreaPadding) Modifier.horizontalSafeAreaPadding() else Modifier),
279
328
  contentAlignment = Alignment.TopCenter,
280
329
  ) {
281
330
  content()
@@ -311,28 +360,44 @@ private fun LiteScreenLayout(
311
360
  }
312
361
 
313
362
  if (!isHeaderNone && onGoBack != null) {
314
- val coreModifier = Modifier
315
- .layoutId(HeaderId.BACK_ID)
316
- .then(animationOpacityModifier)
317
- .clip(CircleShape)
318
- .noFeedbackClickable(onClick = onGoBack)
319
- .setAutomationId("btn_navigation_back")
320
- inputSearchProps?.customBackIcon?.invoke(coreModifier) ?: Box(
321
- modifier = coreModifier
322
- .size(28.dp)
323
- .border(
324
- width = 0.2.dp,
325
- color = headerColor.borderColor,
326
- shape = CircleShape,
327
- )
328
- .background(color = headerColor.backgroundButton)
329
- .padding(Spacing.XS),
330
- ) {
331
- Icon(
332
- source = "arrow-back",
333
- color = headerColor.tintIconColor,
334
- size = 20.dp,
335
- )
363
+ when {
364
+ screenLayout.sideBar != null -> {
365
+ Box(
366
+ modifier = Modifier.layoutId(HeaderId.BACK_ID),
367
+ ) {
368
+ SideBarButton(
369
+ icon = "arrow-back",
370
+ headerColor = sideBarColor,
371
+ onClick = onGoBack,
372
+ automationId = "btn_navigation_back",
373
+ )
374
+ }
375
+ }
376
+ else -> {
377
+ val coreModifier = Modifier
378
+ .layoutId(HeaderId.BACK_ID)
379
+ .then(animationOpacityModifier)
380
+ .clip(CircleShape)
381
+ .noFeedbackClickable(onClick = onGoBack)
382
+ .setAutomationId("btn_navigation_back")
383
+ inputSearchProps?.customBackIcon?.invoke(coreModifier) ?: Box(
384
+ modifier = coreModifier
385
+ .size(28.dp)
386
+ .border(
387
+ width = 0.2.dp,
388
+ color = headerColor.borderColor,
389
+ shape = CircleShape,
390
+ )
391
+ .background(color = headerColor.backgroundButton)
392
+ .padding(Spacing.XS),
393
+ ) {
394
+ Icon(
395
+ source = "arrow-back",
396
+ color = headerColor.tintIconColor,
397
+ size = 20.dp,
398
+ )
399
+ }
400
+ }
336
401
  }
337
402
  }
338
403
 
@@ -376,6 +441,8 @@ private fun LiteScreenLayout(
376
441
  }
377
442
 
378
443
  private class LiteScreenLayoutPolicy(
444
+ private val screenLayout: ScreenLayout,
445
+ private val applySafeAreaPadding: Boolean,
379
446
  private val useAnimationSearch: Boolean,
380
447
  private val isHeaderNone: Boolean,
381
448
  private val isHeaderExtend: Boolean,
@@ -409,10 +476,13 @@ private class LiteScreenLayoutPolicy(
409
476
 
410
477
  val contentMeasurable = measurables.find { it.layoutId == HeaderId.CONTENT_ID }
411
478
 
479
+ val insetLeft = if (applySafeAreaPadding) screenLayout.insetLeft.roundToPx() else 0
480
+ val insetRight = if (applySafeAreaPadding) screenLayout.insetRight.roundToPx() else 0
481
+ val safeWidth = (constraints.maxWidth - insetLeft - insetRight).coerceAtLeast(0)
412
482
  val realConstraints = constraints.copy(
413
483
  minWidth = 0,
414
484
  minHeight = 0,
415
- maxWidth = (constraints.maxWidth - spacing12 * 2)
485
+ maxWidth = (safeWidth - spacing12 * 2)
416
486
  .coerceAtLeast(0),
417
487
  )
418
488
  val backIconPlaceable =
@@ -423,9 +493,10 @@ private class LiteScreenLayoutPolicy(
423
493
  maxWidth = realConstraints.maxWidth / 2,
424
494
  )
425
495
  )
496
+ val headerBackWidth = if (screenLayout.sideBar == null) backIconPlaceable.safeWidth else 0
426
497
  val baseInputSearchConstraints = if (isHeaderExtend) {
427
498
  val minWidth =
428
- if (useAnimationSearch) realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
499
+ if (useAnimationSearch) realConstraints.maxWidth - headerBackWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
429
500
  else realConstraints.maxWidth
430
501
  realConstraints.copy(
431
502
  maxWidth = (realConstraints.maxWidth * (1 - scrollPercent)).toInt()
@@ -433,7 +504,7 @@ private class LiteScreenLayoutPolicy(
433
504
  )
434
505
  } else {
435
506
  var spaceConsumed = 0
436
- if (backIconPlaceable.safeWidth != 0) spaceConsumed += backIconPlaceable.safeWidth + spaceBetween
507
+ if (headerBackWidth != 0) spaceConsumed += headerBackWidth + spaceBetween
437
508
  if (headerRightPlaceable.safeWidth != 0) spaceConsumed += headerRightPlaceable.safeWidth + spaceBetween
438
509
  realConstraints.copy(
439
510
  maxWidth = realConstraints.maxWidth - spaceConsumed
@@ -444,7 +515,7 @@ private class LiteScreenLayoutPolicy(
444
515
  ?.measure(inputSearchConstraints)
445
516
  val titlePlaceable = measurables.find { it.layoutId == HeaderId.TITLE_ID }?.measure(
446
517
  constraints = realConstraints.copy(
447
- maxWidth = realConstraints.maxWidth - backIconPlaceable.safeWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
518
+ maxWidth = realConstraints.maxWidth - headerBackWidth - headerRightPlaceable.safeWidth - spaceBetween * 2
448
519
  )
449
520
  )
450
521
 
@@ -466,6 +537,7 @@ private class LiteScreenLayoutPolicy(
466
537
 
467
538
  val layoutWidth = constraints.maxWidth
468
539
  val layoutHeight = constraints.maxHeight
540
+ val backgroundInsetLeft = if (!applySafeAreaPadding && screenLayout.sideBar != null) screenLayout.insetLeft.roundToPx() else 0
469
541
  val contentHeight = (layoutHeight - headerHeight).coerceAtLeast(0)
470
542
  val contentPlaceable = contentMeasurable?.measure(
471
543
  constraints.copy(
@@ -502,7 +574,7 @@ private class LiteScreenLayoutPolicy(
502
574
  ) {
503
575
  val progress = animationOption?.progress?.value ?: 1f
504
576
 
505
- val startX = spacing12
577
+ val startX = insetLeft + spacing12
506
578
  val startY = headerY + statusBarPx
507
579
  var curX = startX
508
580
  var curY = startY
@@ -517,27 +589,38 @@ private class LiteScreenLayoutPolicy(
517
589
  )
518
590
 
519
591
  backgroundPlaceable?.place(
520
- x = 0,
592
+ x = -backgroundInsetLeft,
521
593
  y = headerY,
522
594
  )
523
595
 
524
596
  if (backIconPlaceable != null) {
525
- backIconPlaceable.place(
526
- x = startX,
527
- y = startY + backIconPlaceable.verticalCenterOffset(headerRowDefault),
528
- )
529
- curX += backIconPlaceable.safeWidth + spaceBetween
597
+ val sideBar = screenLayout.sideBar
598
+ if (sideBar == null) {
599
+ backIconPlaceable.place(
600
+ x = startX,
601
+ y = startY + backIconPlaceable.verticalCenterOffset(headerRowDefault),
602
+ )
603
+ curX += backIconPlaceable.safeWidth + spaceBetween
604
+ } else {
605
+ val x = when (sideBar.edge) {
606
+ SideBarEdge.LEFT -> insetLeft - screenLayout.insetLeft.roundToPx() + sideBar.width.roundToPx() - spacing12 - backIconPlaceable.width
607
+ SideBarEdge.RIGHT -> layoutWidth + (if (applySafeAreaPadding) 0 else screenLayout.insetRight.roundToPx()) - sideBar.width.roundToPx() + spacing12
608
+ }
609
+ backIconPlaceable.place(
610
+ x = x,
611
+ y = sideBar.top.roundToPx() + (DuoMetrics.controlSize.roundToPx() - backIconPlaceable.height) / 2,
612
+ )
613
+ }
530
614
  }
531
-
532
615
  headerRightPlaceable?.place(
533
- x = layoutWidth - spacing12 - headerRightPlaceable.safeWidth,
616
+ x = layoutWidth - insetRight - spacing12 - headerRightPlaceable.safeWidth,
534
617
  y = startY + headerRightPlaceable.verticalCenterOffset(headerRowDefault),
535
618
  )
536
619
 
537
620
  val titleOffset = IntOffset(
538
621
  x = if (titlePosition == TitlePosition.LEFT) curX
539
- else titlePlaceable.horizontalCenterOffset(
540
- space = layoutWidth,
622
+ else insetLeft + titlePlaceable.horizontalCenterOffset(
623
+ space = safeWidth - spacing12 * 2,
541
624
  layoutDirection = layoutDirection,
542
625
  ),
543
626
  y = startY + titlePlaceable.verticalCenterOffset(headerRowDefault),
@@ -545,7 +628,7 @@ private class LiteScreenLayoutPolicy(
545
628
 
546
629
  titlePlaceable?.place(titleOffset)
547
630
 
548
- if (backIconPlaceable != null || headerRightPlaceable != null || titlePlaceable != null) {
631
+ if ((backIconPlaceable != null && screenLayout.sideBar == null) || headerRightPlaceable != null || titlePlaceable != null) {
549
632
  curY += headerRowDefault
550
633
  }
551
634
 
@@ -555,7 +638,7 @@ private class LiteScreenLayoutPolicy(
555
638
  startY + inputSearchPlaceable.verticalCenterOffset(headerRowDefault)
556
639
  )
557
640
  IntOffset(
558
- x = startX + ((backIconPlaceable.safeWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
641
+ x = startX + ((headerBackWidth + spaceBetween) * (scrollPercent * 2f).coerceIn(
559
642
  0f, 1f
560
643
  )).toInt(),
561
644
  y = y,
@@ -17,31 +17,30 @@ import vn.momo.kits.application.IsShowBaseLineDebug
17
17
  import vn.momo.kits.const.*
18
18
  import vn.momo.kits.modifier.conditional
19
19
 
20
+ private val badgePrimaryColors = listOf(
21
+ Color(0xFFF0F0F0),
22
+ Color(0xFFEB2F96),
23
+ Color(0xFF962AF0),
24
+ Color(0xFF4E4BFF),
25
+ Color(0xFF007AFF),
26
+ Color(0xFF13C2C2),
27
+ Color(0xFF34C759),
28
+ Color(0xFFA0D911),
29
+ Color(0xFFFFCC00),
30
+ Color(0xFFFA8C16),
31
+ Color(0xFFFA541C),
32
+ Color(0xFFF5222D)
33
+ )
34
+
35
+ private val badgeNumberRegex = "^\\d+$".toRegex()
20
36
 
21
37
  @Composable
22
38
  fun Badge(label: String = "Label", backgroundColor: Color? = null, modifier: Modifier = Modifier) {
23
- val primaryColors = listOf(
24
- Color(0xFFF0F0F0),
25
- Color(0xFFEB2F96),
26
- Color(0xFF962AF0),
27
- Color(0xFF4E4BFF),
28
- Color(0xFF007AFF),
29
- Color(0xFF13C2C2),
30
- Color(0xFF34C759),
31
- Color(0xFFA0D911),
32
- Color(0xFFFFCC00),
33
- Color(0xFFFA8C16),
34
- Color(0xFFFA541C),
35
- Color(0xFFF5222D)
36
- )
37
-
38
- fun isNumber(label: String): Boolean {
39
- val numberRegex = "^\\d+$".toRegex()
40
- return numberRegex.matches(label)
41
- }
39
+ fun isNumber(label: String): Boolean = badgeNumberRegex.matches(label)
42
40
 
43
41
  fun formatTitle(label: String): String {
44
- if (isNumber(label) && label.toInt() > 99) {
42
+ // Digits too long for Int are well over 99; toInt() would throw.
43
+ if (isNumber(label) && (label.toIntOrNull() ?: Int.MAX_VALUE) > 99) {
45
44
  return "99+"
46
45
  }
47
46
  return label
@@ -54,7 +53,7 @@ fun Badge(label: String = "Label", backgroundColor: Color? = null, modifier: Mod
54
53
  AppTheme.current.colors.warning.primary
55
54
  }
56
55
 
57
- if (backgroundColor != null && primaryColors.contains(backgroundColor)) {
56
+ if (backgroundColor != null && badgePrimaryColors.contains(backgroundColor)) {
58
57
  badgeColor = backgroundColor
59
58
  }
60
59
  val scaleSize = scaleSize(16f)
@@ -146,7 +146,8 @@ fun RenderIcon(
146
146
  ) {
147
147
  val iconSize = remember(size) { getIconSize(size) }
148
148
  val margin = remember(size) { getIconSpace(size) }
149
- val color = if (useTintColor) textColor else Color.Unspecified
149
+ // null = untinted; Color.Unspecified would tint with a transparent color and hide the icon.
150
+ val color = if (useTintColor) textColor else null
150
151
 
151
152
  val modifier = Modifier.padding(
152
153
  start = if (isIconLeft) 0.dp else margin,
@@ -43,6 +43,8 @@ fun Carousel(
43
43
  val pagerState = rememberPagerState(initialPage = startPage) { virtualPageCount }
44
44
 
45
45
  val realIndex: (Int) -> Int = { page -> page % itemCount }
46
+ val currentOnSnapToItem by rememberUpdatedState(onSnapToItem)
47
+ val currentOnScrollIndexChanged by rememberUpdatedState(onScrollIndexChanged)
46
48
 
47
49
  var isUserScrolling by remember { mutableStateOf(false) }
48
50
 
@@ -70,8 +72,8 @@ fun Carousel(
70
72
  LaunchedEffect(pagerState) {
71
73
  snapshotFlow { pagerState.currentPage }.collect { page ->
72
74
  val real = realIndex(page)
73
- onSnapToItem(real)
74
- onScrollIndexChanged(real)
75
+ currentOnSnapToItem(real)
76
+ currentOnScrollIndexChanged(real)
75
77
  }
76
78
  }
77
79
 
@@ -77,7 +77,7 @@ fun Chip(
77
77
  horizontalArrangement = Arrangement.spacedBy(iconSpacing.dp)
78
78
  ) {
79
79
  if (iconLeft != null) {
80
- Icon(source = iconLeft, size = dims.iconSize.dp, color = leftTint)
80
+ Icon(source = iconLeft, size = iconSize.dp, color = leftTint)
81
81
  }
82
82
 
83
83
  if (!label.isNullOrEmpty()) {
@@ -41,7 +41,7 @@ fun Icon(
41
41
  }
42
42
  }
43
43
 
44
- val iconColor = remember(color) {
44
+ val iconColor = remember(source, color) {
45
45
  if (noThemeIcons.contains(source)) null else color
46
46
  }
47
47
 
@@ -90,7 +90,7 @@ fun Information(
90
90
  width = 0.5.dp,
91
91
  shape = RoundedCornerShape(Radius.S),
92
92
  color = borderColor
93
- ).background(color = backgroundColor)
93
+ ).background(color = backgroundColor, shape = RoundedCornerShape(Radius.S))
94
94
  .conditional(IsShowBaseLineDebug) {
95
95
  border(1.dp, Colors.blue_03)
96
96
  }
@@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
16
16
  import androidx.compose.ui.Modifier
17
17
  import androidx.compose.ui.focus.onFocusChanged
18
18
  import androidx.compose.ui.graphics.Color
19
+ import androidx.compose.ui.graphics.SolidColor
19
20
  import androidx.compose.ui.text.AnnotatedString
20
21
  import androidx.compose.ui.text.SpanStyle
21
22
  import androidx.compose.ui.text.TextStyle
@@ -214,7 +215,7 @@ fun Input(
214
215
 
215
216
  // Memoized color calculations
216
217
  val theme = AppTheme.current
217
- val colors = remember(disabled, theme) {
218
+ val colors = remember(disabled, theme, iconColor) {
218
219
  if (disabled) {
219
220
  val disabledColor = theme.colors.text.disable
220
221
  Triple(disabledColor, disabledColor, disabledColor)
@@ -241,8 +242,9 @@ fun Input(
241
242
  val lineHeight = 24.sp
242
243
  val scaleFontSize = scaleSize(fontSize)
243
244
  val scaleLineHeight = scaleSize(lineHeight)
245
+ val scaledHeight = scaleSize(size.values.height)
244
246
 
245
- val textStyle = remember(textColor, fontWeight) {
247
+ val textStyle = remember(textColor, fontWeight, scaleFontSize, scaleLineHeight) {
246
248
  TextStyle(
247
249
  color = textColor,
248
250
  fontSize = scaleFontSize,
@@ -268,7 +270,7 @@ fun Input(
268
270
 
269
271
  val passwordFontFamily = getFontFamily(AppTheme.current.font, FontWeight.Normal)
270
272
  val passwordFontSize = scaleSize(24.sp)
271
- val visualTransformation = remember(secureTextEntry, inputState.passHidden, passwordFontFamily) {
273
+ val visualTransformation = remember(secureTextEntry, inputState.passHidden, passwordFontFamily, passwordFontSize) {
272
274
  if (secureTextEntry && !inputState.passHidden)
273
275
  PasswordVisualTransformation(fontFamily = passwordFontFamily, fontSize = passwordFontSize)
274
276
  else
@@ -287,8 +289,9 @@ fun Input(
287
289
  textStyle = textStyle,
288
290
  visualTransformation = visualTransformation,
289
291
  keyboardOptions = keyboardOptionsConfig,
292
+ cursorBrush = SolidColor(theme.colors.primary),
290
293
  modifier = inputModifier
291
- .height(scaleSize(size.values.height.value).dp)
294
+ .height(scaledHeight)
292
295
  .onFocusChanged { focusState ->
293
296
  val wasFocused = inputState.isFocused
294
297
  inputState = inputState.copy(
@@ -313,7 +316,7 @@ fun Input(
313
316
  floatingIcon = floatingIcon,
314
317
  titleColor = floatingTitleColor,
315
318
  iconColor = floatingIconTintColor,
316
- offsetY = -size.values.height / 2,
319
+ offsetY = -scaledHeight / 2,
317
320
  required = required,
318
321
  )
319
322
 
@@ -359,7 +362,7 @@ fun Input(
359
362
  }
360
363
 
361
364
  // Clear button
362
- if (inputState.isFocused && text.value.isNotEmpty()) {
365
+ if (!readOnly && inputState.isFocused && text.value.isNotEmpty()) {
363
366
  Row(Modifier.padding(horizontal = Spacing.XS)) {
364
367
  Icon(
365
368
  source = "24_navigation_close_circle_full",
@@ -27,6 +27,7 @@ import androidx.compose.ui.Alignment
27
27
  import androidx.compose.ui.Modifier
28
28
  import androidx.compose.ui.focus.onFocusChanged
29
29
  import androidx.compose.ui.graphics.Color
30
+ import androidx.compose.ui.graphics.SolidColor
30
31
  import androidx.compose.ui.semantics.contentDescription
31
32
  import androidx.compose.ui.semantics.semantics
32
33
  import androidx.compose.ui.semantics.testTag
@@ -43,7 +44,9 @@ import vn.momo.kits.application.IsShowBaseLineDebug
43
44
  import vn.momo.kits.const.Colors
44
45
  import vn.momo.kits.const.Typography
45
46
  import vn.momo.kits.const.getFontFamily
47
+ import vn.momo.kits.const.scaleSize
46
48
  import vn.momo.kits.modifier.conditional
49
+ import vn.momo.kits.modifier.setAutomationId
47
50
 
48
51
  private const val MONEY_CURRENCY = "đ"
49
52
 
@@ -134,7 +137,6 @@ fun InputMoney(
134
137
  val current = if (digitsOf(field.text) == digits) field else moneyDisplay(digits).let {
135
138
  TextFieldValue(it, TextRange(caretFromDigitIndex(it, digits.length)))
136
139
  }
137
- var isBlurred = false
138
140
  val (textColor, placeholderColor, iconTintColor, floatingTitleColor, floatingIconTintColor) =
139
141
  getInputColors(disabled, iconColor, floatingValueColor, floatingIconColor)
140
142
 
@@ -153,19 +155,16 @@ fun InputMoney(
153
155
  value = current,
154
156
  textStyle = TextStyle(
155
157
  color = textColor,
156
- fontSize = 20.sp,
157
- lineHeight = 24.sp,
158
+ fontSize = scaleSize(20.sp),
159
+ lineHeight = scaleSize(24.sp),
158
160
  fontWeight = fontWeight.value,
159
161
  fontFamily = getFontFamily(AppTheme.current.font, fontWeight.value)
160
162
  ),
161
163
  keyboardOptions = KeyboardOptions.Default.copy(keyboardType = keyboardType),
164
+ cursorBrush = SolidColor(AppTheme.current.colors.primary),
162
165
  modifier = Modifier.height(size.values.height).onFocusChanged {
166
+ if (it.isFocused) onFocus() else if (isFocused) onBlur()
163
167
  isFocused = it.isFocused
164
- if (it.isFocused) {
165
- onFocus()
166
- }
167
- if (!it.isFocused && isBlurred) onBlur()
168
- if (it.isFocused && !isBlurred) isBlurred = true
169
168
  },
170
169
  onValueChange = { value ->
171
170
  if (value.text == current.text) {
@@ -223,7 +222,7 @@ fun InputMoney(
223
222
  color = leadingIconColor
224
223
  )
225
224
  }
226
- Box(Modifier.weight(9f).padding(start = 0.dp)) {
225
+ Box(Modifier.weight(9f), contentAlignment = Alignment.CenterStart) {
227
226
  if (text.value.isEmpty()) {
228
227
  Text(
229
228
  text = placeholder,
@@ -234,9 +233,9 @@ fun InputMoney(
234
233
  ),
235
234
  color = placeholderColor
236
235
  )
237
- } else {
238
- innerTextField()
239
236
  }
237
+ // Composed even when empty so the caret shows over the placeholder.
238
+ innerTextField()
240
239
  }
241
240
  if (isFocused && text.value.isNotEmpty()) {
242
241
  Row {
@@ -252,7 +251,7 @@ fun InputMoney(
252
251
  },
253
252
  interactionSource = remember { MutableInteractionSource() },
254
253
  indication = null
255
- )
254
+ ).setAutomationId("ic_clear")
256
255
  )
257
256
  }
258
257
  }