@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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 (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  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/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -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,386 @@
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
+ @available(*, deprecated, renamed: "HeaderRight", message: "HeaderRight now reads both environment styles.")
140
+ public struct NavigationHeaderRight: View, ToolkitHeaderRightView {
141
+ var headerRight: HeaderRightData?
142
+ var tintColor: Color?
143
+
144
+ public init(headerRight: HeaderRightData? = nil, tintColor: Color? = nil) {
145
+ self.headerRight = headerRight
146
+ self.tintColor = tintColor
147
+ }
148
+
149
+ public var body: some View {
150
+ Toolkit(headerRight: headerRight ?? HeaderRightData(), tintColor: tintColor)
151
+ }
152
+ }
153
+
154
+ // MARK: - Toolkit
155
+
156
+ private struct NavigationButtonConfig {
157
+ let icon: String
158
+ let onPress: () -> Void
159
+ }
160
+
161
+ struct Toolkit: View {
162
+ let headerRight: HeaderRightData
163
+ var tintColor: Color?
164
+ /// iPhone Duo side bar column instead of the bar row.
165
+ var vertical: Bool = false
166
+
167
+ @State private var isFavorite = false
168
+ @State private var isLoading = false
169
+ @OptionalEnvironmentObject private var injectedEnvironment: ApplicationEnvironment?
170
+ @Environment(\.applicationEnvironment) private var keyedEnvironment
171
+ @Environment(\.headerColor) private var barColor
172
+
173
+ private var environment: ApplicationEnvironment { injectedEnvironment ?? keyedEnvironment }
174
+ private var context: MiniAppContext? { environment.applicationContext }
175
+
176
+ /// The bar's colors when inside a header, otherwise derived from the tint like a plain header.
177
+ private var headerColor: HeaderColor {
178
+ barColor ?? getHeaderColor(isAnimated: false, isSurface: true, opacity: 1, tintColor: tintColor)
179
+ }
180
+
181
+ private func apiValue(_ value: Any?) -> Any { value ?? NSNull() }
182
+
183
+ private var contextMap: [String: Any] {
184
+ [
185
+ "appId": apiValue(context?.appId),
186
+ "code": apiValue(context?.appCode),
187
+ "name": apiValue(context?.appName),
188
+ "icon": apiValue(context?.appIcon),
189
+ "description": apiValue(context?.description),
190
+ "support": apiValue(context?.support),
191
+ "toolkitConfig": apiValue(context?.toolkitConfig),
192
+ "providerId": apiValue(context?.providerId),
193
+ "permissions": apiValue(context?.permissions)
194
+ ]
195
+ }
196
+
197
+ private func parseResponse(_ response: String) -> Any? {
198
+ guard let data = response.data(using: .utf8),
199
+ let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
200
+ return nil
201
+ }
202
+ return json["response"]
203
+ }
204
+
205
+ // MARK: Actions
206
+
207
+ private func loadFavoriteState() {
208
+ _ = environment.composeApi?.request(
209
+ funcName: "isFavoriteApp",
210
+ params: ["code": apiValue(context?.appCode)]
211
+ ) { response in
212
+ isFavorite = parseResponse(response) as? Bool == true
213
+ }
214
+ }
215
+
216
+ private func onPressShortcut() {
217
+ _ = environment.composeApi?.request(
218
+ funcName: "onToolAction",
219
+ params: ["item": ["key": "onFavorite"], "context": contextMap]
220
+ ) { response in
221
+ if let result = parseResponse(response) as? [String: Any], result["success"] as? Bool == true {
222
+ isFavorite.toggle()
223
+ }
224
+ }
225
+ }
226
+
227
+ private func onPressMore() {
228
+ _ = environment.composeApi?.request(
229
+ funcName: "showTools",
230
+ params: [
231
+ "useSystemTools": headerRight.useSystemTools,
232
+ "tools": headerRight.tools.map { $0.toMap() },
233
+ "context": contextMap
234
+ ]
235
+ ) { response in
236
+ if let tool = parseResponse(response) as? String {
237
+ headerRight.toolCallback?(tool)
238
+ }
239
+ }
240
+ }
241
+
242
+ private func onPressHelpCenter() {
243
+ guard context != nil else { return }
244
+ _ = environment.composeApi?.request(
245
+ funcName: "showHelpCenter",
246
+ params: [
247
+ "appId": apiValue(context?.appId),
248
+ "code": apiValue(context?.appCode),
249
+ "name": apiValue(context?.appName),
250
+ "icon": apiValue(context?.appIcon),
251
+ "description": apiValue(context?.description)
252
+ ]
253
+ ) { _ in }
254
+ }
255
+
256
+ private func onPressClose() {
257
+ if headerRight.useCloseIcon {
258
+ environment.requestDismiss()
259
+ } else {
260
+ _ = environment.composeApi?.request(funcName: "dismissAll", params: nil) { _ in }
261
+ }
262
+ }
263
+
264
+ private var navigationButtonConfig: NavigationButtonConfig {
265
+ let totalTools = headerRight.tools.reduce(0) { $0 + $1.items.count }
266
+ if totalTools > 1 || headerRight.useMore {
267
+ return NavigationButtonConfig(icon: "navigation_more_icon", onPress: onPressMore)
268
+ }
269
+ if totalTools == 1, let tool = headerRight.tools.first?.items.first {
270
+ return NavigationButtonConfig(icon: tool.icon) { headerRight.toolCallback?(tool.key) }
271
+ }
272
+ return NavigationButtonConfig(icon: isFavorite ? "pin_star_checked" : "pin_star", onPress: onPressShortcut)
273
+ }
274
+
275
+ // MARK: Body
276
+
277
+ var body: some View {
278
+ Group {
279
+ if vertical {
280
+ verticalBody
281
+ } else {
282
+ horizontalBody
283
+ }
284
+ }
285
+ .onAppear(perform: loadFavoriteState)
286
+ }
287
+
288
+ private var verticalBody: some View {
289
+ let config = navigationButtonConfig
290
+ let useHelpCenter = usesHelpCenter(context)
291
+ return VStack(spacing: Spacing.M) {
292
+ if showsShortcut(headerRight, context) {
293
+ SideBarButton(
294
+ icon: config.icon,
295
+ headerColor: headerColor,
296
+ showBadge: headerRight.showBadge,
297
+ disabled: isLoading,
298
+ action: config.onPress
299
+ )
300
+ }
301
+ SideBarGroup(headerColor: headerColor, itemCount: useHelpCenter ? 2 : 1) {
302
+ if useHelpCenter {
303
+ SideBarGroupItem(
304
+ icon: "help_center",
305
+ headerColor: headerColor,
306
+ accessibilityId: "btn_navigation_help_center",
307
+ action: onPressHelpCenter
308
+ )
309
+ }
310
+ SideBarGroupItem(
311
+ icon: headerRight.closeIcon,
312
+ headerColor: headerColor,
313
+ accessibilityId: "btn_navigation_close",
314
+ action: onPressClose
315
+ )
316
+ }
317
+ }
318
+ }
319
+
320
+ private var horizontalBody: some View {
321
+ let config = navigationButtonConfig
322
+ return HStack(alignment: .center, spacing: 0) {
323
+ if showsShortcut(headerRight, context) {
324
+ NavigationButton(
325
+ disabled: isLoading,
326
+ icon: config.icon,
327
+ showBadge: headerRight.showBadge,
328
+ headerColor: headerColor,
329
+ onClick: config.onPress
330
+ )
331
+ }
332
+
333
+ HStack(alignment: .center, spacing: 0) {
334
+ if usesHelpCenter(context) {
335
+ toolButton(icon: "help_center", accessibilityId: "btn_navigation_help_center", action: onPressHelpCenter)
336
+ Rectangle()
337
+ .fill(tintColor ?? Colors.black20)
338
+ .frame(width: 0.5, height: 12)
339
+ }
340
+ toolButton(icon: headerRight.closeIcon, accessibilityId: "btn_navigation_close", action: onPressClose)
341
+ }
342
+ .frame(width: 65, height: 28)
343
+ .background(headerColor.backgroundButton)
344
+ .cornerRadius(14)
345
+ .overlay(RoundedRectangle(cornerRadius: 14).stroke(headerColor.borderColor, lineWidth: 0.2))
346
+ .padding(.leading, Spacing.S)
347
+ }
348
+ }
349
+
350
+ private func toolButton(icon: String, accessibilityId: String, action: @escaping () -> Void) -> some View {
351
+ SwiftUI.Button(action: action) {
352
+ Icon(source: icon, size: 20, color: headerColor.tintIconColor)
353
+ .padding(4)
354
+ }
355
+ .buttonStyle(ActiveOpacityButtonStyle())
356
+ .accessibilityIdentifier(accessibilityId)
357
+ }
358
+ }
359
+
360
+ /// Mirrors Compose's `NavigationButton`: the 28pt circular shortcut / more button.
361
+ struct NavigationButton: View {
362
+ var disabled: Bool
363
+ var icon: String
364
+ var showBadge: Bool
365
+ var headerColor: HeaderColor
366
+ var onClick: () -> Void
367
+
368
+ var body: some View {
369
+ SwiftUI.Button(action: onClick) {
370
+ ZStack {
371
+ Circle()
372
+ .fill(headerColor.backgroundButton)
373
+ .overlay(Circle().stroke(headerColor.borderColor, lineWidth: 0.2))
374
+ Icon(source: icon, size: 20, color: headerColor.tintIconColor)
375
+ }
376
+ .frame(width: 28, height: 28)
377
+ .overlay(alignment: .topTrailing) {
378
+ if showBadge {
379
+ BadgeDot(size: .small).offset(x: -Spacing.XXS, y: -Spacing.XXS)
380
+ }
381
+ }
382
+ }
383
+ .buttonStyle(ActiveOpacityButtonStyle())
384
+ .disabled(disabled)
385
+ }
386
+ }
@@ -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
+ }
@@ -2,7 +2,7 @@
2
2
  // HeaderUser.swift
3
3
  // MoMoUIKits
4
4
  //
5
- // SwiftUI mirror of Compose's `HeaderUser` (navigation/component/HeaderUser.kt).
5
+ // SwiftUI mirror of Compose's `navigation/component/HeaderUser.kt`.
6
6
  // Renders a `HeaderTitle.User`: an avatar group (1..n avatars with overflow),
7
7
  // a title with optional inline icons, and an optional subtitle. Shows a
8
8
  // skeleton shimmer placeholder while `isLoading`.
@@ -10,7 +10,6 @@
10
10
 
11
11
  import SwiftUI
12
12
 
13
- @available(iOS 16.0, *)
14
13
  public struct HeaderUserView: View {
15
14
  private let data: HeaderUserData
16
15
 
@@ -20,9 +19,7 @@ public struct HeaderUserView: View {
20
19
 
21
20
  private let avatarSize: CGFloat = Spacing.XXL // 32
22
21
 
23
- private var maxWidth: CGFloat {
24
- UIScreen.main.bounds.width - 172
25
- }
22
+ @Environment(\.screenLayout) private var layout
26
23
 
27
24
  public var body: some View {
28
25
  if data.isLoading {
@@ -33,6 +30,14 @@ public struct HeaderUserView: View {
33
30
  }
34
31
 
35
32
  private var content: some View {
33
+ SwiftUI.Button(action: { data.onPress?() }) {
34
+ row
35
+ }
36
+ .buttonStyle(ActiveOpacityButtonStyle())
37
+ .disabled(data.onPress == nil)
38
+ }
39
+
40
+ private var row: some View {
36
41
  HStack(spacing: 0) {
37
42
  HStack(spacing: 0) {
38
43
  ZStack(alignment: .bottomTrailing) {
@@ -49,8 +54,7 @@ public struct HeaderUserView: View {
49
54
 
50
55
  VStack(alignment: .leading, spacing: 0) {
51
56
  HStack(spacing: 0) {
52
- MomoText(data.title, typography: .actionXsBold, color: data.tintColor ?? Colors.black17, maxLines: 1)
53
- .truncationMode(.tail)
57
+ MomoText(data.title, typography: .actionXsBold, color: data.tintColor, maxLines: 1, fontFamily: headerFontFamily)
54
58
 
55
59
  if !data.icons.isEmpty {
56
60
  Spacer().frame(width: 6)
@@ -66,34 +70,27 @@ public struct HeaderUserView: View {
66
70
 
67
71
  if let subTitle = data.subTitle, !subTitle.isEmpty {
68
72
  Spacer().frame(height: 2)
69
- MomoText(subTitle, typography: .descriptionXsRegular, color: data.tintColor ?? Colors.black17, maxLines: 1)
70
- .truncationMode(.tail)
73
+ MomoText(subTitle, typography: .descriptionXsRegular, color: data.tintColor, maxLines: 1)
71
74
  }
72
75
  }
73
76
  }
74
- .frame(maxWidth: maxWidth, alignment: .leading)
77
+ .frame(maxWidth: headerTitleMaxWidth(layout), alignment: .leading)
75
78
 
76
79
  Spacer(minLength: 0)
77
80
  }
78
81
  .frame(maxWidth: .infinity, alignment: .leading)
79
82
  .padding(.vertical, Spacing.XS)
80
83
  .contentShape(Rectangle())
81
- .onTapGesture { data.onPress?() }
82
- .allowsHitTesting(data.onPress != nil)
83
84
  }
84
85
 
85
86
  private var shimmer: some View {
86
87
  HStack(spacing: Spacing.M / 2) {
87
- Circle()
88
- .fill(Colors.black05)
88
+ Skeleton()
89
89
  .frame(width: avatarSize, height: avatarSize)
90
+ .clipShape(Circle())
90
91
  VStack(alignment: .leading, spacing: Spacing.XXS) {
91
- RoundedRectangle(cornerRadius: 8)
92
- .fill(Colors.black05)
93
- .frame(width: 120, height: 18)
94
- RoundedRectangle(cornerRadius: 8)
95
- .fill(Colors.black05)
96
- .frame(width: 120, height: 12)
92
+ titleSkeleton(height: 18)
93
+ titleSkeleton(height: 12)
97
94
  }
98
95
  }
99
96
  .frame(maxWidth: .infinity, alignment: .leading)
@@ -105,8 +102,8 @@ public struct HeaderUserView: View {
105
102
 
106
103
  /// Mirrors Compose's `AvatarGroup`: lays out 1..4 avatars in a fixed collage,
107
104
  /// falls back to initials when empty, and shows a "+n" overflow tile beyond 4.
108
- @available(iOS 16.0, *)
109
105
  private struct AvatarGroup: View {
106
+ @Environment(\.appTheme) private var theme
110
107
  let urls: [String]
111
108
  let title: String
112
109
  let size: CGFloat
@@ -176,7 +173,7 @@ private struct AvatarGroup: View {
176
173
  ImageView(url)
177
174
  .aspectRatio(contentMode: .fill)
178
175
  .clipShape(Circle())
179
- .overlay(Circle().stroke(Colors.black05, lineWidth: 0.5))
176
+ .overlay(Circle().stroke(theme.colors.border.default, lineWidth: 0.5))
180
177
  }
181
178
 
182
179
  private func overflowTile(more: Int) -> some View {
@@ -187,8 +184,8 @@ private struct AvatarGroup: View {
187
184
  }
188
185
  }
189
186
 
190
- @available(iOS 16.0, *)
191
187
  private struct InitialsAvatar: View {
188
+ @Environment(\.appTheme) private var theme
192
189
  let name: String
193
190
  let size: CGFloat
194
191
 
@@ -204,6 +201,6 @@ private struct InitialsAvatar: View {
204
201
  .frame(width: size, height: size)
205
202
  .background(Colors.pink09)
206
203
  .clipShape(Circle())
207
- .overlay(Circle().stroke(Colors.black05, lineWidth: 0.5))
204
+ .overlay(Circle().stroke(theme.colors.border.default, lineWidth: 0.5))
208
205
  }
209
206
  }