@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 (194) hide show
  1. package/CLAUDE.md +36 -8
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +15 -1
  4. package/compose/build.gradle.kts.backup +14 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle.properties +1 -1
  61. package/ios/Application/ApplicationEnvironment.swift +17 -0
  62. package/ios/Application/MaxApi.swift +12 -3
  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/Colors+Radius+Spacing/Radius.swift +6 -0
  75. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  76. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  77. package/ios/Divider/Divider.swift +37 -4
  78. package/ios/Extensions/Color++.swift +38 -9
  79. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  80. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  81. package/ios/Icon/Icon.swift +21 -23
  82. package/ios/Icon/Icons.swift +1329 -0
  83. package/ios/IconButton/IconButton.swift +15 -7
  84. package/ios/Image/Image.swift +7 -23
  85. package/ios/Information/Information.swift +27 -9
  86. package/ios/Input/ErrorView.swift +2 -2
  87. package/ios/Input/Input.swift +160 -147
  88. package/ios/Input/InputPhoneNumber.swift +12 -9
  89. package/ios/Input/InputSearch.swift +102 -75
  90. package/ios/Input/InputTextArea.swift +128 -145
  91. package/ios/InputDropDown/InputDropDown.swift +10 -7
  92. package/ios/InputMoney/InputMoney.swift +33 -11
  93. package/ios/InputOTP/InputOTP.swift +22 -12
  94. package/ios/Layout/Card.swift +50 -0
  95. package/ios/Layout/GridContext.swift +187 -3
  96. package/ios/Layout/Item.swift +5 -4
  97. package/ios/Layout/ScreenLayout.swift +380 -0
  98. package/ios/Layout/Section.swift +4 -100
  99. package/ios/Loader/Loader.swift +5 -9
  100. package/ios/Lottie/LottieView.swift +14 -2
  101. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
  102. package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
  103. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  104. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  105. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  106. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  107. package/ios/Navigation/Component/Header.swift +531 -0
  108. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  109. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  110. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  111. package/ios/Navigation/Component/HeaderRight.swift +377 -0
  112. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  113. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  114. package/ios/Navigation/Component/SideBar.swift +183 -0
  115. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
  116. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  117. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
  118. package/ios/Navigation/Navigation.swift +204 -0
  119. package/ios/Navigation/NavigationContainer.swift +194 -0
  120. package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
  121. package/ios/Navigation/StackScreen.swift +820 -0
  122. package/ios/Navigation/StackTransition.swift +400 -0
  123. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  124. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  125. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  126. package/ios/Pagination/PaginationScroll.swift +2 -2
  127. package/ios/Popup/PopupDisplay.swift +72 -136
  128. package/ios/Popup/PopupInput.swift +10 -13
  129. package/ios/Popup/PopupLayout.swift +140 -0
  130. package/ios/Popup/PopupNotify.swift +56 -73
  131. package/ios/Popup/PopupPromotion.swift +31 -47
  132. package/ios/PopupView/FullscreenPopup.swift +13 -19
  133. package/ios/PopupView/PopupView.swift +1 -1
  134. package/ios/PopupView/Utils++.swift +0 -8
  135. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  136. package/ios/Radio/Radio.swift +2 -1
  137. package/ios/Rating/Rating.swift +9 -1
  138. package/ios/Resources/dot_loading.json +558 -0
  139. package/ios/Resources/lottie_circle_loader.json +1 -0
  140. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  141. package/ios/Skeleton/Skeleton.swift +22 -27
  142. package/ios/Slider/Slider.swift +16 -18
  143. package/ios/Stepper/Stepper.swift +6 -1
  144. package/ios/Steps/Steps.swift +8 -6
  145. package/ios/SuggestAction/SuggestAction.swift +5 -0
  146. package/ios/Swipeable/SwipeCell.swift +10 -6
  147. package/ios/Switch/Switch.swift +22 -20
  148. package/ios/TabView/TabView.swift +102 -21
  149. package/ios/Tag/Tag.swift +15 -4
  150. package/ios/Template/Logo/Logo.swift +2 -1
  151. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  152. package/ios/Title/Title.swift +6 -17
  153. package/ios/Tooltip/Tooltip.swift +29 -14
  154. package/ios/Typography/Text.swift +61 -19
  155. package/ios/Uploader/Uploader.swift +3 -1
  156. package/ios/native-kits.podspec +2 -1
  157. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  163. package/package.json +1 -1
  164. package/scripts/gen-ios-icons.mjs +36 -0
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  177. package/ios/Application/Components.swift +0 -270
  178. package/ios/Application/ComposeApi.swift +0 -22
  179. package/ios/Application/HeaderRight.swift +0 -306
  180. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  181. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  182. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  183. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  184. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  185. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  186. package/ios/Application/Navigation/component/Header.swift +0 -178
  187. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  188. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  189. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  190. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  191. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  192. package/ios/Application/Screen.swift +0 -250
  193. package/ios/Application/StackScreen.swift +0 -492
  194. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,531 @@
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
+ // Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
17
+ private let statusBarRowActionsExtra: CGFloat = 55
18
+
19
+ public enum TitlePosition {
20
+ case left, center
21
+ }
22
+
23
+ public enum HeaderType {
24
+ case `default`, extended, none
25
+ }
26
+
27
+ /// Mirrors Compose's `HeaderType.Animated`; `layoutOffset` lives on `NavigationOptions`.
28
+ public struct AnimatedHeader {
29
+ public var aspectRatio: AnimatedHeaderRatio = .ratio16_9
30
+ public var isSurface: Bool = true
31
+ public var composable: (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
32
+
33
+ public init(
34
+ aspectRatio: AnimatedHeaderRatio = .ratio16_9,
35
+ isSurface: Bool = true,
36
+ composable: @escaping (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
37
+ ) {
38
+ self.aspectRatio = aspectRatio
39
+ self.isSurface = isSurface
40
+ self.composable = composable
41
+ }
42
+ }
43
+
44
+ // MARK: - Header title
45
+
46
+ /// Mirrors Compose's `HeaderTitle.User`.
47
+ public struct HeaderUserData {
48
+ public var title: String
49
+ public var subTitle: String?
50
+ public var image: [String]?
51
+ public var dotColor: Color?
52
+ public var tintColor: Color?
53
+ public var onPress: (() -> Void)?
54
+ public var icons: [String]
55
+ public var isLoading: Bool
56
+
57
+ public init(
58
+ title: String,
59
+ subTitle: String? = nil,
60
+ image: [String]? = nil,
61
+ dotColor: Color? = nil,
62
+ tintColor: Color? = nil,
63
+ onPress: (() -> Void)? = nil,
64
+ icons: [String] = [],
65
+ isLoading: Bool = false
66
+ ) {
67
+ self.title = title
68
+ self.subTitle = subTitle
69
+ self.image = image
70
+ self.dotColor = dotColor
71
+ self.tintColor = tintColor
72
+ self.onPress = onPress
73
+ self.icons = icons
74
+ self.isLoading = isLoading
75
+ }
76
+ }
77
+
78
+ /// Mirrors Compose's `HeaderTitle.Location`. Colors are CSS hex strings, as in RN.
79
+ public struct HeaderLocationData {
80
+ public var description: String?
81
+ public var location: String
82
+ public var tintColor: String?
83
+ public var onPress: (() -> Void)?
84
+ public var isLoading: Bool
85
+
86
+ public init(
87
+ description: String? = nil,
88
+ location: String,
89
+ tintColor: String? = nil,
90
+ onPress: (() -> Void)? = nil,
91
+ isLoading: Bool = false
92
+ ) {
93
+ self.description = description
94
+ self.location = location
95
+ self.tintColor = tintColor
96
+ self.onPress = onPress
97
+ self.isLoading = isLoading
98
+ }
99
+ }
100
+
101
+ /// Mirrors Compose's `HeaderTitle.Journey`. Colors are CSS hex strings, as in RN.
102
+ public struct HeaderJourneyData {
103
+ public var start: String
104
+ public var end: String?
105
+ public var description: String?
106
+ public var icon: String
107
+ public var iconColor: String?
108
+ public var tintColor: String?
109
+ public var onPress: (() -> Void)?
110
+ public var isLoading: Bool
111
+
112
+ public init(
113
+ start: String,
114
+ end: String? = nil,
115
+ description: String? = nil,
116
+ icon: String,
117
+ iconColor: String? = nil,
118
+ tintColor: String? = nil,
119
+ onPress: (() -> Void)? = nil,
120
+ isLoading: Bool = false
121
+ ) {
122
+ self.start = start
123
+ self.end = end
124
+ self.description = description
125
+ self.icon = icon
126
+ self.iconColor = iconColor
127
+ self.tintColor = tintColor
128
+ self.onPress = onPress
129
+ self.isLoading = isLoading
130
+ }
131
+ }
132
+
133
+ /// Mirrors Compose's `sealed class HeaderTitle`.
134
+ public enum HeaderTitle {
135
+ case `default`(String)
136
+ case user(HeaderUserData)
137
+ case location(HeaderLocationData)
138
+ case journey(HeaderJourneyData)
139
+
140
+ /// Plain title text when this is a `.default` title.
141
+ public var defaultTitle: String? {
142
+ if case let .default(text) = self { return text }
143
+ return nil
144
+ }
145
+ }
146
+
147
+ // MARK: - Back props
148
+
149
+ /// Compose's `PopupNotifyProps` as consumed by `headerBackProps.preventBack`.
150
+ public struct PreventBackProps {
151
+ public var image: String
152
+ public var title: String
153
+ public var description: String
154
+ public var error: String
155
+ public var primary: PopupAction?
156
+ public var secondary: PopupAction?
157
+ public var buttonDirection: PopupButtonDirection
158
+ public var isShowCloseIcon: Bool
159
+ public var onIconClose: (() -> Void)?
160
+
161
+ public init(
162
+ image: String = "",
163
+ title: String = "",
164
+ description: String = "",
165
+ error: String = "",
166
+ primary: PopupAction? = nil,
167
+ secondary: PopupAction? = nil,
168
+ buttonDirection: PopupButtonDirection = .row,
169
+ isShowCloseIcon: Bool = true,
170
+ onIconClose: (() -> Void)? = nil
171
+ ) {
172
+ self.image = image
173
+ self.title = title
174
+ self.description = description
175
+ self.error = error
176
+ self.primary = primary
177
+ self.secondary = secondary
178
+ self.buttonDirection = buttonDirection
179
+ self.isShowCloseIcon = isShowCloseIcon
180
+ self.onIconClose = onIconClose
181
+ }
182
+ }
183
+
184
+ /// Mirrors Compose's `HeaderBackProps`: with `preventBack`, `Navigator.onBackSafe` asks first.
185
+ public struct HeaderBackProps {
186
+ public var preventBack: PreventBackProps?
187
+
188
+ public init(preventBack: PreventBackProps? = nil) {
189
+ self.preventBack = preventBack
190
+ }
191
+ }
192
+
193
+ // MARK: - Header colors
194
+
195
+ /// Mirrors Compose's `HeaderColor`.
196
+ public struct HeaderColor: Equatable {
197
+ public let tintIconColor: Color
198
+ public let backgroundButton: Color
199
+ public let borderColor: Color
200
+ }
201
+
202
+ /// Mirrors Compose's `getHeaderColor`; an animated header is a non-nil `AnimatedHeader` here.
203
+ func getHeaderColor(
204
+ isAnimated: Bool,
205
+ isSurface: Bool,
206
+ opacity: CGFloat,
207
+ tintColor: Color?,
208
+ defaultColor: Color = Colors.black17
209
+ ) -> HeaderColor {
210
+ if isAnimated {
211
+ if opacity >= 1 || !isSurface {
212
+ return HeaderColor(
213
+ tintIconColor: Colors.black17,
214
+ backgroundButton: Colors.black01.opacity(0.6),
215
+ borderColor: Colors.black20.opacity(0.2)
216
+ )
217
+ }
218
+ return HeaderColor(
219
+ tintIconColor: Colors.black01,
220
+ backgroundButton: Colors.black20.opacity(0.6),
221
+ borderColor: .clear
222
+ )
223
+ }
224
+
225
+ // SwiftUI's `.white` is not equal to `Colors.black01`; both must get the dark button, as on the side bar
226
+ if tintColor == Colors.black01 || tintColor == .white {
227
+ return HeaderColor(
228
+ tintIconColor: Colors.black01,
229
+ backgroundButton: Colors.black20.opacity(0.6),
230
+ borderColor: Colors.black01.opacity(0.2)
231
+ )
232
+ }
233
+ return HeaderColor(
234
+ tintIconColor: tintColor ?? defaultColor,
235
+ backgroundButton: Colors.black01.opacity(0.6),
236
+ borderColor: Colors.black20.opacity(0.2)
237
+ )
238
+ }
239
+
240
+ /// Mirrors Compose's `headerOpacity()`: the bar turns opaque over one header height of scroll,
241
+ /// over `layoutOffset` for an animated header, and never for a transparent one.
242
+ func headerOpacity(
243
+ scrollOffset: CGFloat,
244
+ isAnimated: Bool,
245
+ layoutOffset: CGFloat,
246
+ transparent: Bool
247
+ ) -> CGFloat {
248
+ if transparent { return 0 }
249
+ let distance = isAnimated ? layoutOffset : HEADER_HEIGHT
250
+ if distance <= 0 { return scrollOffset > 0 ? 1 : 0 }
251
+ return min(max(scrollOffset / distance, 0), 1)
252
+ }
253
+
254
+ private struct HeaderColorKey: EnvironmentKey {
255
+ static let defaultValue: HeaderColor? = nil
256
+ }
257
+
258
+ extension EnvironmentValues {
259
+ /// The enclosing header's colors, so a toolkit follows the bar over a banner image.
260
+ var headerColor: HeaderColor? {
261
+ get { self[HeaderColorKey.self] }
262
+ set { self[HeaderColorKey.self] = newValue }
263
+ }
264
+ }
265
+
266
+ /// Measured width of the header-right area, read by the animated search for its trailing inset
267
+ /// (Compose's `LocalHeaderRightWidthPx`).
268
+ public struct HeaderRightWidthKey: PreferenceKey {
269
+ public static var defaultValue: CGFloat = 0
270
+ public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
271
+ value = max(value, nextValue())
272
+ }
273
+ }
274
+
275
+ /// Compose `SearchAnimated`: a header search fills from surface to the screen background over
276
+ /// the scroll that lifts it into the bar.
277
+ func headerSearchProps(_ props: InputSearchProps, scroll: CGFloat, layout: ScreenLayout, theme: Theme) -> InputSearchProps {
278
+ guard props.backgroundColor == nil else { return props }
279
+ let collapsedTop = max(layout.headerStatusBarHeight + (layout.headerBarHeight - HEADER_HEIGHT) / 2, 1)
280
+ var themed = props
281
+ themed.backgroundColor = theme.colors.background.surface
282
+ .lerp(to: theme.colors.background.default, max(scroll, 0) / collapsedTop)
283
+ return themed
284
+ }
285
+
286
+ /// Max title width: back button and toolkit share the bar unless the iPhone Duo side bar holds them.
287
+ func headerTitleMaxWidth(_ layout: ScreenLayout) -> CGFloat {
288
+ let clearance = layout.statusBarClearance.map { $0.left + $0.right } ?? 0
289
+ if layout.sideBar != nil { return layout.contentWidth - Spacing.M * 2 - clearance }
290
+ return layout.width - headerActionsWidth - (layout.statusBarClearance != nil ? statusBarRowActionsExtra : 0) - clearance
291
+ }
292
+
293
+ // MARK: - Header
294
+
295
+ /// Mirrors Compose's `Header`.
296
+ public struct NavigationHeader: View {
297
+ var headerType: HeaderType = .default
298
+ var titlePosition: TitlePosition
299
+ var title: String
300
+ var headerTitle: HeaderTitle?
301
+ var headerRight: (() -> any View)?
302
+ var goBack: (() -> Void)?
303
+ var opacity: CGFloat = 1
304
+ var animatedHeader: AnimatedHeader?
305
+ var scrollState: CGFloat = 0
306
+ var inputSearchProps: InputSearchProps?
307
+ var tintColor: Color?
308
+ /// The animated search fades the title out as it rises into the bar.
309
+ var titleOpacity: CGFloat = 1
310
+
311
+ public init(
312
+ headerType: HeaderType = .default,
313
+ titlePosition: TitlePosition,
314
+ title: String,
315
+ headerTitle: HeaderTitle? = nil,
316
+ headerRight: (() -> any View)? = { HeaderRight() },
317
+ goBack: (() -> Void)? = nil,
318
+ opacity: CGFloat = 1,
319
+ animatedHeader: AnimatedHeader? = nil,
320
+ scrollState: CGFloat = 0,
321
+ inputSearchProps: InputSearchProps? = nil,
322
+ tintColor: Color? = nil,
323
+ titleOpacity: CGFloat = 1
324
+ ) {
325
+ self.headerType = headerType
326
+ self.titlePosition = titlePosition
327
+ self.title = title
328
+ self.headerTitle = headerTitle
329
+ self.headerRight = headerRight
330
+ self.goBack = goBack
331
+ self.opacity = opacity
332
+ self.animatedHeader = animatedHeader
333
+ self.scrollState = scrollState
334
+ self.inputSearchProps = inputSearchProps
335
+ self.tintColor = tintColor
336
+ self.titleOpacity = titleOpacity
337
+ }
338
+
339
+ @Environment(\.screenLayout) private var layout
340
+ @Environment(\.appTheme) private var theme
341
+ @State private var headerRightWidth: CGFloat = 0
342
+
343
+ /// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
344
+ private var useSideBar: Bool { layout.sideBar != nil }
345
+
346
+ /// iPhone Duo inner portrait: the row sits on the status bar, between its clusters, with 48pt controls.
347
+ private var statusBarRow: Bool { layout.statusBarClearance != nil && !useSideBar }
348
+
349
+ /// Max centered title width; on the status bar row the clusters are lopsided, so both sides keep the wider one's room.
350
+ private var centerTitleMaxWidth: CGFloat {
351
+ guard let clearance = layout.statusBarClearance else { return (layout.contentWidth - Spacing.M * 2) / 2 }
352
+ let back = goBack == nil || useSideBar ? 0 : DuoMetrics.controlSize + Spacing.S
353
+ let inset = max(clearance.left + back, clearance.right + headerRightWidth) + Spacing.S
354
+ return max(layout.contentWidth - (Spacing.M + inset) * 2, 0)
355
+ }
356
+
357
+ private var headerColor: HeaderColor {
358
+ getHeaderColor(
359
+ isAnimated: animatedHeader != nil,
360
+ isSurface: animatedHeader?.isSurface ?? true,
361
+ opacity: opacity,
362
+ tintColor: tintColor
363
+ )
364
+ }
365
+
366
+ /// A centered title overlays the whole bar so it stays screen-centered.
367
+ private var centerTitle: Bool {
368
+ guard inputSearchProps == nil else { return false }
369
+ return titlePosition == .center && (headerTitle == nil || headerTitle?.defaultTitle != nil)
370
+ }
371
+
372
+ public var body: some View {
373
+ if headerType != .none {
374
+ ZStack {
375
+ HStack(spacing: 0) {
376
+ if let goBack, !useSideBar {
377
+ if statusBarRow {
378
+ SideBarButton(
379
+ icon: "arrow-back",
380
+ headerColor: headerColor,
381
+ accessibilityId: "btn_navigation_back",
382
+ action: goBack
383
+ )
384
+ } else {
385
+ BackButton(color: headerColor, onBack: goBack)
386
+ }
387
+ Spacer().frame(width: Spacing.S)
388
+ }
389
+
390
+ if centerTitle {
391
+ Spacer()
392
+ } else {
393
+ titleArea(centered: false)
394
+ .frame(maxWidth: .infinity, alignment: .leading)
395
+ }
396
+
397
+ if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
398
+ Group {
399
+ if statusBarRow, let toolkit = toolkitHeaderRight(headerRight) {
400
+ Toolkit(
401
+ headerRight: toolkit.data ?? HeaderRightData(),
402
+ tintColor: toolkit.tintColor,
403
+ layout: .sideBarRow
404
+ )
405
+ } else {
406
+ AnyView(headerRight())
407
+ }
408
+ }
409
+ .environment(\.headerColor, headerColor)
410
+ .background(
411
+ GeometryReader { geo in
412
+ Color.clear.preference(key: HeaderRightWidthKey.self, value: geo.size.width)
413
+ }
414
+ )
415
+ }
416
+ }
417
+ .padding(.leading, Spacing.M + (layout.statusBarClearance?.left ?? 0))
418
+ .padding(.trailing, Spacing.M + (layout.statusBarClearance?.right ?? 0))
419
+
420
+ if centerTitle {
421
+ titleArea(centered: true)
422
+ // Compose caps a centered title at half the bar
423
+ .frame(maxWidth: centerTitleMaxWidth)
424
+ .padding(.horizontal, Spacing.M)
425
+ }
426
+ }
427
+ .onPreferenceChange(HeaderRightWidthKey.self) { headerRightWidth = $0 }
428
+ .frame(height: layout.headerBarHeight)
429
+ .overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
430
+ // the row starts headerStatusBarHeight below the window top: 0 with the status bar off
431
+ // the top edge, lower on the iPhone Duo outer display to center on the camera
432
+ .padding(.top, layout.headerStatusBarHeight - layout.insets.top)
433
+ }
434
+ }
435
+
436
+ /// Mirrors Compose's `HeaderContent`; a header search replaces the title.
437
+ @ViewBuilder
438
+ private func titleArea(centered: Bool) -> some View {
439
+ if let inputSearchProps {
440
+ InputSearch(props: headerSearchProps(inputSearchProps, scroll: scrollState, layout: layout, theme: theme))
441
+ } else {
442
+ switch headerTitle {
443
+ case let .user(data)?:
444
+ HeaderUserView(data)
445
+ case let .location(data)?:
446
+ HeaderLocationView(data, tintColor: headerColor.tintIconColor)
447
+ case let .journey(data)?:
448
+ HeaderJourneyView(data, tintColor: headerColor.tintIconColor)
449
+ case let .default(text)?:
450
+ HeaderTitleText(text, color: headerColor.tintIconColor, centered: centered)
451
+ .opacity(titleOpacity)
452
+ case nil:
453
+ HeaderTitleText(title, color: headerColor.tintIconColor, centered: centered)
454
+ .opacity(titleOpacity)
455
+ }
456
+ }
457
+ }
458
+ }
459
+
460
+ // MARK: - Bar background
461
+
462
+ /// Compose `Header`'s own background: surface (or the theme's header image) fading in with the
463
+ /// scroll; a transparent header stays clear.
464
+ struct HeaderBarBackground: View {
465
+ let opacity: CGFloat
466
+ var transparent: Bool = false
467
+
468
+ @Environment(\.appTheme) private var theme
469
+ @Environment(\.screenLayout) private var layout
470
+
471
+ var body: some View {
472
+ Group {
473
+ if transparent {
474
+ Color.clear
475
+ } else if theme.assets.isHeaderImage, let image = theme.assets.headerBackground {
476
+ ZStack(alignment: .top) {
477
+ theme.colors.background.surface
478
+ ImageView(image, contentMode: .fill, loading: false)
479
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
480
+ }
481
+ .clipped()
482
+ .opacity(opacity)
483
+ } else {
484
+ theme.colors.background.surface.opacity(opacity)
485
+ }
486
+ }
487
+ .frame(height: layout.headerHeight)
488
+ .allowsHitTesting(false)
489
+ }
490
+ }
491
+
492
+ // MARK: - Back button
493
+
494
+ /// Mirrors Compose's `BackButton`: a 28pt circle with the back arrow.
495
+ private struct BackButton: View {
496
+ let color: HeaderColor
497
+ let onBack: () -> Void
498
+
499
+ var body: some View {
500
+ SwiftUI.Button(action: onBack) {
501
+ Circle()
502
+ .fill(color.backgroundButton)
503
+ .overlay(Circle().stroke(color.borderColor, lineWidth: 0.2))
504
+ .frame(width: 28, height: 28)
505
+ .overlay(Icon(source: "arrow-back", size: 20, color: color.tintIconColor))
506
+ }
507
+ .buttonStyle(ActiveOpacityButtonStyle())
508
+ .accessibilityIdentifier("btn_navigation_back")
509
+ }
510
+ }
511
+
512
+ // MARK: - Vertical shadow
513
+
514
+ /// Mirrors Compose's `VerticalShadow`: shown once the bar is fully opaque.
515
+ private struct VerticalShadow: View {
516
+ let opacity: CGFloat
517
+
518
+ var body: some View {
519
+ if opacity >= 1 {
520
+ LinearGradient(
521
+ gradient: Gradient(colors: [Color.black.opacity(0.05), .clear]),
522
+ startPoint: .top,
523
+ endPoint: .bottom
524
+ )
525
+ .frame(height: 6)
526
+ .frame(maxWidth: .infinity)
527
+ .offset(y: 6)
528
+ .allowsHitTesting(false)
529
+ }
530
+ }
531
+ }
@@ -0,0 +1,133 @@
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
+ /// Lay it out top-aligned: an extended band runs past the header into the content.
28
+ public struct NavigationHeaderBackground: View {
29
+ var headerType: HeaderType = .default
30
+ var scrollState: CGFloat
31
+ var headerTransparent: Bool = false
32
+ var fullScreenContent: Bool = false
33
+ /// The screen background the band fades into; the theme's `background.default` when nil.
34
+ var backgroundColor: Color?
35
+
36
+ public init(
37
+ headerType: HeaderType = .default,
38
+ scrollState: CGFloat = 0,
39
+ headerTransparent: Bool = false,
40
+ fullScreenContent: Bool = false,
41
+ backgroundColor: Color? = nil
42
+ ) {
43
+ self.headerType = headerType
44
+ self.scrollState = scrollState
45
+ self.headerTransparent = headerTransparent
46
+ self.fullScreenContent = fullScreenContent
47
+ self.backgroundColor = backgroundColor
48
+ }
49
+
50
+ @Environment(\.screenLayout) private var layout
51
+ @Environment(\.appTheme) private var theme
52
+
53
+ public var body: some View {
54
+ let opacity = max(0, 1 - scrollState / HEADER_HEIGHT)
55
+ let background = backgroundColor ?? theme.colors.background.default
56
+ let topColor = opacity == 0 ? background : Color(hex: 0xFDCADE)
57
+ let minHeight = fullScreenContent ? 0 : layout.headerHeight
58
+ let isHeaderImage = theme.assets.isHeaderImage
59
+
60
+ let height: CGFloat = {
61
+ if headerTransparent { return 0 }
62
+ switch headerType {
63
+ case .default: return minHeight
64
+ // a header image collapses to the bar once scrolled, the gradient keeps its band
65
+ case .extended: return isHeaderImage && opacity == 0 ? minHeight : HEADER_BACKGROUND_HEIGHT
66
+ case .none: return 0
67
+ }
68
+ }()
69
+
70
+ ZStack(alignment: .top) {
71
+ background
72
+ if isHeaderImage, let image = theme.assets.headerBackground {
73
+ ImageView(image, contentMode: .fill, loading: false)
74
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
75
+ } else {
76
+ LinearGradient(colors: [topColor, background], startPoint: .top, endPoint: .bottom)
77
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
78
+ }
79
+ }
80
+ .frame(height: height, alignment: .top)
81
+ .clipped()
82
+ .allowsHitTesting(false)
83
+ }
84
+ }
85
+
86
+ // MARK: - iPhone Duo glass header gradient
87
+
88
+ // extended headers run their gradient this far below the header, as off Duo
89
+ private let duoGradientExtra: CGFloat = 52
90
+ // scroll distance over which the extended gradient collapses to the header
91
+ private let duoExtendedCollapseDistance: CGFloat = 100
92
+
93
+ /// Port of RN `DuoScrollGradient`: with the glass header the content scrolls under the bar, so the
94
+ /// header gradient is drawn under it. Compose draws this inside `HeaderBackground`.
95
+ struct DuoScrollGradient: View {
96
+ let scrollOffset: CGFloat
97
+ let headerHeight: CGFloat
98
+ let extended: Bool
99
+ /// Quick return header: the gradient leaves with the content instead of fading.
100
+ var followScroll = false
101
+
102
+ @Environment(\.appTheme) private var theme
103
+
104
+ private var scroll: CGFloat { max(scrollOffset, 0) }
105
+
106
+ var body: some View {
107
+ Group {
108
+ if followScroll {
109
+ gradient
110
+ .frame(height: headerHeight + (extended ? duoGradientExtra : 0))
111
+ .offset(y: -scroll)
112
+ } else {
113
+ // fades out on scroll as the extended header does off Duo; extended also collapses
114
+ let progress = min(scroll / duoExtendedCollapseDistance, 1)
115
+ gradient
116
+ .frame(height: headerHeight + (extended ? duoGradientExtra * (1 - progress) : 0))
117
+ .opacity(1 - min(scroll / duoGradientExtra, 1))
118
+ }
119
+ }
120
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
121
+ .ignoresSafeArea()
122
+ .allowsHitTesting(false)
123
+ }
124
+
125
+ /// The fade spans the gradient box.
126
+ private var gradient: LinearGradient {
127
+ LinearGradient(
128
+ colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
129
+ startPoint: .top,
130
+ endPoint: .bottom
131
+ )
132
+ }
133
+ }