@momo-kits/native-kits 0.300.3 → 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 (272) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/build.gradle.kts +13 -0
  4. package/compose/build.gradle.kts +197 -0
  5. package/compose/build.gradle.kts.backup +197 -0
  6. package/compose/compose.podspec +47 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +20 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  12. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +110 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +1071 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +65 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +86 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +17 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +194 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +287 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +116 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +215 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +404 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +732 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +40 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +233 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +217 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +455 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +103 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +413 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +39 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +41 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +135 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  124. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  125. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +62 -0
  126. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +42 -0
  127. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  128. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +36 -0
  129. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  133. package/gradle/libs.versions.toml +68 -0
  134. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  135. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  136. package/gradle.properties +26 -0
  137. package/gradlew +252 -0
  138. package/gradlew.bat +94 -0
  139. package/ios/Application/ApplicationEnvironment.swift +24 -6
  140. package/ios/Application/DesignSystemKits.swift +100 -0
  141. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  142. package/ios/Application/MaxApi.swift +30 -3
  143. package/ios/Avatar/Avatar.swift +4 -3
  144. package/ios/Badge/Badge.swift +6 -4
  145. package/ios/Badge/BadgeDot.swift +1 -1
  146. package/ios/Badge/BadgeRibbon.swift +8 -2
  147. package/ios/Button/Button.swift +15 -12
  148. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  149. package/ios/Carousel/Carousel.swift +29 -12
  150. package/ios/Checkbox/Checkbox.swift +7 -3
  151. package/ios/Chip/Chip.swift +5 -4
  152. package/ios/Collapse/Collapse.swift +70 -19
  153. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  154. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  155. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  156. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  157. package/ios/Divider/Divider.swift +37 -4
  158. package/ios/Extensions/Color++.swift +38 -9
  159. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  160. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  161. package/ios/Icon/Icon.swift +21 -23
  162. package/ios/Icon/Icons.swift +1329 -0
  163. package/ios/IconButton/IconButton.swift +15 -7
  164. package/ios/Image/Image.swift +7 -23
  165. package/ios/Information/Information.swift +27 -9
  166. package/ios/Input/ErrorView.swift +2 -2
  167. package/ios/Input/Input.swift +171 -148
  168. package/ios/Input/InputPhoneNumber.swift +10 -9
  169. package/ios/Input/InputSearch.swift +107 -78
  170. package/ios/Input/InputTextArea.swift +133 -148
  171. package/ios/InputDropDown/InputDropDown.swift +10 -7
  172. package/ios/InputMoney/InputMoney.swift +32 -11
  173. package/ios/InputOTP/InputOTP.swift +22 -12
  174. package/ios/Layout/Card.swift +50 -0
  175. package/ios/Layout/GridContext.swift +187 -3
  176. package/ios/Layout/Item.swift +5 -4
  177. package/ios/Layout/ScreenLayout.swift +415 -0
  178. package/ios/Layout/Section.swift +4 -100
  179. package/ios/Loader/Loader.swift +5 -9
  180. package/ios/Lottie/LottieView.swift +14 -2
  181. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  182. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  183. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  184. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  185. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  186. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  187. package/ios/Navigation/Component/Header.swift +545 -0
  188. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  189. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  190. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  191. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  192. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  193. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  194. package/ios/Navigation/Component/SideBar.swift +183 -0
  195. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  196. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  197. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  198. package/ios/Navigation/Navigation.swift +204 -0
  199. package/ios/Navigation/NavigationContainer.swift +195 -0
  200. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  201. package/ios/Navigation/StackScreen.swift +930 -0
  202. package/ios/Navigation/StackTransition.swift +400 -0
  203. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  204. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  205. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  206. package/ios/Pagination/PaginationScroll.swift +2 -2
  207. package/ios/Popup/PopupDisplay.swift +72 -135
  208. package/ios/Popup/PopupInput.swift +10 -13
  209. package/ios/Popup/PopupLayout.swift +138 -0
  210. package/ios/Popup/PopupNotify.swift +59 -69
  211. package/ios/Popup/PopupPromotion.swift +31 -47
  212. package/ios/PopupView/FullscreenPopup.swift +13 -19
  213. package/ios/PopupView/PopupView.swift +1 -1
  214. package/ios/PopupView/Utils++.swift +0 -8
  215. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  216. package/ios/Radio/Radio.swift +2 -1
  217. package/ios/Rating/Rating.swift +9 -1
  218. package/ios/Resources/dot_loading.json +558 -0
  219. package/ios/Resources/lottie_circle_loader.json +1 -0
  220. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  221. package/ios/Skeleton/Skeleton.swift +22 -27
  222. package/ios/Slider/Slider.swift +16 -18
  223. package/ios/Stepper/Stepper.swift +7 -3
  224. package/ios/Steps/Steps.swift +8 -6
  225. package/ios/SuggestAction/SuggestAction.swift +5 -0
  226. package/ios/Swipeable/SwipeCell.swift +10 -6
  227. package/ios/Switch/Switch.swift +22 -20
  228. package/ios/TabView/TabView.swift +102 -21
  229. package/ios/Tag/Tag.swift +15 -4
  230. package/ios/Template/Logo/Logo.swift +2 -1
  231. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  232. package/ios/Title/Title.swift +6 -17
  233. package/ios/Tooltip/Tooltip.swift +29 -14
  234. package/ios/Typography/FontScale.swift +17 -14
  235. package/ios/Typography/Text.swift +156 -20
  236. package/ios/Typography/Typography.swift +2 -2
  237. package/ios/Uploader/Uploader.swift +3 -1
  238. package/ios/native-kits.podspec +2 -3
  239. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  240. package/ios-demo/README.md +120 -0
  241. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  242. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  243. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  244. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  245. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  246. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  247. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  248. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  249. package/package.json +1 -1
  250. package/publish.sh +19 -2
  251. package/scripts/gen-ios-icons.mjs +36 -0
  252. package/settings.gradle.kts +64 -0
  253. package/ios/Application/Components.swift +0 -270
  254. package/ios/Application/ComposeApi.swift +0 -22
  255. package/ios/Application/HeaderRight.swift +0 -306
  256. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  257. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  258. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  259. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  260. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  261. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  262. package/ios/Application/Navigation/component/Header.swift +0 -178
  263. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  264. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  265. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  266. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  267. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  268. package/ios/Application/Screen.swift +0 -250
  269. package/ios/Application/StackScreen.swift +0 -492
  270. package/ios/Typography/FontScaleStore.swift +0 -58
  271. package/ios/Typography/KitDesignSystem.swift +0 -17
  272. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,415 @@
1
+ //
2
+ // ScreenLayout.swift
3
+ // MoMoUIKits
4
+ //
5
+ // Port of momo-kits RN `useScreenLayout` (Layout/utils.ts) and `Application/device.ts`:
6
+ // window size, safe-area insets, content width, header geometry and the iPhone Duo side bar.
7
+ //
8
+
9
+ import SwiftUI
10
+ import UIKit
11
+
12
+ public enum SideBarEdge: Equatable {
13
+ case left, right
14
+ }
15
+
16
+ public struct SideBarLayout: Equatable {
17
+ public let edge: SideBarEdge
18
+ public let width: CGFloat
19
+ /// Preferred window y of the first control; StackScreen still keeps the column above the bottom bars.
20
+ public let top: CGFloat
21
+ /// Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
22
+ public let bottom: CGFloat
23
+ }
24
+
25
+ /// Room the header row keeps from each side for the status bar clock and battery clusters.
26
+ public struct StatusBarClearance: Equatable {
27
+ public let left: CGFloat
28
+ public let right: CGFloat
29
+ }
30
+
31
+ public struct ScreenLayout: Equatable {
32
+ public let width: CGFloat
33
+ public let height: CGFloat
34
+ public let isIPhoneDuo: Bool
35
+ /// The device has a hinge, folded or not, whatever the window size; drives popups, modal and bottom sheet.
36
+ public let isDuoDevice: Bool
37
+ public let insets: EdgeInsets
38
+ public let insetLeft: CGFloat
39
+ public let insetRight: CGFloat
40
+ public let contentWidth: CGFloat
41
+ public let headerHeight: CGFloat
42
+ /// 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
43
+ public let headerStatusBarHeight: CGFloat
44
+ /// iPhone Duo moves back and toolkit into a column in the side safe area; nil while they stay in the bar.
45
+ public let sideBar: SideBarLayout?
46
+ /// iPhone Duo inner display in portrait: the header row shares the status bar band, between its clusters.
47
+ public let statusBarClearance: StatusBarClearance?
48
+
49
+ public var headerBarHeight: CGFloat { headerHeight - headerStatusBarHeight }
50
+ }
51
+
52
+ // Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
53
+ enum DuoMetrics {
54
+ // Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
55
+ static let maxAspectRatio: CGFloat = 1.6
56
+ // Outer display is 466pt tall in landscape, inner 669pt.
57
+ static let innerMinHeight: CGFloat = 600
58
+ static let controlSize: CGFloat = 48
59
+ static let itemPitch: CGFloat = 50
60
+ static let firstItemTop: CGFloat = 170
61
+ static let firstItemTopLandscape: CGFloat = 120
62
+ // Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
63
+ // lines up with the camera punchout (y 24–66 at the top of the strip).
64
+ static let outerHeaderHeight: CGFloat = 76
65
+ static let outerCameraCenterY: CGFloat = 45
66
+ // Outer display in landscape has no status bar in the strip: controls keep this far from
67
+ // both ends unless a reserved region (the camera at either end) asks for more.
68
+ static let edgeInset: CGFloat = 24
69
+ // Inner display in portrait: the clock and connectivity cluster sits at the top right corner
70
+ // (x 550–643 of 669, clock digits centered on y 47); its reserved region is taller than the glyphs.
71
+ static let innerStatusBarCenterY: CGFloat = 47
72
+ static let innerStatusBarWidth: CGFloat = 120
73
+ // Host app's iOS 26 tab bar on the inner display in portrait: a 62pt glass pill sized to its tabs,
74
+ // QR as a 62pt platter beside it, ≥ 20pt from the sides, 13pt into the home indicator inset.
75
+ static let floatingTabBarHeight: CGFloat = 62
76
+ static let floatingTabBarMargin: CGFloat = 20
77
+ static let floatingTabBarInsetOverlap: CGFloat = 13
78
+ }
79
+
80
+ enum HeaderMetrics {
81
+ static let barHeight: CGFloat = HEADER_HEIGHT
82
+ static let iPhoneMinHeight: CGFloat = 72
83
+ // A smaller top inset means the status bar is not on the top edge.
84
+ static let statusBarMinInset: CGFloat = 20
85
+ }
86
+
87
+ private var isPhoneIdiom: Bool { UIDevice.current.userInterfaceIdiom == .phone }
88
+
89
+ private var keyWindow: UIWindow? {
90
+ UIApplication.shared.connectedScenes
91
+ .compactMap { ($0 as? UIWindowScene)?.keyWindow }
92
+ .first
93
+ }
94
+
95
+ extension ScreenLayout {
96
+ /// Snapshot of the key window; not reactive, so views read `\.screenLayout` instead.
97
+ public static var current: ScreenLayout {
98
+ guard let window = keyWindow else {
99
+ return make(size: UIScreen.main.bounds.size, insets: EdgeInsets())
100
+ }
101
+ let inset = window.safeAreaInsets
102
+ return make(
103
+ size: window.bounds.size,
104
+ insets: EdgeInsets(top: inset.top, leading: inset.left, bottom: inset.bottom, trailing: inset.right)
105
+ )
106
+ }
107
+
108
+ static func make(
109
+ size: CGSize,
110
+ insets: EdgeInsets,
111
+ reservedRegions: [CGRect] = ReservedRegions.current,
112
+ isPhone: Bool = isPhoneIdiom,
113
+ isFoldable: Bool = FoldableDevice.isFoldable(measuredIn: keyWindow)
114
+ ) -> ScreenLayout {
115
+ let width = size.width
116
+ let height = size.height
117
+ let isDuoDevice = isPhone && isFoldable
118
+ var (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
119
+ let isDuo = isPhone && isIPhoneDuoWindow(width: width, height: height, insets: insets)
120
+ guard isDuo else {
121
+ return ScreenLayout(
122
+ width: width, height: height, isIPhoneDuo: false, isDuoDevice: isDuoDevice, insets: insets,
123
+ insetLeft: 0, insetRight: 0, contentWidth: width,
124
+ headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil,
125
+ statusBarClearance: nil
126
+ )
127
+ }
128
+
129
+ var statusBarClearance: StatusBarClearance?
130
+ let isInnerPortrait = height > width && width >= DuoMetrics.innerMinHeight
131
+ if isInnerPortrait && insets.top >= HeaderMetrics.statusBarMinInset {
132
+ statusBarClearance = statusBarClusters(reservedRegions, windowWidth: width, topInset: insets.top)
133
+ ?? StatusBarClearance(left: 0, right: DuoMetrics.innerStatusBarWidth + Spacing.S)
134
+ // centered on the clock digits; the cluster's region center sits above them
135
+ headerHeight = DuoMetrics.innerStatusBarCenterY * 2
136
+ headerStatusBarHeight = 0
137
+ }
138
+
139
+ var sideBar: SideBarLayout?
140
+ let sideInset = max(insets.leading, insets.trailing)
141
+ // narrower strips cannot host a control 12pt from the inner edge
142
+ if sideInset >= DuoMetrics.controlSize + Spacing.M {
143
+ let edge: SideBarEdge = insets.trailing >= insets.leading ? .right : .left
144
+ let stripLeft = edge == .right ? width - sideInset : 0
145
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
146
+ // Activities; region frames already include the system's margin
147
+ let occlusion = stripOcclusion(reservedRegions, stripLeft: stripLeft, stripRight: stripLeft + sideInset, windowHeight: height)
148
+
149
+ var top = statusBarClearance != nil
150
+ ? headerHeight + Spacing.M
151
+ : insets.top + HeaderMetrics.barHeight + Spacing.M
152
+ if insets.top < HeaderMetrics.statusBarMinInset {
153
+ // status bar lives in the strip: start under its cluster, as the native bar does
154
+ if width <= height {
155
+ top = DuoMetrics.firstItemTop
156
+ } else if height >= DuoMetrics.innerMinHeight {
157
+ top = DuoMetrics.firstItemTopLandscape
158
+ } else {
159
+ // outer display in landscape: no status bar in the strip, the title row centers
160
+ // on the camera when it sits at the top end, otherwise on the back button
161
+ top = DuoMetrics.edgeInset
162
+ let titleCenterY = occlusion.top > top ? occlusion.topCenter : top + DuoMetrics.controlSize / 2
163
+ (headerHeight, headerStatusBarHeight) = centeredHeaderGeometry(centerY: titleCenterY)
164
+ }
165
+
166
+ if height > width && width < DuoMetrics.innerMinHeight {
167
+ // outer display: the row starts below the top so the title centers on the camera
168
+ headerHeight = DuoMetrics.outerHeaderHeight
169
+ headerStatusBarHeight = DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
170
+ }
171
+ }
172
+
173
+ sideBar = SideBarLayout(
174
+ edge: edge,
175
+ width: sideInset,
176
+ top: max(top, occlusion.top),
177
+ bottom: max(DuoMetrics.edgeInset, occlusion.bottom)
178
+ )
179
+ }
180
+
181
+ return ScreenLayout(
182
+ width: width, height: height, isIPhoneDuo: true, isDuoDevice: isDuoDevice, insets: insets,
183
+ insetLeft: insets.leading, insetRight: insets.trailing,
184
+ contentWidth: width - insets.leading - insets.trailing,
185
+ headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar,
186
+ statusBarClearance: statusBarClearance
187
+ )
188
+ }
189
+
190
+ private static func headerGeometry(topInset: CGFloat, isPhone: Bool) -> (CGFloat, CGFloat) {
191
+ let height = topInset + HeaderMetrics.barHeight
192
+ guard isPhone else { return (height, topInset) }
193
+ return (
194
+ max(height, HeaderMetrics.iPhoneMinHeight),
195
+ topInset < HeaderMetrics.statusBarMinInset ? 0 : topInset
196
+ )
197
+ }
198
+
199
+ /// Bar row centered on a window y, at least iPhoneMinHeight tall and reaching 8pt below
200
+ /// a side bar control centered there.
201
+ private static func centeredHeaderGeometry(centerY: CGFloat) -> (CGFloat, CGFloat) {
202
+ let height = max(HeaderMetrics.iPhoneMinHeight, centerY + DuoMetrics.controlSize / 2 + Spacing.S)
203
+ return (height, max(0, centerY * 2 - height))
204
+ }
205
+
206
+ /// How far system regions reach into a side strip from its top and from its bottom end
207
+ /// (e.g. the camera, which sits at either end depending on the rotation), and the center y
208
+ /// of the region reaching furthest from the top.
209
+ private static func stripOcclusion(
210
+ _ regions: [CGRect],
211
+ stripLeft: CGFloat,
212
+ stripRight: CGFloat,
213
+ windowHeight: CGFloat
214
+ ) -> (top: CGFloat, topCenter: CGFloat, bottom: CGFloat) {
215
+ regions.reduce(into: (top: CGFloat(0), topCenter: CGFloat(0), bottom: CGFloat(0))) { occlusion, region in
216
+ guard region.minX < stripRight && region.maxX > stripLeft else { return }
217
+ if region.midY < windowHeight / 2 {
218
+ if region.maxY > occlusion.top {
219
+ occlusion.top = region.maxY
220
+ occlusion.topCenter = region.midY
221
+ }
222
+ } else {
223
+ occlusion.bottom = max(occlusion.bottom, windowHeight - region.minY)
224
+ }
225
+ }
226
+ }
227
+
228
+ /// How far the clock and battery clusters of a status bar on the top edge reach in from each
229
+ /// side; nil without reserved regions there.
230
+ private static func statusBarClusters(_ regions: [CGRect], windowWidth: CGFloat, topInset: CGFloat) -> StatusBarClearance? {
231
+ let midX = windowWidth / 2
232
+ let clusters = regions.filter {
233
+ $0.minY < topInset && $0.maxY <= topInset && ($0.maxX <= midX || $0.minX >= midX)
234
+ }
235
+ guard !clusters.isEmpty else { return nil }
236
+ var left: CGFloat = 0
237
+ var right: CGFloat = 0
238
+ for region in clusters {
239
+ if region.minX < midX {
240
+ left = max(left, region.maxX)
241
+ } else {
242
+ right = max(right, windowWidth - region.minX)
243
+ }
244
+ }
245
+ return StatusBarClearance(left: left, right: right)
246
+ }
247
+
248
+ // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
249
+ private static func isIPhoneDuoWindow(width: CGFloat, height: CGFloat, insets: EdgeInsets) -> Bool {
250
+ let shortSide = min(width, height)
251
+ let isSquare = shortSide > 0 && max(width, height) / shortSide < DuoMetrics.maxAspectRatio
252
+ return isSquare || insets.leading != insets.trailing
253
+ }
254
+
255
+ /// Below the side status bar: in portrait it is about twice as tall as its inset is wide.
256
+ public var duoTopInset: CGFloat {
257
+ max(insets.top, height > width ? max(insets.leading, insets.trailing) * 2 : 0)
258
+ }
259
+ }
260
+
261
+ /// Snapshot for call sites without `\.screenLayout`; not reactive. `windowSize` is ignored,
262
+ /// kept for host call sites.
263
+ public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
264
+ isPhoneIdiom && FoldableDevice.isFoldable(measuredIn: keyWindow)
265
+ }
266
+
267
+ /// Apple's foldable check (as the host's `SideTabRailView.isFoldableDevice`): only a device with a
268
+ /// hinge reports a `.division` region, inactive while flat but still there. Screen size cannot tell
269
+ /// it apart from a large iPhone in landscape or a narrowed window.
270
+ enum FoldableDevice {
271
+ // a hinge never goes away, so a positive answer is kept
272
+ private static var isKnownFoldable = false
273
+
274
+ static func isFoldable(measuredIn view: UIView?) -> Bool {
275
+ if isKnownFoldable { return true }
276
+ // compiled out on SDKs older than iOS 27.1 (UIKit 9127.0.85.28)
277
+ #if canImport(UIKit, _version: 9127.0.85.28)
278
+ guard #available(iOS 27.1, *), let view, view.window != nil || view is UIWindow else { return false }
279
+ isKnownFoldable = !view.reservedRegions(kind: .division, options: .includeInactive).isEmpty
280
+ #endif
281
+ return isKnownFoldable
282
+ }
283
+
284
+ /// Catches the hinge when the regions were read too early (before the window had them).
285
+ static func observe(in view: UIView) {
286
+ #if canImport(UIKit, _version: 9127.0.85.28)
287
+ guard #available(iOS 27.1, *) else { return }
288
+ view.addInteraction(UIHingeInteraction { _, update in
289
+ guard update.hinge != nil, !isKnownFoldable else { return }
290
+ isKnownFoldable = true
291
+ NotificationCenter.default.post(name: ReservedRegions.didChange, object: nil)
292
+ })
293
+ #endif
294
+ }
295
+ }
296
+
297
+ // MARK: - Reserved regions
298
+
299
+ /// System occlusion regions (iOS 27.1+) in window coordinates: the iPhone Duo camera, and the
300
+ /// status bar cluster as it grows for a call or a Live Activity. Empty before iOS 27.1.
301
+ enum ReservedRegions {
302
+ // last report of a probe, so `ScreenLayout.current` includes it
303
+ private(set) static var current: [CGRect] = []
304
+ static let didChange = Notification.Name("MoMoUIKitsReservedRegionsDidChange")
305
+
306
+ fileprivate static func publish(_ regions: [CGRect]) {
307
+ guard regions != current else { return }
308
+ current = regions
309
+ NotificationCenter.default.post(name: didChange, object: nil)
310
+ }
311
+ }
312
+
313
+ /// Invisible probe: reading the regions in layoutSubviews makes UIKit run it again when they
314
+ /// change, so nothing polls. Port of RN `@momo-kits/reserved-regions`.
315
+ private final class ReservedRegionsProbe: UIView {
316
+ override init(frame: CGRect) {
317
+ super.init(frame: frame)
318
+ isUserInteractionEnabled = false
319
+ FoldableDevice.observe(in: self)
320
+ }
321
+
322
+ required init?(coder: NSCoder) { nil }
323
+
324
+ override func layoutSubviews() {
325
+ super.layoutSubviews()
326
+ // read even when not publishing, so UIKit keeps re-running this when they change
327
+ let regions = occlusionRegions()
328
+ // every screen has a probe but the regions are window-wide: one sliding in on a push or
329
+ // a present, or off the window, would overwrite them with a partial read
330
+ guard spansWindow else { return }
331
+ // layout pass: publish on the next turn instead of mutating SwiftUI state mid-update
332
+ DispatchQueue.main.async { ReservedRegions.publish(regions) }
333
+ }
334
+
335
+ private var spansWindow: Bool {
336
+ guard let window else { return false }
337
+ let frame = convert(bounds, to: nil)
338
+ return abs(frame.minX) < 1 && abs(frame.maxX - window.bounds.width) < 1 && abs(frame.minY) < 1
339
+ }
340
+
341
+ override func didMoveToWindow() {
342
+ super.didMoveToWindow()
343
+ setNeedsLayout()
344
+ }
345
+
346
+ // The API is newer than the SDKs hosts may build with, so it is reached through the runtime.
347
+ private func occlusionRegions() -> [CGRect] {
348
+ let regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
349
+ guard window != nil, responds(to: regionsOfKind),
350
+ let kindType = NSClassFromString("UIViewReservedRegionKind") as? NSObject.Type,
351
+ let kind = (kindType as AnyObject).perform(NSSelectorFromString("occlusionRegionKind"))?.takeUnretainedValue(),
352
+ let regions = perform(regionsOfKind, with: kind)?.takeUnretainedValue() as? [NSObject]
353
+ else { return [] }
354
+ return regions.compactMap { ($0.value(forKey: "frame") as? NSValue)?.cgRectValue }
355
+ .map { convert($0, to: nil) }
356
+ }
357
+ }
358
+
359
+ private struct ReservedRegionsProbeView: UIViewRepresentable {
360
+ func makeUIView(context: Context) -> ReservedRegionsProbe { ReservedRegionsProbe() }
361
+
362
+ func updateUIView(_ probe: ReservedRegionsProbe, context: Context) {}
363
+ }
364
+
365
+ // MARK: - Environment
366
+
367
+ private struct ScreenLayoutKey: EnvironmentKey {
368
+ // Computed so an un-injected subtree (bare UIHostingController) still reads the live window.
369
+ static var defaultValue: ScreenLayout { .current }
370
+ }
371
+
372
+ public extension EnvironmentValues {
373
+ var screenLayout: ScreenLayout {
374
+ get { self[ScreenLayoutKey.self] }
375
+ set { self[ScreenLayoutKey.self] = newValue }
376
+ }
377
+ }
378
+
379
+ /// Re-reads the window whenever this view's size changes (rotation, fold/unfold, Split View).
380
+ private struct ScreenLayoutReader: ViewModifier {
381
+ @State private var layout: ScreenLayout = .current
382
+
383
+ func body(content: Content) -> some View {
384
+ content
385
+ .environment(\.screenLayout, layout)
386
+ .background(
387
+ GeometryReader { proxy in
388
+ Color.clear
389
+ .onAppear { refresh() }
390
+ .onChange(of: proxy.size) { _ in refresh() }
391
+ .onChange(of: proxy.safeAreaInsets) { _ in refresh() }
392
+ }
393
+ )
394
+ .background(ReservedRegionsProbeView().ignoresSafeArea())
395
+ // any screen's probe may report, so every reader follows the shared value
396
+ .onReceive(NotificationCenter.default.publisher(for: ReservedRegions.didChange)) { _ in
397
+ refresh()
398
+ }
399
+ .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
400
+ refresh()
401
+ }
402
+ }
403
+
404
+ private func refresh() {
405
+ let next = ScreenLayout.current
406
+ if next != layout { layout = next }
407
+ }
408
+ }
409
+
410
+ public extension View {
411
+ /// Provides `\.screenLayout`; NavigationContainer applies it, a bare UIHostingController root should too.
412
+ func momoScreenLayout() -> some View {
413
+ modifier(ScreenLayoutReader())
414
+ }
415
+ }
@@ -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>()
@@ -36,7 +38,8 @@ struct LottieView: UIViewRepresentable {
36
38
  animationView.animation = cachedAnimation
37
39
  setupAnimationView(animationView, view)
38
40
  } else {
39
- LottieAnimation.loadedFrom(url: URL(string: url)!, closure: { animation in
41
+ guard let animationURL = URL(string: url) else { return view }
42
+ LottieAnimation.loadedFrom(url: animationURL, closure: { animation in
40
43
  if let animation = animation {
41
44
  Self.animationCache.setObject(animation, forKey: url as NSString)
42
45
  animationView.animation = animation
@@ -49,7 +52,8 @@ struct LottieView: UIViewRepresentable {
49
52
  if let cachedAnimation = Self.animationCache.object(forKey: name as NSString) {
50
53
  animationView.animation = cachedAnimation
51
54
  setupAnimationView(animationView, view)
52
- } else if let animation = LottieAnimation.named(name) {
55
+ } else if let animation = LottieAnimation.named(name, bundle: MoMoUIKitsResources.bundle)
56
+ ?? LottieAnimation.named(name) {
53
57
  Self.animationCache.setObject(animation, forKey: name as NSString)
54
58
  animationView.animation = animation
55
59
  setupAnimationView(animationView, view)
@@ -62,6 +66,12 @@ struct LottieView: UIViewRepresentable {
62
66
  private func setupAnimationView(_ animationView: LottieAnimationView, _ containerView: UIView) {
63
67
  animationView.contentMode = contentMode
64
68
  animationView.loopMode = loopMode
69
+ if let tintColor {
70
+ animationView.setValueProvider(
71
+ ColorValueProvider(tintColor.lottieColorValue),
72
+ keypath: AnimationKeypath(keypath: "**.Color")
73
+ )
74
+ }
65
75
  animationView.backgroundBehavior = loopMode == .loop ? .pauseAndRestore : .pause
66
76
 
67
77
  // Optimize rendering
@@ -71,6 +81,8 @@ struct LottieView: UIViewRepresentable {
71
81
  containerView.addSubview(animationView)
72
82
 
73
83
  NSLayoutConstraint.activate([
84
+ animationView.centerXAnchor.constraint(equalTo: containerView.centerXAnchor),
85
+ animationView.centerYAnchor.constraint(equalTo: containerView.centerYAnchor),
74
86
  animationView.widthAnchor.constraint(equalTo: containerView.widthAnchor),
75
87
  animationView.heightAnchor.constraint(equalTo: containerView.heightAnchor)
76
88
  ])