@momo-kits/native-kits 0.167.1-duo.8 → 0.167.1-duo.9-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 (254) hide show
  1. package/CLAUDE.md +4 -1
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +204 -0
  4. package/compose/build.gradle.kts.backup +204 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +708 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +209 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +231 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +408 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +92 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +366 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  136. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  137. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  138. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  139. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
  140. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  141. package/gradle/libs.versions.toml +69 -0
  142. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  143. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  144. package/gradle.properties +26 -0
  145. package/gradlew +252 -0
  146. package/gradlew.bat +94 -0
  147. package/ios/Application/ApplicationEnvironment.swift +11 -0
  148. package/ios/Application/ComposeApi.swift +2 -1
  149. package/ios/Application/{Components.swift → LegacyHeader.swift} +6 -62
  150. package/ios/Application/Screen.swift +3 -2
  151. package/ios/Avatar/Avatar.swift +4 -3
  152. package/ios/Badge/Badge.swift +6 -4
  153. package/ios/Badge/BadgeDot.swift +1 -1
  154. package/ios/Badge/BadgeRibbon.swift +0 -1
  155. package/ios/Button/Button.swift +15 -12
  156. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +12 -20
  157. package/ios/Carousel/Carousel.swift +29 -12
  158. package/ios/Checkbox/Checkbox.swift +7 -3
  159. package/ios/Chip/Chip.swift +5 -4
  160. package/ios/Collapse/Collapse.swift +70 -19
  161. package/ios/Colors+Radius+Spacing/Colors.swift +9 -0
  162. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  163. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  164. package/ios/Divider/Divider.swift +37 -4
  165. package/ios/Extensions/Color++.swift +18 -0
  166. package/ios/Icon/Icon.swift +7 -1
  167. package/ios/IconButton/IconButton.swift +15 -7
  168. package/ios/Image/Image.swift +7 -34
  169. package/ios/Information/Information.swift +27 -9
  170. package/ios/Input/ErrorView.swift +2 -2
  171. package/ios/Input/Input.swift +160 -147
  172. package/ios/Input/InputPhoneNumber.swift +12 -9
  173. package/ios/Input/InputSearch.swift +102 -75
  174. package/ios/Input/InputTextArea.swift +128 -145
  175. package/ios/InputDropDown/InputDropDown.swift +10 -7
  176. package/ios/InputMoney/InputMoney.swift +33 -11
  177. package/ios/InputOTP/InputOTP.swift +22 -12
  178. package/ios/Layout/Card.swift +50 -0
  179. package/ios/Layout/GridContext.swift +123 -3
  180. package/ios/Layout/Item.swift +5 -4
  181. package/ios/Layout/Section.swift +4 -100
  182. package/ios/Loader/Loader.swift +5 -9
  183. package/ios/Lottie/LottieView.swift +10 -0
  184. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +61 -9
  185. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTab.swift +60 -34
  186. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +11 -6
  187. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  188. package/ios/Navigation/Component/Header.swift +493 -0
  189. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  190. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  191. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  192. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  193. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  194. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +20 -23
  195. package/ios/{Application/Navigation/component → Navigation/Component}/SideBar.swift +49 -28
  196. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  197. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  198. package/ios/Navigation/Navigation.swift +190 -0
  199. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +46 -41
  200. package/ios/{Application/Navigation → Navigation}/Navigator.swift +65 -9
  201. package/ios/{Application → Navigation}/StackScreen.swift +236 -103
  202. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  203. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  204. package/ios/Pagination/PaginationScroll.swift +2 -2
  205. package/ios/Popup/PopupDisplay.swift +79 -159
  206. package/ios/Popup/PopupInput.swift +10 -13
  207. package/ios/Popup/PopupLayout.swift +97 -2
  208. package/ios/Popup/PopupNotify.swift +27 -55
  209. package/ios/Popup/PopupPromotion.swift +22 -50
  210. package/ios/PopupView/FullscreenPopup.swift +13 -19
  211. package/ios/PopupView/PopupView.swift +1 -1
  212. package/ios/PopupView/Utils++.swift +0 -8
  213. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  214. package/ios/Radio/Radio.swift +2 -1
  215. package/ios/Rating/Rating.swift +9 -1
  216. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  217. package/ios/Skeleton/Skeleton.swift +22 -27
  218. package/ios/Slider/Slider.swift +16 -18
  219. package/ios/Stepper/Stepper.swift +6 -1
  220. package/ios/Steps/Steps.swift +8 -6
  221. package/ios/SuggestAction/SuggestAction.swift +5 -0
  222. package/ios/Swipeable/SwipeCell.swift +10 -6
  223. package/ios/Switch/Switch.swift +22 -20
  224. package/ios/TabView/TabView.swift +102 -21
  225. package/ios/Tag/Tag.swift +15 -4
  226. package/ios/Template/Logo/Logo.swift +2 -1
  227. package/ios/Template/TrustBanner/TrustBanner.swift +41 -27
  228. package/ios/Title/Title.swift +6 -17
  229. package/ios/Tooltip/Tooltip.swift +29 -14
  230. package/ios/Typography/Text.swift +61 -19
  231. package/ios/Uploader/Uploader.swift +3 -1
  232. package/ios/native-kits.podspec +1 -0
  233. package/ios-demo/README.md +120 -0
  234. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  235. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  236. package/package.json +1 -1
  237. package/settings.gradle.kts +64 -0
  238. package/ios/Application/HeaderRight.swift +0 -324
  239. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  240. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  241. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  242. package/ios/Application/Navigation/NavigatorManager.swift +0 -29
  243. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -54
  244. package/ios/Application/Navigation/component/Header.swift +0 -183
  245. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -84
  246. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  247. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  248. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  249. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -280
  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
@@ -328,8 +328,13 @@ private struct ScrollableTabBar: View {
328
328
  Rectangle()
329
329
  .stroke(theme.colors.border.default, lineWidth: 0.5)
330
330
  )
331
+ // Compose scrolls the selected tab to the leading edge, initial page included.
331
332
  .onChange(of: selectedIndex) { index in
332
- withAnimation { proxy.scrollTo(index, anchor: .center) }
333
+ withAnimation { proxy.scrollTo(index, anchor: .leading) }
334
+ }
335
+ .onAppear {
336
+ guard selectedIndex > 0 else { return }
337
+ DispatchQueue.main.async { proxy.scrollTo(selectedIndex, anchor: .leading) }
333
338
  }
334
339
  }
335
340
  }
@@ -346,6 +351,11 @@ private struct TabFramePreferenceKey: PreferenceKey {
346
351
  // Card tab bar
347
352
  // ---------------------------------------------------------------------------
348
353
 
354
+ private let cardTabHeight: CGFloat = 40
355
+ private let cardTabActiveHeight: CGFloat = 44
356
+
357
+ /// Two layers, as in Compose / react-kits: a flat strip where every tab looks unselected,
358
+ /// and one raised card for the active tab, 4pt taller and joined to the strip by curved ears.
349
359
  private struct CardTabBar: View {
350
360
  @Environment(\.appTheme) private var theme
351
361
 
@@ -357,47 +367,116 @@ private struct CardTabBar: View {
357
367
  let onPress: (Int) -> Void
358
368
 
359
369
  var body: some View {
360
- HStack(alignment: .bottom, spacing: 0) {
361
- ForEach(tabs.indices, id: \.self) { index in
362
- let tab = tabs[index]
363
- let isActive = selectedIndex == index
364
- let iconColor = isActive ? selectedColor : unselectedColor
365
-
366
- SwiftUI.Button(action: { onPress(index) }) {
367
- cardItem(tab: tab, isActive: isActive, iconColor: iconColor)
368
- .frame(maxWidth: .infinity)
369
- .frame(height: isActive ? 44 : 40)
370
- .background(isActive ? theme.colors.background.surface : theme.colors.background.tonal)
371
- .clipShape(UnstyledRoundedRectangle(topLeft: Radius.M, topRight: Radius.M))
372
- .contentShape(Rectangle())
370
+ GeometryReader { geo in
371
+ let itemWidth = geo.size.width / CGFloat(Swift.max(tabs.count, 1))
372
+
373
+ ZStack(alignment: .bottomLeading) {
374
+ HStack(spacing: 0) {
375
+ ForEach(tabs.indices, id: \.self) { index in
376
+ SwiftUI.Button(action: { onPress(index) }) {
377
+ cardItem(tab: tabs[index], isActive: false)
378
+ .padding(.horizontal, Spacing.S)
379
+ .frame(width: itemWidth, height: cardTabHeight)
380
+ .background(Colors.black02)
381
+ .clipShape(UnstyledRoundedRectangle(topLeft: Radius.M, topRight: Radius.M))
382
+ .contentShape(Rectangle())
383
+ }
384
+ .buttonStyle(ActiveOpacityButtonStyle())
385
+ }
386
+ }
387
+
388
+ if let active = tabs[safe: selectedIndex] {
389
+ activeCard(active, itemWidth: itemWidth)
373
390
  }
374
- .buttonStyle(ActiveOpacityButtonStyle())
375
391
  }
392
+ .frame(width: geo.size.width, height: cardTabActiveHeight, alignment: .bottomLeading)
376
393
  }
377
- .frame(maxWidth: .infinity)
378
- .padding(.top, Spacing.XS)
394
+ // The raised card overflows the strip into the top `Spacing.XS` margin.
395
+ .frame(height: cardTabActiveHeight)
396
+ }
397
+
398
+ private func activeCard(_ tab: TabViewItem, itemWidth: CGFloat) -> some View {
399
+ let surface = theme.colors.background.surface
400
+ let pathWidth: CGFloat = tabs.count > 2 ? 22 : 28
401
+ let isFirst = selectedIndex == 0
402
+ let isLast = selectedIndex == tabs.count - 1
403
+
404
+ return HStack(alignment: .bottom, spacing: 0) {
405
+ // Each ear is nudged 8pt under the card so the join has no seam.
406
+ if !isFirst { cardEar(mirrored: true, width: pathWidth, color: surface).offset(x: 8) }
407
+ SwiftUI.Button(action: { onPress(selectedIndex) }) {
408
+ cardItem(tab: tab, isActive: true)
409
+ .padding(.horizontal, tabs.count == 3 ? Spacing.S : Spacing.M)
410
+ .frame(width: itemWidth, height: cardTabActiveHeight)
411
+ .background(surface)
412
+ .clipShape(UnstyledRoundedRectangle(topLeft: Radius.M, topRight: Radius.M))
413
+ .contentShape(Rectangle())
414
+ }
415
+ .buttonStyle(ActiveOpacityButtonStyle())
416
+ if !isLast { cardEar(mirrored: false, width: pathWidth, color: surface).offset(x: -8) }
417
+ }
418
+ .offset(x: itemWidth * CGFloat(selectedIndex) - (isFirst ? 0 : pathWidth))
419
+ }
420
+
421
+ private func cardEar(mirrored: Bool, width: CGFloat, color: Color) -> some View {
422
+ CardTabEarShape(mirrored: mirrored)
423
+ .fill(color)
424
+ .frame(width: width, height: cardTabActiveHeight)
425
+ .clipped()
426
+ .allowsHitTesting(false)
379
427
  }
380
428
 
381
429
  @ViewBuilder
382
- private func cardItem(tab: TabViewItem, isActive: Bool, iconColor: Color) -> some View {
430
+ private func cardItem(tab: TabViewItem, isActive: Bool) -> some View {
431
+ let color = isActive ? selectedColor : unselectedColor
383
432
  HStack(spacing: 0) {
384
433
  if let renderIcon = tab.renderIcon {
385
434
  renderIcon(isActive)
386
435
  Spacer().frame(width: Spacing.XS)
387
436
  } else if let icon = tab.icon {
388
- Icon(source: icon, size: 18, color: iconColor)
437
+ Icon(source: icon, size: 18, color: color)
389
438
  Spacer().frame(width: Spacing.XS)
390
439
  }
391
440
  MomoText(
392
441
  tab.title,
393
442
  typography: isActive ? .headerSSemibold : .bodyDefaultRegular,
394
- color: isActive ? selectedColor : unselectedColor
443
+ color: color
395
444
  )
396
445
  .lineLimit(1)
397
446
 
398
447
  tabBadge(tab)
399
448
  }
400
- .padding(.horizontal, Spacing.S)
449
+ }
450
+ }
451
+
452
+ /// react-kits' 28x44 ear path, in points; not scaled to the frame, which clips it at a 22pt width.
453
+ private struct CardTabEarShape: Shape {
454
+ let mirrored: Bool
455
+
456
+ func path(in rect: CGRect) -> Path {
457
+ var path = Path()
458
+ path.move(to: CGPoint(x: 0, y: 0))
459
+ path.addLine(to: CGPoint(x: 0.60765, y: 0))
460
+ path.addCurve(
461
+ to: CGPoint(x: 12.4602, y: 10.0244),
462
+ control1: CGPoint(x: 6.47582, y: 0),
463
+ control2: CGPoint(x: 11.4819, y: 4.23831)
464
+ )
465
+ path.addCurve(
466
+ to: CGPoint(x: 16.6925, y: 34),
467
+ control1: CGPoint(x: 13.9248, y: 18.6872),
468
+ control2: CGPoint(x: 15.9294, y: 30.37)
469
+ )
470
+ path.addCurve(
471
+ to: CGPoint(x: 27.9996, y: 44),
472
+ control1: CGPoint(x: 17.9536, y: 40),
473
+ control2: CGPoint(x: 22.259, y: 44)
474
+ )
475
+ path.addLine(to: CGPoint(x: 0, y: 44))
476
+ path.closeSubpath()
477
+ return mirrored
478
+ ? path.applying(CGAffineTransform(a: -1, b: 0, c: 0, d: 1, tx: rect.width, ty: 0))
479
+ : path
401
480
  }
402
481
  }
403
482
 
@@ -426,6 +505,8 @@ private struct TabItemView: View {
426
505
  rowBody
427
506
  }
428
507
  }
508
+ // Equal-width bars hit-test the whole cell, like Compose's weighted clickable.
509
+ .frame(maxWidth: scrollable ? nil : .infinity)
429
510
  .contentShape(Rectangle())
430
511
  }
431
512
  .buttonStyle(ActiveOpacityButtonStyle())
package/ios/Tag/Tag.swift CHANGED
@@ -45,26 +45,37 @@ public struct Tag: View {
45
45
  }
46
46
  }
47
47
 
48
+ // RN only accepts the `_03` palette as a custom tag color.
49
+ private static let customColors: [Color] = [
50
+ Colors.black03, Colors.pink03, Colors.violet03, Colors.indigo03,
51
+ Colors.blue03, Colors.mint03, Colors.green03, Colors.lime03,
52
+ Colors.yellow03, Colors.gold03, Colors.orange03, Colors.red03
53
+ ]
54
+
48
55
  private let label: String
49
56
  private let icon: String?
50
57
  private let color: TagColor
51
58
  private let size: TagSize
59
+ private let customColor: Color?
52
60
 
53
61
  public init(
54
62
  label: String = "Label",
55
63
  icon: String? = nil,
56
64
  color: TagColor = .default,
57
- size: TagSize = .large
65
+ size: TagSize = .large,
66
+ customColor: Color? = nil
58
67
  ) {
59
68
  self.label = label
60
69
  self.icon = icon
61
70
  self.color = color
62
71
  self.size = size
72
+ self.customColor = customColor
63
73
  }
64
74
 
65
75
  public var body: some View {
66
- let tagColor = color.backgroundColor
67
- let labelColor = color.textColor
76
+ let custom = customColor.flatMap { Self.customColors.contains($0) ? $0 : nil }
77
+ let tagColor = custom ?? color.backgroundColor
78
+ let labelColor = custom != nil ? Colors.black01 : color.textColor
68
79
 
69
80
  HStack(spacing: 0) {
70
81
  if let icon {
@@ -75,7 +86,7 @@ public struct Tag: View {
75
86
  .frame(height: fontScale(size.height))
76
87
  .padding(.horizontal, Spacing.S)
77
88
  .background(
78
- RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
89
+ RoundedRectangle(cornerRadius: Radius.S)
79
90
  .fill(tagColor)
80
91
  )
81
92
  }
@@ -57,7 +57,8 @@ private extension View {
57
57
  func containerBackground (_ type: String, _ useBackground: Bool, _ backgroundColor: Color) -> some View {
58
58
  Group {
59
59
  if (useBackground && type != "icon") {
60
- self.background(backgroundColor)
60
+ // Same shape as the border, or the square corners (and the shadow) show around it.
61
+ self.background(RoundedRectangle(cornerRadius: 8).fill(backgroundColor))
61
62
  } else {
62
63
  self
63
64
  }
@@ -70,7 +70,7 @@ let defaultBanner = TrustBannerData(
70
70
  "vi": "Tìm hiểu thêm",
71
71
  "en": "Learn more"
72
72
  ],
73
- pciImage: "https://static.momocdn.net/app/img/kits/trustBanner/pci.png",
73
+ pciImage: "https://static.momocdn.net/app/img/kits/trustBanner/pci_dss.png",
74
74
  momoImage: "https://static.momocdn.net/app/img/kits/trustBanner/ic_secu.png",
75
75
  sslImage: "https://static.momocdn.net/app/img/kits/trustBanner/ssl.png",
76
76
  icons: [
@@ -87,9 +87,20 @@ public struct TrustBanner: View {
87
87
  @Environment(\.localize) private var localize
88
88
 
89
89
  var onPress: ((String)->Void)?
90
+ private let serviceName: String
91
+ private let screenName: String
92
+ private let trackEvent: ((String, [String: String]) -> Void)?
90
93
 
91
- public init(onPress: ((String)->Void)?) {
94
+ public init(
95
+ onPress: ((String)->Void)?,
96
+ serviceName: String = "",
97
+ screenName: String = "",
98
+ trackEvent: ((String, [String: String]) -> Void)? = nil
99
+ ) {
92
100
  self.onPress = onPress
101
+ self.serviceName = serviceName
102
+ self.screenName = screenName
103
+ self.trackEvent = trackEvent
93
104
  }
94
105
 
95
106
  private var banner: TrustBannerData {
@@ -97,46 +108,50 @@ public struct TrustBanner: View {
97
108
  }
98
109
 
99
110
  func onPressSecurity() {
111
+ trackEvent?("service_component_clicked", [
112
+ "service_name": serviceName,
113
+ "screen_name": screenName,
114
+ "component_name": "logo_trust",
115
+ "url_config": banner.urlConfig
116
+ ])
100
117
  onPress?(banner.urlConfig)
101
118
  }
102
119
 
103
-
104
120
  public var body: some View {
105
- HStack(alignment: .center, spacing: 8) {
121
+ HStack(alignment: .top, spacing: 10) {
106
122
  if let momoImage = banner.momoImage {
107
123
  WebImage(url: URL(string: momoImage))
108
124
  .resizable()
109
125
  .scaledToFit()
110
126
  .frame(width: 64, height: 64)
111
127
  }
112
- VStack(alignment: .leading){
113
- Text(localize.translateData(banner.content))
114
- .foregroundColor(Color(hex: "484848"))
115
- .lineLimit(2)
116
- .font(.system(size: 13, weight: Font.Weight.regular))
117
- .ignoreBoldTextSetting()
118
- .lineSpacing(3)
119
- .padding(.bottom, 8)
120
- HStack {
128
+ VStack(alignment: .leading, spacing: 0) {
129
+ MomoText(
130
+ localize.translateData(banner.content),
131
+ typography: .descriptionDefaultRegular,
132
+ color: Color(hex: "484848"),
133
+ maxLines: 2
134
+ )
135
+ .padding(.bottom, 8)
136
+ HStack(alignment: .top, spacing: 0) {
121
137
  HStack(spacing: 0) {
122
- Text(localize.translateData(banner.subContent))
123
- .foregroundColor(Colors.pink03)
124
- .font(.system(size: 13, weight: .bold))
125
- .ignoreBoldTextSetting()
126
- .padding(.trailing, 4)
127
-
138
+ MomoText(
139
+ localize.translateData(banner.subContent),
140
+ typography: .actionXsBold,
141
+ color: Colors.pink03,
142
+ maxLines: 2
143
+ )
128
144
  Icon(source: "arrow_chevron_right_small", color: Colors.pink03)
129
- }.onTapGesture {
130
- onPressSecurity()
131
145
  }
132
-
133
- Spacer()
134
- HStack{
146
+
147
+ Spacer(minLength: 0)
148
+ HStack(spacing: 0) {
135
149
  if let pciImage = banner.pciImage {
136
150
  WebImage(url: URL(string: pciImage))
137
151
  .resizable()
138
152
  .scaledToFit()
139
- .frame(width: 24, height: 20)
153
+ .frame(width: 27, height: 20)
154
+ .padding(.trailing, 4)
140
155
  }
141
156
  if let sslImage = banner.sslImage {
142
157
  WebImage(url: URL(string: sslImage))
@@ -146,13 +161,12 @@ public struct TrustBanner: View {
146
161
  }
147
162
  }
148
163
  }
149
-
150
164
  }
151
-
152
165
  }
153
166
  .padding(.all, 12)
154
167
  .background(Color(hex: "F2F8FF"))
155
168
  .cornerRadius(12)
169
+ .contentShape(Rectangle())
156
170
  .onTapGesture {
157
171
  onPressSecurity()
158
172
  }
@@ -28,7 +28,6 @@ public enum TitleButtonSize {
28
28
 
29
29
  public struct Title: View {
30
30
  @Environment(\.appTheme) private var theme
31
- @Environment(\.momoFontScale) private var fontScale
32
31
 
33
32
  private let title: String
34
33
  private let type: TitleType
@@ -116,15 +115,9 @@ public struct Title: View {
116
115
  }
117
116
  }
118
117
 
119
- private var titleFontWeight: Font.Weight {
120
- switch type {
121
- case .card: return .regular
122
- case .section: return .bold
123
- }
124
- }
125
-
126
118
  private var titleMaxLines: Int {
127
- (showTrailingAction || badgeLabel != nil) ? 1 : 2
119
+ if textOnly { return Int.max }
120
+ return (showTrailingAction || badgeLabel != nil) ? 1 : 2
128
121
  }
129
122
 
130
123
  private var iconVerticalAlignment: VerticalAlignment {
@@ -151,14 +144,10 @@ public struct Title: View {
151
144
  }
152
145
  }
153
146
 
147
+ // Every Title variant is bold in the theme font; `headerDefaultBold` only supplies the weight.
154
148
  private var titleText: some View {
155
- SwiftUI.Text(title)
156
- .font(.system(size: fontScale(titleFontSize), weight: titleFontWeight))
157
- .ignoreBoldTextSetting()
158
- .foregroundColor(theme.colors.text.default)
159
- .lineSpacing(fontScale(titleLineHeight) - fontScale(titleFontSize))
160
- .lineLimit(titleMaxLines)
161
- .truncationMode(.tail)
149
+ MomoText(title, typography: .headerDefaultBold, maxLines: titleMaxLines)
150
+ .fontSize(titleFontSize, lineHeight: titleLineHeight)
162
151
  }
163
152
 
164
153
  private var fullLayout: some View {
@@ -237,7 +226,7 @@ public struct Title: View {
237
226
  }
238
227
  }
239
228
  .frame(maxWidth: .infinity, alignment: .leading)
240
- .padding(8)
229
+ .padding(.top, type == .section ? Spacing.S : 0)
241
230
  }
242
231
  }
243
232
 
@@ -3,7 +3,7 @@ import SwiftUI
3
3
  // MARK: - Types
4
4
 
5
5
  /// Tooltip placement relative to the anchor element.
6
- public enum TooltipPlacement {
6
+ public enum TooltipPlacement: Sendable {
7
7
  case top
8
8
  case bottom
9
9
  case left
@@ -78,24 +78,37 @@ struct TooltipArrowShape: Shape {
78
78
  func path(in rect: CGRect) -> Path {
79
79
  let w = rect.width
80
80
  let h = rect.height
81
- var path = Path()
81
+ let points: [CGPoint]
82
82
  switch placement {
83
83
  case .top:
84
- path.move(to: CGPoint(x: 0, y: 0))
85
- path.addLine(to: CGPoint(x: w, y: 0))
86
- path.addLine(to: CGPoint(x: w / 2, y: h))
84
+ points = [CGPoint(x: 0, y: 0), CGPoint(x: w, y: 0), CGPoint(x: w / 2, y: h)]
87
85
  case .bottom:
88
- path.move(to: CGPoint(x: 0, y: h))
89
- path.addLine(to: CGPoint(x: w, y: h))
90
- path.addLine(to: CGPoint(x: w / 2, y: 0))
86
+ points = [CGPoint(x: 0, y: h), CGPoint(x: w, y: h), CGPoint(x: w / 2, y: 0)]
91
87
  case .left:
92
- path.move(to: CGPoint(x: 0, y: 0))
93
- path.addLine(to: CGPoint(x: 0, y: h))
94
- path.addLine(to: CGPoint(x: w, y: h / 2))
88
+ points = [CGPoint(x: 0, y: 0), CGPoint(x: 0, y: h), CGPoint(x: w, y: h / 2)]
95
89
  case .right:
96
- path.move(to: CGPoint(x: w, y: 0))
97
- path.addLine(to: CGPoint(x: w, y: h))
98
- path.addLine(to: CGPoint(x: 0, y: h / 2))
90
+ points = [CGPoint(x: w, y: 0), CGPoint(x: w, y: h), CGPoint(x: 0, y: h / 2)]
91
+ }
92
+ // rounded like Compose's cornerPathEffect: each corner is cut `radius` along both edges
93
+ let radius = TooltipConst.arrowCornerRadius
94
+ func toward(_ from: CGPoint, _ to: CGPoint) -> CGPoint {
95
+ let dx = to.x - from.x
96
+ let dy = to.y - from.y
97
+ let length = (dx * dx + dy * dy).squareRoot()
98
+ guard length > 0 else { return from }
99
+ let distance = min(radius, length / 2)
100
+ return CGPoint(x: from.x + dx / length * distance, y: from.y + dy / length * distance)
101
+ }
102
+ var path = Path()
103
+ for index in 0..<points.count {
104
+ let corner = points[index]
105
+ let start = toward(corner, points[(index + points.count - 1) % points.count])
106
+ if index == 0 {
107
+ path.move(to: start)
108
+ } else {
109
+ path.addLine(to: start)
110
+ }
111
+ path.addQuadCurve(to: toward(corner, points[(index + 1) % points.count]), control: corner)
99
112
  }
100
113
  path.closeSubpath()
101
114
  return path
@@ -209,6 +222,8 @@ private struct TooltipPositionModifier: ViewModifier {
209
222
  }
210
223
  )
211
224
  .offset(x: originX, y: originY)
225
+ // the first pass has no size yet and would flash at the wrong spot
226
+ .opacity(popupSize == .zero ? 0 : 1)
212
227
  }
213
228
 
214
229
  private var originX: CGFloat {
@@ -99,28 +99,47 @@ public extension TypographyStyle {
99
99
  }
100
100
  }
101
101
 
102
- private func momoFontName(family: String, weight: Font.Weight) -> String? {
103
- // Ngoai whitelist thi tra nil de momoFont() roi ve .system(...), dung nhu truoc DS-791.
104
- guard KitDesignSystem.shared.isWhiteList else { return nil }
102
+ private enum MomoFontFace {
103
+ case system
104
+ case named(String)
105
+ }
106
+
107
+ /// Port of Compose `getFontFamily`; SF Pro Text is the iOS system font, so the legacy family is `.system`.
108
+ private func momoFontFace(family: String, weight: Font.Weight) -> MomoFontFace? {
109
+ let isNewFamily = family == "MomoTrustSans" || family == headerFontFamily
110
+ // Outside the whitelist the new DS families drop to the legacy font, as before DS-791.
111
+ if family == "SFProText" || (isNewFamily && !KitDesignSystem.shared.isWhiteList) { return .system }
112
+ // single-file families (Compose maps every weight to the same .otf)
113
+ if family == headerFontFamily || family == "MoMoSignature" { return .named(family) }
105
114
  guard family == "MomoTrustSans" else { return nil }
106
115
  switch weight {
107
- case .ultraLight, .thin: return "MomoTrustSans-ExtraLight"
108
- case .light: return "MomoTrustSans-Light"
109
- case .regular: return "MomoTrustSans-Regular"
110
- case .medium: return "MomoTrustSans-Medium"
111
- case .semibold: return "MomoTrustSans-SemiBold"
112
- case .bold: return "MomoTrustSans-Bold"
113
- case .heavy, .black: return "MomoTrustSans-ExtraBold"
114
- default: return "MomoTrustSans-Regular"
116
+ case .ultraLight, .thin: return .named("MomoTrustSans-ExtraLight")
117
+ case .light: return .named("MomoTrustSans-Light")
118
+ case .regular: return .named("MomoTrustSans-Regular")
119
+ case .medium: return .named("MomoTrustSans-Medium")
120
+ case .semibold: return .named("MomoTrustSans-SemiBold")
121
+ case .bold: return .named("MomoTrustSans-Bold")
122
+ case .heavy, .black: return .named("MomoTrustSans-ExtraBold")
123
+ default: return .named("MomoTrustSans-Regular")
115
124
  }
116
125
  }
117
126
 
118
- private func momoFont(size: CGFloat, weight: Font.Weight, family: String) -> Font {
119
- guard let name = momoFontName(family: family, weight: weight),
120
- UIFont(name: name, size: size) != nil else {
127
+ /// Header title family (Compose `HEADER_FONT_FAMILY`), whitelist only like the default font.
128
+ let headerFontFamily = "MoMoTrustDisplay"
129
+
130
+ private func momoFont(size: CGFloat, weight: Font.Weight, family: String, fallbackFamily: String? = nil) -> Font {
131
+ switch momoFontFace(family: family, weight: weight) {
132
+ case .system:
133
+ return .system(size: size, weight: weight)
134
+ case .named(let name) where UIFont(name: name, size: size) != nil:
135
+ return .custom(name, fixedSize: size)
136
+ default:
137
+ // Compose falls back to the default font file when a family is missing
138
+ if let fallbackFamily, fallbackFamily != family {
139
+ return momoFont(size: size, weight: weight, family: fallbackFamily)
140
+ }
121
141
  return .system(size: size, weight: weight)
122
142
  }
123
- return .custom(name, fixedSize: size)
124
143
  }
125
144
 
126
145
  public struct MomoText: View {
@@ -139,7 +158,12 @@ public struct MomoText: View {
139
158
  private let letterSpacing: CGFloat?
140
159
  private let softWrap: Bool
141
160
  private let accessibilityId: String?
161
+ private let fontFamily: String?
142
162
  private var highlightText: [String] = []
163
+ private var sizeOverride: (fontSize: CGFloat, lineHeight: CGFloat)?
164
+
165
+ private var fontSize: CGFloat { sizeOverride?.fontSize ?? typography.fontSize }
166
+ private var lineHeight: CGFloat { sizeOverride?.lineHeight ?? typography.lineHeight }
143
167
 
144
168
  public init(
145
169
  _ content: String,
@@ -153,7 +177,8 @@ public struct MomoText: View {
153
177
  underline: Bool = false,
154
178
  letterSpacing: CGFloat? = nil,
155
179
  softWrap: Bool = true,
156
- accessibilityId: String? = nil
180
+ accessibilityId: String? = nil,
181
+ fontFamily: String? = nil
157
182
  ) {
158
183
  self.content = content
159
184
  self.typography = typography
@@ -175,6 +200,7 @@ public struct MomoText: View {
175
200
  self.letterSpacing = letterSpacing
176
201
  self.softWrap = softWrap
177
202
  self.accessibilityId = accessibilityId
203
+ self.fontFamily = fontFamily
178
204
  }
179
205
 
180
206
  init(_ content: String, typography: TypographyStyle, highlightText: [String]) {
@@ -182,11 +208,27 @@ public struct MomoText: View {
182
208
  self.highlightText = highlightText
183
209
  }
184
210
 
211
+ /// Compose's `style.copy(fontSize, lineHeight)`; the weight still comes from `typography`.
212
+ func fontSize(_ fontSize: CGFloat, lineHeight: CGFloat) -> MomoText {
213
+ var text = self
214
+ text.sizeOverride = (fontSize, lineHeight)
215
+ return text
216
+ }
217
+
218
+ private var font: Font {
219
+ momoFont(
220
+ size: fontScale(fontSize),
221
+ weight: typography.fontWeight,
222
+ family: fontFamily ?? theme.font,
223
+ fallbackFamily: theme.font
224
+ )
225
+ }
226
+
185
227
  // Whole-word matches of `highlightText` render semibold; the longest keyword wins (RN parity).
186
228
  private var baseText: SwiftUI.Text {
187
229
  let keywords = highlightText.filter { !$0.isEmpty }.sorted { $0.count > $1.count }
188
230
  guard !keywords.isEmpty else { return SwiftUI.Text(content) }
189
- let highlightFont = momoFont(size: fontScale(typography.fontSize), weight: TypographyStyle.headerXsSemibold.fontWeight, family: theme.font)
231
+ let highlightFont = momoFont(size: fontScale(fontSize), weight: TypographyStyle.headerXsSemibold.fontWeight, family: fontFamily ?? theme.font, fallbackFamily: theme.font)
190
232
  func isWordChar(_ character: Character?) -> Bool {
191
233
  guard let character else { return false }
192
234
  return character.isLetter || character.isNumber || character == "_"
@@ -223,7 +265,7 @@ public struct MomoText: View {
223
265
  let isStrikethrough = strikethrough || typography.isStrikethrough
224
266
 
225
267
  var text = baseText
226
- .font(momoFont(size: fontScale(typography.fontSize), weight: typography.fontWeight, family: theme.font))
268
+ .font(font)
227
269
  .foregroundColor(textColor)
228
270
  .strikethrough(isStrikethrough, color: textColor)
229
271
  .underline(underline, color: textColor)
@@ -234,7 +276,7 @@ public struct MomoText: View {
234
276
 
235
277
  return text
236
278
  .multilineTextAlignment(textAlign)
237
- .lineSpacing(fontScale(typography.lineHeight) - fontScale(typography.fontSize))
279
+ .lineSpacing(fontScale(lineHeight) - fontScale(fontSize))
238
280
  .modifier(LineLimitModifier(range: lineLimitRange))
239
281
  .truncationMode(truncationMode)
240
282
  .fixedSize(horizontal: !softWrap, vertical: false)
@@ -80,7 +80,9 @@ public struct Uploader: View {
80
80
 
81
81
  ScrollView(.horizontal, showsIndicators: false) {
82
82
  HStack(alignment: .top, spacing: Spacing.S) {
83
- ForEach(Array(images.enumerated()), id: \.element.id) { index, image in
83
+ // Positional like Compose: `UploadImage.id` is a fresh UUID per init, so hosts that
84
+ // rebuild the array each render would otherwise re-create (and reload) every thumbnail.
85
+ ForEach(Array(images.enumerated()), id: \.offset) { index, image in
84
86
  UploaderImageItem(
85
87
  image: image,
86
88
  width: width,
@@ -37,6 +37,7 @@ Pod::Spec.new do |s|
37
37
  "Layout/**/*.{swift,m,h}",
38
38
  "Loader/**/*.{swift,m,h}",
39
39
  "Lottie/**/*.{swift,m,h}",
40
+ "Navigation/**/*.{swift,m,h}",
40
41
  "OTPKeyboard/**/*.{swift,m,h}",
41
42
  "Pagination/**/*.{swift,m,h}",
42
43
  "Popup/**/*.{swift,m,h}",