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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (254) hide show
  1. package/CLAUDE.md +4 -1
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +204 -0
  4. package/compose/build.gradle.kts.backup +204 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +81 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +357 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +115 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +478 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +265 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +235 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +265 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +308 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +330 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +207 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +92 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +560 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +86 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +501 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +236 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +180 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +708 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +209 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +231 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +408 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +92 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +106 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +366 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +133 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  136. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  137. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  138. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  139. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
  140. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  141. package/gradle/libs.versions.toml +69 -0
  142. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  143. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  144. package/gradle.properties +26 -0
  145. package/gradlew +252 -0
  146. package/gradlew.bat +94 -0
  147. package/ios/Application/ApplicationEnvironment.swift +11 -0
  148. package/ios/Application/ComposeApi.swift +2 -1
  149. package/ios/Application/{Components.swift → LegacyHeader.swift} +6 -62
  150. package/ios/Application/Screen.swift +3 -2
  151. package/ios/Avatar/Avatar.swift +4 -3
  152. package/ios/Badge/Badge.swift +6 -4
  153. package/ios/Badge/BadgeDot.swift +1 -1
  154. package/ios/Badge/BadgeRibbon.swift +0 -1
  155. package/ios/Button/Button.swift +15 -12
  156. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +12 -20
  157. package/ios/Carousel/Carousel.swift +29 -12
  158. package/ios/Checkbox/Checkbox.swift +7 -3
  159. package/ios/Chip/Chip.swift +5 -4
  160. package/ios/Collapse/Collapse.swift +70 -19
  161. package/ios/Colors+Radius+Spacing/Colors.swift +9 -0
  162. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  163. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  164. package/ios/Divider/Divider.swift +37 -4
  165. package/ios/Extensions/Color++.swift +18 -0
  166. package/ios/Icon/Icon.swift +7 -1
  167. package/ios/IconButton/IconButton.swift +15 -7
  168. package/ios/Image/Image.swift +7 -34
  169. package/ios/Information/Information.swift +27 -9
  170. package/ios/Input/ErrorView.swift +2 -2
  171. package/ios/Input/Input.swift +160 -147
  172. package/ios/Input/InputPhoneNumber.swift +12 -9
  173. package/ios/Input/InputSearch.swift +102 -75
  174. package/ios/Input/InputTextArea.swift +128 -145
  175. package/ios/InputDropDown/InputDropDown.swift +10 -7
  176. package/ios/InputMoney/InputMoney.swift +33 -11
  177. package/ios/InputOTP/InputOTP.swift +22 -12
  178. package/ios/Layout/Card.swift +50 -0
  179. package/ios/Layout/GridContext.swift +123 -3
  180. package/ios/Layout/Item.swift +5 -4
  181. package/ios/Layout/Section.swift +4 -100
  182. package/ios/Loader/Loader.swift +5 -9
  183. package/ios/Lottie/LottieView.swift +10 -0
  184. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +61 -9
  185. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTab.swift +60 -34
  186. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +11 -6
  187. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  188. package/ios/Navigation/Component/Header.swift +493 -0
  189. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  190. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  191. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  192. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  193. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  194. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +20 -23
  195. package/ios/{Application/Navigation/component → Navigation/Component}/SideBar.swift +49 -28
  196. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  197. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  198. package/ios/Navigation/Navigation.swift +190 -0
  199. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +46 -41
  200. package/ios/{Application/Navigation → Navigation}/Navigator.swift +65 -9
  201. package/ios/{Application → Navigation}/StackScreen.swift +236 -103
  202. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  203. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  204. package/ios/Pagination/PaginationScroll.swift +2 -2
  205. package/ios/Popup/PopupDisplay.swift +79 -159
  206. package/ios/Popup/PopupInput.swift +10 -13
  207. package/ios/Popup/PopupLayout.swift +97 -2
  208. package/ios/Popup/PopupNotify.swift +27 -55
  209. package/ios/Popup/PopupPromotion.swift +22 -50
  210. package/ios/PopupView/FullscreenPopup.swift +13 -19
  211. package/ios/PopupView/PopupView.swift +1 -1
  212. package/ios/PopupView/Utils++.swift +0 -8
  213. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  214. package/ios/Radio/Radio.swift +2 -1
  215. package/ios/Rating/Rating.swift +9 -1
  216. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  217. package/ios/Skeleton/Skeleton.swift +22 -27
  218. package/ios/Slider/Slider.swift +16 -18
  219. package/ios/Stepper/Stepper.swift +6 -1
  220. package/ios/Steps/Steps.swift +8 -6
  221. package/ios/SuggestAction/SuggestAction.swift +5 -0
  222. package/ios/Swipeable/SwipeCell.swift +10 -6
  223. package/ios/Switch/Switch.swift +22 -20
  224. package/ios/TabView/TabView.swift +102 -21
  225. package/ios/Tag/Tag.swift +15 -4
  226. package/ios/Template/Logo/Logo.swift +2 -1
  227. package/ios/Template/TrustBanner/TrustBanner.swift +41 -27
  228. package/ios/Title/Title.swift +6 -17
  229. package/ios/Tooltip/Tooltip.swift +29 -14
  230. package/ios/Typography/Text.swift +61 -19
  231. package/ios/Uploader/Uploader.swift +3 -1
  232. package/ios/native-kits.podspec +1 -0
  233. package/ios-demo/README.md +120 -0
  234. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  235. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  236. package/package.json +1 -1
  237. package/settings.gradle.kts +64 -0
  238. package/ios/Application/HeaderRight.swift +0 -324
  239. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  240. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  241. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  242. package/ios/Application/Navigation/NavigatorManager.swift +0 -29
  243. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -54
  244. package/ios/Application/Navigation/component/Header.swift +0 -183
  245. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -84
  246. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  247. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  248. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  249. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -280
  250. /package/ios/{Application/Layout → Layout}/ScreenLayout.swift +0 -0
  251. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
  252. /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoBarCompression.swift +0 -0
  253. /package/ios/{Application/Navigation → Navigation}/BottomTab/DuoTabBar.swift +0 -0
  254. /package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +0 -0
@@ -10,9 +10,8 @@ private let wheelPickerBorderWidth: CGFloat = 1
10
10
 
11
11
  /// A reusable single-column wheel picker, mirroring Compose `WheelPicker.kt`.
12
12
  ///
13
- /// The data is padded with two empty rows at the top and bottom so the first/last
14
- /// real value can be centered. The currently centered row is reported back via
15
- /// `onChange(name, value)`.
13
+ /// Five rows tall with the selection in the middle row. A drag always settles on a row
14
+ /// (Compose snaps once the scroll stops) and then reports it via `onChange(name, value)`.
16
15
  public struct WheelPicker: View {
17
16
  @Environment(\.appTheme) private var theme
18
17
 
@@ -28,68 +27,50 @@ public struct WheelPicker: View {
28
27
  self.data = data
29
28
  self.selectedData = selectedData
30
29
  self.onChange = onChange
30
+ _restIndex = State(initialValue: Self.index(of: selectedData, in: data))
31
31
  }
32
32
 
33
33
  // MARK: Public
34
34
 
35
35
  public var body: some View {
36
- let paddedData = ["", ""] + data + ["", ""]
37
-
38
- return GeometryReader { geo in
39
- let height = geo.size.height
40
- ScrollViewReader { proxy in
41
- ScrollView(.vertical, showsIndicators: false) {
42
- LazyVStack(spacing: 0) {
43
- ForEach(Array(paddedData.enumerated()), id: \.offset) { index, text in
44
- WheelPickerItem(
45
- text: text,
46
- isSelected: index == selectedIndex,
47
- index: index,
48
- scrollOffset: scrollOffset,
49
- viewportHeight: height
50
- )
51
- .frame(height: wheelItemHeight)
52
- .id(index)
53
- }
54
- }
55
- .background(
56
- GeometryReader { inner in
57
- Color.clear.preference(
58
- key: WheelOffsetKey.self,
59
- value: inner.frame(in: .named(scrollSpace)).minY
60
- )
61
- }
62
- )
63
- }
64
- .coordinateSpace(name: scrollSpace)
65
- .onPreferenceChange(WheelOffsetKey.self) { value in
66
- scrollOffset = value
67
- updateSelection(paddedData: paddedData, viewportHeight: height)
68
- }
69
- .onAppear {
70
- let initial = max(paddedData.firstIndex(of: selectedData) ?? 2, 2)
71
- selectedIndex = initial
72
- DispatchQueue.main.async {
73
- proxy.scrollTo(initial, anchor: .center)
36
+ let position = self.position
37
+ let centerSlot = CGFloat(wheelVisibleCount / 2)
38
+
39
+ return Rectangle()
40
+ .fill(theme.colors.background.surface)
41
+ .overlay(
42
+ // Compose draws the selection band behind the rows.
43
+ Rectangle()
44
+ .fill(Colors.blue10)
45
+ .opacity(0.2)
46
+ .frame(height: wheelItemHeight)
47
+ )
48
+ .overlay(
49
+ VStack(spacing: 0) {
50
+ ForEach(Array(data.enumerated()), id: \.offset) { index, text in
51
+ WheelPickerItem(text: text, distance: abs(CGFloat(index) - position))
74
52
  }
75
53
  }
54
+ .offset(y: (centerSlot - position) * wheelItemHeight),
55
+ alignment: .top
56
+ )
57
+ .frame(maxWidth: .infinity)
58
+ .frame(height: wheelItemHeight * CGFloat(wheelVisibleCount))
59
+ .clipShape(RoundedRectangle(cornerRadius: Radius.S))
60
+ .overlay(
61
+ RoundedRectangle(cornerRadius: Radius.S)
62
+ .strokeBorder(theme.colors.border.default, lineWidth: wheelPickerBorderWidth)
63
+ )
64
+ .contentShape(Rectangle())
65
+ .gesture(dragGesture)
66
+ .onAppear { reportIfUnlisted() }
67
+ .onChange(of: selectedData) { _ in syncToSelection() }
68
+ .onChange(of: data) { _ in syncToSelection() }
69
+ .accessibilityElement()
70
+ .accessibilityValue(data.indices.contains(restIndex) ? data[restIndex] : "")
71
+ .accessibilityAdjustableAction { direction in
72
+ settle(at: restIndex + (direction == .increment ? 1 : -1))
76
73
  }
77
- }
78
- .frame(height: wheelItemHeight * CGFloat(wheelVisibleCount))
79
- .overlay(
80
- RoundedRectangle(cornerRadius: Radius.S)
81
- .stroke(theme.colors.border.default, lineWidth: wheelPickerBorderWidth)
82
- )
83
- .background(theme.colors.background.surface.clipShape(RoundedRectangle(cornerRadius: Radius.S)))
84
- .overlay(
85
- // Centered translucent selection band.
86
- Rectangle()
87
- .fill(Colors.blue10)
88
- .opacity(0.2)
89
- .frame(height: wheelItemHeight)
90
- .allowsHitTesting(false)
91
- )
92
- .clipShape(RoundedRectangle(cornerRadius: Radius.S))
93
74
  }
94
75
 
95
76
  // MARK: Internal
@@ -99,24 +80,60 @@ public struct WheelPicker: View {
99
80
  let selectedData: String
100
81
  let onChange: (String, String) -> Void
101
82
 
102
- @State private var selectedIndex: Int = 0
103
- @State private var scrollOffset: CGFloat = 0
104
-
105
- private let scrollSpace = "wheelScroll"
106
-
107
- /// Map the current scroll offset to the centered index and report changes.
108
- private func updateSelection(paddedData: [String], viewportHeight: CGFloat) {
109
- // `scrollOffset` is the top of the content relative to the viewport (<= 0 when scrolled).
110
- let centerY = viewportHeight / 2
111
- let contentCenter = -scrollOffset + centerY
112
- let rawIndex = Int((contentCenter / wheelItemHeight).rounded()) - 1
113
- let clamped = max(2, min(rawIndex, paddedData.count - 3))
114
- guard clamped != selectedIndex else { return }
115
- selectedIndex = clamped
116
- let value = paddedData[clamped]
117
- if !value.isEmpty {
118
- onChange(name, value)
83
+ // MARK: Private
84
+
85
+ @State private var restIndex: Int
86
+ @State private var dragTranslation: CGFloat = 0
87
+
88
+ /// Fractional row index under the band; past either end the drag meets resistance.
89
+ private var position: CGFloat {
90
+ let raw = CGFloat(restIndex) - dragTranslation / wheelItemHeight
91
+ let last = CGFloat(max(0, data.count - 1))
92
+ if raw < 0 { return raw * 0.3 }
93
+ if raw > last { return last + (raw - last) * 0.3 }
94
+ return raw
95
+ }
96
+
97
+ private var dragGesture: some Gesture {
98
+ DragGesture(minimumDistance: 4)
99
+ .onChanged { value in
100
+ dragTranslation = value.translation.height
101
+ }
102
+ .onEnded { value in
103
+ let predicted = CGFloat(restIndex) - value.predictedEndTranslation.height / wheelItemHeight
104
+ settle(at: Int(predicted.rounded()))
105
+ }
106
+ }
107
+
108
+ /// Like Compose `paddedData.indexOf(selectedData).coerceAtLeast(2)`: an unknown value shows the first row.
109
+ private static func index(of value: String, in data: [String]) -> Int {
110
+ data.firstIndex(of: value) ?? 0
111
+ }
112
+
113
+ private func settle(at index: Int) {
114
+ let target = min(max(index, 0), max(0, data.count - 1))
115
+ let rows = Double(abs(CGFloat(target) - position))
116
+ withAnimation(.easeOut(duration: min(0.5, 0.2 + 0.03 * rows))) {
117
+ restIndex = target
118
+ dragTranslation = 0
119
119
  }
120
+ guard data.indices.contains(target), data[target] != selectedData else { return }
121
+ onChange(name, data[target])
122
+ }
123
+
124
+ /// The parent may coerce the value (e.g. day 31 → 28) or swap the list; jump without animation
125
+ /// like Compose `scrollToItem`.
126
+ private func syncToSelection() {
127
+ restIndex = Self.index(of: selectedData, in: data)
128
+ dragTranslation = 0
129
+ reportIfUnlisted()
130
+ }
131
+
132
+ /// Compose reports the row the wheel lands on, so a value missing from `data` (minute 07 with a
133
+ /// 5-minute interval, an unset AM/PM) takes the shown row's value instead of drifting from it.
134
+ private func reportIfUnlisted() {
135
+ guard data.indices.contains(restIndex), !data.contains(selectedData) else { return }
136
+ onChange(name, data[restIndex])
120
137
  }
121
138
  }
122
139
 
@@ -126,40 +143,21 @@ private struct WheelPickerItem: View {
126
143
  @Environment(\.appTheme) private var theme
127
144
 
128
145
  let text: String
129
- let isSelected: Bool
130
- let index: Int
131
- let scrollOffset: CGFloat
132
- let viewportHeight: CGFloat
146
+ /// Rows between this one and the band.
147
+ let distance: CGFloat
133
148
 
134
149
  var body: some View {
135
- let itemCenterInContent = CGFloat(index) * wheelItemHeight + wheelItemHeight / 2
136
- let itemCenterInViewport = itemCenterInContent + scrollOffset
137
- let distanceFromCenter = abs(itemCenterInViewport - viewportHeight / 2) / wheelItemHeight
138
-
139
- let alpha: Double
140
- switch distanceFromCenter {
141
- case ..<1: alpha = 1.0
142
- case ..<2: alpha = 0.8
143
- default: alpha = 0.4
144
- }
145
-
146
- let scale: CGFloat = isSelected ? 1.0 : 0.87
147
-
148
- return ZStack {
149
- MomoText(text, typography: .actionSBold, color: theme.colors.text.default)
150
- .scaleEffect(scale)
151
- }
152
- .frame(maxWidth: .infinity)
153
- .frame(height: wheelItemHeight)
154
- .opacity(alpha)
150
+ MomoText(text, typography: .actionSBold, color: theme.colors.text.default)
151
+ .scaleEffect(1 - 0.13 * min(distance, 1))
152
+ .frame(maxWidth: .infinity)
153
+ .frame(height: wheelItemHeight)
154
+ .opacity(Self.opacity(distance))
155
155
  }
156
- }
157
-
158
- // MARK: - WheelOffsetKey
159
156
 
160
- private struct WheelOffsetKey: PreferenceKey {
161
- static var defaultValue: CGFloat = 0
162
- static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
163
- value = nextValue()
157
+ // RN parity: 1 on the band, 0.8 one row away, 0.4 from two rows on.
158
+ private static func opacity(_ distance: CGFloat) -> Double {
159
+ if distance <= 1 { return Double(1 - 0.2 * distance) }
160
+ if distance <= 2 { return Double(0.8 - 0.4 * (distance - 1)) }
161
+ return 0.4
164
162
  }
165
163
  }
@@ -1,16 +1,49 @@
1
1
  import Foundation
2
2
  import SwiftUI
3
3
 
4
+ public enum DividerType {
5
+ case `default`
6
+ case dash
7
+ }
8
+
4
9
  public struct MomoDivider: View {
5
10
  @Environment(\.appTheme) private var theme
6
11
 
7
- public init() {}
12
+ private let useMargin: Bool
13
+ private let type: DividerType
14
+
15
+ public init(useMargin: Bool = false, type: DividerType = .default) {
16
+ self.useMargin = useMargin
17
+ self.type = type
18
+ }
8
19
 
9
20
  public var body: some View {
10
- Rectangle()
11
- .fill(theme.colors.border.default)
21
+ line
12
22
  .frame(maxWidth: .infinity)
13
23
  .frame(height: 1)
14
- .padding(.vertical, Spacing.XS)
24
+ .padding(.vertical, useMargin ? Spacing.XS : 0)
25
+ }
26
+
27
+ @ViewBuilder
28
+ private var line: some View {
29
+ switch type {
30
+ case .default:
31
+ Rectangle().fill(theme.colors.border.default)
32
+ case .dash:
33
+ DividerDashLine()
34
+ .stroke(
35
+ theme.colors.border.default,
36
+ style: StrokeStyle(lineWidth: 1, lineCap: .round, dash: [4, 4])
37
+ )
38
+ }
39
+ }
40
+ }
41
+
42
+ private struct DividerDashLine: Shape {
43
+ func path(in rect: CGRect) -> Path {
44
+ var path = Path()
45
+ path.move(to: CGPoint(x: rect.minX, y: rect.midY))
46
+ path.addLine(to: CGPoint(x: rect.maxX, y: rect.midY))
47
+ return path
15
48
  }
16
49
  }
@@ -66,3 +66,21 @@ public extension Color {
66
66
  )
67
67
  }
68
68
  }
69
+
70
+ extension Color {
71
+ /// Compose's `lerp(start, stop, fraction)`, in sRGB; `Color.mix` needs iOS 18.
72
+ func lerp(to other: Color, _ fraction: CGFloat) -> Color {
73
+ let t = min(max(fraction, 0), 1)
74
+ var (r1, g1, b1, a1): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
75
+ var (r2, g2, b2, a2): (CGFloat, CGFloat, CGFloat, CGFloat) = (0, 0, 0, 0)
76
+ UIColor(self).getRed(&r1, green: &g1, blue: &b1, alpha: &a1)
77
+ UIColor(other).getRed(&r2, green: &g2, blue: &b2, alpha: &a2)
78
+ return Color(
79
+ .sRGB,
80
+ red: r1 + (r2 - r1) * t,
81
+ green: g1 + (g2 - g1) * t,
82
+ blue: b1 + (b2 - b1) * t,
83
+ opacity: a1 + (a2 - a1) * t
84
+ )
85
+ }
86
+ }
@@ -1,3 +1,4 @@
1
+ import SDWebImageSwiftUI
1
2
  import SwiftUI
2
3
 
3
4
  struct IconSource: Codable {
@@ -47,7 +48,12 @@ public struct Icon: View {
47
48
  let requestedColor = color == iconThemeColor ? theme.colors.text.default : color
48
49
  // Mirror Compose: certain icons (e.g. branded glyphs) are never tinted
49
50
  let resolvedColor = noThemeIcons.contains(source) ? nil : requestedColor
50
- ImageView(uri, color: resolvedColor)
51
+ // Bare WebImage like Compose's AsyncImage: no skeleton while loading, nothing on failure.
52
+ WebImage(url: URL(string: uri), isAnimating: .constant(true))
53
+ .resizable()
54
+ .renderingMode(resolvedColor != nil ? .template : .original)
55
+ .scaledToFit()
56
+ .foregroundColor(resolvedColor)
51
57
  .frame(width: size, height: size)
52
58
  // Mirrors Compose's auto-generated `contentDescription = "img|$iconUrl"`
53
59
  // when no explicit label is provided.
@@ -28,7 +28,7 @@ public enum IconSize {
28
28
 
29
29
  // MARK: - IconShape
30
30
 
31
- public enum IconShape {
31
+ public enum IconShape: Sendable {
32
32
  case circle
33
33
  case square
34
34
  }
@@ -68,8 +68,7 @@ public struct IconButton: View {
68
68
  public var body: some View {
69
69
  let bg = backgroundColor
70
70
  let fg = contentColor
71
- // OUTLINE allows an override color for its border, mirroring Compose `color ?? theme.colors.primary`.
72
- let border: Color? = type == .outline ? (color ?? theme.colors.primary) : borderColor
71
+ let border = borderColor
73
72
  let borderWidth = type.borderWidth
74
73
 
75
74
  SwiftUI.Button(action: {
@@ -82,9 +81,11 @@ public struct IconButton: View {
82
81
  .background(bg)
83
82
  .clipShape(IconButtonShape(shape))
84
83
  .overlay(
84
+ // Inside the bounds, like Compose `border`.
85
85
  IconButtonShape(shape)
86
- .stroke(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
86
+ .strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
87
87
  )
88
+ .contentShape(IconButtonShape(shape))
88
89
  }
89
90
  .buttonStyle(.plain)
90
91
  .disabled(!isEnabled)
@@ -122,8 +123,9 @@ public struct IconButton: View {
122
123
  /// Type-erased shape switch (`Circle` vs a fixed `Radius.S` rounded rect) — mirrors
123
124
  /// Compose's `IconShape.toShape()`. `AnyShape` needs iOS 17+, so this hand-rolls the same
124
125
  /// erasure at the deployment target's iOS 15 floor.
125
- private struct IconButtonShape: Shape {
126
+ private struct IconButtonShape: InsettableShape {
126
127
  let shape: IconShape
128
+ var insetAmount: CGFloat = 0
127
129
 
128
130
  init(_ shape: IconShape) {
129
131
  self.shape = shape
@@ -132,9 +134,15 @@ private struct IconButtonShape: Shape {
132
134
  func path(in rect: CGRect) -> Path {
133
135
  switch shape {
134
136
  case .circle:
135
- return Circle().path(in: rect)
137
+ return Circle().inset(by: insetAmount).path(in: rect)
136
138
  case .square:
137
- return RoundedRectangle(cornerRadius: Radius.S).path(in: rect)
139
+ return RoundedRectangle(cornerRadius: Radius.S).inset(by: insetAmount).path(in: rect)
138
140
  }
139
141
  }
142
+
143
+ func inset(by amount: CGFloat) -> IconButtonShape {
144
+ var shape = self
145
+ shape.insetAmount += amount
146
+ return shape
147
+ }
140
148
  }
@@ -2,7 +2,6 @@
2
2
 
3
3
  import Foundation
4
4
  import SDWebImageSwiftUI
5
- import SkeletonUI
6
5
  import SwiftUI
7
6
 
8
7
  public enum ImageState {
@@ -39,22 +38,23 @@ public struct ImageView: View {
39
38
  .resizable()
40
39
  .renderingMode(color != nil ? .template : .original)
41
40
  .placeholder {
42
- VStack(alignment: .center, content: {
41
+ VStack(alignment: .center) {
43
42
  if error {
44
- ImageErrorIcon(color: theme.colors.text.disable)
45
- } else if placeholder != nil {
43
+ Icon(source: "media_fail", color: theme.colors.text.disable)
44
+ } else if let placeholder {
46
45
  placeholder
46
+ } else if loading {
47
+ Skeleton()
47
48
  }
48
- })
49
+ }
49
50
  .frame(maxWidth: .infinity, maxHeight: .infinity)
50
- .skeleton(with: loading && !error && placeholder == nil)
51
- .shape(type: .rectangle)
52
51
  }
53
52
  .scaledToFit()
54
53
  .transition(.fade)
55
54
  .aspectRatio(aspectRatio, contentMode: contentMode)
56
55
  .foregroundColor(color)
57
56
  .onAppear { onStateChanged?(.loading) }
57
+ .onChange(of: url) { _ in error = false }
58
58
  }
59
59
 
60
60
  // MARK: Internal
@@ -69,30 +69,3 @@ public struct ImageView: View {
69
69
  @State var error: Bool = false
70
70
  var url: String
71
71
  }
72
-
73
- // A bare WebImage on purpose: going through `Icon` nests another `ImageView`, whose own
74
- // failure would render this placeholder again, without end while offline.
75
- private struct ImageErrorIcon: View {
76
- let color: Color
77
-
78
- var body: some View {
79
- WebImage(url: URL(string: iconSources["media_fail"] ?? ""))
80
- .resizable()
81
- .renderingMode(.template)
82
- .scaledToFit()
83
- .foregroundColor(color)
84
- .frame(width: 24, height: 24)
85
- }
86
- }
87
-
88
- private struct ColorMultiplyModifier: ViewModifier {
89
- let color: Color?
90
-
91
- func body(content: Content) -> some View {
92
- if let color {
93
- content.colorMultiply(color)
94
- } else {
95
- content
96
- }
97
- }
98
- }
@@ -19,6 +19,7 @@ public struct Information: View {
19
19
  private let showIconClose: Bool
20
20
  private let onPressClose: (() -> Void)?
21
21
  private let highlightText: [String]
22
+ private let accessibilityLabel: String?
22
23
 
23
24
  public init(
24
25
  title: String? = nil,
@@ -30,7 +31,8 @@ public struct Information: View {
30
31
  action: String? = nil,
31
32
  showIconClose: Bool = true,
32
33
  onPressClose: (() -> Void)? = nil,
33
- highlightText: [String] = []
34
+ highlightText: [String] = [],
35
+ accessibilityLabel: String? = nil
34
36
  ) {
35
37
  self.title = title
36
38
  self.description = description
@@ -42,6 +44,11 @@ public struct Information: View {
42
44
  self.showIconClose = showIconClose
43
45
  self.onPressClose = onPressClose
44
46
  self.highlightText = highlightText
47
+ self.accessibilityLabel = accessibilityLabel
48
+ }
49
+
50
+ private var automationId: String {
51
+ accessibilityLabel ?? "Information\(title.map { "/\($0)" } ?? "")"
45
52
  }
46
53
 
47
54
  private var borderColor: Color {
@@ -80,20 +87,23 @@ public struct Information: View {
80
87
  VStack(alignment: .leading, spacing: 0) {
81
88
  HStack(alignment: .top, spacing: 0) {
82
89
  if let image {
83
- ImageView(image)
90
+ ImageView(image, contentMode: .fill)
84
91
  .frame(width: 40, height: 40)
85
92
  .clipShape(RoundedRectangle(cornerRadius: Radius.S))
93
+ .accessibilityIdentifier("\(automationId)|img")
86
94
  .padding(.trailing, Spacing.S)
87
95
  } else if showIcon {
88
96
  Icon(source: iconSource, size: 16, color: accentColor)
97
+ .accessibilityIdentifier("\(automationId)|icon")
89
98
  .padding(.trailing, Spacing.S)
90
99
  }
91
100
 
92
101
  VStack(alignment: .leading, spacing: 0) {
93
102
  if let title {
94
- MomoText(title, typography: .labelDefaultMedium)
103
+ MomoText(title, typography: .labelDefaultMedium, accessibilityId: "\(automationId)|title-text")
95
104
  }
96
105
  MomoText(description, typography: .descriptionDefaultRegular, highlightText: highlightText)
106
+ .accessibilityIdentifier("\(automationId)|description-text")
97
107
  }
98
108
  .frame(maxWidth: .infinity, alignment: .leading)
99
109
 
@@ -101,15 +111,21 @@ public struct Information: View {
101
111
  Icon(source: "navigation_close", size: 16, color: theme.colors.text.default)
102
112
  .contentShape(Rectangle())
103
113
  .onTapGesture(perform: onPressClose)
114
+ .accessibilityAddTraits(.isButton)
115
+ .accessibilityIdentifier("\(automationId)|close-icon-touch")
104
116
  }
105
117
  }
106
118
 
107
119
  if let action, let onPressAction {
108
- MomoText(action, typography: .actionXsBold, color: accentColor)
109
- .frame(maxWidth: .infinity, alignment: .trailing)
110
- .contentShape(Rectangle())
111
- .onTapGesture(perform: onPressAction)
112
- .padding(.top, Spacing.S)
120
+ HStack(spacing: 0) {
121
+ MomoText(action, typography: .actionXsBold, color: accentColor, accessibilityId: "\(automationId)|cta-text")
122
+ }
123
+ .frame(maxWidth: .infinity, alignment: .trailing)
124
+ .contentShape(Rectangle())
125
+ .onTapGesture(perform: onPressAction)
126
+ .accessibilityElement(children: .contain)
127
+ .accessibilityIdentifier("\(automationId)|cta-touch")
128
+ .padding(.top, Spacing.S)
113
129
  }
114
130
  }
115
131
  .padding(Spacing.M)
@@ -119,8 +135,10 @@ public struct Information: View {
119
135
  )
120
136
  .overlay(
121
137
  RoundedRectangle(cornerRadius: Radius.S)
122
- .stroke(borderColor, lineWidth: 0.5)
138
+ .strokeBorder(borderColor, lineWidth: 0.5)
123
139
  )
140
+ .accessibilityElement(children: .contain)
141
+ .accessibilityIdentifier(automationId)
124
142
  }
125
143
  }
126
144
 
@@ -37,9 +37,9 @@ public struct ErrorView: View {
37
37
  }
38
38
  .padding(.top, Spacing.XS)
39
39
  } else if errorSpacing {
40
- Spacer()
41
- .padding(.top, Spacing.XS)
40
+ Color.clear
42
41
  .frame(height: 18)
42
+ .padding(.top, Spacing.XS)
43
43
  }
44
44
  }
45
45