@momo-kits/native-kits 0.167.1-duo.8-debug → 0.167.1-duo.9

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 (254) hide show
  1. package/CLAUDE.md +4 -1
  2. package/ios/Application/ApplicationEnvironment.swift +11 -0
  3. package/ios/Application/ComposeApi.swift +2 -1
  4. package/ios/Application/{Components.swift → LegacyHeader.swift} +6 -62
  5. package/ios/Application/Screen.swift +3 -2
  6. package/ios/Avatar/Avatar.swift +4 -3
  7. package/ios/Badge/Badge.swift +6 -4
  8. package/ios/Badge/BadgeDot.swift +1 -1
  9. package/ios/Badge/BadgeRibbon.swift +0 -1
  10. package/ios/Button/Button.swift +15 -12
  11. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +12 -20
  12. package/ios/Carousel/Carousel.swift +29 -12
  13. package/ios/Checkbox/Checkbox.swift +7 -3
  14. package/ios/Chip/Chip.swift +5 -4
  15. package/ios/Collapse/Collapse.swift +70 -19
  16. package/ios/Colors+Radius+Spacing/Colors.swift +9 -0
  17. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  18. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  19. package/ios/Divider/Divider.swift +37 -4
  20. package/ios/Extensions/Color++.swift +18 -0
  21. package/ios/Icon/Icon.swift +7 -1
  22. package/ios/IconButton/IconButton.swift +15 -7
  23. package/ios/Image/Image.swift +7 -34
  24. package/ios/Information/Information.swift +27 -9
  25. package/ios/Input/ErrorView.swift +2 -2
  26. package/ios/Input/Input.swift +160 -147
  27. package/ios/Input/InputPhoneNumber.swift +12 -9
  28. package/ios/Input/InputSearch.swift +102 -75
  29. package/ios/Input/InputTextArea.swift +128 -145
  30. package/ios/InputDropDown/InputDropDown.swift +10 -7
  31. package/ios/InputMoney/InputMoney.swift +33 -11
  32. package/ios/InputOTP/InputOTP.swift +22 -12
  33. package/ios/Layout/Card.swift +50 -0
  34. package/ios/Layout/GridContext.swift +123 -3
  35. package/ios/Layout/Item.swift +5 -4
  36. package/ios/Layout/Section.swift +4 -100
  37. package/ios/Loader/Loader.swift +5 -9
  38. package/ios/Lottie/LottieView.swift +10 -0
  39. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +61 -9
  40. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTab.swift +60 -34
  41. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +11 -6
  42. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  43. package/ios/Navigation/Component/Header.swift +493 -0
  44. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  45. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  46. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  47. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  48. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  49. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +20 -23
  50. package/ios/{Application/Navigation/component → Navigation/Component}/SideBar.swift +49 -28
  51. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  52. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  53. package/ios/Navigation/Navigation.swift +190 -0
  54. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +46 -41
  55. package/ios/{Application/Navigation → Navigation}/Navigator.swift +65 -9
  56. package/ios/{Application → Navigation}/StackScreen.swift +236 -103
  57. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  58. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  59. package/ios/Pagination/PaginationScroll.swift +2 -2
  60. package/ios/Popup/PopupDisplay.swift +79 -159
  61. package/ios/Popup/PopupInput.swift +10 -13
  62. package/ios/Popup/PopupLayout.swift +97 -2
  63. package/ios/Popup/PopupNotify.swift +27 -55
  64. package/ios/Popup/PopupPromotion.swift +22 -50
  65. package/ios/PopupView/FullscreenPopup.swift +13 -19
  66. package/ios/PopupView/PopupView.swift +1 -1
  67. package/ios/PopupView/Utils++.swift +0 -8
  68. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  69. package/ios/Radio/Radio.swift +2 -1
  70. package/ios/Rating/Rating.swift +9 -1
  71. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  72. package/ios/Skeleton/Skeleton.swift +22 -27
  73. package/ios/Slider/Slider.swift +16 -18
  74. package/ios/Stepper/Stepper.swift +6 -1
  75. package/ios/Steps/Steps.swift +8 -6
  76. package/ios/SuggestAction/SuggestAction.swift +5 -0
  77. package/ios/Swipeable/SwipeCell.swift +10 -6
  78. package/ios/Switch/Switch.swift +22 -20
  79. package/ios/TabView/TabView.swift +102 -21
  80. package/ios/Tag/Tag.swift +15 -4
  81. package/ios/Template/Logo/Logo.swift +2 -1
  82. package/ios/Template/TrustBanner/TrustBanner.swift +41 -27
  83. package/ios/Title/Title.swift +6 -17
  84. package/ios/Tooltip/Tooltip.swift +29 -14
  85. package/ios/Typography/Text.swift +61 -19
  86. package/ios/Uploader/Uploader.swift +3 -1
  87. package/ios/native-kits.podspec +1 -0
  88. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  89. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  90. package/package.json +1 -1
  91. package/build.gradle.kts +0 -11
  92. package/compose/build.gradle.kts +0 -204
  93. package/compose/build.gradle.kts.backup +0 -204
  94. package/compose/compose.podspec +0 -47
  95. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  96. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  97. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  98. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  99. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
  100. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -831
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
  149. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  150. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  151. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  152. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  153. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
  154. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  155. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  156. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  157. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
  158. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
  159. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
  160. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
  161. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  162. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  163. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
  164. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -188
  177. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
  178. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -232
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  182. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  183. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  184. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -708
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -203
  199. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -232
  200. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  201. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
  202. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
  203. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  204. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -408
  205. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -92
  206. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -105
  207. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  208. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -366
  209. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
  210. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  211. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -185
  212. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
  213. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  214. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  215. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  216. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  217. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
  218. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
  219. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  220. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  221. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  222. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  223. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
  224. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  225. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  226. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  227. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  228. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
  229. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -86
  230. package/gradle/libs.versions.toml +0 -69
  231. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  232. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  233. package/gradle.properties +0 -26
  234. package/gradlew +0 -252
  235. package/gradlew.bat +0 -94
  236. package/ios/Application/HeaderRight.swift +0 -324
  237. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  238. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  239. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  240. package/ios/Application/Navigation/NavigatorManager.swift +0 -29
  241. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -54
  242. package/ios/Application/Navigation/component/Header.swift +0 -183
  243. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -84
  244. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  245. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  246. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  247. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -280
  248. package/ios-demo/README.md +0 -120
  249. package/settings.gradle.kts +0 -64
  250. /package/ios/{Application/Layout → Layout}/ScreenLayout.swift +0 -0
  251. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
  252. /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoBarCompression.swift +0 -0
  253. /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoTabBar.swift +0 -0
  254. /package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +0 -0
@@ -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,20 @@ 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
+ // Theme family as Compose `getFontFamily`: Momo Trust Sans only for whitelisted hosts, else system.
360
+ private func font(_ context: Context) -> UIFont {
361
+ let size = context.environment.momoFontScale(20)
362
+ if KitDesignSystem.shared.isWhiteList, fontFamily == "MomoTrustSans",
363
+ let font = UIFont(name: fontWeight == .bold ? "MomoTrustSans-Bold" : "MomoTrustSans-Regular", size: size) {
364
+ return font
365
+ }
366
+ return .systemFont(ofSize: size, weight: fontWeight)
367
+ }
368
+
347
369
  final class Coordinator: NSObject, UITextFieldDelegate {
348
370
  var parent: MoneyTextField
349
371
 
@@ -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() {
@@ -0,0 +1,50 @@
1
+ import SwiftUI
2
+
3
+ /// Rounded surface laid out on the kit grid (8pt gutter), inset by the screen margin.
4
+ /// Mirrors `vn.momo.kits.layout.Card`.
5
+ public struct CardView<Content: View>: View {
6
+ @Environment(\.appTheme) private var theme
7
+
8
+ // MARK: Lifecycle
9
+
10
+ public init(
11
+ useShadow: Bool = false,
12
+ backgroundImage: String? = nil,
13
+ backgroundColor: Color? = nil,
14
+ contentPadding: EdgeInsets = EdgeInsets(top: Spacing.M, leading: Spacing.M, bottom: Spacing.M, trailing: Spacing.M),
15
+ @ViewBuilder content: @escaping () -> Content
16
+ ) {
17
+ self.useShadow = useShadow
18
+ self.backgroundImage = backgroundImage
19
+ self.backgroundColor = backgroundColor
20
+ self.contentPadding = contentPadding
21
+ self.content = content
22
+ }
23
+
24
+ // MARK: Public
25
+
26
+ public var body: some View {
27
+ GridFlow(gutter: Spacing.S, content: content)
28
+ .padding(contentPadding)
29
+ .background(background)
30
+ .background(backgroundColor ?? theme.colors.background.surface)
31
+ .clipShape(RoundedRectangle(cornerRadius: Radius.M))
32
+ // Approximates Compose `shadow(6.dp)` elevation.
33
+ .shadow(color: Color.black.opacity(useShadow ? 0.12 : 0), radius: useShadow ? 6 : 0, x: 0, y: useShadow ? 3 : 0)
34
+ .padding(.horizontal, Spacing.M)
35
+ }
36
+
37
+ // MARK: Private
38
+
39
+ private let useShadow: Bool
40
+ private let backgroundImage: String?
41
+ private let backgroundColor: Color?
42
+ private let contentPadding: EdgeInsets
43
+ private let content: () -> Content
44
+
45
+ @ViewBuilder private var background: some View {
46
+ if let backgroundImage {
47
+ ImageView(backgroundImage, contentMode: .fill).clipped()
48
+ }
49
+ }
50
+ }
@@ -1,20 +1,31 @@
1
1
  import SwiftUI
2
2
 
3
- /// Grid metrics published by a layout container (`SectionView`) so span-based children
3
+ /// Grid metrics published by a layout container (`SectionView`, `CardView`) so span-based children
4
4
  /// can size themselves. Mirrors `vn.momo.kits.layout.GridContext`.
5
5
  public struct GridContext: Equatable {
6
6
  public let numberOfColumns: Int
7
7
  public let gutter: CGFloat
8
8
  public let sizePerSpan: CGFloat
9
+ /// Pixels per point; span sizes are floored onto the pixel grid when it is set.
10
+ public let displayScale: CGFloat
9
11
 
10
- public init(numberOfColumns: Int = 12, gutter: CGFloat = Spacing.M, sizePerSpan: CGFloat = 0) {
12
+ public init(
13
+ numberOfColumns: Int = 12,
14
+ gutter: CGFloat = Spacing.M,
15
+ sizePerSpan: CGFloat = 0,
16
+ displayScale: CGFloat = 0
17
+ ) {
11
18
  self.numberOfColumns = numberOfColumns
12
19
  self.gutter = gutter
13
20
  self.sizePerSpan = sizePerSpan
21
+ self.displayScale = displayScale
14
22
  }
15
23
 
16
24
  public func sizeForSpan(_ span: Int) -> CGFloat {
17
- sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
25
+ let size = sizePerSpan * CGFloat(span) + gutter * CGFloat(span - 1)
26
+ guard displayScale > 0 else { return size }
27
+ // Compose floors in pixels so a full row of spans never overflows by a sub-pixel.
28
+ return floor(size * displayScale) / displayScale
18
29
  }
19
30
  }
20
31
 
@@ -28,3 +39,112 @@ public extension EnvironmentValues {
28
39
  set { self[GridContextKey.self] = newValue }
29
40
  }
30
41
  }
42
+
43
+ /// Measures the width it is given, publishes the grid for it and wraps children in a flow row.
44
+ /// Shared body of `SectionView` and `CardView` (Compose `gridColumns()` + `gridContext()` + `FlowRow`).
45
+ struct GridFlow<Content: View>: View {
46
+ let gutter: CGFloat
47
+ let content: () -> Content
48
+
49
+ @Environment(\.screenLayout) private var screenLayout
50
+ @Environment(\.displayScale) private var displayScale
51
+ /// The Compose `BoxWithConstraints` width.
52
+ @State private var availableWidth: CGFloat = 0
53
+
54
+ var body: some View {
55
+ flow
56
+ .environment(\.gridContext, grid)
57
+ .frame(maxWidth: .infinity, alignment: .topLeading)
58
+ .background(widthReader)
59
+ }
60
+
61
+ private var grid: GridContext {
62
+ let columns = screenLayout.contentWidth > 600 ? 24 : 12
63
+ let column = (availableWidth - gutter * CGFloat(columns - 1)) / CGFloat(columns)
64
+ return GridContext(
65
+ numberOfColumns: columns,
66
+ gutter: gutter,
67
+ sizePerSpan: max(0, column),
68
+ displayScale: displayScale
69
+ )
70
+ }
71
+
72
+ @ViewBuilder private var flow: some View {
73
+ if #available(iOS 16.0, *) {
74
+ FlowRowLayout(gutter: gutter) { content() }
75
+ } else {
76
+ // No `Layout` protocol before iOS 16: children stack instead of wrapping.
77
+ // Full-width items — the common case — look identical.
78
+ VStack(alignment: .leading, spacing: gutter) { content() }
79
+ }
80
+ }
81
+
82
+ private var widthReader: some View {
83
+ GeometryReader { proxy in
84
+ Color.clear.preference(key: GridWidthKey.self, value: proxy.size.width)
85
+ }
86
+ .onPreferenceChange(GridWidthKey.self) { availableWidth = $0 }
87
+ }
88
+ }
89
+
90
+ private struct GridWidthKey: PreferenceKey {
91
+ static var defaultValue: CGFloat = 0
92
+
93
+ static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
94
+ value = max(value, nextValue())
95
+ }
96
+ }
97
+
98
+ /// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
99
+ @available(iOS 16.0, *)
100
+ private struct FlowRowLayout: Layout {
101
+ struct Row {
102
+ var items: [(index: Int, size: CGSize)] = []
103
+ var width: CGFloat = 0
104
+ var height: CGFloat = 0
105
+ }
106
+
107
+ let gutter: CGFloat
108
+
109
+ func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
110
+ let maxWidth = proposal.width ?? .infinity
111
+ let rows = rows(for: subviews, maxWidth: maxWidth)
112
+ let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
113
+ return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
114
+ }
115
+
116
+ func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
117
+ var y = bounds.minY
118
+ for row in rows(for: subviews, maxWidth: bounds.width) {
119
+ var x = bounds.minX
120
+ for item in row.items {
121
+ subviews[item.index].place(
122
+ at: CGPoint(x: x, y: y),
123
+ anchor: .topLeading,
124
+ proposal: ProposedViewSize(item.size)
125
+ )
126
+ x += item.size.width + gutter
127
+ }
128
+ y += row.height + gutter
129
+ }
130
+ }
131
+
132
+ private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
133
+ var rows: [Row] = []
134
+ var current = Row()
135
+ for index in subviews.indices {
136
+ let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
137
+ let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
138
+ // Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
139
+ if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
140
+ rows.append(current)
141
+ current = Row()
142
+ }
143
+ current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
144
+ current.height = max(current.height, size.height)
145
+ current.items.append((index, size))
146
+ }
147
+ if !current.items.isEmpty { rows.append(current) }
148
+ return rows
149
+ }
150
+ }
@@ -1,12 +1,13 @@
1
1
  import SwiftUI
2
2
 
3
- /// Span-sized cell inside a `SectionView`. Reads the grid published by its container;
3
+ /// Span-sized cell inside a `SectionView` / `CardView`. Reads the grid published by its container;
4
4
  /// without one it just wraps its content. Mirrors `vn.momo.kits.layout.Item`.
5
5
  public struct ItemView<Content: View>: View {
6
6
  // MARK: Lifecycle
7
7
 
8
+ /// `widthSpan` nil spans the full row, whatever the container's column count.
8
9
  public init(
9
- widthSpan: Int = 12,
10
+ widthSpan: Int? = nil,
10
11
  heightSpan: Int = 0,
11
12
  @ViewBuilder content: @escaping () -> Content
12
13
  ) {
@@ -26,14 +27,14 @@ public struct ItemView<Content: View>: View {
26
27
 
27
28
  // MARK: Private
28
29
 
29
- private let widthSpan: Int
30
+ private let widthSpan: Int?
30
31
  private let heightSpan: Int
31
32
  private let content: () -> Content
32
33
 
33
34
  @Environment(\.gridContext) private var grid
34
35
 
35
36
  private var width: CGFloat? {
36
- grid.map { $0.sizeForSpan(widthSpan) }
37
+ grid.map { $0.sizeForSpan(widthSpan ?? $0.numberOfColumns) }
37
38
  }
38
39
 
39
40
  private var height: CGFloat? {
@@ -1,8 +1,8 @@
1
1
  import SwiftUI
2
2
 
3
- /// 12-column section container: horizontal margin, optional background image, and a
4
- /// wrapping row of children that read their span size from `\.gridContext`.
5
- /// Mirrors `vn.momo.kits.layout.Section`.
3
+ /// Grid section container (12 columns, 24 when the content is wider than 600pt): horizontal
4
+ /// margin, optional background image, and a wrapping row of children that read their span
5
+ /// size from `\.gridContext`. Mirrors `vn.momo.kits.layout.Section`.
6
6
  public struct SectionView<Content: View>: View {
7
7
  // MARK: Lifecycle
8
8
 
@@ -19,116 +19,20 @@ public struct SectionView<Content: View>: View {
19
19
  // MARK: Public
20
20
 
21
21
  public var body: some View {
22
- flow
23
- .environment(\.gridContext, grid)
24
- .frame(maxWidth: .infinity, alignment: .topLeading)
25
- .background(widthReader)
22
+ GridFlow(gutter: Spacing.M, content: content)
26
23
  .background(background)
27
24
  .padding(.horizontal, useMargin ? Spacing.M : 0)
28
25
  }
29
26
 
30
27
  // MARK: Private
31
28
 
32
- private static var columns: Int { 12 }
33
- private static var gutter: CGFloat { Spacing.M }
34
-
35
29
  private let useMargin: Bool
36
30
  private let backgroundImage: String?
37
31
  private let content: () -> Content
38
32
 
39
- /// Width available *inside* the horizontal margin — the Compose `BoxWithConstraints` equivalent.
40
- @State private var availableWidth: CGFloat = 0
41
-
42
- private var grid: GridContext {
43
- let columns = CGFloat(Self.columns)
44
- // The trailing `- 1 / columns` mirrors Compose: it absorbs rounding so 12 spans
45
- // plus their gutters never overflow the row by a sub-point.
46
- let sizePerSpan = (availableWidth - Self.gutter * (columns - 1)) / columns - 1 / columns
47
- return GridContext(numberOfColumns: Self.columns, gutter: Self.gutter, sizePerSpan: max(0, sizePerSpan))
48
- }
49
-
50
- @ViewBuilder private var flow: some View {
51
- if #available(iOS 16.0, *) {
52
- FlowRowLayout(gutter: Self.gutter) { content() }
53
- } else {
54
- // No `Layout` protocol before iOS 16: children stack instead of wrapping.
55
- // Full-width (span 12) items — the common case — look identical.
56
- VStack(alignment: .leading, spacing: Self.gutter) { content() }
57
- }
58
- }
59
-
60
33
  @ViewBuilder private var background: some View {
61
34
  if let backgroundImage {
62
35
  ImageView(backgroundImage, contentMode: .fill).clipped()
63
36
  }
64
37
  }
65
-
66
- private var widthReader: some View {
67
- GeometryReader { proxy in
68
- Color.clear.preference(key: SectionWidthKey.self, value: proxy.size.width)
69
- }
70
- .onPreferenceChange(SectionWidthKey.self) { availableWidth = $0 }
71
- }
72
- }
73
-
74
- private struct SectionWidthKey: PreferenceKey {
75
- static var defaultValue: CGFloat = 0
76
-
77
- static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
78
- value = max(value, nextValue())
79
- }
80
- }
81
-
82
- /// Wrapping row with equal horizontal/vertical spacing — the Compose `FlowRow` equivalent.
83
- @available(iOS 16.0, *)
84
- private struct FlowRowLayout: Layout {
85
- struct Row {
86
- var items: [(index: Int, size: CGSize)] = []
87
- var width: CGFloat = 0
88
- var height: CGFloat = 0
89
- }
90
-
91
- let gutter: CGFloat
92
-
93
- func sizeThatFits(proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) -> CGSize {
94
- let maxWidth = proposal.width ?? .infinity
95
- let rows = rows(for: subviews, maxWidth: maxWidth)
96
- let height = rows.reduce(0) { $0 + $1.height } + gutter * CGFloat(max(0, rows.count - 1))
97
- return CGSize(width: proposal.width ?? rows.map(\.width).max() ?? 0, height: height)
98
- }
99
-
100
- func placeSubviews(in bounds: CGRect, proposal: ProposedViewSize, subviews: Subviews, cache: inout Void) {
101
- var y = bounds.minY
102
- for row in rows(for: subviews, maxWidth: bounds.width) {
103
- var x = bounds.minX
104
- for item in row.items {
105
- subviews[item.index].place(
106
- at: CGPoint(x: x, y: y),
107
- anchor: .topLeading,
108
- proposal: ProposedViewSize(item.size)
109
- )
110
- x += item.size.width + gutter
111
- }
112
- y += row.height + gutter
113
- }
114
- }
115
-
116
- private func rows(for subviews: Subviews, maxWidth: CGFloat) -> [Row] {
117
- var rows: [Row] = []
118
- var current = Row()
119
- for index in subviews.indices {
120
- let size = subviews[index].sizeThatFits(ProposedViewSize(width: maxWidth, height: nil))
121
- let widthIfAppended = current.items.isEmpty ? size.width : current.width + gutter + size.width
122
- // Sub-point tolerance: spans are computed to land exactly on `maxWidth`.
123
- if !current.items.isEmpty, widthIfAppended > maxWidth + 0.5 {
124
- rows.append(current)
125
- current = Row()
126
- }
127
- current.width = current.items.isEmpty ? size.width : current.width + gutter + size.width
128
- current.height = max(current.height, size.height)
129
- current.items.append((index, size))
130
- }
131
- if !current.items.isEmpty { rows.append(current) }
132
- return rows
133
- }
134
38
  }
@@ -12,8 +12,7 @@ public enum LoaderType {
12
12
  /// Mirrors Compose `Loader(type:, tintColor:, modifier:)` which delegates to
13
13
  /// `DotLoader` / `SpinnerLoader`. Both variants render Lottie animations
14
14
  /// (`files/dot_loading` and `files/lottie_circle_loader`) and optionally tint
15
- /// the fill/stroke. The tint is reproduced here by masking a solid color rect
16
- /// with the (monochrome) Lottie animation.
15
+ /// the fill/stroke through a Lottie color value provider.
17
16
  public struct Loader: View {
18
17
  private let type: LoaderType
19
18
  private let tintColor: Color?
@@ -97,17 +96,14 @@ public struct ProgressBar: View {
97
96
  }
98
97
  }
99
98
 
100
- /// Renders a looping Lottie animation, optionally recolored via a mask.
99
+ // Tinted inside Lottie, as Compose does: SwiftUI masks/color effects over a UIKit-backed view aren't reliable.
101
100
  private struct TintedLottie: View {
102
101
  let name: String
103
102
  let tintColor: Color?
104
103
 
105
104
  var body: some View {
106
- if let tintColor = tintColor {
107
- tintColor
108
- .mask(LottieView(name: name, loopMode: .loop))
109
- } else {
110
- LottieView(name: name, loopMode: .loop)
111
- }
105
+ LottieView(name: name, loopMode: .loop, tintColor: tintColor.map { UIColor($0) })
106
+ // The tint is applied once in makeUIView; a new tint needs a new view.
107
+ .id(tintColor)
112
108
  }
113
109
  }
@@ -15,6 +15,8 @@ struct LottieView: UIViewRepresentable {
15
15
  var loopMode: LottieLoopMode = .playOnce
16
16
  var contentMode : UIView.ContentMode = .scaleAspectFit
17
17
  var onDone: (() -> Void)?
18
+ // Recolors every fill and stroke, as Compose's iOS LottieAnimation does; nil keeps the file's colors.
19
+ var tintColor: UIColor?
18
20
 
19
21
  // Add static cache for animations
20
22
  private static var animationCache = NSCache<NSString, LottieAnimation>()
@@ -64,6 +66,12 @@ struct LottieView: UIViewRepresentable {
64
66
  private func setupAnimationView(_ animationView: LottieAnimationView, _ containerView: UIView) {
65
67
  animationView.contentMode = contentMode
66
68
  animationView.loopMode = loopMode
69
+ if let tintColor {
70
+ animationView.setValueProvider(
71
+ ColorValueProvider(tintColor.lottieColorValue),
72
+ keypath: AnimationKeypath(keypath: "**.Color")
73
+ )
74
+ }
67
75
  animationView.backgroundBehavior = loopMode == .loop ? .pauseAndRestore : .pause
68
76
 
69
77
  // Optimize rendering
@@ -73,6 +81,8 @@ struct LottieView: UIViewRepresentable {
73
81
  containerView.addSubview(animationView)
74
82
 
75
83
  NSLayoutConstraint.activate([
84
+ animationView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
85
+ animationView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
76
86
  animationView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
77
87
  animationView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
78
88
  ])