@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20

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 (266) hide show
  1. package/CLAUDE.md +36 -8
  2. package/README.md +2 -2
  3. package/ios/Application/ApplicationEnvironment.swift +17 -0
  4. package/ios/Application/MaxApi.swift +12 -3
  5. package/ios/Avatar/Avatar.swift +4 -3
  6. package/ios/Badge/Badge.swift +6 -4
  7. package/ios/Badge/BadgeDot.swift +1 -1
  8. package/ios/Badge/BadgeRibbon.swift +0 -1
  9. package/ios/Button/Button.swift +15 -12
  10. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  11. package/ios/Carousel/Carousel.swift +29 -12
  12. package/ios/Checkbox/Checkbox.swift +7 -3
  13. package/ios/Chip/Chip.swift +5 -4
  14. package/ios/Collapse/Collapse.swift +70 -19
  15. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  16. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  17. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  18. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  19. package/ios/Divider/Divider.swift +37 -4
  20. package/ios/Extensions/Color++.swift +38 -9
  21. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  22. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  23. package/ios/Icon/Icon.swift +21 -23
  24. package/ios/Icon/Icons.swift +1329 -0
  25. package/ios/IconButton/IconButton.swift +15 -7
  26. package/ios/Image/Image.swift +7 -23
  27. package/ios/Information/Information.swift +27 -9
  28. package/ios/Input/ErrorView.swift +2 -2
  29. package/ios/Input/Input.swift +160 -147
  30. package/ios/Input/InputPhoneNumber.swift +12 -9
  31. package/ios/Input/InputSearch.swift +102 -75
  32. package/ios/Input/InputTextArea.swift +128 -145
  33. package/ios/InputDropDown/InputDropDown.swift +10 -7
  34. package/ios/InputMoney/InputMoney.swift +33 -11
  35. package/ios/InputOTP/InputOTP.swift +22 -12
  36. package/ios/Layout/Card.swift +50 -0
  37. package/ios/Layout/GridContext.swift +187 -3
  38. package/ios/Layout/Item.swift +5 -4
  39. package/ios/Layout/ScreenLayout.swift +380 -0
  40. package/ios/Layout/Section.swift +4 -100
  41. package/ios/Loader/Loader.swift +5 -9
  42. package/ios/Lottie/LottieView.swift +14 -2
  43. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
  44. package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
  45. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  46. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  47. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  48. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  49. package/ios/Navigation/Component/Header.swift +531 -0
  50. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  51. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  52. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  53. package/ios/Navigation/Component/HeaderRight.swift +377 -0
  54. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  55. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  56. package/ios/Navigation/Component/SideBar.swift +183 -0
  57. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
  58. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  59. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
  60. package/ios/Navigation/Navigation.swift +204 -0
  61. package/ios/Navigation/NavigationContainer.swift +194 -0
  62. package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
  63. package/ios/Navigation/StackScreen.swift +820 -0
  64. package/ios/Navigation/StackTransition.swift +400 -0
  65. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  66. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  67. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  68. package/ios/Pagination/PaginationScroll.swift +2 -2
  69. package/ios/Popup/PopupDisplay.swift +72 -136
  70. package/ios/Popup/PopupInput.swift +10 -13
  71. package/ios/Popup/PopupLayout.swift +140 -0
  72. package/ios/Popup/PopupNotify.swift +56 -73
  73. package/ios/Popup/PopupPromotion.swift +31 -47
  74. package/ios/PopupView/FullscreenPopup.swift +13 -19
  75. package/ios/PopupView/PopupView.swift +1 -1
  76. package/ios/PopupView/Utils++.swift +0 -8
  77. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  78. package/ios/Radio/Radio.swift +2 -1
  79. package/ios/Rating/Rating.swift +9 -1
  80. package/ios/Resources/dot_loading.json +558 -0
  81. package/ios/Resources/lottie_circle_loader.json +1 -0
  82. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  83. package/ios/Skeleton/Skeleton.swift +22 -27
  84. package/ios/Slider/Slider.swift +16 -18
  85. package/ios/Stepper/Stepper.swift +6 -1
  86. package/ios/Steps/Steps.swift +8 -6
  87. package/ios/SuggestAction/SuggestAction.swift +5 -0
  88. package/ios/Swipeable/SwipeCell.swift +10 -6
  89. package/ios/Switch/Switch.swift +22 -20
  90. package/ios/TabView/TabView.swift +102 -21
  91. package/ios/Tag/Tag.swift +15 -4
  92. package/ios/Template/Logo/Logo.swift +2 -1
  93. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  94. package/ios/Title/Title.swift +6 -17
  95. package/ios/Tooltip/Tooltip.swift +29 -14
  96. package/ios/Typography/Text.swift +61 -19
  97. package/ios/Uploader/Uploader.swift +3 -1
  98. package/ios/native-kits.podspec +2 -1
  99. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  100. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  101. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  102. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  103. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  104. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  105. package/package.json +1 -1
  106. package/scripts/gen-ios-icons.mjs +36 -0
  107. package/build.gradle.kts +0 -11
  108. package/compose/build.gradle.kts +0 -190
  109. package/compose/build.gradle.kts.backup +0 -190
  110. package/compose/compose.podspec +0 -47
  111. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  112. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  113. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  114. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  115. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -151
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -831
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
  149. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  150. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
  151. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
  152. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  153. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
  154. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  155. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  156. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  157. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  158. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  159. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  160. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  161. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -334
  162. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -98
  163. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
  164. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  177. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  178. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  182. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  183. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
  184. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -188
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -157
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  199. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  200. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  201. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  202. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  203. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -299
  204. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  205. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -129
  206. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  207. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  208. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  209. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  210. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -622
  211. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  212. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  213. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -175
  214. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -232
  215. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  216. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  217. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -381
  218. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -83
  219. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -105
  220. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  221. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -347
  222. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
  223. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  224. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -169
  225. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
  226. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  227. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  228. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  229. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  230. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -71
  231. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  232. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  233. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  234. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  235. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
  236. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  237. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  238. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  239. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  240. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -213
  241. package/gradle/libs.versions.toml +0 -69
  242. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  243. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  244. package/gradle.properties +0 -26
  245. package/gradlew +0 -252
  246. package/gradlew.bat +0 -94
  247. package/ios/Application/Components.swift +0 -270
  248. package/ios/Application/ComposeApi.swift +0 -22
  249. package/ios/Application/HeaderRight.swift +0 -306
  250. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  251. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  252. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  253. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  254. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  255. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  256. package/ios/Application/Navigation/component/Header.swift +0 -178
  257. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  258. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  259. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  260. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  261. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  262. package/ios/Application/Screen.swift +0 -250
  263. package/ios/Application/StackScreen.swift +0 -492
  264. package/ios-demo/README.md +0 -120
  265. package/settings.gradle.kts +0 -64
  266. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
package/CLAUDE.md CHANGED
@@ -18,7 +18,7 @@ Guidance for working in this repo. Keep it accurate — update it when structure
18
18
  ## Source layout (`compose/src/commonMain/kotlin/vn/momo/kits/`)
19
19
 
20
20
  - `components/` — ~50 UI components, **one file per component** (`Button.kt`, `Input*.kt`, `PopupNotify.kt`, …).
21
- - `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `Screen.kt`, header pieces. Also holds the **deprecated** `ApplicationContainer`.
21
+ - `application/` — app-level context & config: `Context.kt` (CompositionLocals, `MiniAppContext`, `DesignSystemConfig`), `Localize.kt` (i18n: `LocalizationObject`, `Localize`, default dictionary), `LiteScreen.kt` (standalone lightweight screen with its own header), `WidgetContainer.kt`.
22
22
  - `navigation/` — `NavigationContainer.kt` (**canonical entry point**), `Navigator.kt`, `Navigation.kt`, `StackScreen.kt`, `bottomtab/`, `component/`, `tracking/`.
23
23
  - `const/` — design tokens: `Theme.kt`, `Colors.kt`, `Typography.kt`, `Spacing.kt`, `Radius.kt`.
24
24
  - `layout/`, `modifier/`, `utils/`.
@@ -35,11 +35,18 @@ No unit-test suite is configured. Validate changes by compiling the relevant tar
35
35
  ./gradlew :sample:androidApp:installDebug # run demo on device/emulator
36
36
  ```
37
37
 
38
+ The SwiftUI kit (`ios/`, pod `MoMoUIKits`) compiles on its own through the sample's Pods project (`pod install` in `sample/iosApp` first when files are added):
39
+
40
+ ```bash
41
+ xcodebuild -project sample/iosApp/Pods/Pods.xcodeproj -scheme MoMoUIKits -sdk iphonesimulator \
42
+ -destination 'generic/platform=iOS Simulator' build
43
+ ```
44
+
38
45
  Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do not publish manually unless asked: `sh publish.sh beta` / `sh publish.sh release` (Maven→GitLab + NPM).
39
46
 
40
47
  ## Architecture essentials
41
48
 
42
- - **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. **Do not use** `ApplicationContainer` (`application/NavigationContainer.kt`) — deprecated and does **not** wire `LocalLocalize` / `LocalNavigator` / `LocalMaxApi`.
49
+ - **Entry point:** wrap content in `NavigationContainer(...)` from `navigation/`. It builds the `Navigator`, takes a host-supplied `Localize` (falls back to an empty one), and provides the CompositionLocals below. The legacy `Screen` / `ApplicationContainer` / `ComposeApi` stack has been removed on both sides — don't reintroduce it.
43
50
  - **Configuration flows via CompositionLocals**, not params. Public ones: `AppTheme`, `AppStatusBar`, `AppNavigationBar` (`const/Theme.kt`); `ApplicationContext`, `AppConfig`, `AppLanguage`, `LocalLocalize` (`application/`); `LocalNavigator`, `LocalMaxApi` (`navigation/`). Read with `X.current`.
44
51
  - **Design-system config** (`trustBanner`, `headerBar`, `headerGradient`): the host seeds `KitDesignSystemConfig.update(...)` once (same pattern as `KitFontScale`); `NavigationContainer` provides it as `LocalDesignSystemConfig` (param `designSystemConfig` → parent → global) and applies `headerBar` as the header image when the theme has none.
45
52
  - **Platform code** goes through `expect`/`actual` in `platform/` — never reference Android/iOS APIs from `commonMain` directly.
@@ -47,11 +54,32 @@ Publishing is automated (`publish.sh` + GitLab CI on `[ci build]` / `main`). Do
47
54
  - Navigation uses a runtime `DynamicScreenRegistry`; overlays (modal/bottom-sheet/snackbar) go through `Navigator` + `OverplayComponentRegistry`.
48
55
  - **iOS swipe-back:** the host owns the only edge-pan recogniser and replays it through `HostBackGestureBridge` (`navigation/HostBackGestureBridge.kt`; the kit framework is never exported, so the host reaches it through its own Koin-provided facade): `checkCanPop` / `canPopInner` / `pop` route the back into the kit stack, `started` / `progressed` / `completed` / `cancelled` seek the NavHost transition, `shouldUseFloatingArrowBack` is set by bottom tabs. `NavigationContainer` attaches itself automatically (`AttachHostBackGesture`, iOS only, detached by identity on dispose) — hosts need no wrapper and no `setNavigator`. `maxApi` is a required parameter of `NavigationContainer` — every container, nested or root, is handed its host bridge explicitly.
49
56
 
50
- ## Screen layout / iPhone Duo (port of RN `useScreenLayout`)
51
-
52
- - `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, side safe-area insets, `contentWidth`, header geometry and the iPhone Duo `sideBar`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
53
- - Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight`. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered.
54
- - iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. The column starts below the camera / status-bar cluster: 170pt portrait, 120pt in any landscape (folded too — RN still uses 170 there), below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN it is clamped 24pt above footer / tab bar, so a long toolkit (shortcut + group) never covers the footer.
57
+ ## SwiftUI kit (`ios/`)
58
+
59
+ - **Layout mirrors Compose:** `ios/Navigation/` follows `navigation/` file for file (`NavigationContainer`, `Navigator`, `Navigation` (options + per-screen `Navigation`), `StackScreen` (incl. `NavigationFooter`), `StackTransition`, `SwipeBackHaptic`, `BottomSheet`, `ModalScreen`, `HostBackGestureBridge`, `BottomTab/`, `Component/`, `Tracking/`); `ios/Layout/ScreenLayout.swift` mirrors `layout/`. `ios/Application/` keeps only context (`ApplicationEnvironment`), `Localize*` and `MaxApi` (Compose `application/`). Change a navigation piece on both sides together.
60
+ - **File basenames must be unique across the pod** — Swift fails with "filename used twice" even in different folders. New folders under `ios/` must be added to `source_files` in `ios/native-kits.podspec`.
61
+ - Screens update their own chrome at runtime through `@EnvironmentObject var navigation: Navigation` → `setOptions { $0.title = … }` (Compose `LocalNavigation`); bottom tabs keep one `Navigation` per tab. The host back bridge is `HostBackGestureBridge.shared` (`NavigatorManager` is a deprecated alias); a bottom tab off its first tab routes back to the first tab (`Navigator.tabBackHandler`, keyed by the hosting screen).
62
+ - Everything, navigation shell included, supports the pod's minimum **iOS 15** — no `@available` gates on public types. iOS 16+ APIs only behind `#available` with an iOS 15 path (`Layout`: `BottomSheet` `CapHeight`, `GridFlow` → `_VariadicView` flow row).
63
+ - **Usage is `NavigationContainer` + `Navigator` + `Navigation` only**; the kit owns the stack, transitions, overplays and back gesture. There is no `NavigationStack`: `StackTransition.swift` holds `StackHost` (representable) + `StackController`, the NavHost counterpart. Each entry gets its own `UIHostingController`, kept while on the stack and removed from the window while covered (state kept, onDisappear / onAppear like UINavigationController). Motion is a SwiftUI `offset` per card driven by one `coverage` value on a display link (a UIKit transform would move the safe area); it is Compose `StackTransition` 1:1 (RN `forStackTransitionWithOverlay`: linear 300ms slide, 0.3 parallax, `pink09` scrim to 0.6, 8pt edge shadow to 0.3). Card roots get the container environment via `.environment(\.self, …)` with the kit environment re-applied on top, so host environment objects reach screens.
64
+ - One back stack, root first (`Navigator.entries`, Compose's back stack): `present` appends a dialog entry that rises (FastOutSlowIn 300ms) and later pushes stack on top of it; `dismiss` drops the topmost dialog and everything above it (slides down whole); `popToRoot` returns to the active flow's root. Overplays (modal / bottom sheet / snackbar) render inside the screen they were shown over (`OverplayItem.screenId`, Compose `currentRootId`), so a later push slides over them.
65
+ - Host back gesture: `HostBackGestureBridge.started` / `progressed` / `completed` / `cancelled` scrub the pop (`Navigator.backStarted` …) only for a plain pop; tab back, `onBackHandler`, `preventBack`, an open overplay or a presented top wait for the release and go through `onBackSafe`. `SwipeBackHaptic` ticks at 50pt (`MaxApi.triggerEventVibration`, default = system light impact).
66
+ - Icons are compiled in: `ios/Icon/Icons.swift` is generated from Compose `utils/Icons.kt` by `node scripts/gen-ios-icons.mjs` — don't hand-edit it. A host `icon.json` in the main bundle only adds keys the kit lacks.
67
+ - Bundled assets (colors, Lottie JSON) live in `ios/Resources` and ship in the `MoMoUIKitsResources` bundle; load them through `MoMoUIKitsResources.bundle`, never `Bundle.main`.
68
+
69
+ ## Screen layout / iPhone Duo (port of RN `useScreenLayout`, momo-kits !664)
70
+
71
+ - `layout/ScreenLayout.kt`: `getScreenLayout()` returns window size, raw top/bottom + side insets, `contentWidth`, header geometry, the iPhone Duo `sideBar` and `duoTopInset`. `NavigationContainer` computes it once and provides `LocalScreenLayout`; reactive to fold/unfold and rotation (window size + `WindowInsets`). Metrics (`DuoMetrics`, 72pt iPhone header floor, 20pt status-bar threshold) mirror RN `Layout/utils.ts` — change both together.
72
+ - iPhone-only: everything gates on `isPhoneIdiom()` (`platform/`), never on "iOS", so iPad windows keep the regular layout. Two detectors, as in RN: `isIPhoneDuo` (window aspect < 1.6 or asymmetric side insets) drives layout; `isIPhoneDuoDevice` (short side of screen or window > 440, RN `device.ts`) drives popups, modal and bottom sheet.
73
+ - Header height is `headerHeight` (not `AppStatusBar + HEADER_HEIGHT`); the bar row is `headerBarHeight` and starts `headerStatusBarHeight` below the window top. When the status bar is off the top edge (iPhone landscape, iPhone Duo) `headerStatusBarHeight` is 0 so the title stays centered — except on the Duo outer display: in portrait the header is 76pt and the row starts at 14pt so the title centers on the camera punchout; in landscape the row centers on the camera when it sits at the top end of the strip (else on the back button) and the header reaches 8pt below the back button.
74
+ - iPhone Duo inner display in portrait (status bar on top): the header row shares the status bar band — `headerHeight` 94pt (2 × the clock digits' y 47), `headerStatusBarHeight` 0 — and `ScreenLayout.statusBarClearance` keeps it between the clock / battery clusters (from reserved regions, else 128pt on the right). Back and toolkit use the 48pt side bar controls laid out horizontally (`ToolkitLayout.SideBarRow`); the collapsed search and a centered title stay clear of the clusters. Port of momo-kits `90af33ba2`.
75
+ - iPhone Duo with a side strip ≥ 60pt: back + toolkit leave the bar for `HeaderSideBar` (`navigation/component/SideBar.kt`, 48pt glass controls); custom header-right stays in the bar. `sideBar.top` is 170pt in portrait, 120pt on the inner display in landscape (height ≥ 600), 24pt on the outer display in landscape, or below the header when the status bar is on top; UIKit's `statusBarFrame` is useless on Duo. Unlike RN the column is clamped above footer (24pt gap) / rail (12pt gap).
76
+ - Reserved regions (iOS 27.1 `reservedRegionsOfKind:`, reached through the ObjC runtime so older SDKs still build): a probe `UIView` (`platform/ReservedRegions.ios.kt` on the Compose root view; `ReservedRegionsProbe` inside SwiftUI `momoScreenLayout()`) re-reads them in `layoutSubviews`, which UIKit re-runs when they change. They push `sideBar.top` below the status cluster as it grows for calls / Live Activities, and `sideBar.bottom` (≥ 24pt) above a camera at the bottom end of the strip.
77
+ - Tab bar on Duo is `bottomtab/DuoTabBar.kt`: a rail in the side strip (`sideBar.bottom` above the window bottom, QR below it) or, without a side bar, a floating bar spanning 6/8 grid columns. Both overlay the content; `duoFloatingTabBarInset` pads tab content, footer and FAB.
78
+ - Bar compression (`bottomtab/DuoBarCompression.kt`, HIG "Vertical controls"): when toolbar + rail don't fit in the strip, one is compressed — the toolkit into a `navigation_menu` button, the rail into one button showing the current tab — and tapping it swaps them; `BottomTab(compressionBehavior)` picks which starts compressed. If neither fits beside the other compressed, both rest compressed and a tap opens one for a moment. Side bar columns report their full height (0 when the screen puts nothing there) so the decision follows each screen's toolkit.
79
+ - Glass header: on Duo with a side bar, Default/Extended headers without search and with a `ScrollState` let content scroll under the bar (`usesDuoGlassHeader`); the bar fades in a translucent surface + hairline over 16pt (Compose has no backdrop blur — RN's fallback look), the gradient leaves with the content.
80
+ - Quick return header: glass headers and Animated headers (with a `ScrollState`) on the Duo side bar layout slide out with the content and back in on a reverse scroll; a half-shown header settles in or out once the scroll stops. The side bar is drawn apart from the header, so it stays put.
81
+ - Overlays on Duo: popups cap at 440pt wide and the room between `duoTopInset` and the bottom inset (body scrolls, actions stay); modal centers inside that room; bottom sheet uses the window height, centers at width ≥ 600 and pads by side insets in landscape.
82
+ - Font scale deliberately uses the short side (`min(w, h)`), unlike RN which reads the window width — landscape must not inflate every font.
55
83
  - `StackScreen` pads content/footer/search/FAB by the side insets and provides an inset-consumed layout to its content; a bottom-tab root stays full width (tab bar + each tab's StackScreen handle insets). Grid columns use `contentWidth`.
56
84
 
57
85
  ## i18n / Localize (port of RN `Localize`)
@@ -102,7 +130,7 @@ Prefer one tight line at the surprising spot over a paragraph. Match the (low) c
102
130
  ## Gotchas
103
131
 
104
132
  - **Resource qualifier drift:** the generated Compose `Res` namespace is `vn.momo.uikits.resources`, but `utils/Resources.kt` and `platform/Platform.ios.kt` reference `vn.momo.compose.resources`. Keep bundled strings/dictionaries as plain Kotlin (the `defaultLanguage` map in `Localize.kt`), not `composeResources/values`, to avoid iOS framework resource-loading issues.
105
- - Prefer `NavigationContainer` over the deprecated `ApplicationContainer`; prefer `IMaxApi`/`LocalMaxApi` over the deprecated `PlatformApi`.
133
+ - `LiteScreen` keeps its own `application.HeaderType` enum (`DEFAULT`/`EXTENDED`/`NONE`), unrelated to the sealed `navigation.component.HeaderType`; its toolkit is the navigation `Toolkit` (through `LocalMaxApi`).
106
134
  - iOS strings/maps must survive Kotlin/Native framework packaging — keep them compiled-in, not loaded from resource bundles.
107
135
 
108
136
  ## Code comments
package/README.md CHANGED
@@ -150,8 +150,8 @@ themeController(defaultTheme.copy(colors = defaultTheme.colors.copy(primary = Co
150
150
  ---
151
151
 
152
152
  ### Lưu ý khác
153
- - `Screen` composable cũ được đánh dấu `@Deprecated`; sử dụng `NavigationContainer + StackScreen` để hưởng lợi dynamic route và animation.
154
- - `DesignSystemWhiteList` trong `application/ApplicationContainer.kt` hỗ trợ fallback cho host cũ; đừng kích hoạt nếu không cần.
153
+ - `Screen` / `ApplicationContainer` cũ (Compose và SwiftUI) đã bị gỡ; dùng `NavigationContainer + StackScreen`.
154
+ - `KitDesignSystem.update(isWhiteList = …)` (`const/KitDesignSystem.kt`) bật font design system mới cho host trong whitelist; đừng kích hoạt nếu không cần.
155
155
  - Khi thêm tài nguyên mới, đặt trong `shared/src/commonMain/composeResources` để Compose Multiplatform plugin sinh accessor tự động.
156
156
  - Repo không bundle SDK bên thứ ba ngoại trừ `vn.momo.corex.maxapi`, Compose/Coil/Ktor **và Compottie** cho animation.
157
157
 
@@ -41,6 +41,12 @@ public class KitConfig {
41
41
  public var trustBanner: TrustBannerData? = nil
42
42
  public var headerBar: String? = nil
43
43
  public var headerGradient: String? = nil
44
+
45
+ public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
46
+ self.trustBanner = trustBanner
47
+ self.headerBar = headerBar
48
+ self.headerGradient = headerGradient
49
+ }
44
50
  }
45
51
 
46
52
  public class ApplicationEnvironment: ObservableObject {
@@ -59,6 +65,17 @@ public class ApplicationEnvironment: ObservableObject {
59
65
  }
60
66
  }
61
67
 
68
+ extension ApplicationEnvironment {
69
+ /// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
70
+ func requestDismiss() {
71
+ if let composeApi {
72
+ composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
73
+ } else {
74
+ maxApi?.dismiss(completion: nil)
75
+ }
76
+ }
77
+ }
78
+
62
79
  private struct ApplicationEnvironmentKey: EnvironmentKey {
63
80
  static let defaultValue = ApplicationEnvironment()
64
81
  }
@@ -5,14 +5,23 @@
5
5
  // Swift mirror of the host-app bridge exposed on Kotlin as `vn.momo.maxapi.IMaxApi`
6
6
  // (`LocalMaxApi`). `IMaxApi` itself is an external compiled dependency with no
7
7
  // source in this repo, so this protocol intentionally exposes only the methods
8
- // this kit's Kotlin source actually calls (`track`, `dismiss`, `getLanguage`) —
9
- // not a full guess at IMaxApi's real surface.
8
+ // this kit's Kotlin source actually calls (`track`, `dismiss`, `getLanguage`,
9
+ // `triggerEventVibration`) — not a full guess at IMaxApi's real surface.
10
10
  //
11
11
 
12
- import Foundation
12
+ import UIKit
13
13
 
14
14
  public protocol MaxApi {
15
15
  func track(eventName: String, params: [String: Any?])
16
16
  func dismiss(completion: (() -> Void)?)
17
17
  func getLanguage(callback: @escaping ([String: Any?]?) -> Void)
18
+ /// `type` is the IMaxApi vibration name (`"light"`, …).
19
+ func triggerEventVibration(_ type: String)
20
+ }
21
+
22
+ public extension MaxApi {
23
+ /// Hosts that don't bridge haptics still get the system light tick.
24
+ func triggerEventVibration(_ type: String) {
25
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
26
+ }
18
27
  }
@@ -74,12 +74,12 @@ public struct Avatar: View {
74
74
  .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
75
75
  .overlay(
76
76
  RoundedRectangle(cornerRadius: cornerRadius)
77
- .stroke(theme.colors.border.default, lineWidth: 1)
77
+ .strokeBorder(theme.colors.border.default, lineWidth: 1)
78
78
  )
79
79
 
80
80
  // Badge icons — outside clip so they are not cut by border
81
81
  if shouldShowIconTop {
82
- ImageView(LOGO_MOMO_URL)
82
+ ImageView(LOGO_MOMO_URL, contentMode: .fill)
83
83
  .frame(width: badgeSizeDp, height: badgeSizeDp)
84
84
  .clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
85
85
  .offset(x: 1, y: -1)
@@ -92,7 +92,7 @@ public struct Avatar: View {
92
92
  }
93
93
 
94
94
  if shouldShowIconBottom {
95
- ImageView(iconSupport)
95
+ ImageView(iconSupport, contentMode: .fill)
96
96
  .frame(width: badgeSizeDp, height: badgeSizeDp)
97
97
  .clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
98
98
  .offset(x: 1, y: 1)
@@ -107,6 +107,7 @@ public struct Avatar: View {
107
107
  .frame(width: size.sizeDp, height: size.sizeDp)
108
108
  .accessibilityElement(children: .ignore)
109
109
  .accessibilityLabel(automationId)
110
+ .accessibilityIdentifier(automationId)
110
111
  }
111
112
 
112
113
  // MARK: Internal
@@ -33,7 +33,8 @@ public struct Badge: View {
33
33
  }
34
34
 
35
35
  private func formatTitle(_ text: String) -> String {
36
- if isNumber(text), let number = Int(text), number > 99 {
36
+ // Digits too long for Int are well over 99.
37
+ if isNumber(text), (Int(text) ?? .max) > 99 {
37
38
  return "99+"
38
39
  }
39
40
  return text
@@ -54,14 +55,15 @@ public struct Badge: View {
54
55
  }
55
56
 
56
57
  public var body: some View {
57
- MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01)
58
+ MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01, maxLines: 1)
58
59
  .padding(.horizontal, Spacing.XS)
59
- .frame(minWidth: fontScale(16), minHeight: fontScale(16))
60
+ .frame(minWidth: fontScale(16))
61
+ .frame(height: fontScale(16))
60
62
  .background(badgeColor)
61
63
  .cornerRadius(Radius.M)
62
64
  .overlay(
63
65
  RoundedRectangle(cornerRadius: Radius.M)
64
- .stroke(Colors.black01, lineWidth: 1)
66
+ .strokeBorder(Colors.black01, lineWidth: 1)
65
67
  )
66
68
  }
67
69
  }
@@ -25,7 +25,7 @@ public struct BadgeDot: View {
25
25
  .frame(width: size.size, height: size.size)
26
26
  .overlay(
27
27
  Circle()
28
- .stroke(Colors.black01, lineWidth: 1)
28
+ .strokeBorder(Colors.black01, lineWidth: 1)
29
29
  )
30
30
  }
31
31
  }
@@ -160,7 +160,6 @@ public struct BadgeRibbon: View {
160
160
  MomoText(label, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
161
161
  .rotationEffect(rotation)
162
162
  .frame(height: RibbonConstants.skewBodyHeight)
163
- .padding(.horizontal, 8)
164
163
  .background(backgroundColor)
165
164
 
166
165
  rightTail
@@ -51,15 +51,17 @@ public extension ButtonType {
51
51
  }
52
52
 
53
53
  func contentColor(loading: Bool, theme: Theme) -> Color {
54
+ let color: Color
54
55
  switch self {
55
- case .disabled: return theme.colors.text.disable.opacity(loading ? 0.75 : 1)
56
- case .primary: return Colors.black01
57
- case .secondary: return theme.colors.text.default
58
- case .outline: return theme.colors.primary
59
- case .tonal: return theme.colors.primary
60
- case .danger: return Colors.black01
61
- case .text: return theme.colors.primary
56
+ case .disabled: color = theme.colors.text.disable
57
+ case .primary: color = Colors.black01
58
+ case .secondary: color = theme.colors.text.default
59
+ case .outline: color = theme.colors.primary
60
+ case .tonal: color = theme.colors.primary
61
+ case .danger: color = Colors.black01
62
+ case .text: color = theme.colors.primary
62
63
  }
64
+ return color.opacity(loading ? 0.75 : 1)
63
65
  }
64
66
 
65
67
  var borderColor: Color? {
@@ -219,9 +221,8 @@ public struct Button: View {
219
221
  let color: Color? = useTintColor ? textColor : nil
220
222
 
221
223
  if forceLoading {
222
- LottieView(name: "lottie_circle_loader", loopMode: .loop)
224
+ LottieView(name: "lottie_circle_loader", loopMode: .loop, tintColor: color.map { UIColor($0) })
223
225
  .frame(width: size.iconSize, height: size.iconSize)
224
- .colorMultiply(color ?? Colors.black01)
225
226
  } else if !icon.isEmpty {
226
227
  Icon(source: icon, size: size.iconSize, color: color)
227
228
  }
@@ -229,8 +230,8 @@ public struct Button: View {
229
230
  }
230
231
 
231
232
  /// Mirrors Compose's press feedback: the background shrinks via an animated
232
- /// horizontal inset while the content fades, rather than a plain opacity dim on
233
- /// the whole button (matching `animatedPadding`/`alpha` in Button.kt).
233
+ /// horizontal inset and the whole button (background included) fades to 0.5,
234
+ /// matching `animatedPadding`/`alpha` in Button.kt.
234
235
  private struct ButtonPressFeedbackStyle: ButtonStyle {
235
236
  let isEnabled: Bool
236
237
  let background: Color
@@ -245,7 +246,6 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
245
246
 
246
247
  configuration.label
247
248
  .modifier(HugContentOnLegacyOS(isFull: isFull))
248
- .opacity(pressed ? 0.5 : 1)
249
249
  .background(
250
250
  RoundedRectangle(cornerRadius: radius)
251
251
  .fill(background)
@@ -257,6 +257,9 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
257
257
  )
258
258
  .frame(height: height)
259
259
  .clipShape(RoundedRectangle(cornerRadius: radius))
260
+ // `.text` has a clear background, which SwiftUI does not hit-test.
261
+ .contentShape(RoundedRectangle(cornerRadius: radius))
262
+ .opacity(pressed ? 0.5 : 1)
260
263
  .animation(.easeInOut(duration: 0.1), value: pressed)
261
264
  }
262
265
  }
@@ -16,55 +16,101 @@ extension String {
16
16
  public struct CalculatorKeyboard: View {
17
17
  // MARK: Lifecycle
18
18
  @Binding var value: String
19
- @State var tempValue: String = ""
20
19
 
21
20
  public init(_ value: Binding<String>) {
22
21
  self._value = value
23
22
  }
23
+
24
+ // Derived from `value` so a host write to the binding (prefill, reset after submit) is honoured.
25
+ private var expression: String {
26
+ value.replacingOccurrences(of: "x", with: "*").replacingOccurrences(of: "÷", with: "/")
27
+ }
24
28
 
25
29
  func onPressBack(){
26
30
  if (value.count > 0 ){
27
31
  value.removeLast()
28
32
  }
29
-
30
33
  }
31
34
 
32
35
  func onPressClear(key: String){
33
36
  value = ""
34
- tempValue = ""
35
37
  }
36
38
 
37
39
  func onPressEqual(key: String){
38
- if (tempValue == "") {
40
+ var expression = self.expression
41
+ if (expression == "") {
39
42
  return
40
43
  }
41
- if (tempValue.last?.isNumber == false){
42
- tempValue.removeLast()
44
+ if (expression.last?.isNumber == false){
45
+ expression.removeLast()
43
46
  }
44
- let expression = NSExpression(format: tempValue)
45
-
46
- value = String(expression.expressionValue(with: nil, context: nil) as! Int)
47
- if (Int(value) ?? 0 < 0){
47
+ // Overflow and division by zero are treated like a negative result: cleared.
48
+ if let result = Self.evaluate(expression), result >= 0 {
49
+ value = String(result)
50
+ } else {
48
51
  value = ""
49
52
  }
50
53
  }
54
+
55
+ /// Integer `+ - * /` with the usual precedence, as NSExpression did, but nil instead of a
56
+ /// crash when a number or result leaves `Int` range.
57
+ static func evaluate(_ expression: String) -> Int? {
58
+ var numbers: [Int] = []
59
+ var operators: [Character] = []
60
+ var digits = ""
61
+ for char in expression {
62
+ if ("0"..."9").contains(char) {
63
+ digits.append(char)
64
+ continue
65
+ }
66
+ guard let number = Int(digits) else { return nil }
67
+ numbers.append(number)
68
+ operators.append(char)
69
+ digits = ""
70
+ }
71
+ guard let last = Int(digits) else { return nil }
72
+ numbers.append(last)
73
+
74
+ var terms = [numbers[0]]
75
+ var signs: [Character] = []
76
+ for (op, number) in zip(operators, numbers.dropFirst()) {
77
+ switch op {
78
+ case "*":
79
+ let (product, overflow) = terms[terms.count - 1].multipliedReportingOverflow(by: number)
80
+ if overflow { return nil }
81
+ terms[terms.count - 1] = product
82
+ case "/":
83
+ guard number != 0 else { return nil }
84
+ terms[terms.count - 1] /= number
85
+ case "+", "-":
86
+ terms.append(number)
87
+ signs.append(op)
88
+ default:
89
+ return nil
90
+ }
91
+ }
92
+
93
+ var result = terms[0]
94
+ for (sign, term) in zip(signs, terms.dropFirst()) {
95
+ let (next, overflow) = sign == "+"
96
+ ? result.addingReportingOverflow(term)
97
+ : result.subtractingReportingOverflow(term)
98
+ if overflow { return nil }
99
+ result = next
100
+ }
101
+ return result
102
+ }
51
103
 
52
104
  func onPressKey(key:String){
53
- if (tempValue.last?.isNumber == false){
105
+ if (value.last?.isNumber == false){
54
106
  if (key.isNumber == false){
55
107
  return
56
108
  }
57
109
  }
58
- var convertedKey = key
59
- if (key == "x") {convertedKey = "*"}
60
- if (key == "÷") {convertedKey = "/"}
61
110
  if (value == "" && (key == "0" || key == "000" || key.isNumber == false)) {
62
111
  return
63
112
  }
64
- else {
65
- value.append(key)
66
- tempValue.append(convertedKey)
67
- }
113
+ value.append(key)
68
114
  }
69
115
 
70
116
  // MARK: Public
@@ -1,4 +1,3 @@
1
- import Combine
2
1
  import SwiftUI
3
2
 
4
3
  /// Native SwiftUI port of the Compose `Carousel` component.
@@ -10,9 +9,9 @@ import SwiftUI
10
9
  /// snap behavior, and scroll enable/disable. Snap / scroll-index callbacks
11
10
  /// report the real item index.
12
11
  ///
13
- /// Available iOS 14+ (uses a custom paging ScrollView so behavior is identical
12
+ /// Uses a custom drag-driven pager (not a ScrollView) so behavior is identical
14
13
  /// across versions and supports the loop virtualization + transform effects
15
- /// that `TabView(.page)` cannot express).
14
+ /// that `TabView(.page)` cannot express.
16
15
  public struct Carousel<Content: View>: View {
17
16
  // MARK: Lifecycle
18
17
 
@@ -82,7 +81,7 @@ public struct Carousel<Content: View>: View {
82
81
  @State private var currentPage: Int
83
82
  @State private var dragOffset: CGFloat = 0
84
83
  @State private var isDragging: Bool = false
85
- @State private var autoplayTick: Int = 0
84
+ @GestureState private var dragActive = false
86
85
 
87
86
  private func realIndex(_ page: Int) -> Int {
88
87
  ((page % itemCount) + itemCount) % itemCount
@@ -124,9 +123,20 @@ public struct Carousel<Content: View>: View {
124
123
  }
125
124
  .frame(width: viewportWidth)
126
125
  .clipped()
127
- .onReceive(autoplayTimer) { _ in
128
- guard autoplay, itemCount > 1, !isDragging else { return }
129
- advance()
126
+ // Restarts on drag like Compose's LaunchedEffect(autoplayInterval, isUserScrolling).
127
+ .task(id: AutoplayKey(enabled: autoplay && itemCount > 1 && !isDragging, interval: autoplayInterval)) { @MainActor in
128
+ guard autoplay, itemCount > 1, !isDragging, autoplayInterval > 0 else { return }
129
+ while !Task.isCancelled {
130
+ try? await Task.sleep(nanoseconds: UInt64(autoplayInterval * 1_000_000_000))
131
+ guard !Task.isCancelled else { return }
132
+ advance()
133
+ }
134
+ }
135
+ // A drag stolen by a parent scroll view never reaches `onEnded`; settle on the current page.
136
+ .onChange(of: dragActive) { active in
137
+ guard !active, isDragging else { return }
138
+ isDragging = false
139
+ withAnimation(.easeInOut(duration: 0.25)) { dragOffset = 0 }
130
140
  }
131
141
  }
132
142
 
@@ -147,9 +157,14 @@ public struct Carousel<Content: View>: View {
147
157
 
148
158
  private func dragGesture(step: CGFloat) -> some Gesture {
149
159
  DragGesture(minimumDistance: 5)
160
+ .updating($dragActive) { _, active, _ in active = true }
150
161
  .onChanged { value in
151
162
  isDragging = true
152
- dragOffset = value.translation.width
163
+ var offset = value.translation.width
164
+ // No slide exists past either end, as with HorizontalPager bounds.
165
+ if currentPage == 0 { offset = min(offset, 0) }
166
+ if currentPage == virtualPageCount - 1 { offset = max(offset, 0) }
167
+ dragOffset = offset
153
168
  }
154
169
  .onEnded { value in
155
170
  isDragging = false
@@ -167,7 +182,8 @@ public struct Carousel<Content: View>: View {
167
182
  let moved = step > 0 ? Int((-dragOffset / step).rounded()) : 0
168
183
  target = min(max(currentPage + moved, 0), virtualPageCount - 1)
169
184
  }
170
- dragOffset = 0
185
+ // Animated so a drag that stays on the same page eases back too.
186
+ withAnimation(.easeInOut(duration: 0.25)) { dragOffset = 0 }
171
187
  setPage(target)
172
188
  }
173
189
  }
@@ -189,8 +205,9 @@ public struct Carousel<Content: View>: View {
189
205
  onScrollIndexChanged(real)
190
206
  }
191
207
  }
208
+ }
192
209
 
193
- private var autoplayTimer: Publishers.Autoconnect<Timer.TimerPublisher> {
194
- Timer.publish(every: autoplayInterval, on: .main, in: .common).autoconnect()
195
- }
210
+ private struct AutoplayKey: Equatable {
211
+ let enabled: Bool
212
+ let interval: TimeInterval
196
213
  }
@@ -21,8 +21,10 @@ public struct Checkbox: View{
21
21
  }
22
22
 
23
23
  func onCheck(){
24
- checked.toggle()
25
- onChange?(checked)
24
+ // Not re-read from the binding: a `.constant` / one-way binding would report the stale value.
25
+ let newValue = !checked
26
+ checked = newValue
27
+ onChange?(newValue)
26
28
  }
27
29
 
28
30
  public var body: some View {
@@ -35,13 +37,15 @@ public struct Checkbox: View{
35
37
  .frame(width: 20, height: 20)
36
38
  .overlay(
37
39
  RoundedRectangle(cornerRadius: Radius.XS)
38
- .stroke(borderColor, lineWidth: Spacing.XXS)
40
+ .strokeBorder(borderColor, lineWidth: Spacing.XXS)
39
41
  )
40
42
 
41
43
  if checked {
42
44
  Icon(source: indeterminate ? "navigation_minus" : "ic_checked", size: 20, color: Colors.black01)
43
45
  }
44
46
  }
47
+ // The unchecked fill is clear, which SwiftUI does not hit-test.
48
+ .contentShape(RoundedRectangle(cornerRadius: Radius.XS))
45
49
  }
46
50
  .buttonStyle(ActiveOpacityButtonStyle(activeOpacity: 0.8))
47
51
  .disabled(disabled)
@@ -89,18 +89,19 @@ public struct Chip: View {
89
89
  .padding(.horizontal, horizontal)
90
90
  .frame(height: height)
91
91
  .background(
92
- RoundedRectangle(cornerRadius: radius, style: .continuous)
92
+ RoundedRectangle(cornerRadius: radius)
93
93
  .fill(bg)
94
94
  )
95
95
  .overlay(
96
96
  Group {
97
97
  if selected {
98
- RoundedRectangle(cornerRadius: radius, style: .continuous)
99
- .stroke(theme.colors.secondary, lineWidth: 2)
98
+ // Inside the bounds, like Compose `border`.
99
+ RoundedRectangle(cornerRadius: radius)
100
+ .strokeBorder(theme.colors.secondary, lineWidth: 2)
100
101
  }
101
102
  }
102
103
  )
103
- .contentShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
104
+ .contentShape(RoundedRectangle(cornerRadius: radius))
104
105
  }
105
106
  .buttonStyle(ActiveOpacityButtonStyle())
106
107
  .modifier(ChipAccessibilityModifier(accessibilityLabel: accessibilityLabel))