@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -76,16 +76,15 @@ public struct InputPhoneNumber: View {
76
76
  }
77
77
  )
78
78
 
79
- VStack(alignment: .leading, spacing: 4) {
79
+ VStack(alignment: .leading, spacing: 0) {
80
80
  HStack(spacing: 0) {
81
81
  // 🇻🇳 Flag
82
82
  ImageView("https://static.momocdn.net/app/img/icon/ic-qrcode-package/ic_vn_flag.png")
83
83
  .frame(width: 24, height: 24)
84
84
  .padding(.trailing, Spacing.XS)
85
-
86
- MomoText("+84", typography: size == .small ? .headerSSemibold : .headerMBold)
87
- .foregroundColor(theme.colors.text.default)
88
-
85
+
86
+ MomoText("+84", typography: size == .small ? .headerSSemibold : .headerMBold, color: theme.colors.text.default)
87
+
89
88
  Rectangle()
90
89
  .fill(theme.colors.border.default)
91
90
  .frame(width: 1, height: size == .small ? 24 : 32)
@@ -105,7 +104,9 @@ public struct InputPhoneNumber: View {
105
104
  handleFocusChange(focused)
106
105
  })
107
106
  .keyboardType(.numberPad)
108
- .font(size == .small ? .header_s_semibold : .header_m_bold)
107
+ .font(size == .small
108
+ ? fontScale.font(size: 14, weight: .semibold)
109
+ : fontScale.font(size: 18, weight: .bold))
109
110
  .ignoreBoldTextSetting()
110
111
  .foregroundColor(theme.colors.text.default)
111
112
  .applyCursorColor(theme.colors.primary)
@@ -125,13 +126,14 @@ public struct InputPhoneNumber: View {
125
126
  onChangeText?("")
126
127
  }) {
127
128
  Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
128
- .padding(.leading, Spacing.S).accessibility(identifier: "ic_clear")
129
+ .padding(.horizontal, Spacing.S)
129
130
  }
131
+ .accessibilityIdentifier("ic_clear")
130
132
  }
131
-
133
+
132
134
  // Loading indicator
133
135
  if loading {
134
- ActivityIndicator(isAnimating: .constant(true), style: .medium)
136
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: rightIconColor ?? theme.colors.text.default)
135
137
  .frame(width: 16, height: 16)
136
138
  }
137
139
 
@@ -166,6 +168,7 @@ public struct InputPhoneNumber: View {
166
168
  // MARK: - Helpers
167
169
 
168
170
  private func handleFocusChange(_ focused: Bool) {
171
+ guard focused != isFocused else { return }
169
172
  isFocused = focused
170
173
  if focused {
171
174
  onFocus?()
@@ -18,16 +18,17 @@ struct RenderRightIconSearch: View {
18
18
  var body: some View {
19
19
  HStack(spacing: 0) {
20
20
  if loading {
21
- ActivityIndicator(isAnimating: .constant(true), style: .medium)
21
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: color)
22
22
  .frame(width: 16, height: 16)
23
23
  .padding(.horizontal, Spacing.S)
24
24
  }
25
25
  if !icon.isEmpty {
26
- Divider()
27
- .frame(width: 1)
28
- .background(theme.colors.primary)
26
+ // Compose: a 1pt rule inset S from the top and bottom of the 36pt bar.
27
+ Rectangle()
28
+ .fill(theme.colors.primary)
29
+ .frame(width: 1, height: 36 - Spacing.S * 2)
29
30
  .padding(.leading, Spacing.S)
30
-
31
+
31
32
  Icon(source: icon, size: 24, color: color)
32
33
  .padding(.leading, Spacing.S)
33
34
  .onTapGesture(perform: onClick)
@@ -58,6 +59,10 @@ public struct InputSearchProps {
58
59
  var searchIcon: (() -> AnyView)? = nil
59
60
  // Mirrors Compose's InputSearchProps.placeholderCustomRender — a custom placeholder view.
60
61
  var placeholderCustomRender: (() -> AnyView)? = nil
62
+ var backgroundColor: Color? = nil
63
+ var clearCondition: (() -> Bool)? = nil
64
+ var onClearPress: () -> Void = {}
65
+ var onSubmit: () -> Void = {}
61
66
 
62
67
  public init(text: Binding<String>,
63
68
  buttonText: String = "",
@@ -77,7 +82,11 @@ public struct InputSearchProps {
77
82
  fontWeight: Font.Weight = .regular,
78
83
  keyboardType: UIKeyboardType = .default,
79
84
  searchIcon: (() -> AnyView)? = nil,
80
- placeholderCustomRender: (() -> AnyView)? = nil) {
85
+ placeholderCustomRender: (() -> AnyView)? = nil,
86
+ backgroundColor: Color? = nil,
87
+ clearCondition: (() -> Bool)? = nil,
88
+ onClearPress: @escaping () -> Void = {},
89
+ onSubmit: @escaping () -> Void = {}) {
81
90
  self._text = text
82
91
  self.buttonText = buttonText
83
92
  self.showButtonText = showButtonText
@@ -97,6 +106,10 @@ public struct InputSearchProps {
97
106
  self.keyboardType = keyboardType
98
107
  self.searchIcon = searchIcon
99
108
  self.placeholderCustomRender = placeholderCustomRender
109
+ self.backgroundColor = backgroundColor
110
+ self.clearCondition = clearCondition
111
+ self.onClearPress = onClearPress
112
+ self.onSubmit = onSubmit
100
113
  }
101
114
  }
102
115
 
@@ -124,6 +137,10 @@ public struct InputSearch: View {
124
137
  var keyboardType: UIKeyboardType = .default
125
138
  var searchIcon: (() -> AnyView)? = nil
126
139
  var placeholderCustomRender: (() -> AnyView)? = nil
140
+ var backgroundColor: Color? = nil
141
+ var clearCondition: (() -> Bool)? = nil
142
+ var onClearPress: () -> Void = {}
143
+ var onSubmit: () -> Void = {}
127
144
 
128
145
  @State private var isFocused = false
129
146
  @State private var isBlurred = false
@@ -146,7 +163,11 @@ public struct InputSearch: View {
146
163
  fontWeight: Font.Weight = .regular,
147
164
  keyboardType: UIKeyboardType = .default,
148
165
  searchIcon: (() -> AnyView)? = nil,
149
- placeholderCustomRender: (() -> AnyView)? = nil) {
166
+ placeholderCustomRender: (() -> AnyView)? = nil,
167
+ backgroundColor: Color? = nil,
168
+ clearCondition: (() -> Bool)? = nil,
169
+ onClearPress: @escaping () -> Void = {},
170
+ onSubmit: @escaping () -> Void = {}) {
150
171
  self._text = text
151
172
  self.buttonText = buttonText
152
173
  self.showButtonText = showButtonText
@@ -166,6 +187,10 @@ public struct InputSearch: View {
166
187
  self.keyboardType = keyboardType
167
188
  self.searchIcon = searchIcon
168
189
  self.placeholderCustomRender = placeholderCustomRender
190
+ self.backgroundColor = backgroundColor
191
+ self.clearCondition = clearCondition
192
+ self.onClearPress = onClearPress
193
+ self.onSubmit = onSubmit
169
194
  }
170
195
 
171
196
  /// Convenience initializer mirroring Compose's `InputSearch(inputSearchProps)`.
@@ -189,88 +214,90 @@ public struct InputSearch: View {
189
214
  fontWeight: props.fontWeight,
190
215
  keyboardType: props.keyboardType,
191
216
  searchIcon: props.searchIcon,
192
- placeholderCustomRender: props.placeholderCustomRender
217
+ placeholderCustomRender: props.placeholderCustomRender,
218
+ backgroundColor: props.backgroundColor,
219
+ clearCondition: props.clearCondition,
220
+ onClearPress: props.onClearPress,
221
+ onSubmit: props.onSubmit
193
222
  )
194
223
  }
195
224
 
196
225
 
197
226
  public var body: some View {
198
227
  let iconColor: Color = disabled ? theme.colors.text.disable : (self.iconColor ?? theme.colors.text.default)
199
- VStack(alignment: .leading, spacing: 0) {
228
+ HStack(spacing: 0) {
200
229
  HStack(spacing: 0) {
201
- HStack {
202
- if let searchIcon = searchIcon {
203
- searchIcon()
204
- } else {
205
- Image(systemName: "magnifyingglass")
206
- .foregroundColor(theme.colors.text.hint)
207
- .frame(width: 24, height: 24)
208
- .padding(.trailing, Spacing.XS)
209
- }
230
+ if let searchIcon = searchIcon {
231
+ searchIcon()
232
+ } else {
233
+ Icon(source: "navigation_search", size: 24, color: theme.colors.text.hint)
234
+ .padding(.trailing, Spacing.XS)
235
+ }
210
236
 
211
- ZStack(alignment: .leading) {
212
- if text.isEmpty {
213
- if let placeholderCustomRender = placeholderCustomRender {
214
- placeholderCustomRender()
215
- } else {
216
- Text(placeholder)
217
- .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.hint)
218
- .font(.system(size: fontScale(14), weight: fontWeight))
219
- .ignoreBoldTextSetting()
220
- }
237
+ ZStack(alignment: .leading) {
238
+ if text.isEmpty {
239
+ if let placeholderCustomRender = placeholderCustomRender {
240
+ placeholderCustomRender()
241
+ } else {
242
+ MomoText(
243
+ placeholder,
244
+ typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
245
+ color: disabled ? theme.colors.text.disable : theme.colors.text.hint,
246
+ maxLines: 1
247
+ )
221
248
  }
222
- TextField("", text: $text, onEditingChanged: { editing in
223
- isFocused = editing
224
- if editing {
225
- onFocus()
226
- } else if isBlurred {
227
- onBlur()
228
- }
229
- if editing && !isBlurred {
230
- isBlurred = true
231
- }
232
- }, onCommit: {})
233
- .disabled(disabled)
234
- .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.default)
235
- .font(.system(size: fontScale(14), weight: fontWeight))
236
- .ignoreBoldTextSetting()
237
- .keyboardType(keyboardType)
238
- .applyCursorColor(theme.colors.primary)
239
249
  }
240
-
241
- if isFocused && !text.isEmpty {
242
- SwiftButton(action: {
243
- text = ""
244
- // Kotlin's clear icon fires a callback distinct from
245
- // onChangeText (`onClearPress`, not ported here); route
246
- // through onChangeText so clearing isn't a silent no-op.
247
- onChangeText("")
248
- }) {
249
- Image(systemName: "xmark.circle.fill")
250
- .foregroundColor(theme.colors.text.hint)
251
- .frame(width: 16, height: 16)
250
+ TextField("", text: $text, onEditingChanged: { editing in
251
+ isFocused = editing
252
+ if editing {
253
+ onFocus()
254
+ } else if isBlurred {
255
+ onBlur()
252
256
  }
253
- .padding(.leading, Spacing.S)
254
- }
255
-
256
- RenderRightIconSearch(loading: loading, icon: icon, color: iconColor, onClick: onRightIconPressed)
257
+ if editing && !isBlurred {
258
+ isBlurred = true
259
+ }
260
+ }, onCommit: onSubmit)
261
+ .disabled(disabled)
262
+ .foregroundColor(disabled ? theme.colors.text.disable : theme.colors.text.default)
263
+ .font(.system(size: fontScale(14), weight: fontWeight))
264
+ .ignoreBoldTextSetting()
265
+ .keyboardType(keyboardType)
266
+ .applyCursorColor(theme.colors.primary)
257
267
  }
258
- .padding(.horizontal, Spacing.M)
259
- .padding(.vertical, Spacing.S)
260
- .background(theme.colors.background.surface)
261
- .cornerRadius(Radius.XL)
262
- .overlay(
263
- RoundedRectangle(cornerRadius: Radius.XL)
264
- .stroke(getBorderColor(isFocused: isFocused, error: error, disabled: disabled), lineWidth: showBorder ? 1 : 0)
265
- )
266
- .frame(maxWidth: showButtonText ? .infinity : nil, alignment: .leading)
267
-
268
- if showButtonText {
269
- SwiftButton(action: onPressButtonText) {
270
- MomoText(buttonText, typography: .actionDefaultBold, color: theme.colors.text.default)
268
+
269
+ if clearCondition?() == true || (isFocused && !text.isEmpty) {
270
+ SwiftButton(action: {
271
+ text = ""
272
+ // Compose only fires onClearPress; onChangeText("") is kept so existing
273
+ // SwiftUI callers that filter on it still see the clear.
274
+ onChangeText("")
275
+ onClearPress()
276
+ }) {
277
+ Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
271
278
  }
272
- .padding(.leading, Spacing.L)
279
+ .padding(.leading, Spacing.XS)
280
+ }
281
+
282
+ RenderRightIconSearch(loading: loading, icon: icon, color: iconColor, onClick: onRightIconPressed)
283
+ }
284
+ .padding(.horizontal, Spacing.M)
285
+ .frame(maxWidth: .infinity, alignment: .leading)
286
+ .frame(height: 36)
287
+ .background(
288
+ RoundedRectangle(cornerRadius: Radius.XL)
289
+ .fill(backgroundColor ?? theme.colors.background.surface)
290
+ )
291
+ .overlay(
292
+ RoundedRectangle(cornerRadius: Radius.XL)
293
+ .stroke(showBorder ? getBorderColor(isFocused: isFocused, error: error, disabled: disabled) : .clear, lineWidth: 1)
294
+ )
295
+
296
+ if showButtonText {
297
+ SwiftButton(action: onPressButtonText) {
298
+ MomoText(buttonText, typography: .actionDefaultBold, color: theme.colors.text.default)
273
299
  }
300
+ .padding(.leading, Spacing.L)
274
301
  }
275
302
  }
276
303
  }
@@ -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
  }
@@ -44,19 +46,23 @@ struct UITextViewWrapper: UIViewRepresentable {
44
46
  weight: fontWeight == .bold ? .bold : .regular
45
47
  )
46
48
  uiView.isEditable = !disabled
49
+ if uiView.keyboardType != keyboardType {
50
+ uiView.keyboardType = keyboardType
51
+ uiView.reloadInputViews()
52
+ }
47
53
  }
48
-
54
+
49
55
  func makeCoordinator() -> Coordinator {
50
56
  Coordinator(self)
51
57
  }
52
-
58
+
53
59
  class Coordinator: NSObject, UITextViewDelegate {
54
60
  var parent: UITextViewWrapper
55
-
61
+
56
62
  init(_ parent: UITextViewWrapper) {
57
63
  self.parent = parent
58
64
  }
59
-
65
+
60
66
  func textViewDidChange(_ textView: UITextView) {
61
67
  let limited = String(textView.text.prefix(parent.maxLength))
62
68
  if textView.text != limited {
@@ -65,20 +71,17 @@ struct UITextViewWrapper: UIViewRepresentable {
65
71
  parent.text = limited
66
72
  parent.onChangeText(limited)
67
73
  }
68
-
74
+
69
75
  func textViewDidBeginEditing(_ textView: UITextView) {
70
76
  parent.onFocus?()
71
77
  }
72
-
78
+
73
79
  func textViewDidEndEditing(_ textView: UITextView) {
74
80
  parent.onBlur?()
75
81
  }
76
-
82
+
83
+ // Like Compose's `onValueChange`, an edit that would exceed maxLength is rejected whole.
77
84
  func textView(_ textView: UITextView, shouldChangeTextIn range: NSRange, replacementText text: String) -> Bool {
78
- if text == "\n" {
79
- parent.onDone?()
80
- return false
81
- }
82
85
  let current = textView.text ?? ""
83
86
  guard let range = Range(range, in: current) else { return false }
84
87
  let proposed = current.replacingCharacters(in: range, with: text)
@@ -90,6 +93,7 @@ struct UITextViewWrapper: UIViewRepresentable {
90
93
  // Make the struct public
91
94
  public struct InputTextArea: View {
92
95
  @Environment(\.appTheme) private var theme
96
+ @Environment(\.momoFontScale) private var fontScale
93
97
  @Binding var text: String
94
98
  var maxLength: Int = MAX_LENGTH
95
99
  var height: CGFloat = DEFAULT_HEIGHT
@@ -111,11 +115,9 @@ public struct InputTextArea: View {
111
115
  var required: Bool = false
112
116
  var fontWeight: Font.Weight = .regular
113
117
  var keyboardType: UIKeyboardType = .default
114
-
118
+
115
119
  @State private var isFocused = false
116
- @State private var isBlurred = false
117
- @State private var floatingLabelHeight: CGFloat = 16
118
-
120
+
119
121
  // Make the initializer public
120
122
  public init(
121
123
  text: Binding<String>,
@@ -162,147 +164,128 @@ public struct InputTextArea: View {
162
164
  self.fontWeight = fontWeight
163
165
  self.keyboardType = keyboardType
164
166
  }
165
-
167
+
166
168
  public var body: some View {
167
169
  VStack(alignment: .leading, spacing: 0) {
170
+ // Compose: `height` is the whole box (text, counter and paddings), scaled.
171
+ VStack(spacing: 0) {
172
+ HStack(alignment: .top, spacing: 0) {
168
173
  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
- }
174
+ if text.isEmpty {
175
+ MomoText(
176
+ placeholder,
177
+ typography: fontWeight == .bold ? .actionSBold : .bodyDefaultRegular,
178
+ color: disabled ? theme.colors.text.disable : theme.colors.text.hint
195
179
  )
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)
180
+ .fontSize(16, lineHeight: 24)
205
181
  }
206
182
 
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
- }
183
+ UITextViewWrapper(
184
+ text: $text,
185
+ maxLength: maxLength,
186
+ textColor: disabled ? theme.colors.text.disable : theme.colors.text.default,
187
+ cursorColor: theme.colors.primary,
188
+ fontWeight: fontWeight,
189
+ keyboardType: keyboardType,
190
+ disabled: disabled,
191
+ onChangeText: onChangeText,
192
+ onFocus: {
193
+ isFocused = true
194
+ onFocus()
195
+ },
196
+ onBlur: {
197
+ isFocused = false
198
+ onBlur()
271
199
  }
200
+ )
201
+ .disabled(disabled)
202
+ }
203
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
272
204
 
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)
205
+ // Clear button (mirrors Compose: focused + non-empty).
206
+ if isFocused && !text.isEmpty {
207
+ SwiftUI.Button(action: {
208
+ text = ""
209
+ onChangeText("")
210
+ }) {
211
+ Icon(source: "24_navigation_close_circle_full", size: 16, color: theme.colors.text.hint)
279
212
  }
280
- .background(theme.colors.background.surface)
281
- .cornerRadius(Radius.S)
282
- .overlay(
283
- RoundedRectangle(cornerRadius: Radius.S)
284
- .stroke(borderColor, lineWidth: 1)
285
- )
213
+ .padding(.leading, Spacing.XS)
286
214
  }
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()
215
+
216
+ // Loading indicator and right icon (independent, mirrors
217
+ // Compose's RenderRightIcon: both can render at once).
218
+ if loading {
219
+ ActivityIndicator(isAnimating: .constant(true), style: .medium, color: iconTintColor)
220
+ .frame(width: 16, height: 16)
221
+ .padding(.leading, Spacing.XS)
294
222
  }
295
- }
296
- .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillHideNotification)) { _ in
297
- if isFocused {
298
- isFocused = false
299
- isBlurred = true
300
- onBlur()
223
+ if !icon.isEmpty {
224
+ SwiftUI.Button(action: onRightIconPressed) {
225
+ Icon(source: icon, size: 24, color: iconTintColor)
226
+ }
227
+ .padding(.leading, Spacing.XS)
301
228
  }
302
229
  }
303
- .ignoreBoldTextSetting()
230
+ .padding([.horizontal, .top], Spacing.M)
231
+ .frame(maxHeight: .infinity, alignment: .top)
232
+
233
+ MomoText("\(text.count)/\(maxLength)", typography: .descriptionXsRegular, color: theme.colors.text.hint)
234
+ .frame(maxWidth: .infinity, alignment: .trailing)
235
+ .padding(.trailing, Spacing.M)
236
+ .padding(.bottom, Spacing.M)
237
+ }
238
+ .frame(height: fontScale(height))
239
+ .background(
240
+ RoundedRectangle(cornerRadius: Radius.S)
241
+ .fill(theme.colors.background.surface)
242
+ )
243
+ .overlay(
244
+ RoundedRectangle(cornerRadius: Radius.S)
245
+ .stroke(borderColor, lineWidth: 1)
246
+ )
247
+ .overlay(alignment: .topLeading) { floatingLabel }
248
+
249
+ ErrorView(errorMessage: error, errorSpacing: errorSpacing, hintText: "")
250
+ }
251
+ .ignoreBoldTextSetting()
304
252
  }
305
-
253
+
254
+ /// Centered on the box's top border, like Compose's `FloatingLabel` (offset by -height / 2).
255
+ @ViewBuilder
256
+ private var floatingLabel: some View {
257
+ if !floatingValue.isEmpty || !floatingIcon.isEmpty {
258
+ HStack(spacing: 0) {
259
+ MomoText(
260
+ floatingValue,
261
+ typography: .labelSMedium,
262
+ color: disabled ? theme.colors.text.disable : (floatingValueColor ?? theme.colors.text.hint)
263
+ )
264
+
265
+ if required {
266
+ MomoText("*", typography: .labelSMedium, color: theme.colors.error.primary)
267
+ }
268
+
269
+ if !floatingIcon.isEmpty {
270
+ Icon(
271
+ source: floatingIcon,
272
+ size: 16,
273
+ color: disabled ? theme.colors.text.disable : (floatingIconColor ?? theme.colors.text.default)
274
+ )
275
+ .padding(.leading, Spacing.XS)
276
+ }
277
+ }
278
+ .padding(.horizontal, Spacing.S)
279
+ .background(theme.colors.background.surface)
280
+ .alignmentGuide(.top) { $0[VerticalAlignment.center] }
281
+ .offset(x: Spacing.S)
282
+ }
283
+ }
284
+
285
+ private var iconTintColor: Color {
286
+ disabled ? theme.colors.text.disable : (iconColor ?? theme.colors.text.default)
287
+ }
288
+
306
289
  private var borderColor: Color {
307
290
  if disabled {
308
291
  return theme.colors.border.disable