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

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/ios/Application/ApplicationEnvironment.swift +11 -0
  3. package/ios/Application/ComposeApi.swift +2 -1
  4. package/ios/Application/{Components.swift → LegacyHeader.swift} +6 -62
  5. package/ios/Application/Screen.swift +3 -2
  6. package/ios/Avatar/Avatar.swift +4 -3
  7. package/ios/Badge/Badge.swift +6 -4
  8. package/ios/Badge/BadgeDot.swift +1 -1
  9. package/ios/Badge/BadgeRibbon.swift +0 -1
  10. package/ios/Button/Button.swift +15 -12
  11. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +12 -20
  12. package/ios/Carousel/Carousel.swift +29 -12
  13. package/ios/Checkbox/Checkbox.swift +7 -3
  14. package/ios/Chip/Chip.swift +5 -4
  15. package/ios/Collapse/Collapse.swift +70 -19
  16. package/ios/Colors+Radius+Spacing/Colors.swift +9 -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 +18 -0
  21. package/ios/Icon/Icon.swift +7 -1
  22. package/ios/IconButton/IconButton.swift +15 -7
  23. package/ios/Image/Image.swift +7 -34
  24. package/ios/Information/Information.swift +27 -9
  25. package/ios/Input/ErrorView.swift +2 -2
  26. package/ios/Input/Input.swift +160 -147
  27. package/ios/Input/InputPhoneNumber.swift +12 -9
  28. package/ios/Input/InputSearch.swift +102 -75
  29. package/ios/Input/InputTextArea.swift +128 -145
  30. package/ios/InputDropDown/InputDropDown.swift +10 -7
  31. package/ios/InputMoney/InputMoney.swift +33 -11
  32. package/ios/InputOTP/InputOTP.swift +22 -12
  33. package/ios/Layout/Card.swift +50 -0
  34. package/ios/Layout/GridContext.swift +123 -3
  35. package/ios/Layout/Item.swift +5 -4
  36. package/ios/Layout/Section.swift +4 -100
  37. package/ios/Loader/Loader.swift +5 -9
  38. package/ios/Lottie/LottieView.swift +10 -0
  39. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +61 -9
  40. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTab.swift +60 -34
  41. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +11 -6
  42. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  43. package/ios/Navigation/Component/Header.swift +493 -0
  44. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  45. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  46. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  47. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  48. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  49. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +20 -23
  50. package/ios/{Application/Navigation/component → Navigation/Component}/SideBar.swift +49 -28
  51. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  52. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  53. package/ios/Navigation/Navigation.swift +190 -0
  54. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +46 -41
  55. package/ios/{Application/Navigation → Navigation}/Navigator.swift +65 -9
  56. package/ios/{Application → Navigation}/StackScreen.swift +236 -103
  57. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  58. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  59. package/ios/Pagination/PaginationScroll.swift +2 -2
  60. package/ios/Popup/PopupDisplay.swift +79 -159
  61. package/ios/Popup/PopupInput.swift +10 -13
  62. package/ios/Popup/PopupLayout.swift +97 -2
  63. package/ios/Popup/PopupNotify.swift +27 -55
  64. package/ios/Popup/PopupPromotion.swift +22 -50
  65. package/ios/PopupView/FullscreenPopup.swift +13 -19
  66. package/ios/PopupView/PopupView.swift +1 -1
  67. package/ios/PopupView/Utils++.swift +0 -8
  68. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  69. package/ios/Radio/Radio.swift +2 -1
  70. package/ios/Rating/Rating.swift +9 -1
  71. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  72. package/ios/Skeleton/Skeleton.swift +22 -27
  73. package/ios/Slider/Slider.swift +16 -18
  74. package/ios/Stepper/Stepper.swift +6 -1
  75. package/ios/Steps/Steps.swift +8 -6
  76. package/ios/SuggestAction/SuggestAction.swift +5 -0
  77. package/ios/Swipeable/SwipeCell.swift +10 -6
  78. package/ios/Switch/Switch.swift +22 -20
  79. package/ios/TabView/TabView.swift +102 -21
  80. package/ios/Tag/Tag.swift +15 -4
  81. package/ios/Template/Logo/Logo.swift +2 -1
  82. package/ios/Template/TrustBanner/TrustBanner.swift +41 -27
  83. package/ios/Title/Title.swift +6 -17
  84. package/ios/Tooltip/Tooltip.swift +29 -14
  85. package/ios/Typography/Text.swift +61 -19
  86. package/ios/Uploader/Uploader.swift +3 -1
  87. package/ios/native-kits.podspec +1 -0
  88. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  89. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  90. package/package.json +1 -1
  91. package/build.gradle.kts +0 -11
  92. package/compose/build.gradle.kts +0 -204
  93. package/compose/build.gradle.kts.backup +0 -204
  94. package/compose/compose.podspec +0 -47
  95. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  96. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  97. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  98. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  99. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
  100. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -831
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
  149. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  150. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  151. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  152. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  153. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
  154. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  155. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  156. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  157. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
  158. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
  159. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
  160. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
  161. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  162. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  163. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
  164. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -188
  177. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
  178. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -232
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  182. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  183. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  184. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -708
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -203
  199. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -232
  200. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  201. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
  202. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
  203. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  204. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -408
  205. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -92
  206. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -105
  207. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  208. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -366
  209. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
  210. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  211. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -185
  212. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
  213. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  214. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  215. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  216. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  217. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
  218. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
  219. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  220. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  221. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  222. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  223. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
  224. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  225. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  226. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  227. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  228. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
  229. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -86
  230. package/gradle/libs.versions.toml +0 -69
  231. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  232. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  233. package/gradle.properties +0 -26
  234. package/gradlew +0 -252
  235. package/gradlew.bat +0 -94
  236. package/ios/Application/HeaderRight.swift +0 -324
  237. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  238. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  239. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  240. package/ios/Application/Navigation/NavigatorManager.swift +0 -29
  241. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -54
  242. package/ios/Application/Navigation/component/Header.swift +0 -183
  243. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -84
  244. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  245. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  246. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  247. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -280
  248. package/ios-demo/README.md +0 -120
  249. package/settings.gradle.kts +0 -64
  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
@@ -32,7 +32,7 @@ public struct Collapse<Content: View>: View {
32
32
  private let titleColor: Color?
33
33
  private let accessibilityId: String?
34
34
  private let onPress: ((Bool) -> Void)?
35
- private let content: (() -> Content)?
35
+ private var content: (() -> Content)?
36
36
 
37
37
  @State private var expanded: Bool
38
38
 
@@ -87,31 +87,36 @@ public struct Collapse<Content: View>: View {
87
87
  header
88
88
 
89
89
  if let content {
90
- if expanded {
91
- VStack(spacing: 0) {
92
- content()
90
+ // Compose's expandVertically: the clip grows with the panel while it slides in from the top.
91
+ VStack(spacing: 0) {
92
+ if expanded {
93
+ VStack(spacing: 0) {
94
+ content()
95
+ }
96
+ .frame(maxWidth: .infinity)
97
+ .overlay(
98
+ showBorder
99
+ ? Rectangle().strokeBorder(theme.colors.border.default, lineWidth: 1)
100
+ : nil
101
+ )
102
+ .transition(.move(edge: .top))
93
103
  }
94
- .frame(maxWidth: .infinity)
95
- .overlay(
96
- showBorder
97
- ? RoundedRectangle(cornerRadius: 0)
98
- .stroke(theme.colors.border.default, lineWidth: 1)
99
- : nil
100
- )
101
- .transition(.move(edge: .top).combined(with: .opacity))
102
104
  }
105
+ .clipped()
103
106
  }
104
107
  }
105
108
  .frame(maxWidth: .infinity)
106
109
  .background(resolvedBackgroundColor)
107
110
  .clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
108
- .accessibilityIdentifier(automationId)
109
111
  .overlay(
110
112
  showBorder
111
113
  ? RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
112
- .stroke(theme.colors.border.default, lineWidth: 1)
114
+ .strokeBorder(theme.colors.border.default, lineWidth: 1)
113
115
  : nil
114
116
  )
117
+ // `.contain` keeps the children's own identifiers instead of overriding them.
118
+ .accessibilityElement(children: .contain)
119
+ .accessibilityIdentifier(automationId)
115
120
  }
116
121
 
117
122
  private var header: some View {
@@ -126,6 +131,7 @@ public struct Collapse<Content: View>: View {
126
131
  ImageView(image, contentMode: .fill)
127
132
  .frame(width: imageSize, height: imageSize)
128
133
  .clipShape(RoundedRectangle(cornerRadius: Radius.XS, style: .continuous))
134
+ .accessibilityIdentifier("\(automationId)|img")
129
135
  Spacer().frame(width: Spacing.S)
130
136
  }
131
137
 
@@ -133,15 +139,17 @@ public struct Collapse<Content: View>: View {
133
139
  MomoText(
134
140
  title,
135
141
  typography: titleSize.typography,
136
- color: titleColor ?? theme.colors.text.default
142
+ color: titleColor ?? theme.colors.text.default,
143
+ accessibilityId: "\(automationId)|text"
137
144
  )
138
145
  if let description {
139
146
  MomoText(
140
147
  description,
141
148
  typography: .bodyDefaultRegular,
142
- color: theme.colors.text.secondary
149
+ color: theme.colors.text.secondary,
150
+ maxLines: 2,
151
+ accessibilityId: "\(automationId)|description-text"
143
152
  )
144
- .lineLimit(2)
145
153
  }
146
154
  }
147
155
  .frame(maxWidth: .infinity, alignment: .leading)
@@ -152,9 +160,10 @@ public struct Collapse<Content: View>: View {
152
160
  MomoText(
153
161
  subTitle,
154
162
  typography: .bodyDefaultRegular,
155
- color: theme.colors.text.secondary
163
+ color: theme.colors.text.secondary,
164
+ maxLines: 1,
165
+ accessibilityId: "\(automationId)|sub-title-text"
156
166
  )
157
- .lineLimit(1)
158
167
  } else if let tagLabel {
159
168
  Tag(label: tagLabel, color: tagColor)
160
169
  }
@@ -187,6 +196,48 @@ public struct Collapse<Content: View>: View {
187
196
  .rotationEffect(.degrees(expanded ? 180 : 0))
188
197
  }
189
198
  }
199
+ .accessibilityIdentifier("\(automationId)|icon")
190
200
  .animation(.easeInOut(duration: 0.3), value: expanded)
191
201
  }
192
202
  }
203
+
204
+ public extension Collapse where Content == EmptyView {
205
+ /// Header-only collapse, mirroring Compose's `content = null`.
206
+ init(
207
+ title: String,
208
+ description: String? = nil,
209
+ image: String? = nil,
210
+ imageSize: CGFloat = 24,
211
+ subTitle: String? = nil,
212
+ tagLabel: String? = nil,
213
+ tagColor: Tag.TagColor = .default,
214
+ showBorder: Bool = false,
215
+ expandDefault: Bool = false,
216
+ titleSize: CollapseTitleSize = .medium,
217
+ useBackgroundCollapseButton: Bool = false,
218
+ backgroundColor: Color? = nil,
219
+ titleColor: Color? = nil,
220
+ accessibilityId: String? = nil,
221
+ onPress: ((Bool) -> Void)? = nil
222
+ ) {
223
+ self.init(
224
+ title: title,
225
+ description: description,
226
+ image: image,
227
+ imageSize: imageSize,
228
+ subTitle: subTitle,
229
+ tagLabel: tagLabel,
230
+ tagColor: tagColor,
231
+ showBorder: showBorder,
232
+ expandDefault: expandDefault,
233
+ titleSize: titleSize,
234
+ useBackgroundCollapseButton: useBackgroundCollapseButton,
235
+ backgroundColor: backgroundColor,
236
+ titleColor: titleColor,
237
+ accessibilityId: accessibilityId,
238
+ onPress: onPress,
239
+ content: { EmptyView() }
240
+ )
241
+ self.content = nil
242
+ }
243
+ }
@@ -172,4 +172,13 @@ public enum Colors {
172
172
  public static let red03 = Color(hex: "f5222d")
173
173
  public static let red02 = Color(hex: "dd1f29")
174
174
  public static let red01 = Color(hex: "c41b24")
175
+
176
+ public static let opacityBlack20 = Color(hex: "00000033")
177
+ public static let opacityBlack40 = Color(hex: "00000066")
178
+ public static let opacityBlack60 = Color(hex: "00000099")
179
+ public static let opacityBlack80 = Color(hex: "000000cc")
180
+ public static let opacityWhite20 = Color(hex: "ffffff33")
181
+ public static let opacityWhite40 = Color(hex: "ffffff66")
182
+ public static let opacityWhite60 = Color(hex: "ffffff99")
183
+ public static let opacityWhite80 = Color(hex: "ffffffcc")
175
184
  }
@@ -36,33 +36,21 @@ public struct DateTimePicker: View {
36
36
  self.onChange = onChange
37
37
 
38
38
  let calendar = Calendar.current
39
- let effectiveSelected = selectedValue ?? DateTimePickerUtils.getCurrentDateTime()
40
39
  let effectiveMin = minDate ?? DateTimePickerUtils.createRelativeDate(years: -10)
41
40
  let effectiveMax = maxDate ?? DateTimePickerUtils.createRelativeDate(years: 10)
42
41
 
43
42
  // needCheckRange is true only when the caller supplied no bounds.
44
43
  let needCheckRange = (minDate == nil && maxDate == nil)
45
44
 
46
- let initialValue: Date
47
- if !needCheckRange {
48
- initialValue = effectiveSelected
49
- } else if effectiveSelected < effectiveMin {
50
- initialValue = effectiveMin
51
- } else if effectiveSelected > effectiveMax {
52
- initialValue = effectiveMax
53
- } else {
54
- initialValue = effectiveSelected
55
- }
56
-
57
- let comps = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: initialValue)
58
- self._currentData = State(initialValue: DateTimePickerData(
59
- day: comps.day ?? 1,
60
- month: comps.month ?? 1,
61
- year: comps.year ?? 2000,
62
- hour: comps.hour ?? 0,
63
- minute: comps.minute ?? 0
45
+ self._currentData = State(initialValue: Self.pickerData(
46
+ selectedValue: selectedValue,
47
+ effectiveMin: effectiveMin,
48
+ effectiveMax: effectiveMax,
49
+ needCheckRange: needCheckRange
64
50
  ))
65
51
 
52
+ self.effectiveMin = effectiveMin
53
+ self.effectiveMax = effectiveMax
66
54
  self.effectiveMinComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: effectiveMin)
67
55
  self.effectiveMaxComponents = calendar.dateComponents([.year, .month, .day, .hour, .minute], from: effectiveMax)
68
56
  self.needCheckRange = needCheckRange
@@ -102,9 +90,10 @@ public struct DateTimePicker: View {
102
90
  }
103
91
  .frame(maxWidth: .infinity)
104
92
  .frame(height: pickerHeight)
105
- .background(theme.colors.background.surface)
106
93
  .padding(.horizontal, Spacing.M)
94
+ .background(theme.colors.background.surface)
107
95
  .onAppear { emitIfValid() }
96
+ .onChange(of: selectedValue) { value in resetSelection(to: value) }
108
97
  }
109
98
 
110
99
  // MARK: Internal
@@ -119,10 +108,55 @@ public struct DateTimePicker: View {
119
108
 
120
109
  @State private var currentData: DateTimePickerData
121
110
 
111
+ private let effectiveMin: Date
112
+ private let effectiveMax: Date
122
113
  private let effectiveMinComponents: DateComponents
123
114
  private let effectiveMaxComponents: DateComponents
124
115
  private let needCheckRange: Bool
125
116
 
117
+ /// Mirrors Compose `initialValue`: the selection is clamped only when no bounds were supplied.
118
+ private static func pickerData(
119
+ selectedValue: Date?,
120
+ effectiveMin: Date,
121
+ effectiveMax: Date,
122
+ needCheckRange: Bool
123
+ ) -> DateTimePickerData {
124
+ let effectiveSelected = selectedValue ?? DateTimePickerUtils.getCurrentDateTime()
125
+ let initialValue: Date
126
+ if !needCheckRange {
127
+ initialValue = effectiveSelected
128
+ } else if effectiveSelected < effectiveMin {
129
+ initialValue = effectiveMin
130
+ } else if effectiveSelected > effectiveMax {
131
+ initialValue = effectiveMax
132
+ } else {
133
+ initialValue = effectiveSelected
134
+ }
135
+
136
+ let comps = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: initialValue)
137
+ return DateTimePickerData(
138
+ day: comps.day ?? 1,
139
+ month: comps.month ?? 1,
140
+ year: comps.year ?? 2000,
141
+ hour: comps.hour ?? 0,
142
+ minute: comps.minute ?? 0
143
+ )
144
+ }
145
+
146
+ /// Compose re-seeds the wheels when `selectedValue` changes (`remember(initialValue)`).
147
+ /// A caller echoing `onChange` back lands on the current selection and is a no-op.
148
+ private func resetSelection(to value: Date?) {
149
+ var next = Self.pickerData(
150
+ selectedValue: value,
151
+ effectiveMin: effectiveMin,
152
+ effectiveMax: effectiveMax,
153
+ needCheckRange: needCheckRange
154
+ )
155
+ // The date carries no AM/PM, so keep the wheel's choice.
156
+ next.timeMode = currentData.timeMode
157
+ if next != currentData { currentData = next }
158
+ }
159
+
126
160
  private func selectedDataFor(_ name: String) -> String {
127
161
  switch name {
128
162
  case "day": return DateTimePickerUtils.paddingNum(currentData.day)
@@ -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
  }