@momo-kits/native-kits 0.300.3-debug → 0.300.4-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/CLAUDE.md +51 -7
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +10 -2
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -3
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
  66. package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
  68. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  69. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
  70. package/gradle/libs.versions.toml +3 -14
  71. package/gradle/wrapper/gradle-wrapper.properties +0 -2
  72. package/gradle.properties +3 -5
  73. package/ios/Application/ApplicationEnvironment.swift +24 -6
  74. package/ios/Application/DesignSystemKits.swift +100 -0
  75. package/ios/Application/DesignSystemKitsPayload.swift +68 -0
  76. package/ios/Application/MaxApi.swift +30 -3
  77. package/ios/Avatar/Avatar.swift +4 -3
  78. package/ios/Badge/Badge.swift +6 -4
  79. package/ios/Badge/BadgeDot.swift +1 -1
  80. package/ios/Badge/BadgeRibbon.swift +8 -2
  81. package/ios/Button/Button.swift +15 -12
  82. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  83. package/ios/Carousel/Carousel.swift +29 -12
  84. package/ios/Checkbox/Checkbox.swift +7 -3
  85. package/ios/Chip/Chip.swift +5 -4
  86. package/ios/Collapse/Collapse.swift +70 -19
  87. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  88. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  89. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  90. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  91. package/ios/Divider/Divider.swift +37 -4
  92. package/ios/Extensions/Color++.swift +38 -9
  93. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  94. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  95. package/ios/Icon/Icon.swift +21 -23
  96. package/ios/Icon/Icons.swift +1329 -0
  97. package/ios/IconButton/IconButton.swift +15 -7
  98. package/ios/Image/Image.swift +7 -23
  99. package/ios/Information/Information.swift +27 -9
  100. package/ios/Input/ErrorView.swift +2 -2
  101. package/ios/Input/Input.swift +171 -148
  102. package/ios/Input/InputPhoneNumber.swift +10 -9
  103. package/ios/Input/InputSearch.swift +107 -78
  104. package/ios/Input/InputTextArea.swift +133 -148
  105. package/ios/InputDropDown/InputDropDown.swift +10 -7
  106. package/ios/InputMoney/InputMoney.swift +32 -11
  107. package/ios/InputOTP/InputOTP.swift +22 -12
  108. package/ios/Layout/Card.swift +50 -0
  109. package/ios/Layout/GridContext.swift +187 -3
  110. package/ios/Layout/Item.swift +5 -4
  111. package/ios/Layout/ScreenLayout.swift +415 -0
  112. package/ios/Layout/Section.swift +4 -100
  113. package/ios/Loader/Loader.swift +5 -9
  114. package/ios/Lottie/LottieView.swift +14 -2
  115. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  116. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  117. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  118. package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
  119. package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
  120. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  121. package/ios/Navigation/Component/Header.swift +545 -0
  122. package/ios/Navigation/Component/HeaderBackground.swift +154 -0
  123. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  124. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  125. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  126. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  127. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  128. package/ios/Navigation/Component/SideBar.swift +183 -0
  129. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  130. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  131. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  132. package/ios/Navigation/Navigation.swift +204 -0
  133. package/ios/Navigation/NavigationContainer.swift +195 -0
  134. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  135. package/ios/Navigation/StackScreen.swift +930 -0
  136. package/ios/Navigation/StackTransition.swift +400 -0
  137. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  138. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  139. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  140. package/ios/Pagination/PaginationScroll.swift +2 -2
  141. package/ios/Popup/PopupDisplay.swift +72 -135
  142. package/ios/Popup/PopupInput.swift +10 -13
  143. package/ios/Popup/PopupLayout.swift +138 -0
  144. package/ios/Popup/PopupNotify.swift +59 -69
  145. package/ios/Popup/PopupPromotion.swift +31 -47
  146. package/ios/PopupView/FullscreenPopup.swift +13 -19
  147. package/ios/PopupView/PopupView.swift +1 -1
  148. package/ios/PopupView/Utils++.swift +0 -8
  149. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  150. package/ios/Radio/Radio.swift +2 -1
  151. package/ios/Rating/Rating.swift +9 -1
  152. package/ios/Resources/dot_loading.json +558 -0
  153. package/ios/Resources/lottie_circle_loader.json +1 -0
  154. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  155. package/ios/Skeleton/Skeleton.swift +22 -27
  156. package/ios/Slider/Slider.swift +16 -18
  157. package/ios/Stepper/Stepper.swift +7 -3
  158. package/ios/Steps/Steps.swift +8 -6
  159. package/ios/SuggestAction/SuggestAction.swift +5 -0
  160. package/ios/Swipeable/SwipeCell.swift +10 -6
  161. package/ios/Switch/Switch.swift +22 -20
  162. package/ios/TabView/TabView.swift +102 -21
  163. package/ios/Tag/Tag.swift +15 -4
  164. package/ios/Template/Logo/Logo.swift +2 -1
  165. package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
  166. package/ios/Title/Title.swift +6 -17
  167. package/ios/Tooltip/Tooltip.swift +29 -14
  168. package/ios/Typography/FontScale.swift +17 -14
  169. package/ios/Typography/Text.swift +156 -20
  170. package/ios/Typography/Typography.swift +2 -2
  171. package/ios/Uploader/Uploader.swift +3 -1
  172. package/ios/native-kits.podspec +2 -3
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  176. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  177. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  178. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  179. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  180. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  181. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  182. package/package.json +1 -1
  183. package/publish.sh +19 -2
  184. package/scripts/gen-ios-icons.mjs +36 -0
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  192. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  193. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  194. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  195. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  196. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  197. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  198. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  199. package/ios/Application/Components.swift +0 -270
  200. package/ios/Application/ComposeApi.swift +0 -22
  201. package/ios/Application/HeaderRight.swift +0 -306
  202. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  203. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  204. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  205. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  206. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  207. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  208. package/ios/Application/Navigation/component/Header.swift +0 -178
  209. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  210. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  211. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  212. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  213. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  214. package/ios/Application/Screen.swift +0 -250
  215. package/ios/Application/StackScreen.swift +0 -492
  216. package/ios/Typography/FontScaleStore.swift +0 -58
  217. package/ios/Typography/KitDesignSystem.swift +0 -17
  218. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,545 @@
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, and its button text pops the screen.
277
+ func headerSearchProps(
278
+ _ props: InputSearchProps,
279
+ scroll: CGFloat,
280
+ layout: ScreenLayout,
281
+ theme: Theme,
282
+ pop: (() -> Void)?
283
+ ) -> InputSearchProps {
284
+ var themed = props
285
+ if !props.hasButtonTextAction, let pop { themed.onPressButtonText = pop }
286
+ guard props.backgroundColor == nil else { return themed }
287
+ let collapsedTop = max(layout.headerStatusBarHeight + (layout.headerBarHeight - HEADER_HEIGHT) / 2, 1)
288
+ themed.backgroundColor = theme.colors.background.surface
289
+ .lerp(to: theme.colors.background.default, max(scroll, 0) / collapsedTop)
290
+ return themed
291
+ }
292
+
293
+ /// Max title width: back button and toolkit share the bar unless the iPhone Duo side bar holds them.
294
+ func headerTitleMaxWidth(_ layout: ScreenLayout) -> CGFloat {
295
+ let clearance = layout.statusBarClearance.map { $0.left + $0.right } ?? 0
296
+ if layout.sideBar != nil { return layout.contentWidth - Spacing.M * 2 - clearance }
297
+ return layout.width - headerActionsWidth - (layout.statusBarClearance != nil ? statusBarRowActionsExtra : 0) - clearance
298
+ }
299
+
300
+ // MARK: - Header
301
+
302
+ /// Mirrors Compose's `Header`.
303
+ public struct NavigationHeader: View {
304
+ var headerType: HeaderType = .default
305
+ var titlePosition: TitlePosition
306
+ var title: String
307
+ var headerTitle: HeaderTitle?
308
+ var headerRight: (() -> any View)?
309
+ var goBack: (() -> Void)?
310
+ var opacity: CGFloat = 1
311
+ var animatedHeader: AnimatedHeader?
312
+ var scrollState: CGFloat = 0
313
+ var inputSearchProps: InputSearchProps?
314
+ var tintColor: Color?
315
+ /// The animated search fades the title out as it rises into the bar.
316
+ var titleOpacity: CGFloat = 1
317
+
318
+ public init(
319
+ headerType: HeaderType = .default,
320
+ titlePosition: TitlePosition,
321
+ title: String,
322
+ headerTitle: HeaderTitle? = nil,
323
+ headerRight: (() -> any View)? = { HeaderRight() },
324
+ goBack: (() -> Void)? = nil,
325
+ opacity: CGFloat = 1,
326
+ animatedHeader: AnimatedHeader? = nil,
327
+ scrollState: CGFloat = 0,
328
+ inputSearchProps: InputSearchProps? = nil,
329
+ tintColor: Color? = nil,
330
+ titleOpacity: CGFloat = 1
331
+ ) {
332
+ self.headerType = headerType
333
+ self.titlePosition = titlePosition
334
+ self.title = title
335
+ self.headerTitle = headerTitle
336
+ self.headerRight = headerRight
337
+ self.goBack = goBack
338
+ self.opacity = opacity
339
+ self.animatedHeader = animatedHeader
340
+ self.scrollState = scrollState
341
+ self.inputSearchProps = inputSearchProps
342
+ self.tintColor = tintColor
343
+ self.titleOpacity = titleOpacity
344
+ }
345
+
346
+ @Environment(\.screenLayout) private var layout
347
+ @Environment(\.appTheme) private var theme
348
+ @OptionalEnvironmentObject private var navigator: Navigator?
349
+ @State private var headerRightWidth: CGFloat = 0
350
+
351
+ /// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
352
+ private var useSideBar: Bool { layout.sideBar != nil }
353
+
354
+ /// iPhone Duo inner portrait: the row sits on the status bar, between its clusters, with 48pt controls.
355
+ private var statusBarRow: Bool { layout.statusBarClearance != nil && !useSideBar }
356
+
357
+ /// Max centered title width; on the status bar row the clusters are lopsided, so both sides keep the wider one's room.
358
+ private var centerTitleMaxWidth: CGFloat {
359
+ guard let clearance = layout.statusBarClearance else { return (layout.contentWidth - Spacing.M * 2) / 2 }
360
+ let back = goBack == nil || useSideBar ? 0 : DuoMetrics.controlSize + Spacing.S
361
+ let inset = max(clearance.left + back, clearance.right + headerRightWidth) + Spacing.S
362
+ return max(layout.contentWidth - (Spacing.M + inset) * 2, 0)
363
+ }
364
+
365
+ private var headerColor: HeaderColor {
366
+ getHeaderColor(
367
+ isAnimated: animatedHeader != nil,
368
+ isSurface: animatedHeader?.isSurface ?? true,
369
+ opacity: opacity,
370
+ tintColor: tintColor
371
+ )
372
+ }
373
+
374
+ /// A centered title overlays the whole bar so it stays screen-centered.
375
+ private var centerTitle: Bool {
376
+ guard inputSearchProps == nil else { return false }
377
+ return titlePosition == .center && (headerTitle == nil || headerTitle?.defaultTitle != nil)
378
+ }
379
+
380
+ public var body: some View {
381
+ if headerType != .none {
382
+ ZStack {
383
+ HStack(spacing: 0) {
384
+ if let goBack, !useSideBar {
385
+ if statusBarRow {
386
+ SideBarButton(
387
+ icon: "arrow-back",
388
+ headerColor: headerColor,
389
+ accessibilityId: "btn_navigation_back",
390
+ action: goBack
391
+ )
392
+ } else {
393
+ BackButton(color: headerColor, onBack: goBack)
394
+ }
395
+ Spacer().frame(width: Spacing.S)
396
+ }
397
+
398
+ if centerTitle {
399
+ Spacer()
400
+ } else {
401
+ titleArea(centered: false)
402
+ .frame(maxWidth: .infinity, alignment: .leading)
403
+ }
404
+
405
+ if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
406
+ Group {
407
+ if statusBarRow, let toolkit = toolkitHeaderRight(headerRight) {
408
+ Toolkit(
409
+ headerRight: toolkit.data ?? HeaderRightData(),
410
+ tintColor: toolkit.tintColor,
411
+ layout: .sideBarRow
412
+ )
413
+ } else {
414
+ AnyView(headerRight())
415
+ }
416
+ }
417
+ .environment(\.headerColor, headerColor)
418
+ .background(
419
+ GeometryReader { geo in
420
+ Color.clear.preference(key: HeaderRightWidthKey.self, value: geo.size.width)
421
+ }
422
+ )
423
+ }
424
+ }
425
+ .padding(.leading, Spacing.M + (layout.statusBarClearance?.left ?? 0))
426
+ .padding(.trailing, Spacing.M + (layout.statusBarClearance?.right ?? 0))
427
+
428
+ if centerTitle {
429
+ titleArea(centered: true)
430
+ // Compose caps a centered title at half the bar
431
+ .frame(maxWidth: centerTitleMaxWidth)
432
+ .padding(.horizontal, Spacing.M)
433
+ }
434
+ }
435
+ .onPreferenceChange(HeaderRightWidthKey.self) { headerRightWidth = $0 }
436
+ .frame(height: layout.headerBarHeight)
437
+ .overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
438
+ // the row starts headerStatusBarHeight below the window top: 0 with the status bar off
439
+ // the top edge, lower on the iPhone Duo outer display to center on the camera
440
+ .padding(.top, layout.headerStatusBarHeight - layout.insets.top)
441
+ }
442
+ }
443
+
444
+ /// Mirrors Compose's `HeaderContent`; a header search replaces the title.
445
+ @ViewBuilder
446
+ private func titleArea(centered: Bool) -> some View {
447
+ if let inputSearchProps {
448
+ InputSearch(props: headerSearchProps(
449
+ inputSearchProps,
450
+ scroll: scrollState,
451
+ layout: layout,
452
+ theme: theme,
453
+ pop: navigator.map { navigator in { navigator.pop() } }
454
+ ))
455
+ } else {
456
+ switch headerTitle {
457
+ case let .user(data)?:
458
+ HeaderUserView(data)
459
+ case let .location(data)?:
460
+ HeaderLocationView(data, tintColor: headerColor.tintIconColor)
461
+ case let .journey(data)?:
462
+ HeaderJourneyView(data, tintColor: headerColor.tintIconColor)
463
+ case let .default(text)?:
464
+ HeaderTitleText(text, color: headerColor.tintIconColor, centered: centered)
465
+ .opacity(titleOpacity)
466
+ case nil:
467
+ HeaderTitleText(title, color: headerColor.tintIconColor, centered: centered)
468
+ .opacity(titleOpacity)
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ // MARK: - Bar background
475
+
476
+ /// Compose `Header`'s own background: surface (or the theme's header image) fading in with the
477
+ /// scroll; a transparent header stays clear.
478
+ struct HeaderBarBackground: View {
479
+ let opacity: CGFloat
480
+ var transparent: Bool = false
481
+
482
+ @Environment(\.appTheme) private var theme
483
+ @Environment(\.screenLayout) private var layout
484
+
485
+ var body: some View {
486
+ Group {
487
+ if transparent {
488
+ Color.clear
489
+ } else if theme.assets.isHeaderImage, let image = theme.assets.headerBackground {
490
+ ZStack(alignment: .top) {
491
+ theme.colors.background.surface
492
+ ImageView(image, contentMode: .fill, loading: false)
493
+ .frame(height: HEADER_BACKGROUND_HEIGHT)
494
+ }
495
+ .clipped()
496
+ .opacity(opacity)
497
+ } else {
498
+ theme.colors.background.surface.opacity(opacity)
499
+ }
500
+ }
501
+ .frame(height: layout.headerHeight)
502
+ .allowsHitTesting(false)
503
+ }
504
+ }
505
+
506
+ // MARK: - Back button
507
+
508
+ /// Mirrors Compose's `BackButton`: a 28pt circle with the back arrow.
509
+ private struct BackButton: View {
510
+ let color: HeaderColor
511
+ let onBack: () -> Void
512
+
513
+ var body: some View {
514
+ SwiftUI.Button(action: onBack) {
515
+ Circle()
516
+ .fill(color.backgroundButton)
517
+ .overlay(Circle().stroke(color.borderColor, lineWidth: 0.2))
518
+ .frame(width: 28, height: 28)
519
+ .overlay(Icon(source: "arrow-back", size: 20, color: color.tintIconColor))
520
+ }
521
+ .buttonStyle(ActiveOpacityButtonStyle())
522
+ .accessibilityIdentifier("btn_navigation_back")
523
+ }
524
+ }
525
+
526
+ // MARK: - Vertical shadow
527
+
528
+ /// Mirrors Compose's `VerticalShadow`: shown once the bar is fully opaque.
529
+ private struct VerticalShadow: View {
530
+ let opacity: CGFloat
531
+
532
+ var body: some View {
533
+ if opacity >= 1 {
534
+ LinearGradient(
535
+ gradient: Gradient(colors: [Color.black.opacity(0.05), .clear]),
536
+ startPoint: .top,
537
+ endPoint: .bottom
538
+ )
539
+ .frame(height: 6)
540
+ .frame(maxWidth: .infinity)
541
+ .offset(y: 6)
542
+ .allowsHitTesting(false)
543
+ }
544
+ }
545
+ }
@@ -0,0 +1,154 @@
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
+ // scroll distance over which the bar fades in
94
+ private let duoBarFadeDistance: CGFloat = 16
95
+
96
+ /// Port of RN `DuoGlassBackground`: translucent surface with a hairline, shown once the content
97
+ /// scrolls `fadeStart` under the header.
98
+ struct DuoHeaderBar: View {
99
+ let scrollOffset: CGFloat
100
+ var fadeStart: CGFloat = 0
101
+
102
+ @Environment(\.appTheme) private var theme
103
+
104
+ var body: some View {
105
+ theme.colors.background.surface.opacity(0.8)
106
+ .overlay(alignment: .bottom) {
107
+ theme.colors.border.default.frame(height: 0.5)
108
+ }
109
+ .opacity(min(max((scrollOffset - fadeStart) / duoBarFadeDistance, 0), 1))
110
+ .allowsHitTesting(false)
111
+ }
112
+ }
113
+
114
+ /// Port of RN `DuoScrollGradient`: with the glass header the content scrolls under the bar, so the
115
+ /// header gradient is drawn under it. Compose draws this inside `HeaderBackground`.
116
+ struct DuoScrollGradient: View {
117
+ let scrollOffset: CGFloat
118
+ let headerHeight: CGFloat
119
+ let extended: Bool
120
+ /// Quick return header: the gradient leaves with the content instead of fading.
121
+ var followScroll = false
122
+
123
+ @Environment(\.appTheme) private var theme
124
+
125
+ private var scroll: CGFloat { max(scrollOffset, 0) }
126
+
127
+ var body: some View {
128
+ Group {
129
+ if followScroll {
130
+ gradient
131
+ .frame(height: headerHeight + (extended ? duoGradientExtra : 0))
132
+ .offset(y: -scroll)
133
+ } else {
134
+ // fades out on scroll as the extended header does off Duo; extended also collapses
135
+ let progress = min(scroll / duoExtendedCollapseDistance, 1)
136
+ gradient
137
+ .frame(height: headerHeight + (extended ? duoGradientExtra * (1 - progress) : 0))
138
+ .opacity(Double(1 - min(scroll / duoGradientExtra, 1)))
139
+ }
140
+ }
141
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
142
+ .ignoresSafeArea()
143
+ .allowsHitTesting(false)
144
+ }
145
+
146
+ /// The fade spans the gradient box.
147
+ private var gradient: LinearGradient {
148
+ LinearGradient(
149
+ colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
150
+ startPoint: .top,
151
+ endPoint: .bottom
152
+ )
153
+ }
154
+ }