@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
@@ -2,13 +2,13 @@ import SwiftUI
2
2
  import SDWebImageSwiftUI
3
3
 
4
4
  public struct PopupPromotion: View {
5
- @Environment(\.appTheme) private var theme
6
5
  @Environment(\.screenLayout) private var layout
7
6
 
8
7
  var imageUrl: String
9
8
  var onIconClose: (() -> Void)?
10
9
  var onAction: (() -> Void)?
11
10
 
11
+ @State private var imageFailed = false
12
12
 
13
13
  public init(
14
14
  imageUrl: String,
@@ -21,66 +21,38 @@ public struct PopupPromotion: View {
21
21
  }
22
22
 
23
23
  private static let aspectRatio: CGFloat = 0.72
24
- // close button row under the image: stack spacing + padded 40pt button
25
- private static let closeRowHeight: CGFloat = 8 + 40 + Spacing.S * 2
24
+ private static let closeTapSize: CGFloat = 40
25
+ // close button row under the image: top padding + 40pt tap target
26
+ private static let closeRowHeight: CGFloat = Spacing.S + closeTapSize
26
27
 
27
28
  // fit the portrait image by height when iPhone Duo leaves too little room
28
29
  private var imageWidth: CGFloat {
29
- let width = min(layout.width - Spacing.XL * 2, layout.popupMaxWidth)
30
+ let width = min(layout.width - Spacing.M * 2, layout.popupMaxWidth)
30
31
  guard let maxHeight = layout.popupMaxHeight else { return width }
31
32
  return min(width, (maxHeight - Self.closeRowHeight) * Self.aspectRatio)
32
33
  }
33
34
 
34
35
  public var body: some View {
35
- VStack(spacing: 8) {
36
- if(imageUrl.isEmpty) {
37
- Icon(source: "media_fail", size: imageWidth)
38
- }
39
- else {
40
- WebImage(url: URL(string: imageUrl))
41
- .resizable()
42
- .placeholder {
43
- Color.gray
44
- .aspectRatio(Self.aspectRatio, contentMode: .fit)
45
- .frame(maxWidth: imageWidth)
46
- .clipped()
47
- }
48
- .aspectRatio(Self.aspectRatio, contentMode: .fit)
49
- .frame(maxWidth: imageWidth, alignment: .center)
50
- .clipped()
51
- .onTapGesture {
52
- onAction?()
53
- }
54
- }
55
- buildCloseIcon()
56
- .accessibility(identifier: "ic_popup_close")
57
- }
58
- .accessibilityElement(children: .ignore)
59
- .accessibility(identifier: "popup_promotion")
60
- }
36
+ VStack(spacing: 0) {
37
+ // stretched like Compose's ContentScale.FillBounds; an empty url shows the error state, as in Compose
38
+ WebImage(url: URL(string: imageUrl))
39
+ .onSuccess { _, _, _ in imageFailed = false }
40
+ .onFailure { _ in imageFailed = true }
41
+ .resizable()
42
+ .placeholder { PopupImagePlaceholder(failed: imageFailed || URL(string: imageUrl) == nil) }
43
+ .aspectRatio(Self.aspectRatio, contentMode: .fit)
44
+ .frame(maxWidth: imageWidth)
45
+ .contentShape(Rectangle())
46
+ .onTapGesture {
47
+ onAction?()
48
+ }
61
49
 
62
- @ViewBuilder
63
- private func buildCloseIcon() -> some View {
64
- HStack(alignment: .center, spacing: 0) {
65
- SwiftUI.Button(action: {
50
+ PopupCloseButton(tapSize: Self.closeTapSize) {
66
51
  onIconClose?()
67
- }) {
68
- ZStack {
69
- Circle()
70
- .strokeBorder(theme.colors.background.surface, lineWidth: 2)
71
- .background(Circle().fill(theme.colors.text.default))
72
- .frame(width: 22, height: 22)
73
-
74
- Icon(source: "navigation_close", size: 16, color: theme.colors.background.surface)
75
- }
76
- .frame(width: 40, height: 40)
77
- .padding(8)
78
- .zIndex(1)
79
52
  }
53
+ .padding(.top, Spacing.S)
80
54
  }
55
+ .accessibilityElement(children: .contain)
56
+ .accessibilityIdentifier("popup_promotion")
81
57
  }
82
-
83
58
  }
84
-
85
-
86
-
@@ -51,12 +51,9 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
51
51
  @State private var opacity = 0.0
52
52
 
53
53
  // MARK: - Autohide
54
- /// Class reference for capturing a weak reference later in dispatch work holder.
55
- private var isPresentedRef: ClassReference<Binding<Bool>>?
56
- private var itemRef: ClassReference<Binding<Item?>>?
57
-
58
- /// holder for autohiding dispatch work (to be able to cancel it when needed)
59
- private var dispatchWorkHolder = DispatchWorkHolder()
54
+ /// holder for autohiding dispatch work (to be able to cancel it when needed);
55
+ /// state so it outlives re-renders, which would otherwise orphan the pending work
56
+ @State private var dispatchWorkHolder = DispatchWorkHolder()
60
57
 
61
58
  // MARK: - Internal
62
59
  /// Set dismiss souce to pass to dismiss callback
@@ -85,9 +82,6 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
85
82
  self.backgroundColor = backgroundColor
86
83
  self.dismissCallback = dismissCallback
87
84
  self.view = view
88
-
89
- self.isPresentedRef = ClassReference(self.$isPresented)
90
- self.itemRef = ClassReference(self.$item)
91
85
  }
92
86
 
93
87
  init(item: Binding<Item?>,
@@ -113,9 +107,6 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
113
107
  self.backgroundColor = backgroundColor
114
108
  self.dismissCallback = dismissCallback
115
109
  self.view = view
116
-
117
- self.isPresentedRef = ClassReference(self.$isPresented)
118
- self.itemRef = ClassReference(self.$item)
119
110
  }
120
111
 
121
112
  public func body(content: Content) -> some View {
@@ -214,7 +205,8 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
214
205
  }
215
206
 
216
207
  func onAnimationCompleted() -> () {
217
- if shouldShowContent { // return if this was called on showing animation, only proceed if called on hiding
208
+ // only proceed on hiding, once: both the offset animation and the fallback timer land here
209
+ if shouldShowContent || !showContent {
218
210
  return
219
211
  }
220
212
  showContent = false // unload popup body after hiding animation is done
@@ -229,12 +221,14 @@ public struct FullscreenPopup<Item: Equatable, PopupContent: View>: ViewModifier
229
221
  if let autohideIn = autohideIn {
230
222
  dispatchWorkHolder.work?.cancel()
231
223
 
232
- // Weak reference to avoid the work item capturing the struct,
233
- // which would create a retain cycle with the work holder itself.
234
- dispatchWorkHolder.work = DispatchWorkItem(block: { [weak isPresentedRef, weak itemRef] in
235
- isPresentedRef?.value.wrappedValue = false
236
- itemRef?.value.wrappedValue = nil
237
- dismissSource = .autohide
224
+ // bindings only: capturing the struct would retain the work holder in a cycle
225
+ let isPresented = $isPresented
226
+ let item = $item
227
+ let dismissSource = $dismissSource
228
+ dispatchWorkHolder.work = DispatchWorkItem(block: {
229
+ dismissSource.wrappedValue = .autohide
230
+ isPresented.wrappedValue = false
231
+ item.wrappedValue = nil
238
232
  })
239
233
  if sheetPresented, let work = dispatchWorkHolder.work {
240
234
  DispatchQueue.main.asyncAfter(deadline: .now() + autohideIn, execute: work)
@@ -239,7 +239,7 @@ public struct Popup<Item: Equatable, PopupContent: View>: ViewModifier {
239
239
  .onAnimationCompleted(for: currentOffset) {
240
240
  animationCompletedCallback()
241
241
  }
242
- .animation(animation)
242
+ .animation(animation, value: currentOffset)
243
243
  }
244
244
 
245
245
  #if !os(tvOS)
@@ -5,14 +5,6 @@ final class DispatchWorkHolder {
5
5
  var work: DispatchWorkItem?
6
6
  }
7
7
 
8
- final class ClassReference<T> {
9
- var value: T
10
-
11
- init(_ value: T) {
12
- self.value = value
13
- }
14
- }
15
-
16
8
  public extension View {
17
9
 
18
10
  @ViewBuilder
@@ -112,7 +112,9 @@ private struct ProgressInfoHorizontal: View {
112
112
 
113
113
  var body: some View {
114
114
  HStack(alignment: .top, spacing: 0) {
115
- ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
115
+ // Keyed by position: `ProgressInfoItem.id` is a fresh UUID per init, so it churns
116
+ // identity whenever the caller rebuilds `steps`.
117
+ ForEach(Array(steps.enumerated()), id: \.offset) { index, item in
116
118
  let isFirst = index == 0
117
119
  let isLast = index == steps.count - 1
118
120
 
@@ -207,7 +209,7 @@ private struct ProgressInfoVertical: View {
207
209
 
208
210
  var body: some View {
209
211
  VStack(alignment: .leading, spacing: 0) {
210
- ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
212
+ ForEach(Array(steps.enumerated()), id: \.offset) { index, item in
211
213
  let isLast = index == steps.count - 1
212
214
 
213
215
  HStack(alignment: .top, spacing: 0) {
@@ -36,8 +36,9 @@ public struct Radio: View {
36
36
  public var body: some View {
37
37
  SwiftUI.Button(action: onTap) {
38
38
  HStack(spacing: 0) {
39
+ // strokeBorder keeps the ring inside 20pt, like Compose `border`.
39
40
  RoundedRectangle(cornerRadius: Radius.M)
40
- .stroke(borderColor, lineWidth: borderWidth)
41
+ .strokeBorder(borderColor, lineWidth: borderWidth)
41
42
  .frame(width: 20, height: 20)
42
43
  .padding(.trailing, label.isEmpty ? 0 : Spacing.XS)
43
44
 
@@ -35,7 +35,7 @@ public struct Rating: View {
35
35
 
36
36
  public var body: some View {
37
37
  HStack(spacing: 2) {
38
- ForEach(0..<numOfStars, id: \.self) { index in
38
+ ForEach(0..<max(numOfStars, 0), id: \.self) { index in
39
39
  star(at: index)
40
40
  }
41
41
  }
@@ -59,7 +59,14 @@ public struct Rating: View {
59
59
  }
60
60
  }
61
61
 
62
+ // Kotlin's toInt() maps NaN to 0 and saturates; Swift's Int(_:) traps on both.
63
+ private var safeRating: Float {
64
+ guard rating.isFinite else { return 0 }
65
+ return min(max(rating, -Float(Int32.max)), Float(Int32.max))
66
+ }
67
+
62
68
  private func starIcon(at index: Int) -> String {
69
+ let rating = safeRating
63
70
  let intRating = Int(rating)
64
71
  if index < intRating {
65
72
  return "reaction_star_full"
@@ -71,6 +78,7 @@ public struct Rating: View {
71
78
  }
72
79
 
73
80
  private func starColor(at index: Int) -> Color {
81
+ let rating = safeRating
74
82
  let intRating = Int(rating)
75
83
  if index < intRating {
76
84
  return Colors.yellow03
@@ -58,19 +58,20 @@ public struct ScrollViewWithScrollBar<Content: View>: View {
58
58
  }
59
59
  }
60
60
  .onPreferenceChange(ViewOffsetKey.self) { offset in
61
- let newPosition =
62
- (offset * ((indicatorContainerWidth - indicatorWidth) / (contentSize - scrollViewWidth)))
63
- + indicatorWidth / 2
64
-
65
- if !newPosition.isNaN {
66
- self.position = CGPoint(x: newPosition, y: INDICATOR_HEIGHT / 2)
67
- }
61
+ // Clamped: content narrower than the viewport, or bounce past either end,
62
+ // would otherwise push the indicator out of its track (or to inf/NaN).
63
+ let maxOffset = contentSize - scrollViewWidth
64
+ let progress = maxOffset > 0 ? min(max(offset / maxOffset, 0), 1) : 0
65
+ self.position = CGPoint(
66
+ x: progress * (indicatorContainerWidth - indicatorWidth) + indicatorWidth / 2,
67
+ y: INDICATOR_HEIGHT / 2
68
+ )
68
69
  }
69
70
  }.background(
70
71
  GeometryReader { proxy in
71
- Color.clear.onAppear {
72
- self.scrollViewWidth = proxy.size.width
73
- }
72
+ Color.clear
73
+ .onAppear { self.scrollViewWidth = proxy.size.width }
74
+ .onChange(of: proxy.size.width) { self.scrollViewWidth = $0 }
74
75
  }
75
76
  ).coordinateSpace(name: "scroll")
76
77
 
@@ -44,8 +44,6 @@ private struct ShimmerLoading: View {
44
44
  let durationMillis: Int
45
45
  let angleOfAxisY: CGFloat
46
46
 
47
- @State private var phase: CGFloat = 0
48
-
49
47
  // Matches Compose `SHADOW_BRUSH_PERCENTAGE = 0.6f`.
50
48
  private static let shadowBrushPercentage: CGFloat = 0.6
51
49
 
@@ -67,33 +65,30 @@ private struct ShimmerLoading: View {
67
65
  size.width + widthOfShadowBrush
68
66
  }
69
67
 
68
+ // Clock-driven rather than an onAppear animation: gradient points aren't reliably animatable,
69
+ // and a view first laid out at zero width (or resized later) would never (re)start.
70
70
  var body: some View {
71
- let startX = phase - widthOfShadowBrush
72
- let endX = phase
71
+ TimelineView(.animation) { timeline in
72
+ Rectangle().fill(gradient(at: timeline.date))
73
+ }
74
+ }
75
+
76
+ private func gradient(at date: Date) -> LinearGradient {
77
+ let duration = Double(max(durationMillis, 1)) / 1000
78
+ let progress = date.timeIntervalSinceReferenceDate.truncatingRemainder(dividingBy: duration) / duration
79
+ let endX = CGFloat(progress) * animationTarget
80
+ let startX = endX - widthOfShadowBrush
73
81
 
74
- Rectangle()
75
- .fill(
76
- LinearGradient(
77
- gradient: Gradient(colors: Self.shimmerColors),
78
- startPoint: .init(
79
- x: size.width > 0 ? startX / size.width : 0,
80
- y: 0
81
- ),
82
- endPoint: .init(
83
- x: size.width > 0 ? endX / size.width : 0,
84
- y: size.height > 0 ? angleOfAxisY / size.height : 0
85
- )
86
- )
82
+ return LinearGradient(
83
+ gradient: Gradient(colors: Self.shimmerColors),
84
+ startPoint: .init(
85
+ x: size.width > 0 ? startX / size.width : 0,
86
+ y: 0
87
+ ),
88
+ endPoint: .init(
89
+ x: size.width > 0 ? endX / size.width : 0,
90
+ y: size.height > 0 ? angleOfAxisY / size.height : 0
87
91
  )
88
- .onAppear {
89
- guard size.width > 0 else { return }
90
- phase = 0
91
- withAnimation(
92
- .linear(duration: Double(durationMillis) / 1000.0)
93
- .repeatForever(autoreverses: false)
94
- ) {
95
- phase = animationTarget
96
- }
97
- }
92
+ )
98
93
  }
99
94
  }
@@ -24,12 +24,14 @@ private func getValueForPosition(
24
24
  step: CGFloat
25
25
  ) -> CGFloat {
26
26
  let availableSpace = containerWidth - thumbWidth
27
- if availableSpace <= 0 { return minV }
28
- let relStepUnit = step / (maxV - minV)
27
+ if availableSpace <= 0 || maxV <= minV { return minV }
29
28
  var relPosition = (positionInView - thumbWidth / 2) / availableSpace
29
+ // A zero step would make the snap math NaN, which later traps in `Int(_:)`.
30
+ if step <= 0 { return clampF(minV + relPosition * (maxV - minV), minV, maxV) }
31
+ let relStepUnit = step / (maxV - minV)
30
32
  let relOffset = relPosition.truncatingRemainder(dividingBy: relStepUnit)
31
33
  relPosition -= relOffset
32
- if relStepUnit != 0, relOffset / relStepUnit >= 0.5 { relPosition += relStepUnit }
34
+ if relOffset / relStepUnit >= 0.5 { relPosition += relStepUnit }
33
35
  let steps = (relPosition / relStepUnit).rounded()
34
36
  return clampF(minV + steps * step, minV, maxV)
35
37
  }
@@ -49,7 +51,8 @@ private func formatLabel(_ value: CGFloat) -> String {
49
51
  if value == value.rounded() {
50
52
  return String(Int(value))
51
53
  }
52
- return String(format: "%.1f", value)
54
+ // Truncates to one decimal like Compose's `(value * 10).toLong() / 10.0`.
55
+ return String(Double((value * 10).rounded(.towardZero)) / 10)
53
56
  }
54
57
 
55
58
  // ── Component ─────────────────────────────────────────────────────────────────
@@ -64,8 +67,6 @@ public struct MomoSlider: View {
64
67
  private let minV: CGFloat
65
68
  private let maxV: CGFloat
66
69
  private let step: CGFloat
67
- private let initLow: CGFloat
68
- private let initHigh: CGFloat
69
70
  private let minRange: CGFloat
70
71
  private let disableRange: Bool
71
72
  private let floatingLabel: Bool
@@ -74,11 +75,10 @@ public struct MomoSlider: View {
74
75
  private let onSliderTouchStart: (CGFloat, CGFloat) -> Void
75
76
  private let onSliderTouchEnd: (CGFloat, CGFloat) -> Void
76
77
 
77
- @State private var lowValue: CGFloat = 0
78
- @State private var highValue: CGFloat = 0
78
+ @State private var lowValue: CGFloat
79
+ @State private var highValue: CGFloat
79
80
  @State private var draggingLow: Bool = true
80
81
  @State private var isDragging: Bool = false
81
- @State private var didInit: Bool = false
82
82
 
83
83
  public init(
84
84
  min: CGFloat,
@@ -105,8 +105,10 @@ public struct MomoSlider: View {
105
105
  self.onSliderTouchStart = onSliderTouchStart
106
106
  self.onSliderTouchEnd = onSliderTouchEnd
107
107
 
108
- self.initLow = clampF(low ?? min, min, max)
109
- self.initHigh = disableRange ? max : clampF(high ?? max, min, max)
108
+ let startLow = clampF(low ?? min, min, max)
109
+ let startHigh = disableRange ? max : clampF(high ?? max, min, max)
110
+ _lowValue = State(initialValue: startLow)
111
+ _highValue = State(initialValue: startHigh)
110
112
  }
111
113
 
112
114
  private var thumbColor: Color { disabled ? theme.colors.text.disable : theme.colors.primary }
@@ -165,17 +167,13 @@ public struct MomoSlider: View {
165
167
  }
166
168
  }
167
169
  .frame(width: trackWidth, height: topPad + THUMB_SIZE, alignment: .topLeading)
170
+ // Compose's spring(stiffness = 1200) is critically damped.
171
+ .animation(.interpolatingSpring(stiffness: 1200, damping: 2 * sqrt(1200)), value: lowValue)
172
+ .animation(.interpolatingSpring(stiffness: 1200, damping: 2 * sqrt(1200)), value: highValue)
168
173
  .contentShape(Rectangle())
169
174
  .gesture(dragGesture(trackWidth: trackWidth))
170
175
  }
171
176
  .frame(height: (floatingLabel ? (THUMB_SIZE + LABEL_GAP) : 0) + THUMB_SIZE)
172
- .onAppear {
173
- if !didInit {
174
- lowValue = initLow
175
- highValue = initHigh
176
- didInit = true
177
- }
178
- }
179
177
  }
180
178
 
181
179
  private var thumb: some View {
@@ -109,9 +109,10 @@ private struct StepperNumberView: View {
109
109
  .font(.system(size: fontScale(12)))
110
110
  .ignoreBoldTextSetting()
111
111
  .foregroundColor(textColor)
112
+ // Compose's min width includes the horizontal padding.
113
+ .padding(.horizontal, Spacing.XS)
112
114
  .frame(minWidth: specs.numberMinWidth)
113
115
  .frame(height: specs.numberHeight)
114
- .padding(.horizontal, Spacing.XS)
115
116
  .overlay(
116
117
  RoundedRectangle(cornerRadius: Radius.S)
117
118
  .stroke(borderColor, lineWidth: 1)
@@ -217,5 +218,9 @@ public struct MomoStepper: View {
217
218
  if clamped != value { value = clamped }
218
219
  editingText = String(clamped)
219
220
  }
221
+ // Keeps the editable field in step with values set by the owner of the binding.
222
+ .onChange(of: value) { newValue in
223
+ editingText = String(newValue)
224
+ }
220
225
  }
221
226
  }
@@ -162,8 +162,9 @@ private struct StepsHorizontal: View {
162
162
 
163
163
  var body: some View {
164
164
  HStack(alignment: .top, spacing: 0) {
165
- ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
166
- column(index: index, item: item)
165
+ // Keyed by position like Compose: `StepItem.id` is a fresh UUID per instance.
166
+ ForEach(steps.indices, id: \.self) { index in
167
+ column(index: index, item: steps[index])
167
168
  .frame(maxWidth: .infinity)
168
169
  }
169
170
  }
@@ -234,7 +235,7 @@ private struct StepsHorizontal: View {
234
235
  .clipShape(RoundedCorner(radius: Radius.XS, corners: [.topLeft, .bottomLeft]))
235
236
  }
236
237
  }
237
- .frame(maxWidth: .infinity)
238
+ .frame(maxWidth: .infinity, alignment: .leading)
238
239
 
239
240
  if !item.title.isEmpty {
240
241
  MomoText(item.title, typography: titleStyle, color: titleColor)
@@ -316,8 +317,8 @@ private struct StepsVertical: View {
316
317
 
317
318
  var body: some View {
318
319
  VStack(spacing: 0) {
319
- ForEach(Array(steps.enumerated()), id: \.element.id) { index, item in
320
- row(index: index, item: item)
320
+ ForEach(steps.indices, id: \.self) { index in
321
+ row(index: index, item: steps[index])
321
322
  }
322
323
  }
323
324
  .frame(maxWidth: .infinity)
@@ -358,8 +359,9 @@ private struct StepsVertical: View {
358
359
  customIcon: customIcon
359
360
  )
360
361
  if !isLast {
362
+ // Compose pads inside the fixed height, so the drawn line is 8pt shorter.
361
363
  lineColor
362
- .frame(width: 2, height: connectorHeight)
364
+ .frame(width: 2, height: connectorHeight - Spacing.XS * 2)
363
365
  .padding(.vertical, Spacing.XS)
364
366
  }
365
367
  }
@@ -65,6 +65,11 @@ public struct SuggestAction: View {
65
65
 
66
66
  Text(annotatedMessage)
67
67
  .foregroundColor(theme.colors.text.default)
68
+ .lineSpacing(
69
+ fontScale(TypographyStyle.bodyDefaultRegular.lineHeight)
70
+ - fontScale(TypographyStyle.bodyDefaultRegular.fontSize)
71
+ )
72
+ .ignoreBoldTextSetting()
68
73
  .frame(maxWidth: .infinity, alignment: .leading)
69
74
  .padding(.trailing, Spacing.M)
70
75
 
@@ -20,6 +20,7 @@ public struct Swipe<Content: View>: View {
20
20
 
21
21
  @State private var offsetX: CGFloat = 0
22
22
  @State private var dragStartOffsetX: CGFloat?
23
+ @GestureState private var isDragging = false
23
24
 
24
25
  public init(
25
26
  leftActions: [SwipeAction] = [],
@@ -87,14 +88,17 @@ public struct Swipe<Content: View>: View {
87
88
  .offset(x: offsetX)
88
89
  .gesture(
89
90
  DragGesture(minimumDistance: 8, coordinateSpace: .local)
91
+ .updating($isDragging) { _, dragging, _ in dragging = true }
90
92
  .onChanged { value in
91
93
  dragOnChanged(value: value, leftWidth: leftWidth, rightWidth: rightWidth)
92
94
  }
93
- .onEnded { _ in
94
- dragStartOffsetX = nil
95
- snapToClosest(leftWidth: leftWidth, rightWidth: rightWidth)
96
- }
97
95
  )
96
+ // Gesture state also resets on cancel, so a stolen drag still snaps (Compose `onDragCancel`).
97
+ .onChange(of: isDragging) { dragging in
98
+ guard !dragging else { return }
99
+ dragStartOffsetX = nil
100
+ snapToClosest(leftWidth: leftWidth, rightWidth: rightWidth)
101
+ }
98
102
  }
99
103
  .frame(maxWidth: .infinity)
100
104
  .frame(height: height)
@@ -166,7 +170,7 @@ private struct SwipeActionButton: View {
166
170
 
167
171
  var body: some View {
168
172
  SwiftUI.Button(action: action.onPress) {
169
- VStack(spacing: Spacing.XXS) {
173
+ VStack(spacing: 0) {
170
174
  if !action.icon.isEmpty {
171
175
  Icon(source: action.icon, size: 20, color: Colors.black01)
172
176
  }
@@ -184,7 +188,7 @@ private struct SwipeActionButton: View {
184
188
  .background(action.backgroundColor ?? defaultBackgroundColor)
185
189
  .clipShape(RoundedRectangle(cornerRadius: itemRadius))
186
190
  }
187
- .buttonStyle(.plain)
191
+ .buttonStyle(ActiveOpacityButtonStyle())
188
192
  }
189
193
  }
190
194
 
@@ -41,29 +41,31 @@ public struct Switch: View {
41
41
  accessibilityId?.isEmpty == false ? accessibilityId! : "toggle_\(title?.isEmpty == false ? title! : "unknown")_\(value)"
42
42
  }
43
43
 
44
+ private func toggle() {
45
+ // Not re-read from the binding: a `.constant` / one-way binding would report the stale value.
46
+ let newValue = !value
47
+ value = newValue
48
+ onChange?(newValue)
49
+ }
50
+
44
51
  public var body: some View {
45
- SwiftUI.Button(action: {
46
- withAnimation {
47
- value.toggle()
48
- onChange?(value)
49
- }
50
- }) {
51
- HStack {
52
- HStack {}
53
- .frame(width: 6, height: 6)
54
- .background(backgroundColor)
55
- .clipShape(Circle())
56
- }
57
- .frame(width: 14, height: 14)
58
- .background(circleColor)
59
- .clipShape(Circle())
60
- .padding(.leading, Spacing.XS)
61
- .padding(.trailing, Spacing.XS)
52
+ // The whole track is the label so all of it is tappable, as in Compose.
53
+ SwiftUI.Button(action: toggle) {
54
+ Circle()
55
+ .fill(backgroundColor)
56
+ .frame(width: 6, height: 6)
57
+ .frame(width: 14, height: 14)
58
+ .background(Circle().fill(circleColor))
59
+ .padding(.horizontal, Spacing.XS)
60
+ .frame(width: 38, height: 20, alignment: value ? .trailing : .leading)
61
+ .background(backgroundColor)
62
+ .clipShape(Capsule())
63
+ .contentShape(Capsule())
64
+ .animation(.default, value: value)
62
65
  }
66
+ // Custom style so `.disabled` doesn't add a system dim on top of the disabled colors.
67
+ .buttonStyle(ActiveOpacityButtonStyle())
63
68
  .disabled(disabled)
64
- .frame(width: 38, height: 20, alignment: value ? .trailing : .leading)
65
- .background(backgroundColor)
66
- .clipShape(Capsule())
67
69
  .accessibilityIdentifier(automationId)
68
70
  }
69
71
  }