@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,493 @@
1
+ //
2
+ // Header.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/Header.kt`: the header bar (back button,
6
+ // title, header right), its colors, opacity and the header models.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ /// Header bar height, mirrors Compose's `HEADER_HEIGHT`.
12
+ public let HEADER_HEIGHT: CGFloat = 52
13
+
14
+ /// Room the back button and toolkit take from the bar (Compose `HEADER_ACTIONS_WIDTH`).
15
+ let headerActionsWidth: CGFloat = 172
16
+
17
+ public enum TitlePosition {
18
+ case left, center
19
+ }
20
+
21
+ public enum HeaderType {
22
+ case `default`, extended, none
23
+ }
24
+
25
+ /// Mirrors Compose's `HeaderType.Animated`; `layoutOffset` lives on `NavigationOptions`.
26
+ public struct AnimatedHeader {
27
+ public var aspectRatio: AnimatedHeaderRatio = .ratio16_9
28
+ public var isSurface: Bool = true
29
+ public var composable: (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
30
+
31
+ public init(
32
+ aspectRatio: AnimatedHeaderRatio = .ratio16_9,
33
+ isSurface: Bool = true,
34
+ composable: @escaping (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
35
+ ) {
36
+ self.aspectRatio = aspectRatio
37
+ self.isSurface = isSurface
38
+ self.composable = composable
39
+ }
40
+ }
41
+
42
+ // MARK: - Header title
43
+
44
+ /// Mirrors Compose's `HeaderTitle.User`.
45
+ public struct HeaderUserData {
46
+ public var title: String
47
+ public var subTitle: String?
48
+ public var image: [String]?
49
+ public var dotColor: Color?
50
+ public var tintColor: Color?
51
+ public var onPress: (() -> Void)?
52
+ public var icons: [String]
53
+ public var isLoading: Bool
54
+
55
+ public init(
56
+ title: String,
57
+ subTitle: String? = nil,
58
+ image: [String]? = nil,
59
+ dotColor: Color? = nil,
60
+ tintColor: Color? = nil,
61
+ onPress: (() -> Void)? = nil,
62
+ icons: [String] = [],
63
+ isLoading: Bool = false
64
+ ) {
65
+ self.title = title
66
+ self.subTitle = subTitle
67
+ self.image = image
68
+ self.dotColor = dotColor
69
+ self.tintColor = tintColor
70
+ self.onPress = onPress
71
+ self.icons = icons
72
+ self.isLoading = isLoading
73
+ }
74
+ }
75
+
76
+ /// Mirrors Compose's `HeaderTitle.Location`. Colors are CSS hex strings, as in RN.
77
+ public struct HeaderLocationData {
78
+ public var description: String?
79
+ public var location: String
80
+ public var tintColor: String?
81
+ public var onPress: (() -> Void)?
82
+ public var isLoading: Bool
83
+
84
+ public init(
85
+ description: String? = nil,
86
+ location: String,
87
+ tintColor: String? = nil,
88
+ onPress: (() -> Void)? = nil,
89
+ isLoading: Bool = false
90
+ ) {
91
+ self.description = description
92
+ self.location = location
93
+ self.tintColor = tintColor
94
+ self.onPress = onPress
95
+ self.isLoading = isLoading
96
+ }
97
+ }
98
+
99
+ /// Mirrors Compose's `HeaderTitle.Journey`. Colors are CSS hex strings, as in RN.
100
+ public struct HeaderJourneyData {
101
+ public var start: String
102
+ public var end: String?
103
+ public var description: String?
104
+ public var icon: String
105
+ public var iconColor: String?
106
+ public var tintColor: String?
107
+ public var onPress: (() -> Void)?
108
+ public var isLoading: Bool
109
+
110
+ public init(
111
+ start: String,
112
+ end: String? = nil,
113
+ description: String? = nil,
114
+ icon: String,
115
+ iconColor: String? = nil,
116
+ tintColor: String? = nil,
117
+ onPress: (() -> Void)? = nil,
118
+ isLoading: Bool = false
119
+ ) {
120
+ self.start = start
121
+ self.end = end
122
+ self.description = description
123
+ self.icon = icon
124
+ self.iconColor = iconColor
125
+ self.tintColor = tintColor
126
+ self.onPress = onPress
127
+ self.isLoading = isLoading
128
+ }
129
+ }
130
+
131
+ /// Mirrors Compose's `sealed class HeaderTitle`.
132
+ public enum HeaderTitle {
133
+ case `default`(String)
134
+ case user(HeaderUserData)
135
+ case location(HeaderLocationData)
136
+ case journey(HeaderJourneyData)
137
+
138
+ /// Plain title text when this is a `.default` title.
139
+ public var defaultTitle: String? {
140
+ if case let .default(text) = self { return text }
141
+ return nil
142
+ }
143
+ }
144
+
145
+ // MARK: - Back props
146
+
147
+ /// Compose's `PopupNotifyProps` as consumed by `headerBackProps.preventBack`.
148
+ public struct PreventBackProps {
149
+ public var image: String
150
+ public var title: String
151
+ public var description: String
152
+ public var error: String
153
+ public var primary: PopupAction?
154
+ public var secondary: PopupAction?
155
+ public var buttonDirection: PopupButtonDirection
156
+ public var isShowCloseIcon: Bool
157
+ public var onIconClose: (() -> Void)?
158
+
159
+ public init(
160
+ image: String = "",
161
+ title: String = "",
162
+ description: String = "",
163
+ error: String = "",
164
+ primary: PopupAction? = nil,
165
+ secondary: PopupAction? = nil,
166
+ buttonDirection: PopupButtonDirection = .row,
167
+ isShowCloseIcon: Bool = true,
168
+ onIconClose: (() -> Void)? = nil
169
+ ) {
170
+ self.image = image
171
+ self.title = title
172
+ self.description = description
173
+ self.error = error
174
+ self.primary = primary
175
+ self.secondary = secondary
176
+ self.buttonDirection = buttonDirection
177
+ self.isShowCloseIcon = isShowCloseIcon
178
+ self.onIconClose = onIconClose
179
+ }
180
+ }
181
+
182
+ /// Mirrors Compose's `HeaderBackProps`: with `preventBack`, `Navigator.onBackSafe` asks first.
183
+ public struct HeaderBackProps {
184
+ public var preventBack: PreventBackProps?
185
+
186
+ public init(preventBack: PreventBackProps? = nil) {
187
+ self.preventBack = preventBack
188
+ }
189
+ }
190
+
191
+ // MARK: - Header colors
192
+
193
+ /// Mirrors Compose's `HeaderColor`.
194
+ public struct HeaderColor: Equatable {
195
+ public let tintIconColor: Color
196
+ public let backgroundButton: Color
197
+ public let borderColor: Color
198
+ }
199
+
200
+ /// Mirrors Compose's `getHeaderColor`; an animated header is a non-nil `AnimatedHeader` here.
201
+ func getHeaderColor(
202
+ isAnimated: Bool,
203
+ isSurface: Bool,
204
+ opacity: CGFloat,
205
+ tintColor: Color?,
206
+ defaultColor: Color = Colors.black17
207
+ ) -> HeaderColor {
208
+ if isAnimated {
209
+ if opacity >= 1 || !isSurface {
210
+ return HeaderColor(
211
+ tintIconColor: Colors.black17,
212
+ backgroundButton: Colors.black01.opacity(0.6),
213
+ borderColor: Colors.black20.opacity(0.2)
214
+ )
215
+ }
216
+ return HeaderColor(
217
+ tintIconColor: Colors.black01,
218
+ backgroundButton: Colors.black20.opacity(0.6),
219
+ borderColor: .clear
220
+ )
221
+ }
222
+
223
+ if tintColor == Colors.black01 {
224
+ return HeaderColor(
225
+ tintIconColor: Colors.black01,
226
+ backgroundButton: Colors.black20.opacity(0.6),
227
+ borderColor: Colors.black01.opacity(0.2)
228
+ )
229
+ }
230
+ return HeaderColor(
231
+ tintIconColor: tintColor ?? defaultColor,
232
+ backgroundButton: Colors.black01.opacity(0.6),
233
+ borderColor: Colors.black20.opacity(0.2)
234
+ )
235
+ }
236
+
237
+ /// Mirrors Compose's `headerOpacity()`: the bar turns opaque over one header height of scroll,
238
+ /// over `layoutOffset` for an animated header, and never for a transparent one.
239
+ func headerOpacity(
240
+ scrollOffset: CGFloat,
241
+ isAnimated: Bool,
242
+ layoutOffset: CGFloat,
243
+ transparent: Bool
244
+ ) -> CGFloat {
245
+ if transparent { return 0 }
246
+ let distance = isAnimated ? layoutOffset : HEADER_HEIGHT
247
+ if distance <= 0 { return scrollOffset > 0 ? 1 : 0 }
248
+ return min(max(scrollOffset / distance, 0), 1)
249
+ }
250
+
251
+ private struct HeaderColorKey: EnvironmentKey {
252
+ static let defaultValue: HeaderColor? = nil
253
+ }
254
+
255
+ extension EnvironmentValues {
256
+ /// The enclosing header's colors, so a toolkit follows the bar over a banner image.
257
+ var headerColor: HeaderColor? {
258
+ get { self[HeaderColorKey.self] }
259
+ set { self[HeaderColorKey.self] = newValue }
260
+ }
261
+ }
262
+
263
+ /// Measured width of the header-right area, read by the animated search for its trailing inset
264
+ /// (Compose's `LocalHeaderRightWidthPx`).
265
+ public struct HeaderRightWidthKey: PreferenceKey {
266
+ public static var defaultValue: CGFloat = 0
267
+ public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
268
+ value = max(value, nextValue())
269
+ }
270
+ }
271
+
272
+ /// Compose `SearchAnimated`: a header search fills from surface to the screen background over
273
+ /// the scroll that lifts it into the bar.
274
+ func headerSearchProps(_ props: InputSearchProps, scroll: CGFloat, layout: ScreenLayout, theme: Theme) -> InputSearchProps {
275
+ guard props.backgroundColor == nil else { return props }
276
+ let collapsedTop = max(layout.headerStatusBarHeight + (layout.headerBarHeight - HEADER_HEIGHT) / 2, 1)
277
+ var themed = props
278
+ themed.backgroundColor = theme.colors.background.surface
279
+ .lerp(to: theme.colors.background.default, max(scroll, 0) / collapsedTop)
280
+ return themed
281
+ }
282
+
283
+ /// Max title width: back button and toolkit share the bar unless the iPhone Duo side bar holds them.
284
+ func headerTitleMaxWidth(_ layout: ScreenLayout) -> CGFloat {
285
+ layout.sideBar != nil ? layout.contentWidth - Spacing.M * 2 : layout.width - headerActionsWidth
286
+ }
287
+
288
+ // MARK: - Header
289
+
290
+ /// Mirrors Compose's `Header`. Named `NavigationHeader` to coexist with the deprecated `Header`.
291
+ public struct NavigationHeader: View {
292
+ var headerType: HeaderType = .default
293
+ var titlePosition: TitlePosition
294
+ var title: String
295
+ var headerTitle: HeaderTitle?
296
+ var headerRight: (() -> any View)?
297
+ var goBack: (() -> Void)?
298
+ var opacity: CGFloat = 1
299
+ var animatedHeader: AnimatedHeader?
300
+ var scrollState: CGFloat = 0
301
+ var inputSearchProps: InputSearchProps?
302
+ var tintColor: Color?
303
+ /// The animated search fades the title out as it rises into the bar.
304
+ var titleOpacity: CGFloat = 1
305
+
306
+ public init(
307
+ headerType: HeaderType = .default,
308
+ titlePosition: TitlePosition,
309
+ title: String,
310
+ headerTitle: HeaderTitle? = nil,
311
+ headerRight: (() -> any View)? = { HeaderRight() },
312
+ goBack: (() -> Void)? = nil,
313
+ opacity: CGFloat = 1,
314
+ animatedHeader: AnimatedHeader? = nil,
315
+ scrollState: CGFloat = 0,
316
+ inputSearchProps: InputSearchProps? = nil,
317
+ tintColor: Color? = nil,
318
+ titleOpacity: CGFloat = 1
319
+ ) {
320
+ self.headerType = headerType
321
+ self.titlePosition = titlePosition
322
+ self.title = title
323
+ self.headerTitle = headerTitle
324
+ self.headerRight = headerRight
325
+ self.goBack = goBack
326
+ self.opacity = opacity
327
+ self.animatedHeader = animatedHeader
328
+ self.scrollState = scrollState
329
+ self.inputSearchProps = inputSearchProps
330
+ self.tintColor = tintColor
331
+ self.titleOpacity = titleOpacity
332
+ }
333
+
334
+ @Environment(\.screenLayout) private var layout
335
+ @Environment(\.appTheme) private var theme
336
+
337
+ /// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
338
+ private var useSideBar: Bool { layout.sideBar != nil }
339
+
340
+ private var headerColor: HeaderColor {
341
+ getHeaderColor(
342
+ isAnimated: animatedHeader != nil,
343
+ isSurface: animatedHeader?.isSurface ?? true,
344
+ opacity: opacity,
345
+ tintColor: tintColor
346
+ )
347
+ }
348
+
349
+ /// A centered title overlays the whole bar so it stays screen-centered.
350
+ private var centerTitle: Bool {
351
+ guard inputSearchProps == nil else { return false }
352
+ return titlePosition == .center && (headerTitle == nil || headerTitle?.defaultTitle != nil)
353
+ }
354
+
355
+ public var body: some View {
356
+ if headerType != .none {
357
+ ZStack {
358
+ HStack(spacing: 0) {
359
+ if let goBack, !useSideBar {
360
+ BackButton(color: headerColor, onBack: goBack)
361
+ Spacer().frame(width: Spacing.S)
362
+ }
363
+
364
+ if centerTitle {
365
+ Spacer()
366
+ } else {
367
+ titleArea(centered: false)
368
+ .frame(maxWidth: .infinity, alignment: .leading)
369
+ }
370
+
371
+ if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
372
+ AnyView(headerRight())
373
+ .environment(\.headerColor, headerColor)
374
+ .background(
375
+ GeometryReader { geo in
376
+ Color.clear.preference(key: HeaderRightWidthKey.self, value: geo.size.width)
377
+ }
378
+ )
379
+ }
380
+ }
381
+ .padding(.horizontal, Spacing.M)
382
+
383
+ if centerTitle {
384
+ titleArea(centered: true)
385
+ // Compose caps a centered title at half the bar
386
+ .frame(maxWidth: (layout.contentWidth - Spacing.M * 2) / 2)
387
+ .padding(.horizontal, Spacing.M)
388
+ }
389
+ }
390
+ .frame(height: layout.headerBarHeight)
391
+ .overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
392
+ // the row starts headerStatusBarHeight below the window top: 0 with the status bar off
393
+ // the top edge, lower on the iPhone Duo outer display to center on the camera
394
+ .padding(.top, layout.headerStatusBarHeight - layout.insets.top)
395
+ }
396
+ }
397
+
398
+ /// Mirrors Compose's `HeaderContent`; a header search replaces the title.
399
+ @ViewBuilder
400
+ private func titleArea(centered: Bool) -> some View {
401
+ if let inputSearchProps {
402
+ InputSearch(props: headerSearchProps(inputSearchProps, scroll: scrollState, layout: layout, theme: theme))
403
+ } else {
404
+ switch headerTitle {
405
+ case let .user(data)?:
406
+ HeaderUserView(data)
407
+ case let .location(data)?:
408
+ HeaderLocationView(data, tintColor: headerColor.tintIconColor)
409
+ case let .journey(data)?:
410
+ HeaderJourneyView(data, tintColor: headerColor.tintIconColor)
411
+ case let .default(text)?:
412
+ HeaderTitleText(text, color: headerColor.tintIconColor, centered: centered)
413
+ .opacity(titleOpacity)
414
+ case nil:
415
+ HeaderTitleText(title, color: headerColor.tintIconColor, centered: centered)
416
+ .opacity(titleOpacity)
417
+ }
418
+ }
419
+ }
420
+ }
421
+
422
+ // MARK: - Bar background
423
+
424
+ /// Compose `Header`'s own background: surface (or the theme's header image) fading in with the
425
+ /// scroll; a transparent header stays clear.
426
+ struct HeaderBarBackground: View {
427
+ let opacity: CGFloat
428
+ var transparent: Bool = false
429
+
430
+ @Environment(\.appTheme) private var theme
431
+ @Environment(\.screenLayout) private var layout
432
+
433
+ var body: some View {
434
+ Group {
435
+ if transparent {
436
+ Color.clear
437
+ } else if theme.assets.isHeaderImage, let image = theme.assets.headerBackground {
438
+ ZStack(alignment: .top) {
439
+ theme.colors.background.surface
440
+ ImageView(image, contentMode: .fill, loading: false)
441
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
442
+ }
443
+ .clipped()
444
+ .opacity(opacity)
445
+ } else {
446
+ theme.colors.background.surface.opacity(opacity)
447
+ }
448
+ }
449
+ .frame(height: layout.headerHeight)
450
+ .allowsHitTesting(false)
451
+ }
452
+ }
453
+
454
+ // MARK: - Back button
455
+
456
+ /// Mirrors Compose's `BackButton`: a 28pt circle with the back arrow.
457
+ private struct BackButton: View {
458
+ let color: HeaderColor
459
+ let onBack: () -> Void
460
+
461
+ var body: some View {
462
+ SwiftUI.Button(action: onBack) {
463
+ Circle()
464
+ .fill(color.backgroundButton)
465
+ .overlay(Circle().stroke(color.borderColor, lineWidth: 0.2))
466
+ .frame(width: 28, height: 28)
467
+ .overlay(Icon(source: "arrow-back", size: 20, color: color.tintIconColor))
468
+ }
469
+ .buttonStyle(ActiveOpacityButtonStyle())
470
+ .accessibilityIdentifier("btn_navigation_back")
471
+ }
472
+ }
473
+
474
+ // MARK: - Vertical shadow
475
+
476
+ /// Mirrors Compose's `VerticalShadow`: shown once the bar is fully opaque.
477
+ private struct VerticalShadow: View {
478
+ let opacity: CGFloat
479
+
480
+ var body: some View {
481
+ if opacity >= 1 {
482
+ LinearGradient(
483
+ gradient: Gradient(colors: [Color.black.opacity(0.05), .clear]),
484
+ startPoint: .top,
485
+ endPoint: .bottom
486
+ )
487
+ .frame(height: 6)
488
+ .frame(maxWidth: .infinity)
489
+ .offset(y: 6)
490
+ .allowsHitTesting(false)
491
+ }
492
+ }
493
+ }
@@ -0,0 +1,134 @@
1
+ //
2
+ // HeaderBackground.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/HeaderBackground.kt`: the band behind the
6
+ // header — the pink gradient (or the theme's header image) fading into the screen background.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ public enum AnimatedHeaderRatio {
12
+ case ratio16_9
13
+ case ratio1_1
14
+ case ratio3_2
15
+
16
+ var value: CGFloat {
17
+ switch self {
18
+ case .ratio16_9: return 16.0 / 9.0
19
+ case .ratio1_1: return 1.0
20
+ case .ratio3_2: return 3.0 / 2.0
21
+ }
22
+ }
23
+ }
24
+
25
+ let HEADER_BACKGROUND_HEIGHT: CGFloat = 154
26
+
27
+ /// Named `NavigationHeaderBackground` to coexist with the deprecated `HeaderBackground`.
28
+ /// Lay it out top-aligned: an extended band runs past the header into the content.
29
+ public struct NavigationHeaderBackground: View {
30
+ var headerType: HeaderType = .default
31
+ var scrollState: CGFloat
32
+ var headerTransparent: Bool = false
33
+ var fullScreenContent: Bool = false
34
+ /// The screen background the band fades into; the theme's `background.default` when nil.
35
+ var backgroundColor: Color?
36
+
37
+ public init(
38
+ headerType: HeaderType = .default,
39
+ scrollState: CGFloat = 0,
40
+ headerTransparent: Bool = false,
41
+ fullScreenContent: Bool = false,
42
+ backgroundColor: Color? = nil
43
+ ) {
44
+ self.headerType = headerType
45
+ self.scrollState = scrollState
46
+ self.headerTransparent = headerTransparent
47
+ self.fullScreenContent = fullScreenContent
48
+ self.backgroundColor = backgroundColor
49
+ }
50
+
51
+ @Environment(\.screenLayout) private var layout
52
+ @Environment(\.appTheme) private var theme
53
+
54
+ public var body: some View {
55
+ let opacity = max(0, 1 - scrollState / HEADER_HEIGHT)
56
+ let background = backgroundColor ?? theme.colors.background.default
57
+ let topColor = opacity == 0 ? background : Color(hex: 0xFDCADE)
58
+ let minHeight = fullScreenContent ? 0 : layout.headerHeight
59
+ let isHeaderImage = theme.assets.isHeaderImage
60
+
61
+ let height: CGFloat = {
62
+ if headerTransparent { return 0 }
63
+ switch headerType {
64
+ case .default: return minHeight
65
+ // a header image collapses to the bar once scrolled, the gradient keeps its band
66
+ case .extended: return isHeaderImage && opacity == 0 ? minHeight : HEADER_BACKGROUND_HEIGHT
67
+ case .none: return 0
68
+ }
69
+ }()
70
+
71
+ ZStack(alignment: .top) {
72
+ background
73
+ if isHeaderImage, let image = theme.assets.headerBackground {
74
+ ImageView(image, contentMode: .fill, loading: false)
75
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
76
+ } else {
77
+ LinearGradient(colors: [topColor, background], startPoint: .top, endPoint: .bottom)
78
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
79
+ }
80
+ }
81
+ .frame(height: height, alignment: .top)
82
+ .clipped()
83
+ .allowsHitTesting(false)
84
+ }
85
+ }
86
+
87
+ // MARK: - iPhone Duo glass header gradient
88
+
89
+ // extended headers run their gradient this far below the header, as off Duo
90
+ private let duoGradientExtra: CGFloat = 52
91
+ // scroll distance over which the extended gradient collapses to the header
92
+ private let duoExtendedCollapseDistance: CGFloat = 100
93
+
94
+ /// Port of RN `DuoScrollGradient`: with the glass header the content scrolls under the bar, so the
95
+ /// header gradient is drawn under it. Compose draws this inside `HeaderBackground`.
96
+ struct DuoScrollGradient: View {
97
+ let scrollOffset: CGFloat
98
+ let headerHeight: CGFloat
99
+ let extended: Bool
100
+ /// Quick return header: the gradient leaves with the content instead of fading.
101
+ var followScroll = false
102
+
103
+ @Environment(\.appTheme) private var theme
104
+
105
+ private var scroll: CGFloat { max(scrollOffset, 0) }
106
+
107
+ var body: some View {
108
+ Group {
109
+ if followScroll {
110
+ gradient
111
+ .frame(height: headerHeight + (extended ? duoGradientExtra : 0))
112
+ .offset(y: -scroll)
113
+ } else {
114
+ // fades out on scroll as the extended header does off Duo; extended also collapses
115
+ let progress = min(scroll / duoExtendedCollapseDistance, 1)
116
+ gradient
117
+ .frame(height: headerHeight + (extended ? duoGradientExtra * (1 - progress) : 0))
118
+ .opacity(1 - min(scroll / duoGradientExtra, 1))
119
+ }
120
+ }
121
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
122
+ .ignoresSafeArea()
123
+ .allowsHitTesting(false)
124
+ }
125
+
126
+ /// The fade spans the gradient box.
127
+ private var gradient: LinearGradient {
128
+ LinearGradient(
129
+ colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
130
+ startPoint: .top,
131
+ endPoint: .bottom
132
+ )
133
+ }
134
+ }
@@ -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
+ }