@momo-kits/native-kits 0.162.44-debug → 0.162.45-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 (199) hide show
  1. package/CLAUDE.md +45 -3
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +9 -1
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +2 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +314 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +127 -83
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +116 -26
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle/libs.versions.toml +1 -1
  61. package/gradle.properties +1 -1
  62. package/ios/Application/ApplicationEnvironment.swift +17 -0
  63. package/ios/Application/MaxApi.swift +30 -3
  64. package/ios/Avatar/Avatar.swift +4 -3
  65. package/ios/Badge/Badge.swift +6 -4
  66. package/ios/Badge/BadgeDot.swift +1 -1
  67. package/ios/Badge/BadgeRibbon.swift +0 -1
  68. package/ios/Button/Button.swift +15 -12
  69. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  70. package/ios/Carousel/Carousel.swift +29 -12
  71. package/ios/Checkbox/Checkbox.swift +7 -3
  72. package/ios/Chip/Chip.swift +5 -4
  73. package/ios/Collapse/Collapse.swift +70 -19
  74. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  75. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  76. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  77. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  78. package/ios/Divider/Divider.swift +37 -4
  79. package/ios/Extensions/Color++.swift +38 -9
  80. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  81. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  82. package/ios/Icon/Icon.swift +21 -23
  83. package/ios/Icon/Icons.swift +1329 -0
  84. package/ios/IconButton/IconButton.swift +15 -7
  85. package/ios/Image/Image.swift +7 -23
  86. package/ios/Information/Information.swift +27 -9
  87. package/ios/Input/ErrorView.swift +2 -2
  88. package/ios/Input/Input.swift +160 -147
  89. package/ios/Input/InputPhoneNumber.swift +12 -9
  90. package/ios/Input/InputSearch.swift +107 -77
  91. package/ios/Input/InputTextArea.swift +128 -145
  92. package/ios/InputDropDown/InputDropDown.swift +10 -7
  93. package/ios/InputMoney/InputMoney.swift +33 -11
  94. package/ios/InputOTP/InputOTP.swift +22 -12
  95. package/ios/Layout/Card.swift +50 -0
  96. package/ios/Layout/GridContext.swift +187 -3
  97. package/ios/Layout/Item.swift +5 -4
  98. package/ios/Layout/ScreenLayout.swift +385 -0
  99. package/ios/Layout/Section.swift +4 -100
  100. package/ios/Loader/Loader.swift +5 -9
  101. package/ios/Lottie/LottieView.swift +14 -2
  102. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  103. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  104. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  105. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  106. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  107. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  108. package/ios/Navigation/Component/Header.swift +545 -0
  109. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  110. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  111. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  112. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  113. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  114. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  115. package/ios/Navigation/Component/SideBar.swift +183 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  117. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  118. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  119. package/ios/Navigation/Navigation.swift +204 -0
  120. package/ios/Navigation/NavigationContainer.swift +195 -0
  121. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  122. package/ios/Navigation/StackScreen.swift +930 -0
  123. package/ios/Navigation/StackTransition.swift +400 -0
  124. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  125. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  126. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  127. package/ios/Pagination/PaginationScroll.swift +2 -2
  128. package/ios/Popup/PopupDisplay.swift +72 -135
  129. package/ios/Popup/PopupInput.swift +10 -13
  130. package/ios/Popup/PopupLayout.swift +140 -0
  131. package/ios/Popup/PopupNotify.swift +59 -69
  132. package/ios/Popup/PopupPromotion.swift +31 -47
  133. package/ios/PopupView/FullscreenPopup.swift +13 -19
  134. package/ios/PopupView/PopupView.swift +1 -1
  135. package/ios/PopupView/Utils++.swift +0 -8
  136. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  137. package/ios/Radio/Radio.swift +2 -1
  138. package/ios/Rating/Rating.swift +9 -1
  139. package/ios/Resources/dot_loading.json +558 -0
  140. package/ios/Resources/lottie_circle_loader.json +1 -0
  141. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  142. package/ios/Skeleton/Skeleton.swift +22 -27
  143. package/ios/Slider/Slider.swift +16 -18
  144. package/ios/Stepper/Stepper.swift +6 -1
  145. package/ios/Steps/Steps.swift +8 -6
  146. package/ios/SuggestAction/SuggestAction.swift +5 -0
  147. package/ios/Swipeable/SwipeCell.swift +10 -6
  148. package/ios/Switch/Switch.swift +22 -20
  149. package/ios/TabView/TabView.swift +102 -21
  150. package/ios/Tag/Tag.swift +15 -4
  151. package/ios/Template/Logo/Logo.swift +2 -1
  152. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  153. package/ios/Title/Title.swift +6 -17
  154. package/ios/Tooltip/Tooltip.swift +29 -14
  155. package/ios/Typography/FontScale.swift +3 -3
  156. package/ios/Typography/Text.swift +81 -19
  157. package/ios/Uploader/Uploader.swift +3 -1
  158. package/ios/native-kits.podspec +2 -1
  159. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  163. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  164. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  165. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  166. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  167. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  168. package/package.json +1 -1
  169. package/scripts/gen-ios-icons.mjs +36 -0
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  177. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  178. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  182. package/ios/Application/Components.swift +0 -270
  183. package/ios/Application/ComposeApi.swift +0 -22
  184. package/ios/Application/HeaderRight.swift +0 -306
  185. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  186. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  187. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  188. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  189. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  190. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  191. package/ios/Application/Navigation/component/Header.swift +0 -178
  192. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  193. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  194. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  195. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  196. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  197. package/ios/Application/Screen.swift +0 -250
  198. package/ios/Application/StackScreen.swift +0 -492
  199. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,94 @@
1
+ package vn.momo.kits.platform
2
+
3
+ import androidx.compose.runtime.Composable
4
+ import androidx.compose.runtime.DisposableEffect
5
+ import androidx.compose.runtime.getValue
6
+ import androidx.compose.runtime.mutableStateOf
7
+ import androidx.compose.runtime.remember
8
+ import androidx.compose.runtime.setValue
9
+ import androidx.compose.ui.uikit.LocalUIViewController
10
+ import androidx.compose.ui.unit.dp
11
+ import kotlinx.cinterop.BetaInteropApi
12
+ import kotlinx.cinterop.ExperimentalForeignApi
13
+ import kotlinx.cinterop.interpretObjCPointer
14
+ import kotlinx.cinterop.objcPtr
15
+ import kotlinx.cinterop.readValue
16
+ import kotlinx.cinterop.useContents
17
+ import platform.CoreGraphics.CGRectZero
18
+ import platform.Foundation.NSArray
19
+ import platform.Foundation.NSSelectorFromString
20
+ import platform.Foundation.NSValue
21
+ import platform.Foundation.valueForKey
22
+ import platform.UIKit.CGRectValue
23
+ import platform.UIKit.UIView
24
+ import platform.UIKit.UIViewAutoresizingFlexibleHeight
25
+ import platform.UIKit.UIViewAutoresizingFlexibleWidth
26
+ import platform.darwin.NSObject
27
+ import platform.Foundation.NSClassFromString
28
+
29
+ // The API is newer than the SDK Kotlin/Native binds, so it is reached through the runtime.
30
+ @OptIn(ExperimentalForeignApi::class)
31
+ private val regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
32
+
33
+ @OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
34
+ private fun UIView.occlusionRegions(target: UIView): List<ReservedRegion> {
35
+ if (window == null || !respondsToSelector(regionsOfKind)) return emptyList()
36
+ // a class object fails `as? NSObjectProtocol` (K/N checks it as an instance), so it is reinterpreted
37
+ val kindClass = NSClassFromString("UIViewReservedRegionKind")
38
+ ?.let { interpretObjCPointer<NSObject>(it.objcPtr()) } ?: return emptyList()
39
+ val kind = kindClass.performSelector(NSSelectorFromString("occlusionRegionKind")) ?: return emptyList()
40
+ val regions = when (val result = performSelector(regionsOfKind, withObject = kind)) {
41
+ is List<*> -> result
42
+ is NSArray -> List(result.count.toInt()) { result.objectAtIndex(it.toULong()) }
43
+ else -> return emptyList()
44
+ }
45
+ return regions.mapNotNull { region ->
46
+ val frame = ((region as? NSObject)?.valueForKey("frame") as? NSValue)?.CGRectValue ?: return@mapNotNull null
47
+ convertRect(frame, toView = target).useContents {
48
+ ReservedRegion(origin.x.dp, origin.y.dp, size.width.dp, size.height.dp)
49
+ }
50
+ }
51
+ }
52
+
53
+ // Invisible full-size probe: reading the regions in layoutSubviews makes UIKit run it again
54
+ // when they change, so nothing polls.
55
+ @OptIn(ExperimentalForeignApi::class)
56
+ private class ReservedRegionsProbe(
57
+ private val onChange: (List<ReservedRegion>) -> Unit,
58
+ ) : UIView(frame = CGRectZero.readValue()) {
59
+ private var last: List<ReservedRegion>? = null
60
+
61
+ init {
62
+ userInteractionEnabled = false
63
+ autoresizingMask = UIViewAutoresizingFlexibleWidth or UIViewAutoresizingFlexibleHeight
64
+ }
65
+
66
+ override fun layoutSubviews() {
67
+ super.layoutSubviews()
68
+ val regions = superview?.let { occlusionRegions(it) } ?: emptyList()
69
+ if (regions != last) {
70
+ last = regions
71
+ onChange(regions)
72
+ }
73
+ }
74
+
75
+ override fun didMoveToWindow() {
76
+ super.didMoveToWindow()
77
+ setNeedsLayout()
78
+ }
79
+ }
80
+
81
+ @OptIn(ExperimentalForeignApi::class)
82
+ @Composable
83
+ internal actual fun rememberReservedRegions(): List<ReservedRegion> {
84
+ if (!isPhoneIdiom()) return emptyList()
85
+ val host = LocalUIViewController.current.view
86
+ var regions by remember { mutableStateOf(emptyList<ReservedRegion>()) }
87
+ DisposableEffect(host) {
88
+ val probe = ReservedRegionsProbe { regions = it }
89
+ probe.setFrame(host.bounds)
90
+ host.insertSubview(probe, atIndex = 0)
91
+ onDispose { probe.removeFromSuperview() }
92
+ }
93
+ return regions
94
+ }
@@ -19,7 +19,7 @@ androidx-appcompat = "1.7.0"
19
19
  material = "1.10.0"
20
20
  maxapi = "0.1.1"
21
21
  vanniktechMavenPublish = "0.34.0"
22
- kits = "0.164.1-beta.1"
22
+ kits = "0.167.1-duo.1"
23
23
  nativemaxapi = "0.0.6"
24
24
  gif-image = "0.1.2"
25
25
 
package/gradle.properties CHANGED
@@ -18,7 +18,7 @@ kotlin.apple.xcodeCompatibility.nowarn=true
18
18
  name="ComposeKits"
19
19
  group=vn.momo.kits
20
20
  artifact.id=kits
21
- version=0.162.44
21
+ version=0.162.45
22
22
 
23
23
  repo=GitLab
24
24
  url=https://gitlab.mservice.com.vn/api/v4/projects/5400/packages/maven
@@ -41,6 +41,12 @@ public class KitConfig {
41
41
  public var trustBanner: TrustBannerData? = nil
42
42
  public var headerBar: String? = nil
43
43
  public var headerGradient: String? = nil
44
+
45
+ public init(trustBanner: TrustBannerData? = nil, headerBar: String? = nil, headerGradient: String? = nil) {
46
+ self.trustBanner = trustBanner
47
+ self.headerBar = headerBar
48
+ self.headerGradient = headerGradient
49
+ }
44
50
  }
45
51
 
46
52
  public class ApplicationEnvironment: ObservableObject {
@@ -59,6 +65,17 @@ public class ApplicationEnvironment: ObservableObject {
59
65
  }
60
66
  }
61
67
 
68
+ extension ApplicationEnvironment {
69
+ /// Closes the mini app: through `composeApi` as before, else `maxApi` (Compose `maxApi.dismiss`).
70
+ func requestDismiss() {
71
+ if let composeApi {
72
+ composeApi.requestCallback(funcName: "dismiss", params: nil, onResponse: nil)
73
+ } else {
74
+ maxApi?.dismiss(completion: nil)
75
+ }
76
+ }
77
+ }
78
+
62
79
  private struct ApplicationEnvironmentKey: EnvironmentKey {
63
80
  static let defaultValue = ApplicationEnvironment()
64
81
  }
@@ -5,14 +5,41 @@
5
5
  // Swift mirror of the host-app bridge exposed on Kotlin as `vn.momo.maxapi.IMaxApi`
6
6
  // (`LocalMaxApi`). `IMaxApi` itself is an external compiled dependency with no
7
7
  // source in this repo, so this protocol intentionally exposes only the methods
8
- // this kit's Kotlin source actually calls (`track`, `dismiss`, `getLanguage`) —
9
- // not a full guess at IMaxApi's real surface.
8
+ // this kit's Kotlin source actually calls — not a full guess at IMaxApi's real surface.
9
+ // Callbacks carry IMaxApi's `["response": …]` payload.
10
10
  //
11
11
 
12
- import Foundation
12
+ import UIKit
13
13
 
14
14
  public protocol MaxApi {
15
15
  func track(eventName: String, params: [String: Any?])
16
16
  func dismiss(completion: (() -> Void)?)
17
17
  func getLanguage(callback: @escaping ([String: Any?]?) -> Void)
18
+ /// `type` is the IMaxApi vibration name (`"light"`, …).
19
+ func triggerEventVibration(_ type: String)
20
+ // Toolkit (Compose `HeaderRight.Toolkit`).
21
+ func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
22
+ func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
23
+ func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
24
+ func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
25
+ /// Back to the host home, closing every mini app.
26
+ func dismissAll(completion: (() -> Void)?)
27
+ }
28
+
29
+ public extension MaxApi {
30
+ /// Hosts that don't bridge haptics still get the system light tick.
31
+ func triggerEventVibration(_ type: String) {
32
+ UIImpactFeedbackGenerator(style: .light).impactOccurred()
33
+ }
34
+
35
+ // Hosts that don't bridge the toolkit keep it inert, as before these joined the protocol.
36
+ func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
37
+ func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
38
+ func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
39
+ func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
40
+
41
+ /// Without a home to return to, closing this mini app is the closest thing.
42
+ func dismissAll(completion: (() -> Void)?) {
43
+ dismiss(completion: completion)
44
+ }
18
45
  }
@@ -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
@@ -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))