@momo-kits/native-kits 0.300.3-debug → 0.300.4-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 (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -16,22 +16,24 @@ struct UITextViewWrapper: UIViewRepresentable {
16
16
  var textColor: Color
17
17
  var cursorColor: Color
18
18
  var fontWeight: Font.Weight
19
+ var keyboardType: UIKeyboardType = .default
19
20
  var disabled: Bool
20
21
  var onChangeText: (String) -> Void
21
- var onDone: (() -> Void)?
22
22
  var onFocus: (() -> Void)?
23
23
  var onBlur: (() -> Void)?
24
-
24
+
25
25
  func makeUIView(context: Context) -> UITextView {
26
26
  let textView = UITextView()
27
27
  textView.delegate = context.coordinator
28
28
  textView.backgroundColor = .clear
29
29
  textView.textContainerInset = .zero
30
30
  textView.textContainer.lineFragmentPadding = 0
31
+ textView.keyboardType = keyboardType
31
32
  return textView
32
33
  }
33
-
34
+
34
35
  func updateUIView(_ uiView: UITextView, context: Context) {
36
+ context.coordinator.parent = self
35
37
  if uiView.text != text {
36
38
  uiView.text = text
37
39
  }
@@ -39,24 +41,30 @@ struct UITextViewWrapper: UIViewRepresentable {
39
41
  uiView.tintColor = UIColor(cursorColor)
40
42
  // Read through `context.environment` rather than the global `scaleSize`: that is what makes
41
43
  // SwiftUI re-run updateUIView when the font scale changes.
42
- uiView.font = UIFont.systemFont(
43
- ofSize: context.environment.momoFontScale(16),
44
- weight: fontWeight == .bold ? .bold : .regular
44
+ uiView.font = momoUIFont(
45
+ size: context.environment.momoFontScale(16),
46
+ weight: fontWeight == .bold ? .bold : .regular,
47
+ family: context.environment.appTheme.font,
48
+ isWhiteList: context.environment.designSystemKitsConfigs.isWhiteList
45
49
  )
46
50
  uiView.isEditable = !disabled
51
+ if uiView.keyboardType != keyboardType {
52
+ uiView.keyboardType = keyboardType
53
+ uiView.reloadInputViews()
54
+ }
47
55
  }
48
-
56
+
49
57
  func makeCoordinator() -> Coordinator {
50
58
  Coordinator(self)
51
59
  }
52
-
60
+
53
61
  class Coordinator: NSObject, UITextViewDelegate {
54
62
  var parent: UITextViewWrapper
55
-
63
+
56
64
  init(_ parent: UITextViewWrapper) {
57
65
  self.parent = parent
58
66
  }
59
-
67
+
60
68
  func textViewDidChange(_ textView: UITextView) {
61
69
  let limited = String(textView.text.prefix(parent.maxLength))
62
70
  if textView.text != limited {
@@ -65,20 +73,17 @@ struct UITextViewWrapper: UIViewRepresentable {
65
73
  parent.text = limited
66
74
  parent.onChangeText(limited)
67
75
  }
68
-
76
+
69
77
  func textViewDidBeginEditing(_ textView: UITextView) {
70
78
  parent.onFocus?()
71
79
  }
72
-
80
+
73
81
  func textViewDidEndEditing(_ textView: UITextView) {
74
82
  parent.onBlur?()
75
83
  }
76
-
84
+
85
+ // Like Compose's `onValueChange`, an edit that would exceed maxLength is rejected whole.
77
86
  func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
78
- if text == "\n" {
79
- parent.onDone?()
80
- return false
81
- }
82
87
  let current = textView.text ?? ""
83
88
  guard let range = Range(range, in: current) else { return false }
84
89
  let proposed = current.replacingCharacters(in: range, with: text)
@@ -90,6 +95,7 @@ struct UITextViewWrapper: UIViewRepresentable {
90
95
  // Make the struct public
91
96
  public struct InputTextArea: View {
92
97
  @Environment(\.appTheme) private var theme
98
+ @Environment(\.momoFontScale) private var fontScale
93
99
  @Binding var text: String
94
100
  var maxLength: Int = MAX_LENGTH
95
101
  var height: CGFloat = DEFAULT_HEIGHT
@@ -111,11 +117,9 @@ public struct InputTextArea: View {
111
117
  var required: Bool = false
112
118
  var fontWeight: Font.Weight = .regular
113
119
  var keyboardType: UIKeyboardType = .default
114
-
120
+
115
121
  @State private var isFocused = false
116
- @State private var isBlurred = false
117
- @State private var floatingLabelHeight: CGFloat = 16
118
-
122
+
119
123
  // Make the initializer public
120
124
  public init(
121
125
  text: Binding<String>,
@@ -162,147 +166,128 @@ public struct InputTextArea: View {
162
166
  self.fontWeight = fontWeight
163
167
  self.keyboardType = keyboardType
164
168
  }
165
-
169
+
166
170
  public var body: some View {
167
171
  VStack(alignment: .leading, spacing: 0) {
172
+ // Compose: `height` is the whole box (text, counter and paddings), scaled.
173
+ VStack(spacing: 0) {
174
+ HStack(alignment: .top, spacing: 0) {
168
175
  ZStack(alignment: .topLeading) {
169
- if !floatingValue.isEmpty || !floatingIcon.isEmpty {
170
- HStack(spacing: 4) {
171
- MomoText(
172
- floatingValue,
173
- typography: .labelSMedium,
174
- color: disabled ? theme.colors.text.disable : (floatingValueColor ?? theme.colors.text.hint)
175
- )
176
-
177
- if required {
178
- MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
179
- }
180
-
181
- if !floatingIcon.isEmpty {
182
- Icon(
183
- source: floatingIcon,
184
- size: 16,
185
- color: disabled ? theme.colors.text.disable : (floatingIconColor ?? theme.colors.text.default)
186
- )
187
- .padding(.leading, Spacing.XS)
188
- }
189
- }
190
- .padding(.horizontal, Spacing.S)
191
- .background(
192
- GeometryReader { geo in
193
- Color.clear.onAppear { floatingLabelHeight = geo.size.height }
194
- }
176
+ if text.isEmpty {
177
+ MomoText(
178
+ placeholder,
179
+ typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
180
+ color: disabled ? theme.colors.text.disable : theme.colors.text.hint
195
181
  )
196
- .background(theme.colors.background.surface)
197
- // Kotlin centers the label within the box-height-tall decorationBox
198
- // by default, then applies `offsetY = -scaleHeight / 2` — net effect:
199
- // the label ends up centered exactly on the box's top border. Our
200
- // ZStack anchors top-leading instead, so matching that requires
201
- // offsetting by half the label's OWN measured height, not half the
202
- // (104pt-tall) text-area box height.
203
- .offset(x: Spacing.S, y: -floatingLabelHeight / 2)
204
- .zIndex(10)
182
+ .fontSize(16, lineHeight: 24)
205
183
  }
206
184
 
207
- VStack {
208
- HStack(alignment: .top, spacing: 0) {
209
- ZStack(alignment: .topLeading) {
210
- if text.isEmpty {
211
- Text(placeholder)
212
- .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
213
- .font(.system(size: 16, weight: fontWeight))
214
- .padding(.horizontal, 16)
215
- .padding(.vertical, 12)
216
- }
217
-
218
- UITextViewWrapper(
219
- text: $text,
220
- maxLength: maxLength,
221
- textColor: disabled ? theme.colors.text.disable : theme.colors.text.default,
222
- cursorColor: theme.colors.primary,
223
- fontWeight: fontWeight,
224
- disabled: disabled,
225
- onChangeText: onChangeText,
226
- onDone: {
227
- // Handle done action if needed
228
- },
229
- onFocus: {
230
- isFocused = true
231
- onFocus()
232
- }, onBlur: {
233
- isFocused = false
234
- isBlurred = true
235
- onBlur()
236
- })
237
- .frame(height: height)
238
- .padding(.horizontal, 12)
239
- .padding(.vertical, 8)
240
- .disabled(disabled)
241
- }
242
- .frame(maxWidth: .infinity)
243
-
244
- // Clear button (mirrors Compose: focused + non-empty).
245
- if isFocused && !text.isEmpty {
246
- SwiftUI.Button(action: {
247
- text = ""
248
- onChangeText("")
249
- }) {
250
- Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
251
- }
252
- .padding(.leading, Spacing.XS)
253
- .padding(.top, 12)
254
- }
255
-
256
- // Loading indicator and right icon (independent, mirrors
257
- // Compose's RenderRightIcon: both can render at once).
258
- if loading {
259
- ActivityIndicator(isAnimating: .constant(true), style: .medium)
260
- .frame(width: 16, height: 16)
261
- .padding(.leading, Spacing.XS)
262
- .padding(.top, 12)
263
- }
264
- if !icon.isEmpty {
265
- SwiftUI.Button(action: onRightIconPressed) {
266
- Icon(source: icon, size: 24, color: disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default))
267
- }
268
- .padding(.leading, Spacing.XS)
269
- .padding(.top, 12)
270
- }
185
+ UITextViewWrapper(
186
+ text: $text,
187
+ maxLength: maxLength,
188
+ textColor: disabled ? theme.colors.text.disable : theme.colors.text.default,
189
+ cursorColor: theme.colors.primary,
190
+ fontWeight: fontWeight,
191
+ keyboardType: keyboardType,
192
+ disabled: disabled,
193
+ onChangeText: onChangeText,
194
+ onFocus: {
195
+ isFocused = true
196
+ onFocus()
197
+ },
198
+ onBlur: {
199
+ isFocused = false
200
+ onBlur()
271
201
  }
202
+ )
203
+ .disabled(disabled)
204
+ }
205
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
272
206
 
273
- HStack {
274
- Spacer()
275
- MomoText("\(text.count)/\(maxLength)", typography: .descriptionXsRegular, color: theme.colors.text.hint)
276
- }
277
- .padding(.horizontal, 16)
278
- .padding(.bottom, 12)
207
+ // Clear button (mirrors Compose: focused + non-empty).
208
+ if isFocused && !text.isEmpty {
209
+ SwiftUI.Button(action: {
210
+ text = ""
211
+ onChangeText("")
212
+ }) {
213
+ Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
279
214
  }
280
- .background(theme.colors.background.surface)
281
- .cornerRadius(Radius.S)
282
- .overlay(
283
- RoundedRectangle(cornerRadius: Radius.S)
284
- .stroke(borderColor, lineWidth: 1)
285
- )
215
+ .padding(.leading, Spacing.XS)
286
216
  }
287
-
288
- ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
289
- }
290
- .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillShowNotification)) { _ in
291
- if !isBlurred {
292
- isFocused = true
293
- onFocus()
217
+
218
+ // Loading indicator and right icon (independent, mirrors
219
+ // Compose's RenderRightIcon: both can render at once).
220
+ if loading {
221
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: iconTintColor)
222
+ .frame(width: 16, height: 16)
223
+ .padding(.leading, Spacing.XS)
294
224
  }
295
- }
296
- .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
297
- if isFocused {
298
- isFocused = false
299
- isBlurred = true
300
- onBlur()
225
+ if !icon.isEmpty {
226
+ SwiftUI.Button(action: onRightIconPressed) {
227
+ Icon(source: icon, size: 24, color: iconTintColor)
228
+ }
229
+ .padding(.leading, Spacing.XS)
301
230
  }
302
231
  }
303
- .ignoreBoldTextSetting()
232
+ .padding([.horizontal, .top], Spacing.M)
233
+ .frame(maxHeight: .infinity, alignment: .top)
234
+
235
+ MomoText("\(text.count)/\(maxLength)", typography: .descriptionXsRegular, color: theme.colors.text.hint)
236
+ .frame(maxWidth: .infinity, alignment: .trailing)
237
+ .padding(.trailing, Spacing.M)
238
+ .padding(.bottom, Spacing.M)
239
+ }
240
+ .frame(height: fontScale(height))
241
+ .background(
242
+ RoundedRectangle(cornerRadius: Radius.S)
243
+ .fill(theme.colors.background.surface)
244
+ )
245
+ .overlay(
246
+ RoundedRectangle(cornerRadius: Radius.S)
247
+ .stroke(borderColor, lineWidth: 1)
248
+ )
249
+ .overlay(alignment: .topLeading) { floatingLabel }
250
+
251
+ ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
252
+ }
253
+ .ignoreBoldTextSetting()
304
254
  }
305
-
255
+
256
+ /// Centered on the box's top border, like Compose's `FloatingLabel` (offset by -height / 2).
257
+ @ViewBuilder
258
+ private var floatingLabel: some View {
259
+ if !floatingValue.isEmpty || !floatingIcon.isEmpty {
260
+ HStack(spacing: 0) {
261
+ MomoText(
262
+ floatingValue,
263
+ typography: .labelSMedium,
264
+ color: disabled ? theme.colors.text.disable : (floatingValueColor ?? theme.colors.text.hint)
265
+ )
266
+
267
+ if required {
268
+ MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
269
+ }
270
+
271
+ if !floatingIcon.isEmpty {
272
+ Icon(
273
+ source: floatingIcon,
274
+ size: 16,
275
+ color: disabled ? theme.colors.text.disable : (floatingIconColor ?? theme.colors.text.default)
276
+ )
277
+ .padding(.leading, Spacing.XS)
278
+ }
279
+ }
280
+ .padding(.horizontal, Spacing.S)
281
+ .background(theme.colors.background.surface)
282
+ .alignmentGuide(.top) { $0[VerticalAlignment.center] }
283
+ .offset(x: Spacing.S)
284
+ }
285
+ }
286
+
287
+ private var iconTintColor: Color {
288
+ disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default)
289
+ }
290
+
306
291
  private var borderColor: Color {
307
292
  if disabled {
308
293
  return theme.colors.border.disable
@@ -19,7 +19,7 @@ private struct RenderRightIcon: View {
19
19
  // Mirrors Compose's two independent `if` blocks: loading spinner and icon
20
20
  // can both render at once, not exclusively (not an if/else).
21
21
  if loading {
22
- ActivityIndicator(isAnimating: .constant(true), style: .medium)
22
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: color)
23
23
  .frame(width: 16, height: 16)
24
24
  }
25
25
  if !icon.isEmpty {
@@ -98,7 +98,7 @@ public struct InputDropDown: View {
98
98
  }
99
99
 
100
100
  public var body: some View {
101
- VStack(alignment: .leading, spacing: 4) {
101
+ VStack(alignment: .leading, spacing: 0) {
102
102
  ZStack(alignment: .topLeading) {
103
103
  // Floating label
104
104
  if !floatingValue.isEmpty || !floatingIcon.isEmpty {
@@ -114,7 +114,7 @@ public struct InputDropDown: View {
114
114
  }
115
115
  .padding(.horizontal, Spacing.S)
116
116
  .background(theme.colors.background.surface)
117
- .offset(x: Spacing.S, y: -8)
117
+ .offset(x: Spacing.S, y: -Spacing.S)
118
118
  .zIndex(10)
119
119
  }
120
120
 
@@ -130,6 +130,7 @@ public struct InputDropDown: View {
130
130
  typography: .bodyDefaultRegular,
131
131
  color: value.isEmpty ? placeholderColor : textColor
132
132
  )
133
+ .fontSize(16, lineHeight: 24)
133
134
  .frame(maxWidth: .infinity, alignment: .leading)
134
135
 
135
136
  RenderRightIcon(
@@ -146,10 +147,6 @@ public struct InputDropDown: View {
146
147
  .stroke(getBorderColor(isFocused: false, error: error, disabled: disabled), lineWidth: 1)
147
148
  )
148
149
  }
149
- .contentShape(Rectangle())
150
- .onTapGesture {
151
- if !disabled { onPress() }
152
- }
153
150
 
154
151
  ErrorView(
155
152
  errorMessage: error,
@@ -157,8 +154,14 @@ public struct InputDropDown: View {
157
154
  hintText: hintText
158
155
  )
159
156
  }
157
+ // Compose makes the whole column clickable, error/hint row included.
158
+ .contentShape(Rectangle())
159
+ .onTapGesture {
160
+ if !disabled { onPress() }
161
+ }
160
162
  .accessibilityElement(children: .combine)
161
163
  .accessibility(label: Text(floatingValue))
164
+ .accessibilityIdentifier(disabled ? "input_\(floatingValue)_disabled" : "input_\(floatingValue)")
162
165
  }
163
166
 
164
167
  // MARK: - Computed colors
@@ -3,9 +3,8 @@
3
3
  // MoMoUIKits
4
4
  //
5
5
  // Native SwiftUI reimplementation of Compose `InputMoney`.
6
- // The bound `text` holds RAW DIGITS only (mirrors Compose, where the
7
- // field stores the numeric string and a VisualTransformation renders the
8
- // thousands-separated currency). Display formatting is applied live.
6
+ // The bound `text` holds RAW DIGITS only, as in Compose; the field shows
7
+ // the thousands-separated currency and edits apply at the caret.
9
8
  //
10
9
 
11
10
  import Foundation
@@ -165,17 +164,18 @@ public struct InputMoney: View {
165
164
  // MARK: - Body
166
165
 
167
166
  public var body: some View {
168
- VStack(alignment: .leading, spacing: 4) {
167
+ VStack(alignment: .leading, spacing: 0) {
169
168
  ZStack(alignment: .topLeading) {
170
169
  // Floating label
171
170
  if !floatingValue.isEmpty || !floatingIcon.isEmpty {
172
- HStack(spacing: Spacing.XS) {
171
+ HStack(spacing: 0) {
173
172
  MomoText(floatingValue, typography: .labelSMedium, color: floatingTitleColor)
174
173
  if required {
175
174
  MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
176
175
  }
177
176
  if !floatingIcon.isEmpty {
178
177
  Icon(source: floatingIcon, size: 16, color: floatingIconTintColor)
178
+ .padding(.leading, Spacing.XS)
179
179
  }
180
180
  }
181
181
  .padding(.horizontal, Spacing.S)
@@ -204,17 +204,19 @@ public struct InputMoney: View {
204
204
 
205
205
  // Money field
206
206
  ZStack(alignment: .leading) {
207
- if text.filter({ $0.isNumber }).isEmpty {
207
+ if !hasDigits {
208
208
  MomoText(
209
209
  placeholder,
210
210
  typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
211
211
  color: placeholderColor
212
212
  )
213
+ .fontSize(20, lineHeight: 24)
213
214
  }
214
215
  MoneyTextField(
215
216
  digits: $text,
216
217
  keyboardType: keyboardType,
217
218
  fontWeight: fontWeight == .bold ? .bold : .regular,
219
+ fontFamily: theme.font,
218
220
  textColor: UIColor(textColor),
219
221
  cursorColor: UIColor(theme.colors.primary),
220
222
  isDisabled: disabled,
@@ -225,7 +227,7 @@ public struct InputMoney: View {
225
227
  .frame(maxWidth: .infinity, alignment: .leading)
226
228
 
227
229
  // Clear button (focused + has value)
228
- if isFocused && !text.filter({ $0.isNumber }).isEmpty {
230
+ if isFocused && hasDigits {
229
231
  SwiftUI.Button(action: {
230
232
  text = ""
231
233
  onChangeText?("")
@@ -239,7 +241,7 @@ public struct InputMoney: View {
239
241
  // Loading indicator and right icon can both render (mirrors
240
242
  // Compose's two independent `if` blocks, not if/else).
241
243
  if loading {
242
- ActivityIndicator(isAnimating: .constant(true), style: .medium)
244
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: iconTintColor)
243
245
  .frame(width: 16, height: 16)
244
246
  .padding(.leading, Spacing.S)
245
247
  }
@@ -264,6 +266,9 @@ public struct InputMoney: View {
264
266
 
265
267
  ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
266
268
  }
269
+ .accessibilityElement(children: .contain)
270
+ .accessibilityLabel(floatingValue)
271
+ .accessibilityIdentifier(disabled ? "input_\(floatingValue)_disabled" : "input_\(floatingValue)")
267
272
  .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
268
273
  if isFocused {
269
274
  DispatchQueue.main.async { handleFocusChange(false) }
@@ -273,6 +278,8 @@ public struct InputMoney: View {
273
278
 
274
279
  // MARK: - Helpers
275
280
 
281
+ private var hasDigits: Bool { text.contains(where: isMoneyDigit) }
282
+
276
283
  private func handleFocusChange(_ focused: Bool) {
277
284
  guard isFocused != focused else { return }
278
285
  isFocused = focused
@@ -303,6 +310,7 @@ private struct MoneyTextField: UIViewRepresentable {
303
310
  @Binding var digits: String
304
311
  var keyboardType: UIKeyboardType
305
312
  var fontWeight: UIFont.Weight
313
+ var fontFamily: String
306
314
  var textColor: UIColor
307
315
  var cursorColor: UIColor
308
316
  var isDisabled: Bool
@@ -315,7 +323,7 @@ private struct MoneyTextField: UIViewRepresentable {
315
323
  let field = UITextField()
316
324
  field.delegate = context.coordinator
317
325
  field.keyboardType = keyboardType
318
- field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
326
+ field.font = font(context)
319
327
  field.textColor = textColor
320
328
  field.isEnabled = !isDisabled
321
329
  field.tintColor = cursorColor
@@ -328,10 +336,14 @@ private struct MoneyTextField: UIViewRepresentable {
328
336
  func updateUIView(_ field: UITextField, context: Context) {
329
337
  context.coordinator.parent = self
330
338
  // Re-applied here, not just in makeUIView, so a font-scale change reaches an existing field.
331
- field.font = UIFont.systemFont(ofSize: context.environment.momoFontScale(20), weight: fontWeight)
339
+ field.font = font(context)
332
340
  field.isEnabled = !isDisabled
333
341
  field.textColor = textColor
334
342
  field.tintColor = cursorColor
343
+ if field.keyboardType != keyboardType {
344
+ field.keyboardType = keyboardType
345
+ field.reloadInputViews()
346
+ }
335
347
  let expected = displayValue(digits)
336
348
  if field.text != expected {
337
349
  field.text = expected
@@ -340,10 +352,19 @@ private struct MoneyTextField: UIViewRepresentable {
340
352
  }
341
353
 
342
354
  fileprivate func displayValue(_ raw: String) -> String {
343
- let numeric = raw.filter { $0.isNumber }
355
+ let numeric = raw.filter(isMoneyDigit)
344
356
  return numeric.isEmpty ? "" : formatNumberToMoney(numeric)
345
357
  }
346
358
 
359
+ private func font(_ context: Context) -> UIFont {
360
+ momoUIFont(
361
+ size: context.environment.momoFontScale(20),
362
+ weight: fontWeight,
363
+ family: fontFamily,
364
+ isWhiteList: context.environment.designSystemKitsConfigs.isWhiteList
365
+ )
366
+ }
367
+
347
368
  final class Coordinator: NSObject, UITextFieldDelegate {
348
369
  var parent: MoneyTextField
349
370
 
@@ -48,6 +48,7 @@ public struct InputOTP: View {
48
48
 
49
49
  @FocusState private var isFocused: Bool
50
50
  @State private var hasBeenFocused: Bool = false
51
+ @State private var fieldText: String
51
52
 
52
53
  private var maxLength: Int { length ?? 10 }
53
54
 
@@ -64,6 +65,7 @@ public struct InputOTP: View {
64
65
  onBlur: (() -> Void)? = nil
65
66
  ) {
66
67
  self._value = value
68
+ self._fieldText = State(initialValue: value.wrappedValue)
67
69
  self.length = length
68
70
  self.errorMessage = errorMessage
69
71
  self.errorSpacing = errorSpacing
@@ -105,7 +107,7 @@ public struct InputOTP: View {
105
107
  HStack {
106
108
  Spacer()
107
109
  SwiftUI.Button(action: clearText) {
108
- Icon(source: "24_navigation_close_circle_full", size: 12, color: theme.colors.text.hint)
110
+ Icon(source: "24_navigation_close_circle_full", size: 12, color: theme.colors.text.default)
109
111
  .padding(.trailing, 12)
110
112
  }
111
113
  }
@@ -122,17 +124,24 @@ public struct InputOTP: View {
122
124
  // MARK: Subviews
123
125
 
124
126
  private var hiddenField: some View {
125
- TextField("", text: Binding(
126
- get: { value },
127
- set: { handleChangeText($0) }
128
- ))
127
+ TextField("", text: $fieldText)
129
128
  .keyboardType(dataType == "number" ? .numberPad : .asciiCapable)
129
+ .textInputAutocapitalization(.never)
130
+ .autocorrectionDisabled()
130
131
  .focused($isFocused)
131
132
  .foregroundColor(.clear)
132
133
  .accentColor(.clear)
133
134
  .frame(maxWidth: .infinity)
134
135
  .frame(height: 56)
135
136
  .opacity(0.01)
137
+ // Writes a rejected edit back, so the field stays in step with `value` like Compose's
138
+ // controlled field instead of keeping the extra character and swallowing the next key.
139
+ .onChange(of: fieldText) { text in
140
+ if text != value && !handleChangeText(text) { fieldText = value }
141
+ }
142
+ .onChange(of: value) { newValue in
143
+ if fieldText != newValue { fieldText = newValue }
144
+ }
136
145
  .onChange(of: isFocused) { focused in
137
146
  if focused {
138
147
  onFocus?()
@@ -147,7 +156,7 @@ public struct InputOTP: View {
147
156
  private var cells: some View {
148
157
  if let length = length {
149
158
  HStack(spacing: Spacing.L) {
150
- ForEach(0 ..< length, id: \.self) { i in
159
+ ForEach(0 ..< max(length, 0), id: \.self) { i in
151
160
  cell(at: i)
152
161
  }
153
162
  }
@@ -155,7 +164,7 @@ public struct InputOTP: View {
155
164
  } else {
156
165
  HStack(spacing: Spacing.L) {
157
166
  if value.isEmpty && !isFocused && !placeholder.isEmpty {
158
- MomoText(placeholder, typography: .descriptionDefaultRegular, color: theme.colors.text.hint)
167
+ MomoText(placeholder, typography: .bodyDefaultRegular, color: theme.colors.text.hint)
159
168
  } else {
160
169
  ForEach(Array(value.enumerated()), id: \.offset) { _, ch in
161
170
  MomoText(String(ch), typography: .headerDefaultBold, color: theme.colors.text.default)
@@ -186,13 +195,14 @@ public struct InputOTP: View {
186
195
 
187
196
  // MARK: Logic
188
197
 
189
- private func handleChangeText(_ text: String) {
198
+ @discardableResult
199
+ private func handleChangeText(_ text: String) -> Bool {
190
200
  let validData = dataType != "number" || text.allSatisfy { $0.isNumber }
191
201
  let changed = text.count != value.count
192
- if text.count <= maxLength && validData && changed {
193
- value = text
194
- onChangeText?(text)
195
- }
202
+ guard text.count <= maxLength && validData && changed else { return false }
203
+ value = text
204
+ onChangeText?(text)
205
+ return true
196
206
  }
197
207
 
198
208
  private func clearText() {