@momo-kits/native-kits 0.162.21 → 0.162.22-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 (186) hide show
  1. package/build.gradle.kts +11 -0
  2. package/compose/build.gradle.kts +190 -0
  3. package/compose/build.gradle.kts.backup +190 -0
  4. package/compose/compose.podspec +47 -0
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +123 -0
  8. package/compose/src/commonMain/composeResources/font/momosignature.otf +0 -0
  9. package/compose/src/commonMain/composeResources/font/momotrustdisplay.otf +0 -0
  10. package/compose/src/commonMain/composeResources/font/sfprotext_black.otf +0 -0
  11. package/compose/src/commonMain/composeResources/font/sfprotext_black.ttf +0 -0
  12. package/compose/src/commonMain/composeResources/font/sfprotext_bold.ttf +0 -0
  13. package/compose/src/commonMain/composeResources/font/sfprotext_heavy.ttf +0 -0
  14. package/compose/src/commonMain/composeResources/font/sfprotext_light.ttf +0 -0
  15. package/compose/src/commonMain/composeResources/font/sfprotext_medium.ttf +0 -0
  16. package/compose/src/commonMain/composeResources/font/sfprotext_regular.ttf +0 -0
  17. package/compose/src/commonMain/composeResources/font/sfprotext_semibold.ttf +0 -0
  18. package/compose/src/commonMain/composeResources/font/sfprotext_thin.otf +0 -0
  19. package/compose/src/commonMain/composeResources/font/sfprotext_thin.ttf +0 -0
  20. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.otf +0 -0
  21. package/compose/src/commonMain/composeResources/font/sfprotext_ultralight.ttf +0 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +106 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +294 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +826 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +543 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +23 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +112 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +477 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +214 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +216 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +259 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +182 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +328 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +97 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +217 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +470 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +131 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +203 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +175 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +306 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +276 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +2 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +35 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +2 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +253 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +100 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +172 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +360 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +581 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +156 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +225 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +320 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +81 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +309 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +34 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +360 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +128 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +70 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +58 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +105 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +184 -0
  134. package/gradle/libs.versions.toml +69 -0
  135. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  136. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  137. package/gradle.properties +26 -0
  138. package/gradlew +252 -0
  139. package/gradlew.bat +94 -0
  140. package/ios/Application/ApplicationEnvironment.swift +4 -2
  141. package/ios/Application/MaxApi.swift +18 -0
  142. package/ios/Application/Navigation/NavigationContainer.swift +21 -3
  143. package/ios/Application/Navigation/Navigator.swift +50 -29
  144. package/ios/Application/Navigation/Overplay/ModalScreen.swift +6 -0
  145. package/ios/Application/Navigation/Tracking/ScreenTracker.swift +117 -0
  146. package/ios/Application/StackScreen.swift +41 -0
  147. package/ios/Badge/BadgeRibbon.swift +80 -0
  148. package/ios/Button/Button.swift +67 -11
  149. package/ios/Carousel/Carousel.swift +16 -1
  150. package/ios/Checkbox/Checkbox.swift +14 -20
  151. package/ios/Chip/Chip.swift +35 -26
  152. package/ios/Collapse/Collapse.swift +11 -1
  153. package/ios/DateTimePicker/DateTimePicker.swift +4 -2
  154. package/ios/DateTimePicker/WheelPicker.swift +7 -3
  155. package/ios/Extensions/ActiveOpacityButtonStyle.swift +14 -0
  156. package/ios/Icon/Icon.swift +5 -1
  157. package/ios/IconButton/IconButton.swift +35 -7
  158. package/ios/Image/Image.swift +16 -3
  159. package/ios/Information/Information.swift +3 -1
  160. package/ios/Input/ErrorView.swift +3 -1
  161. package/ios/Input/Input.swift +31 -8
  162. package/ios/Input/InputPhoneNumber.swift +14 -3
  163. package/ios/Input/InputSearch.swift +7 -1
  164. package/ios/Input/InputTextArea.swift +78 -34
  165. package/ios/InputDropDown/InputDropDown.swift +4 -1
  166. package/ios/InputMoney/InputMoney.swift +16 -4
  167. package/ios/Popup/PopupDisplay.swift +3 -4
  168. package/ios/Popup/PopupNotify.swift +3 -4
  169. package/ios/Popup/PopupPromotion.swift +16 -8
  170. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  171. package/ios/Radio/Radio.swift +12 -13
  172. package/ios/Rating/Rating.swift +1 -1
  173. package/ios/Stepper/Stepper.swift +2 -2
  174. package/ios/Steps/Steps.swift +22 -6
  175. package/ios/SuggestAction/SuggestAction.swift +5 -3
  176. package/ios/Swipeable/SwipeCell.swift +5 -3
  177. package/ios/Switch/Switch.swift +53 -28
  178. package/ios/TabView/TabView.swift +18 -14
  179. package/ios/Template/TrustBanner/TrustBanner.swift +6 -7
  180. package/ios/Tooltip/Tooltip.swift +3 -0
  181. package/ios/Uploader/Uploader.swift +0 -9
  182. package/ios/native-kits.podspec +1 -1
  183. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  184. package/ios-demo/README.md +120 -0
  185. package/package.json +1 -1
  186. package/settings.gradle.kts +64 -0
@@ -0,0 +1,117 @@
1
+ //
2
+ // ScreenTracker.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/tracking/ScreenTracker.kt`. Pure
6
+ // logic, no platform dependency beyond the `MaxApi` bridge.
7
+ //
8
+
9
+ import Foundation
10
+
11
+ public final class ScreenTrackingState {
12
+ public var time: Double = Date().timeIntervalSince1970 * 1000
13
+ public var releaseNavigated: Bool = false
14
+ public var releaseLoad: Bool = false
15
+ public var releaseInteraction: Bool = false
16
+ public var previousScreenName: String?
17
+ public var action: String = "push"
18
+
19
+ public init() {}
20
+ }
21
+
22
+ public enum ScreenTracker {
23
+ private static var lastScreenName: String?
24
+
25
+ public static func trackScreenNavigated(
26
+ maxApi: MaxApi?,
27
+ context: MiniAppContext?,
28
+ screenName: String,
29
+ action: String,
30
+ state: ScreenTrackingState
31
+ ) {
32
+ guard !state.releaseNavigated else { return }
33
+
34
+ let previousScreen = lastScreenName
35
+ lastScreenName = screenName
36
+
37
+ var params = buildBaseParams(context: context, screenName: screenName)
38
+ params["pre_screen_name"] = previousScreen
39
+ params["state"] = "navigated"
40
+ params["action"] = action
41
+ maxApi?.track(eventName: "auto_screen_navigated", params: params)
42
+
43
+ state.releaseNavigated = true
44
+ state.previousScreenName = previousScreen
45
+ }
46
+
47
+ public static func trackScreenDisplayed(
48
+ maxApi: MaxApi?,
49
+ context: MiniAppContext?,
50
+ screenName: String,
51
+ duration: Double,
52
+ state: ScreenTrackingState
53
+ ) {
54
+ guard !state.releaseLoad else { return }
55
+
56
+ var params = buildBaseParams(context: context, screenName: screenName)
57
+ params["state"] = "load"
58
+ params["duration"] = duration
59
+ maxApi?.track(eventName: "auto_screen_displayed", params: params)
60
+
61
+ state.releaseLoad = true
62
+ }
63
+
64
+ public static func trackScreenInteracted(
65
+ maxApi: MaxApi?,
66
+ context: MiniAppContext?,
67
+ screenName: String,
68
+ duration: Double,
69
+ totalDuration: Double,
70
+ state: ScreenTrackingState
71
+ ) {
72
+ guard !state.releaseInteraction else { return }
73
+
74
+ var params = buildBaseParams(context: context, screenName: screenName)
75
+ params["state"] = "interaction"
76
+ params["duration"] = duration
77
+ params["total_duration"] = totalDuration
78
+ maxApi?.track(eventName: "auto_screen_interacted", params: params)
79
+
80
+ state.releaseInteraction = true
81
+ }
82
+
83
+ public static func trackPopupDisplayed(
84
+ maxApi: MaxApi?,
85
+ context: MiniAppContext?,
86
+ parentScreenName: String?
87
+ ) {
88
+ maxApi?.track(eventName: "auto_popup_displayed", params: [
89
+ "screen_name": parentScreenName,
90
+ "tracking_source": 1,
91
+ "app_id": context?.appId,
92
+ "feature_code": context?.appCode,
93
+ "kits_version": nil,
94
+ "component_name": "Modal"
95
+ ])
96
+ }
97
+
98
+ private static func buildBaseParams(
99
+ context: MiniAppContext?,
100
+ screenName: String,
101
+ componentName: String = "Screen"
102
+ ) -> [String: Any?] {
103
+ [
104
+ "app_id": context?.appId,
105
+ "feature_code": context?.appCode,
106
+ "screen_name": screenName,
107
+ "component_name": componentName,
108
+ "tracking_source": 1
109
+ ]
110
+ }
111
+
112
+ public static func getLastScreenName() -> String? { lastScreenName }
113
+
114
+ public static func reset() {
115
+ lastScreenName = nil
116
+ }
117
+ }
@@ -39,6 +39,7 @@ public struct StackScreen<Content: View>: View {
39
39
  var layoutOffset: CGFloat
40
40
  var inputSearchProps: InputSearchProps?
41
41
  var fabProps: FabProps?
42
+ var screenName: String = ""
42
43
  var content: () -> Content
43
44
 
44
45
  public init(
@@ -66,6 +67,7 @@ public struct StackScreen<Content: View>: View {
66
67
  layoutOffset: CGFloat = 56,
67
68
  inputSearchProps: InputSearchProps? = nil,
68
69
  fabProps: FabProps? = nil,
70
+ screenName: String = "",
69
71
  @ViewBuilder content: @escaping () -> Content
70
72
  ) {
71
73
  self.backgroundColor = backgroundColor
@@ -92,6 +94,7 @@ public struct StackScreen<Content: View>: View {
92
94
  self.layoutOffset = layoutOffset
93
95
  self.inputSearchProps = inputSearchProps
94
96
  self.fabProps = fabProps
97
+ self.screenName = screenName
95
98
  self.content = content
96
99
  }
97
100
 
@@ -100,6 +103,13 @@ public struct StackScreen<Content: View>: View {
100
103
  @State private var headerRightWidth: CGFloat = 0
101
104
  @State private var isBottomTabRoot: Bool = false
102
105
 
106
+ @Environment(\.applicationEnvironment) private var appEnv
107
+
108
+ // Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
109
+ // the once-computed push/back `action`).
110
+ @State private var trackingState = ScreenTrackingState()
111
+ @State private var trackingAction: String = ScreenTracker.getLastScreenName() != nil ? "push" : "back"
112
+
103
113
  /// Header background fade tied to scroll: 0 → transparent (over a hero image
104
114
  /// or transparent header), 1 → opaque surface with the drop shadow.
105
115
  private var scrollOpacity: CGFloat { min(scrollOffset / 114, 1) }
@@ -193,6 +203,37 @@ public struct StackScreen<Content: View>: View {
193
203
  // taps still reach buttons/fields while also dismissing the keyboard.
194
204
  .modifier(HideKeyboardOnTap(enabled: keyboardShouldPersistTaps))
195
205
  }
206
+ .task(id: screenName) {
207
+ ScreenTracker.trackScreenNavigated(
208
+ maxApi: appEnv.maxApi,
209
+ context: appEnv.applicationContext,
210
+ screenName: screenName,
211
+ action: trackingAction,
212
+ state: trackingState
213
+ )
214
+ do {
215
+ try await Task.sleep(nanoseconds: 2_000_000_000)
216
+ } catch {
217
+ return
218
+ }
219
+ let loadTime = Date().timeIntervalSince1970 * 1000 - trackingState.time
220
+ ScreenTracker.trackScreenDisplayed(
221
+ maxApi: appEnv.maxApi,
222
+ context: appEnv.applicationContext,
223
+ screenName: screenName,
224
+ duration: loadTime,
225
+ state: trackingState
226
+ )
227
+ let interactionTime = Date().timeIntervalSince1970 * 1000 - trackingState.time
228
+ ScreenTracker.trackScreenInteracted(
229
+ maxApi: appEnv.maxApi,
230
+ context: appEnv.applicationContext,
231
+ screenName: screenName,
232
+ duration: interactionTime - loadTime,
233
+ totalDuration: interactionTime,
234
+ state: trackingState
235
+ )
236
+ }
196
237
  }
197
238
 
198
239
  // MARK: - Header + content layer
@@ -196,6 +196,86 @@ public struct BadgeRibbon: View {
196
196
  }
197
197
  }
198
198
 
199
+ /// Ported from Compose's `RoundedBadgeRibbon` (BadgeRibbon.kt:164-326): a canvas-drawn
200
+ /// ribbon shape sized to its text, mirrored per corner via scaleX/scaleY (never an actual
201
+ /// rotation) — the label gets the same scale applied a second time so it stays right-reading
202
+ /// while only the ribbon shape flips.
203
+ public struct RoundedBadgeRibbon: View {
204
+ let text: String
205
+ let position: RibbonPosition
206
+
207
+ public init(text: String = "Label", position: RibbonPosition = .topLeft) {
208
+ self.text = text
209
+ self.position = position
210
+ }
211
+
212
+ private var scale: (x: CGFloat, y: CGFloat) {
213
+ switch position {
214
+ case .topLeft: return (-1, 1)
215
+ case .topRight: return (1, 1)
216
+ case .bottomRight: return (1, -1)
217
+ case .bottomLeft: return (-1, -1)
218
+ }
219
+ }
220
+
221
+ private var measuredTextSize: CGSize {
222
+ let fontSize = scaleSize(TypographyStyle.labelXsMedium.fontSize)
223
+ let font = UIFont.systemFont(ofSize: fontSize, weight: .medium)
224
+ return text.size(withAttributes: [.font: font])
225
+ }
226
+
227
+ public var body: some View {
228
+ let textSize = measuredTextSize
229
+ let minRibbonHeight = max(textSize.height * 1.2, 16)
230
+ let padding = minRibbonHeight / 4
231
+ let badgeHeight = minRibbonHeight + padding
232
+ let badgeWidth = max(textSize.width + padding * 2, 28)
233
+
234
+ ZStack {
235
+ Canvas { context, size in
236
+ let ribbonHeight = size.height - padding
237
+ let cornerRadius = (badgeHeight - padding) / 2
238
+ let mainColor = Colors.orange03
239
+ let tailColor = Colors.red01
240
+
241
+ let headWidth: CGFloat = 4
242
+ // Kotlin computes `leftSectionWidth` + `middleSectionWidth` here, but those
243
+ // cancel algebraically to `width - headWidth` — kept as the direct result.
244
+ let rightX = size.width - headWidth
245
+
246
+ let leftMiddlePath = RoundedCorners(topLeft: cornerRadius, topRight: 0, bottomLeft: cornerRadius, bottomRight: 0)
247
+ .path(in: CGRect(x: 0, y: 0, width: rightX + 1, height: ribbonHeight))
248
+ context.fill(leftMiddlePath, with: .color(mainColor))
249
+
250
+ let headPath = RoundedCorners(topLeft: 0, topRight: cornerRadius, bottomLeft: 0, bottomRight: 0)
251
+ .path(in: CGRect(x: rightX, y: 0, width: headWidth, height: ribbonHeight))
252
+ context.fill(headPath, with: .color(mainColor))
253
+
254
+ let halfHeadWidth = headWidth / 2
255
+ let tailStartX = rightX + headWidth - halfHeadWidth
256
+ let tailY = ribbonHeight
257
+ let tailHeight = padding
258
+
259
+ context.fill(
260
+ Path(CGRect(x: tailStartX, y: tailY, width: halfHeadWidth, height: halfHeadWidth)),
261
+ with: .color(mainColor)
262
+ )
263
+
264
+ let tailRightPath = RoundedCorners(topLeft: 0, topRight: tailHeight, bottomLeft: 0, bottomRight: tailHeight)
265
+ .path(in: CGRect(x: tailStartX, y: tailY, width: halfHeadWidth, height: tailHeight))
266
+ context.fill(tailRightPath, with: .color(tailColor))
267
+ }
268
+
269
+ MomoText(text, typography: .labelXsMedium, color: Colors.black01, maxLines: 1)
270
+ .padding(.bottom, padding)
271
+ .padding(.horizontal, padding / 2)
272
+ .scaleEffect(x: scale.x, y: scale.y)
273
+ }
274
+ .frame(width: badgeWidth, height: badgeHeight)
275
+ .scaleEffect(x: scale.x, y: scale.y)
276
+ }
277
+ }
278
+
199
279
  // MARK: - Constants
200
280
  private struct RibbonConstants {
201
281
  static let ribbonHeight: CGFloat = 20
@@ -123,7 +123,6 @@ extension ButtonSize {
123
123
  case .large: return .actionDefaultBold
124
124
  case .medium: return .actionSBold
125
125
  case .small: return .actionXsBold
126
- default : return .actionDefaultBold
127
126
  }
128
127
  }
129
128
  }
@@ -162,6 +161,35 @@ public struct Button: View {
162
161
  self.useTintColor = useTintColor
163
162
  }
164
163
 
164
+ /// Mirrors Kotlin's `Button(iconLeft: String, iconRight: String, ...)`, which resolves
165
+ /// icons by design-system source name through the shared `Icon` component rather than
166
+ /// a caller-supplied view. `iconLeft` is required here (unlike Kotlin's `= ""` default)
167
+ /// solely to keep this initializer unambiguous alongside the `AnyView`-based one above —
168
+ /// pass `""` explicitly for an icon-right-only button.
169
+ public init(
170
+ title: String = "Button",
171
+ action: @escaping () -> Void,
172
+ type: ButtonType = .primary,
173
+ size: ButtonSize = .large,
174
+ iconLeft: String,
175
+ iconRight: String = "",
176
+ isFull: Bool = true,
177
+ loading: Bool = false,
178
+ useTintColor: Bool = true
179
+ ) {
180
+ self.init(
181
+ title: title,
182
+ action: action,
183
+ type: type,
184
+ size: size,
185
+ iconLeft: iconLeft.isEmpty ? nil : AnyView(Icon(source: iconLeft)),
186
+ iconRight: iconRight.isEmpty ? nil : AnyView(Icon(source: iconRight)),
187
+ isFull: isFull,
188
+ loading: loading,
189
+ useTintColor: useTintColor
190
+ )
191
+ }
192
+
165
193
  private func shouldLoadingOnLeft(_ left: AnyView?, _ right: AnyView?) -> Bool {
166
194
  let hasLeft = left != nil
167
195
  let hasRight = right != nil
@@ -214,14 +242,15 @@ public struct Button: View {
214
242
  .frame(maxWidth: isFull ? .infinity : nil)
215
243
  .padding(.horizontal, size.padding)
216
244
  .frame(height: size.height)
217
- .background(bg)
218
- .overlay(
219
- RoundedRectangle(cornerRadius: size.radius)
220
- .strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
221
- )
222
- .clipShape(RoundedRectangle(cornerRadius: size.radius))
223
245
  }
224
- .buttonStyle(ButtonPressFeedbackStyle(isEnabled: isEnabled))
246
+ .buttonStyle(ButtonPressFeedbackStyle(
247
+ isEnabled: isEnabled,
248
+ background: bg,
249
+ border: border,
250
+ borderWidth: borderWidth,
251
+ radius: size.radius,
252
+ height: size.height
253
+ ))
225
254
  .disabled(!isEnabled)
226
255
  }
227
256
 
@@ -238,12 +267,39 @@ public struct Button: View {
238
267
  }
239
268
  }
240
269
 
270
+ /// Mirrors Compose's press feedback: the background shrinks via an animated
271
+ /// horizontal inset while the content fades, rather than a plain opacity dim on
272
+ /// the whole button (matching `animatedPadding`/`alpha` in Button.kt).
241
273
  private struct ButtonPressFeedbackStyle: ButtonStyle {
242
274
  let isEnabled: Bool
275
+ let background: Color
276
+ let border: Color?
277
+ let borderWidth: CGFloat
278
+ let radius: CGFloat
279
+ let height: CGFloat
243
280
 
244
281
  func makeBody(configuration: Configuration) -> some View {
245
- configuration.label
246
- .opacity(configuration.isPressed && isEnabled ? 0.5 : 1)
247
- .animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
282
+ let pressed = configuration.isPressed && isEnabled
283
+
284
+ ZStack {
285
+ RoundedRectangle(cornerRadius: radius)
286
+ .fill(background)
287
+ .overlay(
288
+ RoundedRectangle(cornerRadius: radius)
289
+ .strokeBorder(border ?? .clear, lineWidth: border != nil ? borderWidth : 0)
290
+ )
291
+ .padding(.horizontal, pressed ? 2 : 0)
292
+
293
+ configuration.label
294
+ .opacity(pressed ? 0.5 : 1)
295
+ }
296
+ // Pin the whole button to its intended height explicitly: the bare
297
+ // RoundedRectangle background has no intrinsic size of its own, so left
298
+ // unconstrained in a ZStack inside a non-height-constraining container
299
+ // (e.g. a footer VStack) it can report an oversized ideal height and
300
+ // stretch the button — and its container — far taller than intended.
301
+ .frame(height: height)
302
+ .clipShape(RoundedRectangle(cornerRadius: radius))
303
+ .animation(.easeInOut(duration: 0.1), value: pressed)
248
304
  }
249
305
  }
@@ -94,12 +94,27 @@ public struct Carousel<Content: View>: View {
94
94
  // Offset so the current page is centered within the viewport padding.
95
95
  let baseOffset = -CGFloat(currentPage) * step + contentPadding
96
96
 
97
+ // Windows the rendered range around `currentPage` instead of instantiating all
98
+ // `virtualPageCount` slides up front — with `loop=true`, that's `itemCount * 1000`
99
+ // eager views, a real memory/perf regression versus Compose's lazy `HorizontalPager`.
100
+ // Clear spacers of the skipped width keep the HStack's total layout size (and thus
101
+ // the existing `baseOffset` math) unchanged.
102
+ let windowRadius = 3
103
+ let windowStart = max(0, currentPage - windowRadius)
104
+ let windowEnd = min(virtualPageCount - 1, currentPage + windowRadius)
105
+
97
106
  ZStack {
98
107
  HStack(spacing: 0) {
99
- ForEach(0 ..< virtualPageCount, id: \.self) { page in
108
+ if windowStart > 0 {
109
+ Color.clear.frame(width: CGFloat(windowStart) * step)
110
+ }
111
+ ForEach(windowStart...windowEnd, id: \.self) { page in
100
112
  slide(page: page, step: step, viewportWidth: viewportWidth)
101
113
  .frame(width: pageWidth)
102
114
  }
115
+ if windowEnd < virtualPageCount - 1 {
116
+ Color.clear.frame(width: CGFloat(virtualPageCount - 1 - windowEnd) * step)
117
+ }
103
118
  }
104
119
  .offset(x: baseOffset + dragOffset)
105
120
  .animation(isDragging ? nil : .easeInOut(duration: 0.25), value: currentPage)
@@ -28,29 +28,23 @@ public struct Checkbox: View{
28
28
  public var body: some View {
29
29
  HStack(spacing: Spacing.S) {
30
30
  // Checkbox
31
- ZStack {
32
- RoundedRectangle(cornerRadius: Radius.XS)
33
- .fill(backgroundColor)
34
- .frame(width: 20, height: 20)
35
- .overlay(
36
- RoundedRectangle(cornerRadius: Radius.XS)
37
- .stroke(borderColor, lineWidth: Spacing.XXS)
38
- )
31
+ SwiftUI.Button(action: onCheck) {
32
+ ZStack {
33
+ RoundedRectangle(cornerRadius: Radius.XS)
34
+ .fill(backgroundColor)
35
+ .frame(width: 20, height: 20)
36
+ .overlay(
37
+ RoundedRectangle(cornerRadius: Radius.XS)
38
+ .stroke(borderColor, lineWidth: Spacing.XXS)
39
+ )
39
40
 
40
- if checked {
41
- Image(systemName: indeterminate ? "minus" : "checkmark")
42
- .resizable()
43
- .scaledToFit()
44
- .foregroundColor(Colors.black01)
45
- .frame(width: 12, height: 12)
46
- }
47
- }
48
-
49
- .onTapGesture {
50
- if !disabled {
51
- onCheck()
41
+ if checked {
42
+ Icon(source: indeterminate ? "navigation_minus" : "ic_checked", size: 20, color: Colors.black01)
43
+ }
52
44
  }
53
45
  }
46
+ .buttonStyle(ActiveOpacityButtonStyle(activeOpacity: 0.8))
47
+ .disabled(disabled)
54
48
 
55
49
  // Title
56
50
  if !title.isEmpty {
@@ -52,6 +52,13 @@ public struct Chip: View {
52
52
 
53
53
  public var body: some View {
54
54
  let dims = (size == .small) ? Self.Small : Self.Large
55
+ // Mirrors Compose's `scaleSize(...)` on every chip dimension so it responds
56
+ // to Dynamic Type scaling the same way the rest of the kit does.
57
+ let height = scaleSize(dims.height)
58
+ let horizontal = scaleSize(dims.horizontal)
59
+ let iconSize = scaleSize(dims.iconSize)
60
+ let iconSpacing = scaleSize(dims.iconSpacing)
61
+ let radius = scaleSize(Radius.L)
55
62
 
56
63
  let bg: Color = {
57
64
  if selected { return theme.colors.background.selected }
@@ -63,36 +70,38 @@ public struct Chip: View {
63
70
  let leftTint: Color = selected ? theme.colors.primary : iconLeftTint ?? theme.colors.text.default
64
71
  let rightTint: Color = selected ? theme.colors.primary : iconRightTint ?? theme.colors.text.default
65
72
 
66
- HStack(spacing: dims.iconSpacing) {
67
- if let iconLeft {
68
- Icon(source: iconLeft, size: dims.iconSize, color: leftTint)
69
- }
73
+ SwiftUI.Button(action: onClick) {
74
+ HStack(spacing: iconSpacing) {
75
+ if let iconLeft {
76
+ Icon(source: iconLeft, size: iconSize, color: leftTint)
77
+ }
70
78
 
71
- if let text = label, !text.isEmpty {
72
- MomoText(text, typography: (size == .large) ? .labelDefaultMedium : .labelSMedium, color: textColor)
73
- .lineLimit(1)
74
- }
79
+ if let text = label, !text.isEmpty {
80
+ MomoText(text, typography: (size == .large) ? .labelDefaultMedium : .labelSMedium, color: textColor)
81
+ .lineLimit(1)
82
+ }
75
83
 
76
- if let iconRight {
77
- Icon(source: iconRight, size: dims.iconSize, color: rightTint)
78
- }
79
- }
80
- .padding(.horizontal, dims.horizontal)
81
- .frame(height: dims.height)
82
- .background(
83
- RoundedRectangle(cornerRadius: Radius.L, style: .continuous)
84
- .fill(bg)
85
- )
86
- .overlay(
87
- Group {
88
- if selected {
89
- RoundedRectangle(cornerRadius: Radius.L, style: .continuous)
90
- .stroke(theme.colors.secondary, lineWidth: 2)
84
+ if let iconRight {
85
+ Icon(source: iconRight, size: iconSize, color: rightTint)
91
86
  }
92
87
  }
93
- )
94
- .contentShape(RoundedRectangle(cornerRadius: Radius.L, style: .continuous))
95
- .onTapGesture(perform: onClick)
88
+ .padding(.horizontal, horizontal)
89
+ .frame(height: height)
90
+ .background(
91
+ RoundedRectangle(cornerRadius: radius, style: .continuous)
92
+ .fill(bg)
93
+ )
94
+ .overlay(
95
+ Group {
96
+ if selected {
97
+ RoundedRectangle(cornerRadius: radius, style: .continuous)
98
+ .stroke(theme.colors.secondary, lineWidth: 2)
99
+ }
100
+ }
101
+ )
102
+ .contentShape(RoundedRectangle(cornerRadius: radius, style: .continuous))
103
+ }
104
+ .buttonStyle(ActiveOpacityButtonStyle())
96
105
  .modifier(ChipAccessibilityModifier(accessibilityLabel: accessibilityLabel))
97
106
  }
98
107
  }
@@ -30,6 +30,7 @@ public struct Collapse<Content: View>: View {
30
30
  private let useBackgroundCollapseButton: Bool
31
31
  private let backgroundColor: Color?
32
32
  private let titleColor: Color?
33
+ private let accessibilityId: String?
33
34
  private let onPress: ((Bool) -> Void)?
34
35
  private let content: (() -> Content)?
35
36
 
@@ -49,6 +50,7 @@ public struct Collapse<Content: View>: View {
49
50
  useBackgroundCollapseButton: Bool = false,
50
51
  backgroundColor: Color? = nil,
51
52
  titleColor: Color? = nil,
53
+ accessibilityId: String? = nil,
52
54
  onPress: ((Bool) -> Void)? = nil,
53
55
  @ViewBuilder content: @escaping () -> Content
54
56
  ) {
@@ -65,11 +67,17 @@ public struct Collapse<Content: View>: View {
65
67
  self.useBackgroundCollapseButton = useBackgroundCollapseButton
66
68
  self.backgroundColor = backgroundColor
67
69
  self.titleColor = titleColor
70
+ self.accessibilityId = accessibilityId
68
71
  self.onPress = onPress
69
72
  self.content = content
70
73
  _expanded = State(initialValue: expandDefault)
71
74
  }
72
75
 
76
+ // Mirrors Kotlin's `accessibilityId ?: "Collapse/$title"`.
77
+ private var automationId: String {
78
+ accessibilityId ?? "Collapse/\(title)"
79
+ }
80
+
73
81
  private var resolvedBackgroundColor: Color {
74
82
  backgroundColor ?? theme.colors.background.surface
75
83
  }
@@ -97,6 +105,7 @@ public struct Collapse<Content: View>: View {
97
105
  .frame(maxWidth: .infinity)
98
106
  .background(resolvedBackgroundColor)
99
107
  .clipShape(RoundedRectangle(cornerRadius: Radius.S, style: .continuous))
108
+ .accessibilityIdentifier(automationId)
100
109
  .overlay(
101
110
  showBorder
102
111
  ? RoundedRectangle(cornerRadius: Radius.S, style: .continuous)
@@ -159,7 +168,8 @@ public struct Collapse<Content: View>: View {
159
168
  .frame(maxWidth: .infinity)
160
169
  .contentShape(Rectangle())
161
170
  }
162
- .buttonStyle(.plain)
171
+ .buttonStyle(ActiveOpacityButtonStyle())
172
+ .accessibilityIdentifier("\(automationId)|touch")
163
173
  }
164
174
 
165
175
  private var chevron: some View {
@@ -14,6 +14,8 @@ private let datePickerWithLabelsHeight: CGFloat = 238
14
14
  /// - `"HH:mm"` shows hour + minute; a lone `"HH"` switches the hour wheel to a
15
15
  /// 12-hour clock with an AM/PM column.
16
16
  public struct DateTimePicker: View {
17
+ @Environment(\.appTheme) private var theme
18
+
17
19
  // MARK: Lifecycle
18
20
 
19
21
  public init(
@@ -84,7 +86,7 @@ public struct DateTimePicker: View {
84
86
 
85
87
  VStack(spacing: 0) {
86
88
  if hasLabel {
87
- MomoText(arrayLabelTime[index], typography: .actionSBold, color: Colors.text)
89
+ MomoText(arrayLabelTime[index], typography: .actionSBold, color: theme.colors.text.default)
88
90
  .padding(.bottom, Spacing.S)
89
91
  }
90
92
 
@@ -100,7 +102,7 @@ public struct DateTimePicker: View {
100
102
  }
101
103
  .frame(maxWidth: .infinity)
102
104
  .frame(height: pickerHeight)
103
- .background(Colors.background)
105
+ .background(theme.colors.background.surface)
104
106
  .padding(.horizontal, Spacing.M)
105
107
  .onAppear { emitIfValid() }
106
108
  }
@@ -14,6 +14,8 @@ private let wheelPickerBorderWidth: CGFloat = 1
14
14
  /// real value can be centered. The currently centered row is reported back via
15
15
  /// `onChange(name, value)`.
16
16
  public struct WheelPicker: View {
17
+ @Environment(\.appTheme) private var theme
18
+
17
19
  // MARK: Lifecycle
18
20
 
19
21
  public init(
@@ -76,9 +78,9 @@ public struct WheelPicker: View {
76
78
  .frame(height: wheelItemHeight * CGFloat(wheelVisibleCount))
77
79
  .overlay(
78
80
  RoundedRectangle(cornerRadius: Radius.S)
79
- .stroke(Colors.border, lineWidth: wheelPickerBorderWidth)
81
+ .stroke(theme.colors.border.default, lineWidth: wheelPickerBorderWidth)
80
82
  )
81
- .background(Colors.background.clipShape(RoundedRectangle(cornerRadius: Radius.S)))
83
+ .background(theme.colors.background.surface.clipShape(RoundedRectangle(cornerRadius: Radius.S)))
82
84
  .overlay(
83
85
  // Centered translucent selection band.
84
86
  Rectangle()
@@ -121,6 +123,8 @@ public struct WheelPicker: View {
121
123
  // MARK: - WheelPickerItem
122
124
 
123
125
  private struct WheelPickerItem: View {
126
+ @Environment(\.appTheme) private var theme
127
+
124
128
  let text: String
125
129
  let isSelected: Bool
126
130
  let index: Int
@@ -142,7 +146,7 @@ private struct WheelPickerItem: View {
142
146
  let scale: CGFloat = isSelected ? 1.0 : 0.87
143
147
 
144
148
  return ZStack {
145
- MomoText(text, typography: .actionSBold, color: Colors.text)
149
+ MomoText(text, typography: .actionSBold, color: theme.colors.text.default)
146
150
  .scaleEffect(scale)
147
151
  }
148
152
  .frame(maxWidth: .infinity)
@@ -0,0 +1,14 @@
1
+ import SwiftUI
2
+
3
+ /// Mirrors Compose's `Modifier.activeOpacityClickable(activeOpacity:)` (modifier/Clickable.kt):
4
+ /// fades the whole tappable subtree to `activeOpacity` while pressed, animated, instead of
5
+ /// relying on a platform-default press indication.
6
+ struct ActiveOpacityButtonStyle: ButtonStyle {
7
+ var activeOpacity: Double = 0.2
8
+
9
+ func makeBody(configuration: Configuration) -> some View {
10
+ configuration.label
11
+ .opacity(configuration.isPressed ? activeOpacity : 1)
12
+ .animation(.default, value: configuration.isPressed)
13
+ }
14
+ }