@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
@@ -2,7 +2,7 @@
2
2
  // BottomTabBar.swift
3
3
  // MoMoUIKits
4
4
  //
5
- // SwiftUI mirror of Compose's `BottomTabBar` (navigation/bottomtab/BottomTabBar.kt).
5
+ // SwiftUI mirror of Compose's `navigation/bottomtab/BottomTabBar.kt`.
6
6
  // Renders the row of tab items, an animated selection indicator, a top shadow,
7
7
  // badges/dots, and an optional center floating button (with the curved notch).
8
8
  //
@@ -20,6 +20,7 @@ public struct BottomTabItem: Identifiable {
20
20
  public let showDot: Bool
21
21
  public let badgeLabel: String?
22
22
  public let options: NavigationOptions?
23
+ public let initialParams: Any?
23
24
  public let screen: () -> AnyView
24
25
 
25
26
  public init(
@@ -29,6 +30,7 @@ public struct BottomTabItem: Identifiable {
29
30
  showDot: Bool = false,
30
31
  badgeLabel: String? = nil,
31
32
  options: NavigationOptions? = nil,
33
+ initialParams: Any? = nil,
32
34
  @ViewBuilder screen: @escaping () -> some View
33
35
  ) {
34
36
  self.name = name
@@ -37,6 +39,7 @@ public struct BottomTabItem: Identifiable {
37
39
  self.showDot = showDot
38
40
  self.badgeLabel = badgeLabel
39
41
  self.options = options
42
+ self.initialParams = initialParams
40
43
  self.screen = { AnyView(screen()) }
41
44
  }
42
45
  }
@@ -65,7 +68,11 @@ struct BottomTabBar: View {
65
68
  let selectedIndex: Int
66
69
  let onTabSelected: (Int) -> Void
67
70
 
68
- private var screenWidth: CGFloat { UIScreen.main.bounds.width }
71
+ @Environment(\.screenLayout) private var layout
72
+ @Environment(\.appTheme) private var theme
73
+
74
+ // the bar sits between the side safe areas
75
+ private var screenWidth: CGFloat { layout.width - layout.insets.leading - layout.insets.trailing }
69
76
  private var mid: Int { items.count / 2 }
70
77
 
71
78
  private var tabWidth: CGFloat {
@@ -95,11 +102,11 @@ struct BottomTabBar: View {
95
102
  }
96
103
  .frame(maxWidth: .infinity)
97
104
  .frame(height: BOTTOM_TAB_BAR_HEIGHT)
98
- .background(Colors.black01)
105
+ .background(theme.colors.background.surface.ignoresSafeArea(edges: .horizontal))
99
106
 
100
107
  // Animated selection indicator.
101
108
  RoundedRectangle(cornerRadius: 2)
102
- .fill(Colors.primary)
109
+ .fill(theme.colors.primary)
103
110
  .frame(width: 44, height: 2)
104
111
  .offset(x: indicatorOffset, y: -BOTTOM_TAB_BAR_HEIGHT + 2)
105
112
  .animation(.easeInOut(duration: 0.25), value: selectedIndex)
@@ -140,7 +147,9 @@ private struct TabBarItem: View {
140
147
  let selected: Bool
141
148
  let onClick: () -> Void
142
149
 
143
- private var tintColor: Color { selected ? Colors.primary : Colors.black10 }
150
+ @Environment(\.appTheme) private var theme
151
+
152
+ private var tintColor: Color { selected ? theme.colors.primary : theme.colors.text.hint }
144
153
 
145
154
  /// Mirrors Compose's `formatLabel`: hide a badge that is the number "0".
146
155
  private var badgeLabel: String? {
@@ -192,18 +201,20 @@ private struct TabBarItem: View {
192
201
  private struct CenterFloatingButton: View {
193
202
  let data: BottomTabFloatingButton
194
203
 
204
+ @Environment(\.appTheme) private var theme
205
+
195
206
  var body: some View {
196
207
  VStack(spacing: 0) {
197
208
  CurvedContainer()
198
209
  Circle()
199
- .fill(Colors.primary)
210
+ .fill(theme.colors.primary)
200
211
  .frame(width: 48, height: 48)
201
212
  .overlay(Icon(source: data.icon, size: 28, color: .white))
202
213
  Spacer().frame(height: 4)
203
214
  MomoText(data.label, typography: .labelXsMedium, color: Colors.black01, textAlign: .center, maxLines: 1)
204
215
  .frame(maxWidth: .infinity)
205
216
  .frame(height: 14)
206
- .background(Colors.primary)
217
+ .background(theme.colors.primary)
207
218
  .clipShape(RoundedRectangle(cornerRadius: Radius.XL))
208
219
  }
209
220
  .frame(width: bottomTabFloatingButtonWidth)
@@ -0,0 +1,104 @@
1
+ //
2
+ // DuoBarCompression.swift
3
+ // MoMoUIKits
4
+ //
5
+ // Port of RN `BottomTab/DuoBarCompression.tsx` (HIG, Designing for iPhone Duo: Vertical controls):
6
+ // keeps the side bar's toolbar and tab bar clear of each other.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ /// iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
12
+ public enum DuoBarCompressionBehavior {
13
+ case toolbar, tabBar
14
+ }
15
+
16
+ enum DuoBar {
17
+ case toolbar, tabBar
18
+ }
19
+
20
+ struct DuoBarCompression {
21
+ var toolbarCompressed = false
22
+ var tabBarCompressed = false
23
+ /// The open rail needs the toolbar button slot too: only Back stays.
24
+ var toolbarHidden = false
25
+ var expandToolbar: () -> Void = {}
26
+ var expandTabBar: () -> Void = {}
27
+ /// A rail opened from the compressed state closes once a tab is picked.
28
+ var onTabSelected: () -> Void = {}
29
+ /// Side bar toolkits report their full height so the tab bar knows what is left.
30
+ var setToolbarHeight: (CGFloat) -> Void = { _ in }
31
+
32
+ /// Space kept between the top and the bottom groups (HIG Vertical controls).
33
+ static let groupGap = Spacing.M
34
+ /// Back, then the right toolkit: help + home capsule.
35
+ static let defaultToolbarHeight = DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch
36
+ /// Back, then the single button the toolbar folds into.
37
+ private static let compressedToolbarHeight = 2 * DuoMetrics.controlSize + Spacing.M
38
+
39
+ /// - both fit: both stay whole
40
+ /// - one fits whole beside the other compressed: one is compressed, tapping it swaps them
41
+ /// - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
42
+ /// again once a tab is picked)
43
+ static func resolve(
44
+ layout: ScreenLayout,
45
+ tabCount: Int,
46
+ hasQr: Bool,
47
+ toolbarHeight: CGFloat,
48
+ expanded: Binding<DuoBar>,
49
+ opened: Binding<DuoBar?>,
50
+ setToolbarHeight: @escaping (CGFloat) -> Void
51
+ ) -> DuoBarCompression {
52
+ guard let sideBar = layout.sideBar else { return DuoBarCompression() }
53
+ let room = layout.height - sideBar.bottom - sideBar.top - groupGap
54
+ let tabBar = duoRailHeight(itemCount: tabCount, hasQr: hasQr)
55
+ let compressedTabBar = duoRailHeight(itemCount: 1, hasQr: hasQr, compressed: true)
56
+ func fits(_ toolbar: CGFloat, _ tabs: CGFloat) -> Bool { toolbar + tabs <= room }
57
+
58
+ if fits(toolbarHeight, tabBar) {
59
+ return DuoBarCompression(setToolbarHeight: setToolbarHeight)
60
+ }
61
+ if fits(compressedToolbarHeight, tabBar) && fits(toolbarHeight, compressedTabBar) {
62
+ return DuoBarCompression(
63
+ toolbarCompressed: expanded.wrappedValue == .tabBar,
64
+ tabBarCompressed: expanded.wrappedValue == .toolbar,
65
+ expandToolbar: { expanded.wrappedValue = .toolbar },
66
+ expandTabBar: { expanded.wrappedValue = .tabBar },
67
+ setToolbarHeight: setToolbarHeight
68
+ )
69
+ }
70
+
71
+ let expandToolbar = { opened.wrappedValue = .toolbar }
72
+ let expandTabBar = { opened.wrappedValue = .tabBar }
73
+ // an opened rail always shows, even when only the group gap is short: it closes once a tab is picked
74
+ if opened.wrappedValue == .tabBar {
75
+ return DuoBarCompression(
76
+ toolbarCompressed: true,
77
+ tabBarCompressed: false,
78
+ toolbarHidden: true,
79
+ expandToolbar: expandToolbar,
80
+ expandTabBar: expandTabBar,
81
+ onTabSelected: { opened.wrappedValue = nil },
82
+ setToolbarHeight: setToolbarHeight
83
+ )
84
+ }
85
+ return DuoBarCompression(
86
+ toolbarCompressed: opened.wrappedValue != .toolbar,
87
+ tabBarCompressed: true,
88
+ expandToolbar: expandToolbar,
89
+ expandTabBar: expandTabBar,
90
+ setToolbarHeight: setToolbarHeight
91
+ )
92
+ }
93
+ }
94
+
95
+ private struct DuoBarCompressionKey: EnvironmentKey {
96
+ static let defaultValue = DuoBarCompression()
97
+ }
98
+
99
+ extension EnvironmentValues {
100
+ var duoBarCompression: DuoBarCompression {
101
+ get { self[DuoBarCompressionKey.self] }
102
+ set { self[DuoBarCompressionKey.self] = newValue }
103
+ }
104
+ }
@@ -0,0 +1,329 @@
1
+ //
2
+ // DuoTabBar.swift
3
+ // MoMoUIKits
4
+ //
5
+ // iPhone Duo tab bar, port of RN `BottomTab/DuoTabBar.tsx` (Figma Fold-Design-System MT1/MT2):
6
+ // a rail in the side bar, or a floating bar when unfolded portrait.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ private let itemSize = DuoMetrics.controlSize
12
+ private let iconSize: CGFloat = 28
13
+ private let qrIconSize: CGFloat = 24
14
+ private let railPadding: CGFloat = 6
15
+ private let railGap = Spacing.XXS
16
+ // selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
17
+ private let railBubbleWidth: CGFloat = 44
18
+ private let railBubbleHeight: CGFloat = 58
19
+ private let floatingHeight = DuoMetrics.floatingTabBarHeight
20
+ private let floatingPadding = Spacing.XS
21
+ // the system bar sizes its pill to the tabs instead of stretching it across the display
22
+ private let floatingItemWidth: CGFloat = 80
23
+ // while pressed the selection bubble grows into a lens past the pill's edges
24
+ private let lensScaleX: CGFloat = 1.2
25
+ private let lensScaleY: CGFloat = 1.3
26
+ // Figma Home-2026 tab bar "Selection": brand pink at 8%
27
+ private let selectionTintOpacity: Double = 0x14 / 255
28
+ private let tabSpring = Animation.interpolatingSpring(mass: 0.8, stiffness: 220, damping: 18)
29
+
30
+ /// Room tab screens leave for the Duo tab bar, provided by BottomTab to its tab StackScreens.
31
+ struct DuoTabBarInsets: Equatable {
32
+ /// Floating bar overlays the content: scroll content, footer and FAB keep clear of it.
33
+ var overlay: CGFloat = 0
34
+ /// Rail sits at the bottom of the side strip: the header side bar stays this far above the window bottom.
35
+ var sideBarReserve: CGFloat = 0
36
+ }
37
+
38
+ private struct DuoTabBarInsetsKey: EnvironmentKey {
39
+ static let defaultValue = DuoTabBarInsets()
40
+ }
41
+
42
+ extension EnvironmentValues {
43
+ var duoTabBarInsets: DuoTabBarInsets {
44
+ get { self[DuoTabBarInsetsKey.self] }
45
+ set { self[DuoTabBarInsetsKey.self] = newValue }
46
+ }
47
+ }
48
+
49
+ /// Rail plus the QR button under it; compressed, the rail is a single button.
50
+ func duoRailHeight(itemCount: Int, hasQr: Bool, compressed: Bool = false) -> CGFloat {
51
+ let count = CGFloat(itemCount)
52
+ let rail = compressed ? itemSize : railPadding * 2 + count * itemSize + max(count - 1, 0) * railGap
53
+ return rail + (hasQr ? Spacing.M + itemSize : 0)
54
+ }
55
+
56
+ extension ScreenLayout {
57
+ /// Overlaps the home indicator inset like the system tab bar (21pt above the bottom with a 34pt inset).
58
+ var duoFloatingTabBarBottom: CGFloat {
59
+ max(insets.bottom - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S)
60
+ }
61
+
62
+ func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool, tabBarCompressed: Bool = false) -> DuoTabBarInsets {
63
+ guard isIPhoneDuo else { return DuoTabBarInsets() }
64
+ guard let sideBar else {
65
+ // overlay is measured from the bottom safe area, the bar reaches into it
66
+ return DuoTabBarInsets(overlay: floatingHeight + duoFloatingTabBarBottom - insets.bottom)
67
+ }
68
+ let rail = duoRailHeight(itemCount: itemCount, hasQr: hasFloatingButton, compressed: tabBarCompressed)
69
+ return DuoTabBarInsets(sideBarReserve: sideBar.bottom + rail + DuoBarCompression.groupGap)
70
+ }
71
+ }
72
+
73
+ /// RN `BarMaterial`: surface with a 1pt border.
74
+ struct DuoBarMaterial<S: InsettableShape>: ViewModifier {
75
+ let shape: S
76
+
77
+ @Environment(\.appTheme) private var theme
78
+
79
+ func body(content: Content) -> some View {
80
+ content.background(
81
+ shape.fill(theme.colors.background.surface)
82
+ .overlay(shape.strokeBorder(theme.colors.border.default, lineWidth: 1))
83
+ .allowsHitTesting(false)
84
+ )
85
+ }
86
+ }
87
+
88
+ struct DuoTabBar: View {
89
+ let items: [BottomTabItem]
90
+ let floatingButton: BottomTabFloatingButton?
91
+ let selectedIndex: Int
92
+ let onTabSelected: (Int) -> Void
93
+ var compression = DuoBarCompression()
94
+
95
+ @Environment(\.screenLayout) private var layout
96
+
97
+ var body: some View {
98
+ if let sideBar = layout.sideBar {
99
+ DuoRailTabBar(sideBar: sideBar, items: items, floatingButton: floatingButton,
100
+ selectedIndex: selectedIndex, onTabSelected: onTabSelected, compression: compression)
101
+ } else {
102
+ DuoFloatingTabBar(items: items, floatingButton: floatingButton,
103
+ selectedIndex: selectedIndex, onTabSelected: onTabSelected)
104
+ }
105
+ }
106
+ }
107
+
108
+ /// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E).
109
+ /// When the side bar is short it can shrink to one button showing the current tab
110
+ /// (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar.
111
+ private struct DuoRailTabBar: View {
112
+ let sideBar: SideBarLayout
113
+ let items: [BottomTabItem]
114
+ let floatingButton: BottomTabFloatingButton?
115
+ let selectedIndex: Int
116
+ let onTabSelected: (Int) -> Void
117
+ let compression: DuoBarCompression
118
+
119
+ @State private var pressedIndex: Int?
120
+
121
+ private var isRight: Bool { sideBar.edge == .right }
122
+
123
+ var body: some View {
124
+ VStack(spacing: Spacing.M) {
125
+ if compression.tabBarCompressed, items.indices.contains(selectedIndex) {
126
+ SwiftUI.Button(action: compression.expandTabBar) {
127
+ DuoTabIcon(item: items[selectedIndex], selected: true)
128
+ .frame(width: itemSize, height: itemSize)
129
+ .contentShape(Circle())
130
+ }
131
+ .buttonStyle(.plain)
132
+ .modifier(DuoBarMaterial(shape: Circle()))
133
+ .accessibilityLabel(items[selectedIndex].label)
134
+ .accessibilityIdentifier("bottom_tab_bar")
135
+ } else {
136
+ rail
137
+ }
138
+
139
+ if let floatingButton {
140
+ DuoQrButton(data: floatingButton, size: itemSize)
141
+ }
142
+ }
143
+ .padding(isRight ? .leading : .trailing, Spacing.M)
144
+ .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
145
+ .padding(.bottom, sideBar.bottom)
146
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .bottomTrailing : .bottomLeading)
147
+ .ignoresSafeArea()
148
+ }
149
+
150
+ /// Column of icon-only tab items with the sliding selection bubble.
151
+ private var rail: some View {
152
+ ZStack(alignment: .top) {
153
+ DuoSelectionMaterial(lens: pressedIndex != nil)
154
+ .frame(width: railBubbleWidth, height: railBubbleHeight)
155
+ .offset(y: railPadding - (railBubbleHeight - itemSize) / 2
156
+ + CGFloat(pressedIndex ?? selectedIndex) * (itemSize + railGap))
157
+ .animation(tabSpring, value: selectedIndex)
158
+ .animation(tabSpring, value: pressedIndex)
159
+
160
+ VStack(spacing: railGap) {
161
+ ForEach(items.indices, id: \.self) { index in
162
+ SwiftUI.Button {
163
+ onTabSelected(index)
164
+ compression.onTabSelected()
165
+ } label: {
166
+ DuoTabIcon(item: items[index], selected: index == selectedIndex)
167
+ .frame(width: itemSize, height: itemSize)
168
+ .contentShape(Rectangle())
169
+ }
170
+ .buttonStyle(DuoLensPressStyle(index: index, pressedIndex: $pressedIndex))
171
+ .accessibilityLabel(items[index].label)
172
+ }
173
+ }
174
+ .padding(.vertical, railPadding)
175
+ }
176
+ .frame(width: itemSize)
177
+ .modifier(DuoBarMaterial(shape: Capsule()))
178
+ .accessibilityIdentifier("bottom_tab_bar")
179
+ }
180
+ }
181
+
182
+ /// Floating labelled bar for the unfolded portrait display, QR pinned in its own platter beside it.
183
+ private struct DuoFloatingTabBar: View {
184
+ let items: [BottomTabItem]
185
+ let floatingButton: BottomTabFloatingButton?
186
+ let selectedIndex: Int
187
+ let onTabSelected: (Int) -> Void
188
+
189
+ @Environment(\.screenLayout) private var layout
190
+ @Environment(\.appTheme) private var theme
191
+ @State private var pressedIndex: Int?
192
+
193
+ private var barWidth: CGFloat {
194
+ let margins = layout.insets.leading + layout.insets.trailing + 2 * DuoMetrics.floatingTabBarMargin
195
+ let qr = floatingButton == nil ? 0 : floatingHeight + Spacing.S
196
+ let preferred = CGFloat(items.count) * floatingItemWidth + 2 * floatingPadding
197
+ return max(min(preferred, layout.width - margins - qr), 0)
198
+ }
199
+
200
+ private var cellWidth: CGFloat { (barWidth - floatingPadding * 2) / CGFloat(max(items.count, 1)) }
201
+
202
+ var body: some View {
203
+ HStack(spacing: Spacing.S) {
204
+ bar
205
+ if let floatingButton {
206
+ DuoQrButton(data: floatingButton, size: floatingHeight)
207
+ .shadow(color: Color.black.opacity(0.16), radius: 16, x: 0, y: 6)
208
+ }
209
+ }
210
+ .padding(.bottom, layout.duoFloatingTabBarBottom)
211
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
212
+ .ignoresSafeArea()
213
+ }
214
+
215
+ private var bar: some View {
216
+ ZStack(alignment: .leading) {
217
+ DuoSelectionMaterial(lens: pressedIndex != nil)
218
+ .frame(width: cellWidth)
219
+ .offset(x: CGFloat(pressedIndex ?? selectedIndex) * cellWidth)
220
+ .animation(tabSpring, value: selectedIndex)
221
+ .animation(tabSpring, value: pressedIndex)
222
+
223
+ HStack(spacing: 0) {
224
+ ForEach(items.indices, id: \.self) { index in
225
+ tabCell(index)
226
+ }
227
+ }
228
+ }
229
+ .padding(floatingPadding)
230
+ .frame(width: barWidth, height: floatingHeight)
231
+ .modifier(DuoBarMaterial(shape: Capsule()))
232
+ .accessibilityIdentifier("bottom_tab_bar")
233
+ }
234
+
235
+ private func tabCell(_ index: Int) -> some View {
236
+ let selected = index == selectedIndex
237
+ return SwiftUI.Button { onTabSelected(index) } label: {
238
+ VStack(spacing: Spacing.XXS) {
239
+ DuoTabIcon(item: items[index], selected: selected)
240
+ MomoText(
241
+ items[index].label,
242
+ typography: .labelXsMedium,
243
+ color: selected ? theme.colors.primary : theme.colors.text.hint,
244
+ textAlign: .center,
245
+ maxLines: 1
246
+ )
247
+ // like the system tab bar the label keeps its design size; a constant snapshot, nothing to observe
248
+ .environment(\.momoFontScale, FontScale(userScaleRate: 1, useOSScaleRate: false))
249
+ }
250
+ .frame(width: cellWidth)
251
+ .frame(maxHeight: .infinity)
252
+ .contentShape(Rectangle())
253
+ }
254
+ .buttonStyle(DuoLensPressStyle(index: index, pressedIndex: $pressedIndex))
255
+ .accessibilityLabel(items[index].label)
256
+ }
257
+ }
258
+
259
+ /// Pink tinted capsule under the selected tab; while pressed it grows into a lens (80x54 to about 96x70).
260
+ private struct DuoSelectionMaterial: View {
261
+ let lens: Bool
262
+
263
+ @Environment(\.appTheme) private var theme
264
+
265
+ var body: some View {
266
+ Capsule()
267
+ .fill(theme.colors.primary.opacity(selectionTintOpacity))
268
+ .scaleEffect(x: lens ? lensScaleX : 1, y: lens ? lensScaleY : 1)
269
+ .allowsHitTesting(false)
270
+ }
271
+ }
272
+
273
+ /// Tracks the finger so the selection lens jumps to the pressed tab before it is selected.
274
+ private struct DuoLensPressStyle: ButtonStyle {
275
+ let index: Int
276
+ @Binding var pressedIndex: Int?
277
+
278
+ func makeBody(configuration: Configuration) -> some View {
279
+ configuration.label.onChange(of: configuration.isPressed) { pressed in
280
+ if pressed {
281
+ pressedIndex = index
282
+ } else if pressedIndex == index {
283
+ pressedIndex = nil
284
+ }
285
+ }
286
+ }
287
+ }
288
+
289
+ private struct DuoTabIcon: View {
290
+ let item: BottomTabItem
291
+ let selected: Bool
292
+
293
+ @Environment(\.appTheme) private var theme
294
+
295
+ private var badgeLabel: String? {
296
+ guard let label = item.badgeLabel else { return nil }
297
+ if let n = Int(label), n == 0 { return nil }
298
+ return label
299
+ }
300
+
301
+ var body: some View {
302
+ Icon(source: item.icon, size: iconSize, color: selected ? theme.colors.primary : theme.colors.text.hint)
303
+ .overlay(alignment: .topTrailing) {
304
+ if let badgeLabel, !badgeLabel.isEmpty {
305
+ Badge(label: badgeLabel).offset(x: Spacing.S, y: -Spacing.XS)
306
+ } else if badgeLabel != nil || item.showDot {
307
+ BadgeDot(size: .small)
308
+ }
309
+ }
310
+ }
311
+ }
312
+
313
+ private struct DuoQrButton: View {
314
+ let data: BottomTabFloatingButton
315
+ let size: CGFloat
316
+
317
+ @Environment(\.appTheme) private var theme
318
+
319
+ var body: some View {
320
+ SwiftUI.Button(action: data.onPress) {
321
+ Circle()
322
+ .fill(theme.colors.primary)
323
+ .frame(width: size, height: size)
324
+ .overlay(Icon(source: data.icon, size: qrIconSize, color: Colors.black01))
325
+ }
326
+ .buttonStyle(.plain)
327
+ .accessibilityLabel(data.label)
328
+ }
329
+ }
@@ -1,5 +1,12 @@
1
+ //
2
+ // FloatingButton.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/FloatingButton.kt`: the screen FAB, whose
6
+ // label collapses while the content scrolls down.
7
+ //
8
+
1
9
  import SwiftUI
2
- import Combine
3
10
 
4
11
  public enum FABSize {
5
12
  case small
@@ -58,14 +65,10 @@ public struct FabProps {
58
65
  }
59
66
 
60
67
  public struct FloatingButton: View {
68
+ @State private var isExpanded: Bool = true
61
69
  @State private var lastScrollOffset: CGFloat = 0
62
- @State private var isExpanded: Bool = false
63
- @State private var internalScrollOffset: CGFloat = 0
64
- @State private var showText: Bool = true
65
- @State private var fullWidth: CGFloat? = nil
66
- @State private var currentWidth: CGFloat = 0
67
- @State private var lastDirection: String? = nil
68
-
70
+ @State private var settleScroll: Task<Void, Never>?
71
+
69
72
  private let props: FabProps
70
73
  private var keyboardOffset: CGFloat
71
74
  private var scrollOffset: CGFloat
@@ -111,64 +114,50 @@ public struct FloatingButton: View {
111
114
  content
112
115
  }
113
116
  }
114
- .onTapGesture {
115
- onClick?()
116
- }
117
117
  .padding(Spacing.M)
118
118
  .frame(height: height + (Spacing.M * 2))
119
119
  .background(containerColor)
120
120
  .cornerRadius(100)
121
+ // Compose FloatingActionButton elevation 4dp
122
+ .shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 2)
123
+ .contentShape(Capsule())
124
+ .onTapGesture {
125
+ onClick?()
126
+ }
127
+ .accessibilityElement(children: .combine)
128
+ .accessibilityAddTraits(.isButton)
121
129
  .padding(.bottom, bottomPadding)
122
130
  .frame(alignment: position == .center ? .center : .trailing)
123
- .onAppear {
124
- internalScrollOffset = scrollOffset
125
- isExpanded = label != nil
126
- }
127
- .onReceive(Just(scrollOffset)) { newValue in
128
- let direction = newValue > internalScrollOffset ? "down" : "up"
131
+ .onAppear { lastScrollOffset = scrollOffset }
132
+ .onChange(of: scrollOffset, perform: updateExpanded)
133
+ .onDisappear { settleScroll?.cancel() }
134
+ }
129
135
 
130
- if direction != lastDirection {
131
- lastDirection = direction
132
- if direction == "down" {
133
- withAnimation(.easeOut(duration: 0.1)) {
134
- isExpanded = false
135
- }
136
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
137
- showText = false
138
- }
139
- } else {
140
- showText = true
141
- withAnimation(.easeIn(duration: 0.1)) {
142
- isExpanded = true
143
- }
144
-
145
- }
146
- }
136
+ /// Compose's `collectLatest { isExpanded = new <= last; delay(100); last = new }`: the label
137
+ /// shows while the content scrolls back up, and the reference only moves once the scroll pauses.
138
+ private func updateExpanded(_ offset: CGFloat) {
139
+ let expanded = offset <= lastScrollOffset
140
+ if expanded != isExpanded {
141
+ withAnimation(.linear(duration: 0.1)) { isExpanded = expanded }
142
+ }
143
+ settleScroll?.cancel()
144
+ settleScroll = Task { @MainActor in
145
+ try? await Task.sleep(nanoseconds: 100_000_000)
146
+ guard !Task.isCancelled else { return }
147
+ lastScrollOffset = offset
147
148
  }
148
149
  }
149
-
150
+
150
151
  private var content: some View {
151
152
  HStack(spacing: 0) {
152
- ImageView(icon ?? "")
153
- .scaledToFit()
154
- .frame(width: iconSize, height: iconSize)
155
- .foregroundColor(iconColor)
156
-
157
- if let label = label {
158
- if showText {
159
- Spacer().frame(width: 8)
160
- MomoText(label, typography: .actionDefaultBold, color: .white, maxLines: 1)
161
- .background(
162
- GeometryReader { geo in
163
- Color.clear.onAppear {
164
- fullWidth = geo.size.width
165
- }
166
- }
167
- )
168
- .opacity(isExpanded ? 1 : 0)
169
- }
170
- }
153
+ // Compose `Icon`: an icon key or an https URL, tinted
154
+ Icon(source: icon ?? "", size: iconSize ?? defaultIconSize, color: iconColor)
171
155
 
156
+ if let label, isExpanded {
157
+ MomoText(label, typography: .actionDefaultBold, color: .white, maxLines: 1)
158
+ .padding(.leading, 12)
159
+ .transition(.opacity.combined(with: .move(edge: .leading)))
160
+ }
172
161
  }
173
162
  }
174
163
  }