@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
@@ -0,0 +1,73 @@
1
+ //
2
+ // HeaderJourney.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/HeaderJourney.kt`: a "start → end" title
6
+ // with an optional description below.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ public struct HeaderJourneyView: View {
12
+ private let data: HeaderJourneyData
13
+ private let tintColor: Color
14
+
15
+ public init(_ data: HeaderJourneyData, tintColor: Color) {
16
+ self.data = data
17
+ self.tintColor = tintColor
18
+ }
19
+
20
+ @Environment(\.screenLayout) private var layout
21
+
22
+ public var body: some View {
23
+ if data.isLoading {
24
+ VStack(alignment: .leading, spacing: Spacing.XXS) {
25
+ titleSkeleton(height: 18)
26
+ titleSkeleton(height: 12)
27
+ }
28
+ .frame(maxWidth: .infinity, alignment: .leading)
29
+ } else {
30
+ let tint = parseHeaderTint(data.tintColor) ?? tintColor
31
+ let iconTint = parseHeaderTint(data.iconColor) ?? tint
32
+ SwiftUI.Button(action: { data.onPress?() }) {
33
+ VStack(alignment: .leading, spacing: 0) {
34
+ HStack(spacing: 0) {
35
+ MomoText(data.start, typography: .actionXsBold, color: tint, maxLines: 1)
36
+ Spacer().frame(width: Spacing.S)
37
+ if data.icon.trimmingCharacters(in: .whitespaces).isEmpty {
38
+ Spacer().frame(width: 16)
39
+ } else {
40
+ Icon(source: data.icon, size: 16, color: iconTint)
41
+ }
42
+ Spacer().frame(width: Spacing.S)
43
+ if let end = data.end, !end.isEmpty {
44
+ MomoText(end, typography: .actionXsBold, color: tint, maxLines: 1)
45
+ }
46
+ Spacer().frame(width: Spacing.XS)
47
+ Icon(source: "16_arrow_chevron_down_small", size: 16, color: tint)
48
+ }
49
+ if let description = data.description, !description.isEmpty {
50
+ MomoText(description, typography: .descriptionXsRegular, color: tint, maxLines: 1)
51
+ }
52
+ }
53
+ .frame(maxWidth: headerTitleMaxWidth(layout), alignment: .leading)
54
+ }
55
+ .buttonStyle(ActiveOpacityButtonStyle())
56
+ .disabled(data.onPress == nil)
57
+ }
58
+ }
59
+ }
60
+
61
+ func titleSkeleton(height: CGFloat) -> some View {
62
+ Skeleton()
63
+ .frame(width: 120, height: height)
64
+ .clipShape(RoundedRectangle(cornerRadius: 8))
65
+ }
66
+
67
+ /// CSS hex (`RGB`, `RRGGBB`, `RRGGBBAA`) as RN passes header tints; nil when unset or invalid.
68
+ func parseHeaderTint(_ value: String?) -> Color? {
69
+ guard var digits = value?.trimmingCharacters(in: .whitespaces), !digits.isEmpty else { return nil }
70
+ if digits.hasPrefix("#") { digits.removeFirst() }
71
+ guard [3, 6, 8].contains(digits.count), digits.allSatisfy(\.isHexDigit) else { return nil }
72
+ return Color(hex: digits)
73
+ }
@@ -0,0 +1,48 @@
1
+ //
2
+ // HeaderLocation.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/HeaderLocation.kt`: a description above a
6
+ // tappable location with a chevron.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ public struct HeaderLocationView: View {
12
+ private let data: HeaderLocationData
13
+ private let tintColor: Color
14
+
15
+ public init(_ data: HeaderLocationData, tintColor: Color) {
16
+ self.data = data
17
+ self.tintColor = tintColor
18
+ }
19
+
20
+ @Environment(\.screenLayout) private var layout
21
+
22
+ public var body: some View {
23
+ if data.isLoading {
24
+ VStack(alignment: .leading, spacing: Spacing.XXS) {
25
+ titleSkeleton(height: 12)
26
+ titleSkeleton(height: 18)
27
+ }
28
+ .frame(maxWidth: .infinity, alignment: .leading)
29
+ } else {
30
+ let tint = parseHeaderTint(data.tintColor) ?? tintColor
31
+ SwiftUI.Button(action: { data.onPress?() }) {
32
+ VStack(alignment: .leading, spacing: 0) {
33
+ if let description = data.description, !description.isEmpty {
34
+ MomoText(description, typography: .descriptionXsRegular, color: tint, maxLines: 1)
35
+ }
36
+ HStack(spacing: 0) {
37
+ MomoText(data.location, typography: .actionXsBold, color: tint, maxLines: 1)
38
+ Spacer().frame(width: Spacing.XS)
39
+ Icon(source: "16_arrow_chevron_down_small", size: 16, color: tint)
40
+ }
41
+ }
42
+ .frame(maxWidth: headerTitleMaxWidth(layout), alignment: .leading)
43
+ }
44
+ .buttonStyle(ActiveOpacityButtonStyle())
45
+ .disabled(data.onPress == nil)
46
+ }
47
+ }
48
+ }
@@ -0,0 +1,425 @@
1
+ //
2
+ // HeaderRight.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/HeaderRight.kt`: the mini-app toolkit
6
+ // (shortcut / more / single tool, help center and close) in the bar or the iPhone Duo side bar.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ /// Mirrors Compose's `HeaderRight.Toolkit`.
12
+ public struct HeaderRightData {
13
+ var useShortcut: Bool
14
+ var useMore: Bool
15
+ var tools: [ToolGroup]
16
+ var toolCallback: ((String) -> Void)?
17
+ var useSystemTools: Bool
18
+ /// Close dismisses only this mini app instead of returning home.
19
+ var useCloseIcon: Bool
20
+
21
+ public init(
22
+ useShortcut: Bool = false,
23
+ useMore: Bool = false,
24
+ tools: [ToolGroup] = [],
25
+ toolCallback: ((String) -> Void)? = nil,
26
+ useSystemTools: Bool = true,
27
+ useCloseIcon: Bool = false
28
+ ) {
29
+ self.useShortcut = useShortcut
30
+ self.useMore = useMore
31
+ self.tools = tools
32
+ self.toolCallback = toolCallback
33
+ self.useSystemTools = useSystemTools
34
+ self.useCloseIcon = useCloseIcon
35
+ }
36
+
37
+ var closeIcon: String { useCloseIcon ? "16_navigation_close_circle" : "16_basic_home" }
38
+
39
+ var showBadge: Bool { tools.contains { group in group.items.contains { $0.showBadge } } }
40
+ }
41
+
42
+ public struct Tool {
43
+ var key: String
44
+ var icon: String
45
+ var showBadge: Bool
46
+ var name: [String: String]
47
+ var showRightIcon: Bool
48
+
49
+ public init(
50
+ key: String,
51
+ icon: String,
52
+ name: [String: String] = [:],
53
+ showBadge: Bool = false,
54
+ showRightIcon: Bool = true
55
+ ) {
56
+ self.key = key
57
+ self.icon = icon
58
+ self.showBadge = showBadge
59
+ self.name = name
60
+ self.showRightIcon = showRightIcon
61
+ }
62
+
63
+ func toMap() -> [String: Any] {
64
+ [
65
+ "key": key,
66
+ "icon": icon,
67
+ "showBadge": showBadge,
68
+ "name": name,
69
+ "showRightIcon": showRightIcon
70
+ ]
71
+ }
72
+ }
73
+
74
+ public struct ToolGroup {
75
+ var title: [String: String]
76
+ var items: [Tool]
77
+
78
+ public init(title: [String: String] = [:], items: [Tool]) {
79
+ self.title = title
80
+ self.items = items
81
+ }
82
+
83
+ func toMap() -> [String: Any] {
84
+ [
85
+ "title": title,
86
+ "items": items.map { $0.toMap() }
87
+ ]
88
+ }
89
+ }
90
+
91
+ private let helpCenterAppId = "vn.momo.helpcenter"
92
+
93
+ /// The Help Center mini app hides its own help button; without a context there is nothing to open.
94
+ private func usesHelpCenter(_ context: MiniAppContext?) -> Bool {
95
+ guard let context else { return false }
96
+ return context.appId != helpCenterAppId
97
+ }
98
+
99
+ private func showsShortcut(_ data: HeaderRightData, _ context: MiniAppContext?) -> Bool {
100
+ data.useShortcut && !(data.useMore && context == nil)
101
+ }
102
+
103
+ /// Height of the vertical toolkit: the optional shortcut, then the help/home capsule.
104
+ func toolkitColumnHeight(_ data: HeaderRightData, context: MiniAppContext?) -> CGFloat {
105
+ let shortcut = showsShortcut(data, context) ? DuoMetrics.controlSize + Spacing.M : 0
106
+ return shortcut + (usesHelpCenter(context) ? 2 * DuoMetrics.itemPitch : DuoMetrics.controlSize)
107
+ }
108
+
109
+ /// A header right that renders the kit toolkit, so the Duo side bar can take it over.
110
+ protocol ToolkitHeaderRightView {
111
+ var headerRight: HeaderRightData? { get }
112
+ var tintColor: Color? { get }
113
+ }
114
+
115
+ /// Toolkit data when `headerRight` renders the kit toolkit; nil for a custom header right.
116
+ func toolkitHeaderRight(_ headerRight: (() -> any View)?) -> (data: HeaderRightData?, tintColor: Color?)? {
117
+ guard let toolkit = headerRight?() as? ToolkitHeaderRightView else { return nil }
118
+ return (toolkit.headerRight, toolkit.tintColor)
119
+ }
120
+
121
+ // MARK: - Public views
122
+
123
+ /// The kit toolkit. Works under `NavigationContainer` (keyed environment) and under hosts that
124
+ /// inject `ApplicationEnvironment` with `.environmentObject`.
125
+ public struct HeaderRight: View, ToolkitHeaderRightView {
126
+ var headerRight: HeaderRightData?
127
+ var tintColor: Color?
128
+
129
+ public init(headerRight: HeaderRightData? = nil, tintColor: Color? = nil) {
130
+ self.headerRight = headerRight
131
+ self.tintColor = tintColor
132
+ }
133
+
134
+ public var body: some View {
135
+ Toolkit(headerRight: headerRight ?? HeaderRightData(), tintColor: tintColor)
136
+ }
137
+ }
138
+
139
+ // MARK: - Toolkit
140
+
141
+ private struct NavigationButtonConfig {
142
+ let icon: String
143
+ let onPress: () -> Void
144
+ }
145
+
146
+ /// `sideBarColumn`: the iPhone Duo side bar; `sideBarRow`: the Duo header row on the status bar, same 48pt controls.
147
+ enum ToolkitLayout {
148
+ case bar, sideBarColumn, sideBarRow
149
+ }
150
+
151
+ struct Toolkit: View {
152
+ let headerRight: HeaderRightData
153
+ var tintColor: Color?
154
+ var layout: ToolkitLayout = .bar
155
+
156
+ @State private var isFavorite = false
157
+ @State private var isLoading = false
158
+ @OptionalEnvironmentObject private var injectedEnvironment: ApplicationEnvironment?
159
+ @Environment(\.applicationEnvironment) private var keyedEnvironment
160
+ @Environment(\.headerColor) private var barColor
161
+
162
+ private var environment: ApplicationEnvironment { injectedEnvironment ?? keyedEnvironment }
163
+ private var context: MiniAppContext? { environment.applicationContext }
164
+
165
+ /// The bar's colors when inside a header, otherwise derived from the tint like a plain header.
166
+ private var headerColor: HeaderColor {
167
+ barColor ?? getHeaderColor(isAnimated: false, isSurface: true, opacity: 1, tintColor: tintColor)
168
+ }
169
+
170
+ private func apiValue(_ value: Any?) -> Any { value ?? NSNull() }
171
+
172
+ private var contextMap: [String: Any] {
173
+ [
174
+ "appId": apiValue(context?.appId),
175
+ "code": apiValue(context?.appCode),
176
+ "name": apiValue(context?.appName),
177
+ "icon": apiValue(context?.appIcon),
178
+ "description": apiValue(context?.description),
179
+ "support": apiValue(context?.support),
180
+ "toolkitConfig": apiValue(context?.toolkitConfig),
181
+ "providerId": apiValue(context?.providerId),
182
+ "permissions": apiValue(context?.permissions)
183
+ ]
184
+ }
185
+
186
+ private func parseResponse(_ response: String) -> Any? {
187
+ guard let data = response.data(using: .utf8),
188
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
189
+ return nil
190
+ }
191
+ return json["response"]
192
+ }
193
+
194
+ private typealias MaxApiRequest = (MaxApi, [String: Any?], @escaping ([String: Any?]?) -> Void) -> Void
195
+
196
+ /// Through `composeApi` while the host still passes one, else `maxApi` (Compose `IMaxApi`).
197
+ /// Hosts may answer off the main thread, so the response is handed back on it.
198
+ private func request(
199
+ _ funcName: String,
200
+ _ params: [String: Any],
201
+ maxApi maxApiRequest: MaxApiRequest,
202
+ onResponse: @escaping (Any?) -> Void = { _ in }
203
+ ) {
204
+ if let composeApi = environment.composeApi {
205
+ _ = composeApi.request(funcName: funcName, params: params) { response in
206
+ let value = parseResponse(response)
207
+ DispatchQueue.main.async { onResponse(value) }
208
+ }
209
+ } else if let maxApi = environment.maxApi {
210
+ maxApiRequest(maxApi, params.mapValues { $0 as Any? }) { data in
211
+ let value = data?["response"] ?? nil
212
+ DispatchQueue.main.async { onResponse(value) }
213
+ }
214
+ }
215
+ }
216
+
217
+ // MARK: Actions
218
+
219
+ private func loadFavoriteState() {
220
+ request(
221
+ "isFavoriteApp",
222
+ ["code": apiValue(context?.appCode)],
223
+ maxApi: { $0.isFavoriteApp($1, callback: $2) }
224
+ ) { response in
225
+ isFavorite = response as? Bool == true
226
+ }
227
+ }
228
+
229
+ private func onPressShortcut() {
230
+ request(
231
+ "onToolAction",
232
+ ["item": ["key": "onFavorite"], "context": contextMap],
233
+ maxApi: { $0.onToolAction($1, callback: $2) }
234
+ ) { response in
235
+ if let result = response as? [String: Any], result["success"] as? Bool == true {
236
+ isFavorite.toggle()
237
+ }
238
+ }
239
+ }
240
+
241
+ private func onPressMore() {
242
+ request(
243
+ "showTools",
244
+ [
245
+ "useSystemTools": headerRight.useSystemTools,
246
+ "tools": headerRight.tools.map { $0.toMap() },
247
+ "context": contextMap
248
+ ],
249
+ maxApi: { $0.showTools($1, callback: $2) }
250
+ ) { response in
251
+ if let tool = response as? String {
252
+ headerRight.toolCallback?(tool)
253
+ }
254
+ }
255
+ }
256
+
257
+ private func onPressHelpCenter() {
258
+ guard context != nil else { return }
259
+ request(
260
+ "showHelpCenter",
261
+ [
262
+ "appId": apiValue(context?.appId),
263
+ "code": apiValue(context?.appCode),
264
+ "name": apiValue(context?.appName),
265
+ "icon": apiValue(context?.appIcon),
266
+ "description": apiValue(context?.description)
267
+ ],
268
+ maxApi: { $0.showHelpCenter($1, callback: $2) }
269
+ )
270
+ }
271
+
272
+ private func onPressClose() {
273
+ if headerRight.useCloseIcon {
274
+ environment.requestDismiss()
275
+ } else if let composeApi = environment.composeApi {
276
+ _ = composeApi.request(funcName: "dismissAll", params: nil) { _ in }
277
+ } else {
278
+ environment.maxApi?.dismissAll(completion: nil)
279
+ }
280
+ }
281
+
282
+ private var navigationButtonConfig: NavigationButtonConfig {
283
+ let totalTools = headerRight.tools.reduce(0) { $0 + $1.items.count }
284
+ if totalTools > 1 || headerRight.useMore {
285
+ return NavigationButtonConfig(icon: "navigation_more_icon", onPress: onPressMore)
286
+ }
287
+ if totalTools == 1, let tool = headerRight.tools.first?.items.first {
288
+ return NavigationButtonConfig(icon: tool.icon) { headerRight.toolCallback?(tool.key) }
289
+ }
290
+ return NavigationButtonConfig(icon: isFavorite ? "pin_star_checked" : "pin_star", onPress: onPressShortcut)
291
+ }
292
+
293
+ // MARK: Body
294
+
295
+ var body: some View {
296
+ Group {
297
+ switch layout {
298
+ case .bar:
299
+ horizontalBody
300
+ case .sideBarColumn:
301
+ VStack(spacing: Spacing.M) { sideBarControls(horizontal: false) }
302
+ case .sideBarRow:
303
+ HStack(spacing: Spacing.M) { sideBarControls(horizontal: true) }
304
+ }
305
+ }
306
+ .onAppear(perform: loadFavoriteState)
307
+ }
308
+
309
+ @ViewBuilder
310
+ private func sideBarControls(horizontal: Bool) -> some View {
311
+ let config = navigationButtonConfig
312
+ let useHelpCenter = usesHelpCenter(context)
313
+ if showsShortcut(headerRight, context) {
314
+ SideBarButton(
315
+ icon: config.icon,
316
+ headerColor: headerColor,
317
+ showBadge: headerRight.showBadge,
318
+ disabled: isLoading,
319
+ action: config.onPress
320
+ )
321
+ }
322
+ SideBarGroup(headerColor: headerColor, itemCount: useHelpCenter ? 2 : 1, horizontal: horizontal) {
323
+ if useHelpCenter {
324
+ SideBarGroupItem(
325
+ icon: "help_center",
326
+ headerColor: headerColor,
327
+ accessibilityId: "btn_navigation_help_center",
328
+ action: onPressHelpCenter
329
+ )
330
+ }
331
+ SideBarGroupItem(
332
+ icon: headerRight.closeIcon,
333
+ headerColor: headerColor,
334
+ accessibilityId: "btn_navigation_close",
335
+ action: onPressClose
336
+ )
337
+ }
338
+ }
339
+
340
+ private var horizontalBody: some View {
341
+ let config = navigationButtonConfig
342
+ return HStack(alignment: .center, spacing: 0) {
343
+ if showsShortcut(headerRight, context) {
344
+ NavigationButton(
345
+ disabled: isLoading,
346
+ icon: config.icon,
347
+ showBadge: headerRight.showBadge,
348
+ headerColor: headerColor,
349
+ onClick: config.onPress
350
+ )
351
+ }
352
+
353
+ HStack(alignment: .center, spacing: 0) {
354
+ if usesHelpCenter(context) {
355
+ toolButton(icon: "help_center", accessibilityId: "btn_navigation_help_center", action: onPressHelpCenter)
356
+ Rectangle()
357
+ .fill(tintColor ?? Colors.black20)
358
+ .frame(width: 0.5, height: 12)
359
+ }
360
+ toolButton(icon: headerRight.closeIcon, accessibilityId: "btn_navigation_close", action: onPressClose)
361
+ }
362
+ .frame(width: 65, height: 28)
363
+ .background(headerColor.backgroundButton)
364
+ .cornerRadius(14)
365
+ .overlay(RoundedRectangle(cornerRadius: 14).stroke(headerColor.borderColor, lineWidth: 0.2))
366
+ .padding(.leading, Spacing.S)
367
+ }
368
+ }
369
+
370
+ private func toolButton(icon: String, accessibilityId: String, action: @escaping () -> Void) -> some View {
371
+ SwiftUI.Button(action: action) {
372
+ Icon(source: icon, size: 20, color: headerColor.tintIconColor)
373
+ .padding(4)
374
+ }
375
+ .buttonStyle(ActiveOpacityButtonStyle())
376
+ .accessibilityIdentifier(accessibilityId)
377
+ }
378
+ }
379
+
380
+ /// Mirrors Compose's `NavigationButton`: the 28pt circular shortcut / more button.
381
+ public struct NavigationButton: View {
382
+ var disabled: Bool
383
+ var icon: String
384
+ var showBadge: Bool
385
+ var headerColor: HeaderColor?
386
+ var onClick: () -> Void
387
+
388
+ @Environment(\.headerColor) private var barColor
389
+
390
+ /// Colors follow the enclosing header, so it fits a custom header right next to the toolkit.
391
+ public init(icon: String, showBadge: Bool = false, disabled: Bool = false, onClick: @escaping () -> Void) {
392
+ self.init(disabled: disabled, icon: icon, showBadge: showBadge, headerColor: nil, onClick: onClick)
393
+ }
394
+
395
+ init(disabled: Bool, icon: String, showBadge: Bool, headerColor: HeaderColor?, onClick: @escaping () -> Void) {
396
+ self.disabled = disabled
397
+ self.icon = icon
398
+ self.showBadge = showBadge
399
+ self.headerColor = headerColor
400
+ self.onClick = onClick
401
+ }
402
+
403
+ private var colors: HeaderColor {
404
+ headerColor ?? barColor ?? getHeaderColor(isAnimated: false, isSurface: true, opacity: 1, tintColor: nil)
405
+ }
406
+
407
+ public var body: some View {
408
+ SwiftUI.Button(action: onClick) {
409
+ ZStack {
410
+ Circle()
411
+ .fill(colors.backgroundButton)
412
+ .overlay(Circle().stroke(colors.borderColor, lineWidth: 0.2))
413
+ Icon(source: icon, size: 20, color: colors.tintIconColor)
414
+ }
415
+ .frame(width: 28, height: 28)
416
+ .overlay(alignment: .topTrailing) {
417
+ if showBadge {
418
+ BadgeDot(size: .small).offset(x: -Spacing.XXS, y: -Spacing.XXS)
419
+ }
420
+ }
421
+ }
422
+ .buttonStyle(ActiveOpacityButtonStyle())
423
+ .disabled(disabled)
424
+ }
425
+ }
@@ -0,0 +1,36 @@
1
+ //
2
+ // HeaderTitle.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/HeaderTitle.kt`: the plain header title.
6
+ //
7
+
8
+ import SwiftUI
9
+
10
+ /// `actionSBold` at 15/22 in the header family, single line.
11
+ struct HeaderTitleText: View {
12
+ let title: String
13
+ let color: Color
14
+ var centered: Bool = false
15
+
16
+ init(_ title: String, color: Color, centered: Bool = false) {
17
+ self.title = title
18
+ self.color = color
19
+ self.centered = centered
20
+ }
21
+
22
+ var body: some View {
23
+ MomoText(
24
+ title,
25
+ typography: .actionSBold,
26
+ color: color,
27
+ textAlign: centered ? .center : .leading,
28
+ maxLines: 1,
29
+ truncationMode: .tail,
30
+ accessibilityId: "title_navigation_header",
31
+ fontFamily: headerFontFamily
32
+ )
33
+ .fontSize(15, lineHeight: 22)
34
+ .frame(maxWidth: centered ? .infinity : nil, alignment: centered ? .center : .leading)
35
+ }
36
+ }