@momo-kits/native-kits 0.162.21 → 0.162.22-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 (186) hide show
  1. package/build.gradle.kts +11 -0
  2. package/compose/build.gradle.kts +190 -0
  3. package/compose/build.gradle.kts.backup +190 -0
  4. package/compose/compose.podspec +47 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +123 -0
  8. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  9. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  10. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  11. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  12. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +294 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +112 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +477 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +276 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +253 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +100 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +172 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +360 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +581 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +156 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +128 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +70 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +58 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +184 -0
  134. package/gradle/libs.versions.toml +69 -0
  135. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  136. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  137. package/gradle.properties +26 -0
  138. package/gradlew +252 -0
  139. package/gradlew.bat +94 -0
  140. package/ios/Application/ApplicationEnvironment.swift +4 -2
  141. package/ios/Application/MaxApi.swift +18 -0
  142. package/ios/Application/Navigation/NavigationContainer.swift +21 -3
  143. package/ios/Application/Navigation/Navigator.swift +50 -29
  144. package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
  145. package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
  146. package/ios/Application/StackScreen.swift +41 -0
  147. package/ios/Badge/BadgeRibbon.swift +80 -0
  148. package/ios/Button/Button.swift +67 -11
  149. package/ios/Carousel/Carousel.swift +16 -1
  150. package/ios/Checkbox/Checkbox.swift +14 -20
  151. package/ios/Chip/Chip.swift +35 -26
  152. package/ios/Collapse/Collapse.swift +11 -1
  153. package/ios/DateTimePicker/DateTimePicker.swift +4 -2
  154. package/ios/DateTimePicker/WheelPicker.swift +7 -3
  155. package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
  156. package/ios/Icon/Icon.swift +5 -1
  157. package/ios/IconButton/IconButton.swift +35 -7
  158. package/ios/Image/Image.swift +16 -3
  159. package/ios/Information/Information.swift +3 -1
  160. package/ios/Input/ErrorView.swift +3 -1
  161. package/ios/Input/Input.swift +31 -8
  162. package/ios/Input/InputPhoneNumber.swift +14 -3
  163. package/ios/Input/InputSearch.swift +7 -1
  164. package/ios/Input/InputTextArea.swift +78 -34
  165. package/ios/InputDropDown/InputDropDown.swift +4 -1
  166. package/ios/InputMoney/InputMoney.swift +16 -4
  167. package/ios/Popup/PopupDisplay.swift +3 -4
  168. package/ios/Popup/PopupNotify.swift +3 -4
  169. package/ios/Popup/PopupPromotion.swift +16 -8
  170. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  171. package/ios/Radio/Radio.swift +12 -13
  172. package/ios/Rating/Rating.swift +1 -1
  173. package/ios/Stepper/Stepper.swift +2 -2
  174. package/ios/Steps/Steps.swift +22 -6
  175. package/ios/SuggestAction/SuggestAction.swift +5 -3
  176. package/ios/Swipeable/SwipeCell.swift +5 -3
  177. package/ios/Switch/Switch.swift +53 -28
  178. package/ios/TabView/TabView.swift +18 -14
  179. package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
  180. package/ios/Tooltip/Tooltip.swift +3 -0
  181. package/ios/Uploader/Uploader.swift +0 -9
  182. package/ios/native-kits.podspec +1 -1
  183. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  184. package/ios-demo/README.md +120 -0
  185. package/package.json +1 -1
  186. package/settings.gradle.kts +64 -0
@@ -49,7 +49,11 @@ public struct Icon: View {
49
49
  if let uri = iconSources[source]?.uri {
50
50
  // Mirror Compose: certain icons (e.g. branded glyphs) are never tinted
51
51
  let resolvedColor = noThemeIcons.contains(source) ? nil : color
52
- ImageView(uri, color: resolvedColor).frame(width: size, height: size)
52
+ ImageView(uri, color: resolvedColor)
53
+ .frame(width: size, height: size)
54
+ // Mirrors Compose's auto-generated `contentDescription = "img|$iconUrl"`
55
+ // when no explicit label is provided.
56
+ .accessibilityLabel(accessibilityLabel ?? "img|\(uri)")
53
57
  }
54
58
  }
55
59
  }
@@ -5,13 +5,15 @@ import SwiftUI
5
5
 
6
6
  public enum IconSize {
7
7
  case large
8
+ case medium
8
9
  case small
9
10
 
10
11
  /// Container width/height of the button.
11
12
  var containerSize: CGFloat {
12
13
  switch self {
13
14
  case .large: return 48
14
- case .small: return 40
15
+ case .medium: return 36
16
+ case .small: return 28
15
17
  }
16
18
  }
17
19
 
@@ -19,13 +21,16 @@ public enum IconSize {
19
21
  var iconSize: CGFloat {
20
22
  switch self {
21
23
  case .large: return 24
22
- case .small: return 16
24
+ case .medium, .small: return 16
23
25
  }
24
26
  }
27
+ }
25
28
 
26
- var radius: CGFloat {
27
- Radius.XL
28
- }
29
+ // MARK: - IconShape
30
+
31
+ public enum IconShape {
32
+ case circle
33
+ case square
29
34
  }
30
35
 
31
36
  // MARK: - IconButton
@@ -37,6 +42,7 @@ public struct IconButton: View {
37
42
  var onClick: () -> Void
38
43
  var type: ButtonType
39
44
  var size: IconSize
45
+ var shape: IconShape
40
46
  var color: Color?
41
47
 
42
48
  public init(
@@ -44,12 +50,14 @@ public struct IconButton: View {
44
50
  onClick: @escaping () -> Void,
45
51
  type: ButtonType = .primary,
46
52
  size: IconSize = .small,
53
+ shape: IconShape = .circle,
47
54
  color: Color? = nil
48
55
  ) {
49
56
  self.icon = icon
50
57
  self.onClick = onClick
51
58
  self.type = type
52
59
  self.size = size
60
+ self.shape = shape
53
61
  self.color = color
54
62
  }
55
63
 
@@ -72,9 +80,9 @@ public struct IconButton: View {
72
80
  Icon(source: icon, size: size.iconSize, color: fg)
73
81
  .frame(width: size.containerSize, height: size.containerSize)
74
82
  .background(bg)
75
- .clipShape(RoundedRectangle(cornerRadius: size.radius))
83
+ .clipShape(IconButtonShape(shape))
76
84
  .overlay(
77
- RoundedRectangle(cornerRadius: size.radius)
85
+ IconButtonShape(shape)
78
86
  .stroke(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
79
87
  )
80
88
  }
@@ -110,3 +118,23 @@ public struct IconButton: View {
110
118
  }
111
119
  }
112
120
  }
121
+
122
+ /// Type-erased shape switch (`Circle` vs a fixed `Radius.S` rounded rect) — mirrors
123
+ /// Compose's `IconShape.toShape()`. `AnyShape` needs iOS 17+, so this hand-rolls the same
124
+ /// erasure at the deployment target's iOS 15 floor.
125
+ private struct IconButtonShape: Shape {
126
+ let shape: IconShape
127
+
128
+ init(_ shape: IconShape) {
129
+ self.shape = shape
130
+ }
131
+
132
+ func path(in rect: CGRect) -> Path {
133
+ switch shape {
134
+ case .circle:
135
+ return Circle().path(in: rect)
136
+ case .square:
137
+ return RoundedRectangle(cornerRadius: Radius.S).path(in: rect)
138
+ }
139
+ }
140
+ }
@@ -5,15 +5,23 @@ import SDWebImageSwiftUI
5
5
  import SkeletonUI
6
6
  import SwiftUI
7
7
 
8
+ public enum ImageState {
9
+ case loading, success, error
10
+ }
11
+
8
12
  public struct ImageView: View {
13
+ @Environment(\.appTheme) private var theme
14
+
9
15
  // MARK: Lifecycle
10
16
 
11
- public init(_ url: String, aspectRatio: CGFloat? = nil, contentMode: ContentMode = ContentMode.fit, placeholder: AnyView? = nil, color: Color? = nil) {
17
+ public init(_ url: String, aspectRatio: CGFloat? = nil, contentMode: ContentMode = ContentMode.fit, placeholder: AnyView? = nil, color: Color? = nil, loading: Bool = true, onStateChanged: ((ImageState) -> Void)? = nil) {
12
18
  self.url = url
13
19
  self.aspectRatio = aspectRatio
14
20
  self.contentMode = contentMode
15
21
  self.placeholder = placeholder
16
22
  self.color = color
23
+ self.loading = loading
24
+ self.onStateChanged = onStateChanged
17
25
  }
18
26
 
19
27
  // MARK: Public
@@ -22,9 +30,11 @@ public struct ImageView: View {
22
30
  return WebImage(url: URL(string: url), isAnimating: .constant(true))
23
31
  .onFailure { _ in
24
32
  error = true
33
+ onStateChanged?(.error)
25
34
  }
26
35
  .onSuccess { _, _, _ in
27
36
  error = false
37
+ onStateChanged?(.success)
28
38
  }
29
39
  .resizable()
30
40
  .renderingMode(color != nil ? .template : .original)
@@ -34,20 +44,21 @@ public struct ImageView: View {
34
44
  Image("media_fail")
35
45
  .resizable()
36
46
  .renderingMode(.template)
37
- .foregroundColor(Colors.black08)
47
+ .foregroundColor(theme.colors.text.disable)
38
48
  .frame(width: 24, height: 24)
39
49
  } else if placeholder != nil {
40
50
  placeholder
41
51
  }
42
52
  })
43
53
  .frame(maxWidth: .infinity, maxHeight: .infinity)
44
- .skeleton(with: !error && placeholder == nil)
54
+ .skeleton(with: loading && !error && placeholder == nil)
45
55
  .shape(type: .rectangle)
46
56
  }
47
57
  .scaledToFit()
48
58
  .transition(.fade)
49
59
  .aspectRatio(aspectRatio, contentMode: contentMode)
50
60
  .foregroundColor(color)
61
+ .onAppear { onStateChanged?(.loading) }
51
62
  }
52
63
 
53
64
  // MARK: Internal
@@ -56,6 +67,8 @@ public struct ImageView: View {
56
67
  var contentMode: ContentMode
57
68
  var placeholder: AnyView?
58
69
  var color: Color?
70
+ var loading: Bool
71
+ var onStateChanged: ((ImageState) -> Void)?
59
72
 
60
73
  @State var error: Bool = false
61
74
  var url: String
@@ -7,6 +7,8 @@ public enum InformationState {
7
7
  }
8
8
 
9
9
  public struct Information: View {
10
+ @Environment(\.appTheme) private var theme
11
+
10
12
  private let title: String?
11
13
  private let description: String
12
14
  private let state: InformationState
@@ -93,7 +95,7 @@ public struct Information: View {
93
95
  .frame(maxWidth: .infinity, alignment: .leading)
94
96
 
95
97
  if let onPressClose, showIconClose {
96
- Icon(source: "navigation_close", size: 16, color: Colors.black17)
98
+ Icon(source: "navigation_close", size: 16, color: theme.colors.text.default)
97
99
  .contentShape(Rectangle())
98
100
  .onTapGesture(perform: onPressClose)
99
101
  }
@@ -8,6 +8,8 @@
8
8
  import SwiftUI
9
9
 
10
10
  public struct ErrorView: View {
11
+ @Environment(\.appTheme) private var theme
12
+
11
13
  public var errorMessage: String
12
14
  public var errorSpacing: Bool
13
15
  public var hintText: String
@@ -43,7 +45,7 @@ public struct ErrorView: View {
43
45
  // MARK: - Helpers
44
46
 
45
47
  private func displayColor() -> Color {
46
- !errorMessage.isEmpty ? Colors.red03 : Colors.black12
48
+ !errorMessage.isEmpty ? theme.colors.error.primary : theme.colors.text.hint
47
49
  }
48
50
 
49
51
  private func displayText() -> String {
@@ -35,6 +35,8 @@ public struct Input: View {
35
35
 
36
36
  @State private var isFocused: Bool = false
37
37
  @State private var isPasswordHidden: Bool = true
38
+ @FocusState private var isFieldFocused: Bool
39
+ @State private var floatingLabelHeight: CGFloat = 16
38
40
 
39
41
  public init(
40
42
  text: Binding<String>,
@@ -99,8 +101,13 @@ public struct Input: View {
99
101
  let textBinding = Binding<String>(
100
102
  get: { self.text },
101
103
  set: { newValue in
102
- self.text = newValue
103
- self.onChangeText?(limitText(newValue))
104
+ // Trim atomically before publishing, mirroring Kotlin's `onValueChange`
105
+ // (`newText.take(maxLength)` before the state ever updates) — avoids a
106
+ // render with the untrimmed value that a separate `.onChange` correction
107
+ // would otherwise briefly show.
108
+ let limited = limitText(newValue)
109
+ self.text = limited
110
+ self.onChangeText?(limited)
104
111
  }
105
112
  )
106
113
 
@@ -118,8 +125,19 @@ public struct Input: View {
118
125
  }
119
126
  }
120
127
  .padding(.horizontal, Spacing.S)
128
+ .background(
129
+ GeometryReader { geo in
130
+ Color.clear.onAppear { floatingLabelHeight = geo.size.height }
131
+ }
132
+ )
121
133
  .background(theme.colors.background.surface)
122
- .offset(x: Spacing.S, y: -8)
134
+ // Kotlin centers the label within the (box-height-tall) decorationBox
135
+ // by default, then applies `offsetY = -size.values.height / 2` — net
136
+ // effect: the label ends up centered exactly on the box's top border
137
+ // (spanning -labelHeight/2 ... +labelHeight/2 around y=0). Our ZStack
138
+ // anchors top-leading instead, so reproducing that requires offsetting
139
+ // by half the label's OWN measured height, not half the box height.
140
+ .offset(x: Spacing.S, y: -floatingLabelHeight / 2)
123
141
  .zIndex(10)
124
142
  }
125
143
 
@@ -151,6 +169,7 @@ public struct Input: View {
151
169
  cursorColor: UIColor(theme.colors.primary),
152
170
  isDisabled: disabled || readOnly,
153
171
  maxLength: maxLength,
172
+ autofocus: autofocus,
154
173
  onFocusChange: { focused in
155
174
  handleFocusChange(focused)
156
175
  },
@@ -166,11 +185,9 @@ public struct Input: View {
166
185
  .foregroundColor(getTextColor())
167
186
  .disabled(disabled || readOnly)
168
187
  .applyCursorColor(theme.colors.primary)
169
- .onChange(of: text) { newValue in
170
- let limited = limitText(newValue)
171
- if limited != newValue {
172
- text = limited
173
- }
188
+ .focused($isFieldFocused)
189
+ .onAppear {
190
+ if autofocus { isFieldFocused = true }
174
191
  }
175
192
  }
176
193
  }
@@ -312,6 +329,7 @@ private struct SecureInputField: UIViewRepresentable {
312
329
  var cursorColor: UIColor
313
330
  var isDisabled: Bool
314
331
  var maxLength: Int?
332
+ var autofocus: Bool = false
315
333
  var onFocusChange: (Bool) -> Void
316
334
  var onChangeText: ((String) -> Void)?
317
335
 
@@ -336,6 +354,11 @@ private struct SecureInputField: UIViewRepresentable {
336
354
  action: #selector(Coordinator.textFieldDidChange(_:)),
337
355
  for: .editingChanged
338
356
  )
357
+ if autofocus {
358
+ DispatchQueue.main.async {
359
+ textField.becomeFirstResponder()
360
+ }
361
+ }
339
362
  return textField
340
363
  }
341
364
 
@@ -16,6 +16,7 @@ public struct InputPhoneNumber: View {
16
16
  public var error: String
17
17
  public var errorSpacing: Bool
18
18
  public var loading: Bool
19
+ public var required: Bool
19
20
  public var rightIcon: String
20
21
  public var rightIconColor: Color?
21
22
  public var autofocus: Bool
@@ -24,8 +25,9 @@ public struct InputPhoneNumber: View {
24
25
  public var onBlur: (() -> Void)?
25
26
  public var onRightIconPressed: (() -> Void)?
26
27
  public var accessibilityLabel: String?
27
-
28
+
28
29
  @State private var isFocused: Bool = false
30
+ @FocusState private var isFieldFocused: Bool
29
31
 
30
32
  public init(
31
33
  text: Binding<String>,
@@ -52,6 +54,7 @@ public struct InputPhoneNumber: View {
52
54
  self.error = error
53
55
  self.errorSpacing = errorSpacing
54
56
  self.loading = loading
57
+ self.required = required
55
58
  self.rightIcon = rightIcon
56
59
  self.rightIconColor = rightIconColor
57
60
  self.autofocus = autofocus
@@ -90,9 +93,13 @@ public struct InputPhoneNumber: View {
90
93
  // Text input
91
94
  ZStack(alignment: .leading) {
92
95
  if text.isEmpty {
93
- MomoText(placeholder, typography: size == .small ? .headerSSemibold : .headerMBold, color: theme.colors.text.hint)
96
+ MomoText(
97
+ placeholder + (required ? " *" : ""),
98
+ typography: size == .small ? .headerSSemibold : .headerMBold,
99
+ color: theme.colors.text.hint
100
+ )
94
101
  }
95
-
102
+
96
103
  TextField("", text: textBinding, onEditingChanged: { focused in
97
104
  handleFocusChange(focused)
98
105
  })
@@ -102,6 +109,10 @@ public struct InputPhoneNumber: View {
102
109
  .foregroundColor(theme.colors.text.default)
103
110
  .applyCursorColor(theme.colors.primary)
104
111
  .lineLimit(1)
112
+ .focused($isFieldFocused)
113
+ .onAppear {
114
+ if autofocus { isFieldFocused = true }
115
+ }
105
116
  .accessibility(identifier: accessibilityLabel ?? "")
106
117
  .accessibilityValue(textBinding.wrappedValue.isEmpty ? placeholder : textBinding.wrappedValue)
107
118
  }
@@ -238,7 +238,13 @@ public struct InputSearch: View {
238
238
  }
239
239
 
240
240
  if isFocused && !text.isEmpty {
241
- SwiftButton(action: { text = "" }) {
241
+ SwiftButton(action: {
242
+ text = ""
243
+ // Kotlin's clear icon fires a callback distinct from
244
+ // onChangeText (`onClearPress`, not ported here); route
245
+ // through onChangeText so clearing isn't a silent no-op.
246
+ onChangeText("")
247
+ }) {
242
248
  Image(systemName: "xmark.circle.fill")
243
249
  .foregroundColor(theme.colors.text.hint)
244
250
  .frame(width: 16, height: 16)
@@ -109,6 +109,7 @@ public struct InputTextArea: View {
109
109
 
110
110
  @State private var isFocused = false
111
111
  @State private var isBlurred = false
112
+ @State private var floatingLabelHeight: CGFloat = 16
112
113
 
113
114
  // Make the initializer public
114
115
  public init(
@@ -182,45 +183,88 @@ public struct InputTextArea: View {
182
183
  }
183
184
  }
184
185
  .padding(.horizontal, Spacing.S)
186
+ .background(
187
+ GeometryReader { geo in
188
+ Color.clear.onAppear { floatingLabelHeight = geo.size.height }
189
+ }
190
+ )
185
191
  .background(theme.colors.background.surface)
186
- .offset(x: Spacing.S, y: -height / 10)
192
+ // Kotlin centers the label within the box-height-tall decorationBox
193
+ // by default, then applies `offsetY = -scaleHeight / 2` — net effect:
194
+ // the label ends up centered exactly on the box's top border. Our
195
+ // ZStack anchors top-leading instead, so matching that requires
196
+ // offsetting by half the label's OWN measured height, not half the
197
+ // (104pt-tall) text-area box height.
198
+ .offset(x: Spacing.S, y: -floatingLabelHeight / 2)
187
199
  .zIndex(10)
188
200
  }
189
-
201
+
190
202
  VStack {
191
- ZStack(alignment: .topLeading) {
192
- if text.isEmpty {
193
- Text(placeholder)
194
- .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
195
- .font(.system(size: 16, weight: fontWeight))
196
- .padding(.horizontal, 16)
197
- .padding(.vertical, 12)
203
+ HStack(alignment: .top, spacing: 0) {
204
+ ZStack(alignment: .topLeading) {
205
+ if text.isEmpty {
206
+ Text(placeholder)
207
+ .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
208
+ .font(.system(size: 16, weight: fontWeight))
209
+ .padding(.horizontal, 16)
210
+ .padding(.vertical, 12)
211
+ }
212
+
213
+ UITextViewWrapper(
214
+ text: $text,
215
+ maxLength: maxLength,
216
+ textColor: disabled ? theme.colors.text.disable : theme.colors.text.default,
217
+ cursorColor: theme.colors.primary,
218
+ fontWeight: fontWeight,
219
+ disabled: disabled,
220
+ onChangeText: onChangeText,
221
+ onDone: {
222
+ // Handle done action if needed
223
+ },
224
+ onFocus: {
225
+ isFocused = true
226
+ onFocus()
227
+ }, onBlur: {
228
+ isFocused = false
229
+ isBlurred = true
230
+ onBlur()
231
+ })
232
+ .frame(height: height)
233
+ .padding(.horizontal, 12)
234
+ .padding(.vertical, 8)
235
+ .disabled(disabled)
198
236
  }
199
-
200
- UITextViewWrapper(
201
- text: $text,
202
- maxLength: maxLength,
203
- textColor: disabled ? theme.colors.text.disable : theme.colors.text.default,
204
- cursorColor: theme.colors.primary,
205
- fontWeight: fontWeight,
206
- disabled: disabled,
207
- onChangeText: onChangeText,
208
- onDone: {
209
- // Handle done action if needed
210
- },
211
- onFocus: {
212
- isFocused = true
213
- onFocus()
214
- }, onBlur: {
215
- isFocused = false
216
- isBlurred = true
217
- onBlur()
218
- })
219
- .frame(height: height)
220
- .padding(.horizontal, 12)
221
- .padding(.vertical, 8)
222
- .disabled(disabled) }
223
-
237
+ .frame(maxWidth: .infinity)
238
+
239
+ // Clear button (mirrors Compose: focused + non-empty).
240
+ if isFocused && !text.isEmpty {
241
+ SwiftUI.Button(action: {
242
+ text = ""
243
+ onChangeText("")
244
+ }) {
245
+ Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
246
+ }
247
+ .padding(.leading, Spacing.XS)
248
+ .padding(.top, 12)
249
+ }
250
+
251
+ // Loading indicator and right icon (independent, mirrors
252
+ // Compose's RenderRightIcon: both can render at once).
253
+ if loading {
254
+ ActivityIndicator(isAnimating: .constant(true), style: .medium)
255
+ .frame(width: 16, height: 16)
256
+ .padding(.leading, Spacing.XS)
257
+ .padding(.top, 12)
258
+ }
259
+ if !icon.isEmpty {
260
+ SwiftUI.Button(action: onRightIconPressed) {
261
+ Icon(source: icon, size: 24, color: disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default))
262
+ }
263
+ .padding(.leading, Spacing.XS)
264
+ .padding(.top, 12)
265
+ }
266
+ }
267
+
224
268
  HStack {
225
269
  Spacer()
226
270
  MomoText("\(text.count)/\(maxLength)", typography: .descriptionXsRegular, color: theme.colors.text.hint)
@@ -16,10 +16,13 @@ private struct RenderRightIcon: View {
16
16
  let onPressed: () -> Void
17
17
 
18
18
  var body: some View {
19
+ // Mirrors Compose's two independent `if` blocks: loading spinner and icon
20
+ // can both render at once, not exclusively (not an if/else).
19
21
  if loading {
20
22
  ActivityIndicator(isAnimating: .constant(true), style: .medium)
21
23
  .frame(width: 16, height: 16)
22
- } else if !icon.isEmpty {
24
+ }
25
+ if !icon.isEmpty {
23
26
  SwiftUI.Button(action: onPressed) {
24
27
  Icon(source: icon, size: 24, color: color)
25
28
  }
@@ -75,6 +75,7 @@ public struct InputMoney: View {
75
75
  public var onBlur: (() -> Void)?
76
76
 
77
77
  @State private var isFocused: Bool = false
78
+ @State private var floatingLabelHeight: CGFloat = 16
78
79
 
79
80
  public init(
80
81
  text: Binding<String>,
@@ -141,8 +142,18 @@ public struct InputMoney: View {
141
142
  }
142
143
  }
143
144
  .padding(.horizontal, Spacing.S)
145
+ .background(
146
+ GeometryReader { geo in
147
+ Color.clear.onAppear { floatingLabelHeight = geo.size.height }
148
+ }
149
+ )
144
150
  .background(theme.colors.background.surface)
145
- .offset(x: Spacing.S, y: -8)
151
+ // Kotlin centers the label within the box-height-tall decorationBox by
152
+ // default, then applies `offsetY = -size.values.height / 2` — net effect:
153
+ // the label ends up centered exactly on the box's top border. Our ZStack
154
+ // anchors top-leading instead, so matching that requires offsetting by
155
+ // half the label's OWN measured height, not half the box height.
156
+ .offset(x: Spacing.S, y: -floatingLabelHeight / 2)
146
157
  .zIndex(10)
147
158
  }
148
159
 
@@ -188,13 +199,14 @@ public struct InputMoney: View {
188
199
  }
189
200
  }
190
201
 
191
- // Loading indicator
202
+ // Loading indicator and right icon can both render (mirrors
203
+ // Compose's two independent `if` blocks, not if/else).
192
204
  if loading {
193
205
  ActivityIndicator(isAnimating: .constant(true), style: .medium)
194
206
  .frame(width: 16, height: 16)
195
207
  .padding(.leading, Spacing.S)
196
- } else if !icon.isEmpty {
197
- // Right icon
208
+ }
209
+ if !icon.isEmpty {
198
210
  SwiftUI.Button(action: { onPressIcon?() }) {
199
211
  Icon(source: icon, size: 24, color: iconTintColor)
200
212
  .padding(.leading, Spacing.S)
@@ -54,6 +54,8 @@ extension View {
54
54
  // MARK: - PopupDisplay
55
55
 
56
56
  public struct PopupDisplay: View {
57
+ @EnvironmentObject private var localize: Localize
58
+
57
59
  // MARK: Lifecycle
58
60
 
59
61
  public init(isPresented: Binding<Bool>, title: String = "", description: String = "", url: String = "", buttonDirection: PopupButtonDirection = .column, buttonType: PopupButtonType = .button, actionButtonTitle: String = "", closeButtonTitle: String = "", onPressAction: @escaping () -> Void = {}, onPressCloseButton: @escaping () -> Void = {}, onClose: @escaping () -> Void = {}, errorCode: String = "", isShowCloseIcon: Bool = true) {
@@ -91,9 +93,6 @@ public struct PopupDisplay: View {
91
93
  @State private var isScrollable = false
92
94
 
93
95
  public var body: some View {
94
- let language = UserDefaults.standard.string(forKey: "language")?.replacingOccurrences(of: "\"", with: "") ?? "vi"
95
- let errorCodeLabels: [String: String] = ["vi": "Mã lỗi: ", "en": "Error code: "]
96
-
97
96
  var shouldUseColumn: Bool {
98
97
  buttonDirection == .column || (buttonDirection == .auto && (closeButtonTitle.count > 12 || actionButtonTitle.count > 12))
99
98
  }
@@ -170,7 +169,7 @@ public struct PopupDisplay: View {
170
169
  .padding(.bottom, 8)
171
170
 
172
171
  if(!errorCode.isEmpty) {
173
- MomoText((errorCodeLabels[language] ?? "Mã lỗi: ") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
172
+ MomoText(localize.translate("errorCode") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
174
173
  .lineLimit(1)
175
174
  .padding(.bottom, 8)
176
175
  }
@@ -38,15 +38,14 @@ public struct PopupNotifyProps {
38
38
  }
39
39
 
40
40
  public struct PopupNotify: View {
41
+ @EnvironmentObject private var localize: Localize
42
+
41
43
  public let props: PopupNotifyProps
42
44
 
43
45
  @State private var descriptionHeight: CGFloat = 0
44
46
 
45
47
  private var errorCodePrefix: String {
46
- let language = UserDefaults.standard
47
- .string(forKey: "language")?
48
- .replacingOccurrences(of: "\"", with: "") ?? "vi"
49
- return language == "en" ? "Error code: " : "Mã lỗi: "
48
+ localize.translate("errorCode")
50
49
  }
51
50
 
52
51
  private var maxDescriptionHeight: CGFloat {