@momo-kits/native-kits 0.300.3-debug → 0.300.4-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -74,12 +74,12 @@ public struct Avatar: View {
74
74
  .clipShape(RoundedRectangle(cornerRadius: cornerRadius))
75
75
  .overlay(
76
76
  RoundedRectangle(cornerRadius: cornerRadius)
77
- .stroke(theme.colors.border.default, lineWidth: 1)
77
+ .strokeBorder(theme.colors.border.default, lineWidth: 1)
78
78
  )
79
79
 
80
80
  // Badge icons — outside clip so they are not cut by border
81
81
  if shouldShowIconTop {
82
- ImageView(LOGO_MOMO_URL)
82
+ ImageView(LOGO_MOMO_URL, contentMode: .fill)
83
83
  .frame(width: badgeSizeDp, height: badgeSizeDp)
84
84
  .clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
85
85
  .offset(x: 1, y: -1)
@@ -92,7 +92,7 @@ public struct Avatar: View {
92
92
  }
93
93
 
94
94
  if shouldShowIconBottom {
95
- ImageView(iconSupport)
95
+ ImageView(iconSupport, contentMode: .fill)
96
96
  .frame(width: badgeSizeDp, height: badgeSizeDp)
97
97
  .clipShape(RoundedRectangle(cornerRadius: badgeCornerRadius))
98
98
  .offset(x: 1, y: 1)
@@ -107,6 +107,7 @@ public struct Avatar: View {
107
107
  .frame(width: size.sizeDp, height: size.sizeDp)
108
108
  .accessibilityElement(children: .ignore)
109
109
  .accessibilityLabel(automationId)
110
+ .accessibilityIdentifier(automationId)
110
111
  }
111
112
 
112
113
  // MARK: Internal
@@ -33,7 +33,8 @@ public struct Badge: View {
33
33
  }
34
34
 
35
35
  private func formatTitle(_ text: String) -> String {
36
- if isNumber(text), let number = Int(text), number > 99 {
36
+ // Digits too long for Int are well over 99.
37
+ if isNumber(text), (Int(text) ?? .max) > 99 {
37
38
  return "99+"
38
39
  }
39
40
  return text
@@ -54,14 +55,15 @@ public struct Badge: View {
54
55
  }
55
56
 
56
57
  public var body: some View {
57
- MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01)
58
+ MomoText(formatTitle(label), typography: .actionXxsBold, color: Colors.black01, maxLines: 1)
58
59
  .padding(.horizontal, Spacing.XS)
59
- .frame(minWidth: fontScale(16), minHeight: fontScale(16))
60
+ .frame(minWidth: fontScale(16))
61
+ .frame(height: fontScale(16))
60
62
  .background(badgeColor)
61
63
  .cornerRadius(Radius.M)
62
64
  .overlay(
63
65
  RoundedRectangle(cornerRadius: Radius.M)
64
- .stroke(Colors.black01, lineWidth: 1)
66
+ .strokeBorder(Colors.black01, lineWidth: 1)
65
67
  )
66
68
  }
67
69
  }
@@ -25,7 +25,7 @@ public struct BadgeDot: View {
25
25
  .frame(width: size.size, height: size.size)
26
26
  .overlay(
27
27
  Circle()
28
- .stroke(Colors.black01, lineWidth: 1)
28
+ .strokeBorder(Colors.black01, lineWidth: 1)
29
29
  )
30
30
  }
31
31
  }
@@ -160,7 +160,6 @@ public struct BadgeRibbon: View {
160
160
  MomoText(label, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
161
161
  .rotationEffect(rotation)
162
162
  .frame(height: RibbonConstants.skewBodyHeight)
163
- .padding(.horizontal, 8)
164
163
  .background(backgroundColor)
165
164
 
166
165
  rightTail
@@ -204,7 +203,9 @@ public struct BadgeRibbon: View {
204
203
  /// rotation) — the label gets the same scale applied a second time so it stays right-reading
205
204
  /// while only the ribbon shape flips.
206
205
  public struct RoundedBadgeRibbon: View {
206
+ @Environment(\.appTheme) private var theme
207
207
  @Environment(\.momoFontScale) private var fontScale
208
+ @Environment(\.designSystemKitsConfigs) private var designSystemKitsConfigs
208
209
  let text: String
209
210
  let position: RibbonPosition
210
211
 
@@ -224,7 +225,12 @@ public struct RoundedBadgeRibbon: View {
224
225
 
225
226
  private var measuredTextSize: CGSize {
226
227
  let fontSize = fontScale(TypographyStyle.labelXsMedium.fontSize)
227
- let font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
228
+ let font = momoUIFont(
229
+ size: fontSize,
230
+ weight: .medium,
231
+ family: theme.font,
232
+ isWhiteList: designSystemKitsConfigs.isWhiteList
233
+ )
228
234
  return text.size(withAttributes: [.font: font])
229
235
  }
230
236
 
@@ -51,15 +51,17 @@ public extension ButtonType {
51
51
  }
52
52
 
53
53
  func contentColor(loading: Bool, theme: Theme) -> Color {
54
+ let color: Color
54
55
  switch self {
55
- case .disabled: return theme.colors.text.disable.opacity(loading ? 0.75 : 1)
56
- case .primary: return Colors.black01
57
- case .secondary: return theme.colors.text.default
58
- case .outline: return theme.colors.primary
59
- case .tonal: return theme.colors.primary
60
- case .danger: return Colors.black01
61
- case .text: return theme.colors.primary
56
+ case .disabled: color = theme.colors.text.disable
57
+ case .primary: color = Colors.black01
58
+ case .secondary: color = theme.colors.text.default
59
+ case .outline: color = theme.colors.primary
60
+ case .tonal: color = theme.colors.primary
61
+ case .danger: color = Colors.black01
62
+ case .text: color = theme.colors.primary
62
63
  }
64
+ return color.opacity(loading ? 0.75 : 1)
63
65
  }
64
66
 
65
67
  var borderColor: Color? {
@@ -219,9 +221,8 @@ public struct Button: View {
219
221
  let color: Color? = useTintColor ? textColor : nil
220
222
 
221
223
  if forceLoading {
222
- LottieView(name: "lottie_circle_loader", loopMode: .loop)
224
+ LottieView(name: "lottie_circle_loader", loopMode: .loop, tintColor: color.map { UIColor($0) })
223
225
  .frame(width: size.iconSize, height: size.iconSize)
224
- .colorMultiply(color ?? Colors.black01)
225
226
  } else if !icon.isEmpty {
226
227
  Icon(source: icon, size: size.iconSize, color: color)
227
228
  }
@@ -229,8 +230,8 @@ public struct Button: View {
229
230
  }
230
231
 
231
232
  /// Mirrors Compose's press feedback: the background shrinks via an animated
232
- /// horizontal inset while the content fades, rather than a plain opacity dim on
233
- /// the whole button (matching `animatedPadding`/`alpha` in Button.kt).
233
+ /// horizontal inset and the whole button (background included) fades to 0.5,
234
+ /// matching `animatedPadding`/`alpha` in Button.kt.
234
235
  private struct ButtonPressFeedbackStyle: ButtonStyle {
235
236
  let isEnabled: Bool
236
237
  let background: Color
@@ -245,7 +246,6 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
245
246
 
246
247
  configuration.label
247
248
  .modifier(HugContentOnLegacyOS(isFull: isFull))
248
- .opacity(pressed ? 0.5 : 1)
249
249
  .background(
250
250
  RoundedRectangle(cornerRadius: radius)
251
251
  .fill(background)
@@ -257,6 +257,9 @@ private struct ButtonPressFeedbackStyle: ButtonStyle {
257
257
  )
258
258
  .frame(height: height)
259
259
  .clipShape(RoundedRectangle(cornerRadius: radius))
260
+ // `.text` has a clear background, which SwiftUI does not hit-test.
261
+ .contentShape(RoundedRectangle(cornerRadius: radius))
262
+ .opacity(pressed ? 0.5 : 1)
260
263
  .animation(.easeInOut(duration: 0.1), value: pressed)
261
264
  }
262
265
  }
@@ -16,55 +16,101 @@ extension String {
16
16
  public struct CalculatorKeyboard: View {
17
17
  // MARK: Lifecycle
18
18
  @Binding var value: String
19
- @State var tempValue: String = ""
20
19
 
21
20
  public init(_ value: Binding<String>) {
22
21
  self._value = value
23
22
  }
23
+
24
+ // Derived from `value` so a host write to the binding (prefill, reset after submit) is honoured.
25
+ private var expression: String {
26
+ value.replacingOccurrences(of: "x", with: "*").replacingOccurrences(of: "÷", with: "/")
27
+ }
24
28
 
25
29
  func onPressBack(){
26
30
  if (value.count > 0 ){
27
31
  value.removeLast()
28
32
  }
29
-
30
33
  }
31
34
 
32
35
  func onPressClear(key: String){
33
36
  value = ""
34
- tempValue = ""
35
37
  }
36
38
 
37
39
  func onPressEqual(key: String){
38
- if (tempValue == "") {
40
+ var expression = self.expression
41
+ if (expression == "") {
39
42
  return
40
43
  }
41
- if (tempValue.last?.isNumber == false){
42
- tempValue.removeLast()
44
+ if (expression.last?.isNumber == false){
45
+ expression.removeLast()
43
46
  }
44
- let expression = NSExpression(format: tempValue)
45
-
46
- value = String(expression.expressionValue(with: nil, context: nil) as! Int)
47
- if (Int(value) ?? 0 < 0){
47
+ // Overflow and division by zero are treated like a negative result: cleared.
48
+ if let result = Self.evaluate(expression), result >= 0 {
49
+ value = String(result)
50
+ } else {
48
51
  value = ""
49
52
  }
50
53
  }
54
+
55
+ /// Integer `+ - * /` with the usual precedence, as NSExpression did, but nil instead of a
56
+ /// crash when a number or result leaves `Int` range.
57
+ static func evaluate(_ expression: String) -> Int? {
58
+ var numbers: [Int] = []
59
+ var operators: [Character] = []
60
+ var digits = ""
61
+ for char in expression {
62
+ if ("0"..."9").contains(char) {
63
+ digits.append(char)
64
+ continue
65
+ }
66
+ guard let number = Int(digits) else { return nil }
67
+ numbers.append(number)
68
+ operators.append(char)
69
+ digits = ""
70
+ }
71
+ guard let last = Int(digits) else { return nil }
72
+ numbers.append(last)
73
+
74
+ var terms = [numbers[0]]
75
+ var signs: [Character] = []
76
+ for (op, number) in zip(operators, numbers.dropFirst()) {
77
+ switch op {
78
+ case "*":
79
+ let (product, overflow) = terms[terms.count - 1].multipliedReportingOverflow(by: number)
80
+ if overflow { return nil }
81
+ terms[terms.count - 1] = product
82
+ case "/":
83
+ guard number != 0 else { return nil }
84
+ terms[terms.count - 1] /= number
85
+ case "+", "-":
86
+ terms.append(number)
87
+ signs.append(op)
88
+ default:
89
+ return nil
90
+ }
91
+ }
92
+
93
+ var result = terms[0]
94
+ for (sign, term) in zip(signs, terms.dropFirst()) {
95
+ let (next, overflow) = sign == "+"
96
+ ? result.addingReportingOverflow(term)
97
+ : result.subtractingReportingOverflow(term)
98
+ if overflow { return nil }
99
+ result = next
100
+ }
101
+ return result
102
+ }
51
103
 
52
104
  func onPressKey(key:String){
53
- if (tempValue.last?.isNumber == false){
105
+ if (value.last?.isNumber == false){
54
106
  if (key.isNumber == false){
55
107
  return
56
108
  }
57
109
  }
58
- var convertedKey = key
59
- if (key == "x") {convertedKey = "*"}
60
- if (key == "÷") {convertedKey = "/"}
61
110
  if (value == "" && (key == "0" || key == "000" || key.isNumber == false)) {
62
111
  return
63
112
  }
64
- else {
65
- value.append(key)
66
- tempValue.append(convertedKey)
67
- }
113
+ value.append(key)
68
114
  }
69
115
 
70
116
  // MARK: Public
@@ -1,4 +1,3 @@
1
- import Combine
2
1
  import SwiftUI
3
2
 
4
3
  /// Native SwiftUI port of the Compose `Carousel` component.
@@ -10,9 +9,9 @@ import SwiftUI
10
9
  /// snap behavior, and scroll enable/disable. Snap / scroll-index callbacks
11
10
  /// report the real item index.
12
11
  ///
13
- /// Available iOS 14+ (uses a custom paging ScrollView so behavior is identical
12
+ /// Uses a custom drag-driven pager (not a ScrollView) so behavior is identical
14
13
  /// across versions and supports the loop virtualization + transform effects
15
- /// that `TabView(.page)` cannot express).
14
+ /// that `TabView(.page)` cannot express.
16
15
  public struct Carousel<Content: View>: View {
17
16
  // MARK: Lifecycle
18
17
 
@@ -82,7 +81,7 @@ public struct Carousel<Content: View>: View {
82
81
  @State private var currentPage: Int
83
82
  @State private var dragOffset: CGFloat = 0
84
83
  @State private var isDragging: Bool = false
85
- @State private var autoplayTick: Int = 0
84
+ @GestureState private var dragActive = false
86
85
 
87
86
  private func realIndex(_ page: Int) -> Int {
88
87
  ((page % itemCount) + itemCount) % itemCount
@@ -124,9 +123,20 @@ public struct Carousel<Content: View>: View {
124
123
  }
125
124
  .frame(width: viewportWidth)
126
125
  .clipped()
127
- .onReceive(autoplayTimer) { _ in
128
- guard autoplay, itemCount > 1, !isDragging else { return }
129
- advance()
126
+ // Restarts on drag like Compose's LaunchedEffect(autoplayInterval, isUserScrolling).
127
+ .task(id: AutoplayKey(enabled: autoplay && itemCount > 1 && !isDragging, interval: autoplayInterval)) { @MainActor in
128
+ guard autoplay, itemCount > 1, !isDragging, autoplayInterval > 0 else { return }
129
+ while !Task.isCancelled {
130
+ try? await Task.sleep(nanoseconds: UInt64(autoplayInterval * 1_000_000_000))
131
+ guard !Task.isCancelled else { return }
132
+ advance()
133
+ }
134
+ }
135
+ // A drag stolen by a parent scroll view never reaches `onEnded`; settle on the current page.
136
+ .onChange(of: dragActive) { active in
137
+ guard !active, isDragging else { return }
138
+ isDragging = false
139
+ withAnimation(.easeInOut(duration: 0.25)) { dragOffset = 0 }
130
140
  }
131
141
  }
132
142
 
@@ -147,9 +157,14 @@ public struct Carousel<Content: View>: View {
147
157
 
148
158
  private func dragGesture(step: CGFloat) -> some Gesture {
149
159
  DragGesture(minimumDistance: 5)
160
+ .updating($dragActive) { _, active, _ in active = true }
150
161
  .onChanged { value in
151
162
  isDragging = true
152
- dragOffset = value.translation.width
163
+ var offset = value.translation.width
164
+ // No slide exists past either end, as with HorizontalPager bounds.
165
+ if currentPage == 0 { offset = min(offset, 0) }
166
+ if currentPage == virtualPageCount - 1 { offset = max(offset, 0) }
167
+ dragOffset = offset
153
168
  }
154
169
  .onEnded { value in
155
170
  isDragging = false
@@ -167,7 +182,8 @@ public struct Carousel<Content: View>: View {
167
182
  let moved = step > 0 ? Int((-dragOffset / step).rounded()) : 0
168
183
  target = min(max(currentPage + moved, 0), virtualPageCount - 1)
169
184
  }
170
- dragOffset = 0
185
+ // Animated so a drag that stays on the same page eases back too.
186
+ withAnimation(.easeInOut(duration: 0.25)) { dragOffset = 0 }
171
187
  setPage(target)
172
188
  }
173
189
  }
@@ -189,8 +205,9 @@ public struct Carousel<Content: View>: View {
189
205
  onScrollIndexChanged(real)
190
206
  }
191
207
  }
208
+ }
192
209
 
193
- private var autoplayTimer: Publishers.Autoconnect<Timer.TimerPublisher> {
194
- Timer.publish(every: autoplayInterval, on: .main, in: .common).autoconnect()
195
- }
210
+ private struct AutoplayKey: Equatable {
211
+ let enabled: Bool
212
+ let interval: TimeInterval
196
213
  }
@@ -21,8 +21,10 @@ public struct Checkbox: View{
21
21
  }
22
22
 
23
23
  func onCheck(){
24
- checked.toggle()
25
- onChange?(checked)
24
+ // Not re-read from the binding: a `.constant` / one-way binding would report the stale value.
25
+ let newValue = !checked
26
+ checked = newValue
27
+ onChange?(newValue)
26
28
  }
27
29
 
28
30
  public var body: some View {
@@ -35,13 +37,15 @@ public struct Checkbox: View{
35
37
  .frame(width: 20, height: 20)
36
38
  .overlay(
37
39
  RoundedRectangle(cornerRadius: Radius.XS)
38
- .stroke(borderColor, lineWidth: Spacing.XXS)
40
+ .strokeBorder(borderColor, lineWidth: Spacing.XXS)
39
41
  )
40
42
 
41
43
  if checked {
42
44
  Icon(source: indeterminate ? "navigation_minus" : "ic_checked", size: 20, color: Colors.black01)
43
45
  }
44
46
  }
47
+ // The unchecked fill is clear, which SwiftUI does not hit-test.
48
+ .contentShape(RoundedRectangle(cornerRadius: Radius.XS))
45
49
  }
46
50
  .buttonStyle(ActiveOpacityButtonStyle(activeOpacity: 0.8))
47
51
  .disabled(disabled)
@@ -89,18 +89,19 @@ public struct Chip: View {
89
89
  .padding(.horizontal, horizontal)
90
90
  .frame(height: height)
91
91
  .background(
92
- RoundedRectangle(cornerRadius: radius, style: .continuous)
92
+ RoundedRectangle(cornerRadius: radius)
93
93
  .fill(bg)
94
94
  )
95
95
  .overlay(
96
96
  Group {
97
97
  if selected {
98
- RoundedRectangle(cornerRadius: radius, style: .continuous)
99
- .stroke(theme.colors.secondary, lineWidth: 2)
98
+ // Inside the bounds, like Compose `border`.
99
+ RoundedRectangle(cornerRadius: radius)
100
+ .strokeBorder(theme.colors.secondary, lineWidth: 2)
100
101
  }
101
102
  }
102
103
  )
103
- .contentShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
104
+ .contentShape(RoundedRectangle(cornerRadius: radius))
104
105
  }
105
106
  .buttonStyle(ActiveOpacityButtonStyle())
106
107
  .modifier(ChipAccessibilityModifier(accessibilityLabel: accessibilityLabel))
@@ -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
+ }
@@ -133,7 +133,7 @@ public enum Colors {
133
133
  public static let yellow06 = Color(hex: "ffe066")
134
134
  public static let yellow05 = Color(hex: "ffd633")
135
135
  public static let yellow04 = Color(hex: "ffd119")
136
- public static let yellow03 = Color(hex: "fc0")
136
+ public static let yellow03 = Color(hex: "ffcc00")
137
137
  public static let yellow02 = Color(hex: "e6b800")
138
138
  public static let yellow01 = Color(hex: "cca300")
139
139
 
@@ -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
  }
@@ -15,6 +15,12 @@ public struct RoundedCorner: Shape {
15
15
  var radius: CGFloat = .infinity
16
16
  var corners: UIRectCorner = .allCorners
17
17
 
18
+ /// iOS 15 stand-in for `UnevenRoundedRectangle`.
19
+ public init(radius: CGFloat = .infinity, corners: UIRectCorner = .allCorners) {
20
+ self.radius = radius
21
+ self.corners = corners
22
+ }
23
+
18
24
  public func path(in rect: CGRect) -> Path {
19
25
  let path = UIBezierPath(roundedRect: rect, byRoundingCorners: corners, cornerRadii: CGSize(width: radius, height: radius))
20
26
  return Path(path.cgPath)