@momo-kits/native-kits 0.167.1-beta.9-debug → 0.167.1-duo-search.1

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 (189) hide show
  1. package/CLAUDE.md +36 -0
  2. package/ios/Application/ApplicationEnvironment.swift +6 -0
  3. package/ios/Application/Components.swift +1 -3
  4. package/ios/Application/HeaderRight.swift +32 -14
  5. package/ios/Application/Layout/ScreenLayout.swift +312 -0
  6. package/ios/Application/Navigation/BottomTab/BottomTab.swift +67 -10
  7. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
  8. package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  9. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +304 -0
  10. package/ios/Application/Navigation/HeaderUser.swift +4 -4
  11. package/ios/Application/Navigation/NavigationContainer.swift +48 -42
  12. package/ios/Application/Navigation/Navigator.swift +122 -29
  13. package/ios/Application/Navigation/NavigatorManager.swift +13 -6
  14. package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
  15. package/ios/Application/Navigation/Overplay/ModalScreen.swift +7 -0
  16. package/ios/Application/Navigation/component/DuoScrollGradient.swift +54 -0
  17. package/ios/Application/Navigation/component/Header.swift +11 -6
  18. package/ios/Application/Navigation/component/HeaderBackground.swift +3 -2
  19. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  20. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +93 -9
  21. package/ios/Application/Navigation/component/SideBar.swift +155 -0
  22. package/ios/Application/StackScreen.swift +175 -18
  23. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
  24. package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
  25. package/ios/Extensions/Color++.swift +20 -9
  26. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  27. package/ios/Icon/Icon.swift +14 -22
  28. package/ios/Icon/Icons.swift +1329 -0
  29. package/ios/Image/Image.swift +16 -5
  30. package/ios/Lottie/LottieView.swift +4 -2
  31. package/ios/Popup/PopupDisplay.swift +84 -67
  32. package/ios/Popup/PopupLayout.swift +45 -0
  33. package/ios/Popup/PopupNotify.swift +44 -26
  34. package/ios/Popup/PopupPromotion.swift +18 -6
  35. package/ios/Resources/dot_loading.json +558 -0
  36. package/ios/Resources/lottie_circle_loader.json +1 -0
  37. package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
  38. package/ios/Typography/FontScale.swift +3 -3
  39. package/ios/native-kits.podspec +1 -1
  40. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  41. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  42. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  43. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  44. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  45. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  46. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  47. package/package.json +1 -1
  48. package/scripts/gen-ios-icons.mjs +36 -0
  49. package/build.gradle.kts +0 -11
  50. package/compose/build.gradle.kts +0 -190
  51. package/compose/build.gradle.kts.backup +0 -190
  52. package/compose/compose.podspec +0 -47
  53. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  54. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  55. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  56. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  57. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -151
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -831
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -328
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -97
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -14
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -221
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -270
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -52
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -38
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -39
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -299
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -129
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -201
  149. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  150. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  151. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -595
  152. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  153. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  154. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -175
  155. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -225
  156. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  157. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  158. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -358
  159. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -83
  160. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
  161. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -75
  162. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -309
  163. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -40
  164. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -364
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -71
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -105
  175. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
  176. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  177. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  178. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  179. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  180. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -213
  181. package/gradle/libs.versions.toml +0 -69
  182. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  183. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  184. package/gradle.properties +0 -26
  185. package/gradlew +0 -252
  186. package/gradlew.bat +0 -94
  187. package/ios-demo/README.md +0 -120
  188. package/local.properties +0 -8
  189. package/settings.gradle.kts +0 -64
@@ -26,9 +26,12 @@ public struct NavigationHeaderRight: View {
26
26
  struct ToolkitNavigationHeaderRight: View {
27
27
  var headerRight: HeaderRightData?
28
28
  var tintColor: Color?
29
+ /// iPhone Duo side bar column instead of the bar row.
30
+ var vertical: Bool = false
29
31
  @State private var isFavorite: Bool = false
30
32
  @State private var isLoading: Bool = false
31
33
  @Environment(\.applicationEnvironment) private var environment
34
+ @Environment(\.duoBarCompression) private var compression
32
35
 
33
36
  private func apiValue(_ value: Any?) -> Any {
34
37
  value ?? NSNull()
@@ -142,15 +145,99 @@ struct ToolkitNavigationHeaderRight: View {
142
145
  return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
143
146
  }
144
147
 
148
+ private var showHelpCenter: Bool {
149
+ environment.applicationContext != nil && showsHelpCenter(environment.applicationContext)
150
+ }
151
+
152
+ private var closeIcon: String { headerRight?.closeIcon ?? "16_basic_home" }
153
+
145
154
  var body: some View {
155
+ Group {
156
+ if vertical {
157
+ verticalBody
158
+ } else {
159
+ horizontalBody
160
+ }
161
+ }
162
+ .onAppear {
163
+ loadFavoriteState()
164
+ }
165
+ }
166
+
167
+ private var showBadge: Bool {
168
+ headerRight?.tools.contains { group in
169
+ group.items.contains { $0.showBadge }
170
+ } ?? false
171
+ }
172
+
173
+ /// Back slot, the optional shortcut, then the help/home capsule.
174
+ private var fullToolbarHeight: CGFloat {
175
+ DuoMetrics.controlSize + Spacing.M
176
+ + (shouldShowShortcut ? DuoMetrics.controlSize + Spacing.M : 0)
177
+ + (showHelpCenter ? 2 * DuoMetrics.itemPitch : DuoMetrics.controlSize)
178
+ }
179
+
180
+ @ViewBuilder
181
+ private var verticalBody: some View {
182
+ Group {
183
+ if compression.toolbarHidden {
184
+ // an open tab rail takes this slot for a moment
185
+ EmptyView()
186
+ } else if compression.toolbarCompressed {
187
+ // HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
188
+ SideBarButton(
189
+ icon: "navigation_menu",
190
+ tintColor: tintColor,
191
+ showBadge: showBadge,
192
+ accessibilityId: "btn_navigation_toolbar",
193
+ action: compression.expandToolbar
194
+ )
195
+ } else {
196
+ toolbarColumn
197
+ }
198
+ }
199
+ // full height, so the tab bar compression decision matches this screen's toolkit
200
+ .onAppear { compression.setToolbarHeight(fullToolbarHeight) }
201
+ .onChange(of: fullToolbarHeight) { compression.setToolbarHeight($0) }
202
+ }
203
+
204
+ private var toolbarColumn: some View {
205
+ let navButtonConfig = getNavigationButtonConfig()
206
+ return VStack(spacing: Spacing.M) {
207
+ if shouldShowShortcut {
208
+ SideBarButton(
209
+ icon: navButtonConfig.icon,
210
+ tintColor: tintColor,
211
+ showBadge: showBadge,
212
+ disabled: isLoading,
213
+ action: navButtonConfig.onPress
214
+ )
215
+ }
216
+ SideBarGroup(tintColor: tintColor, itemCount: showHelpCenter ? 2 : 1) {
217
+ if showHelpCenter {
218
+ SideBarGroupItem(
219
+ icon: "help_center",
220
+ tintColor: tintColor,
221
+ accessibilityId: "btn_navigation_help_center",
222
+ action: onPressHelpCenter
223
+ )
224
+ }
225
+ SideBarGroupItem(
226
+ icon: closeIcon,
227
+ tintColor: tintColor,
228
+ accessibilityId: "btn_navigation_close",
229
+ action: onPressClose
230
+ )
231
+ }
232
+ }
233
+ }
234
+
235
+ private var horizontalBody: some View {
146
236
  let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
147
237
  let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
148
238
  let navButtonConfig = getNavigationButtonConfig()
149
- let showBadge = headerRight?.tools.contains { group in
150
- group.items.contains { $0.showBadge }
151
- } ?? false
152
239
 
153
- HStack(alignment: .center, spacing: 0) {
240
+ return HStack(alignment: .center, spacing: 0) {
154
241
  if shouldShowShortcut {
155
242
  NavigationButton(
156
243
  disabled: isLoading,
@@ -162,7 +249,7 @@ struct ToolkitNavigationHeaderRight: View {
162
249
  }
163
250
 
164
251
  HStack(alignment: .center, spacing: 0) {
165
- if environment.applicationContext != nil {
252
+ if showHelpCenter {
166
253
  Icon(source: "help_center", size: 20, color: tintColor ?? Colors.black17)
167
254
  .padding(4)
168
255
  .onTapGesture {
@@ -174,7 +261,7 @@ struct ToolkitNavigationHeaderRight: View {
174
261
  .frame(width: 0.5, height: 12)
175
262
  }
176
263
 
177
- Icon(source: "16_basic_home", size: 20, color: tintColor ?? Colors.black17)
264
+ Icon(source: closeIcon, size: 20, color: tintColor ?? Colors.black17)
178
265
  .padding(4)
179
266
  .onTapGesture {
180
267
  onPressClose()
@@ -189,8 +276,5 @@ struct ToolkitNavigationHeaderRight: View {
189
276
  )
190
277
  .padding(.leading, Spacing.S)
191
278
  }
192
- .onAppear {
193
- loadFavoriteState()
194
- }
195
279
  }
196
280
  }
@@ -0,0 +1,155 @@
1
+ //
2
+ // SideBar.swift
3
+ // MoMoUIKits
4
+ //
5
+ // iPhone Duo: back and toolkit leave the header bar for a column in the side safe area,
6
+ // as the system bars do. Port of RN `SideBarButton.tsx` + `useSideBarItemStyle`.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ private let sideBarIconSize: CGFloat = 24
12
+
13
+ /// Toolkit data when `headerRight` renders the kit toolkit; nil for a custom header right.
14
+ func toolkitHeaderRight(_ headerRight: (() -> any View)?) -> (data: HeaderRightData?, tintColor: Color?)? {
15
+ guard let view = headerRight?() else { return nil }
16
+ if let toolkit = view as? HeaderRight { return (toolkit.headerRight, toolkit.tintColor) }
17
+ if let toolkit = view as? NavigationHeaderRight { return (toolkit.headerRight, toolkit.tintColor) }
18
+ return nil
19
+ }
20
+
21
+ /// The back button and toolkit column, placed in window coordinates over the side strip.
22
+ struct HeaderSideBar: View {
23
+ let sideBar: SideBarLayout
24
+ let goBack: (() -> Void)?
25
+ let toolkit: (data: HeaderRightData?, tintColor: Color?)?
26
+ let headerColor: HeaderColor
27
+ /// What footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
28
+ let bottomInset: CGFloat
29
+
30
+ @Environment(\.screenLayout) private var layout
31
+ @State private var columnHeight: CGFloat = 0
32
+
33
+ private var isRight: Bool { sideBar.edge == .right }
34
+
35
+ private var top: CGFloat {
36
+ let maxY = layout.height - bottomInset - columnHeight
37
+ return max(min(sideBar.top, maxY), 0)
38
+ }
39
+
40
+ var body: some View {
41
+ VStack(alignment: isRight ? .leading : .trailing, spacing: Spacing.M) {
42
+ if let goBack {
43
+ SideBarButton(
44
+ icon: "arrow-back",
45
+ tintColor: headerColor.tintIconColor,
46
+ accessibilityId: "btn_navigation_back",
47
+ action: goBack
48
+ )
49
+ } else {
50
+ // keep the slot so the toolkit does not jump between root and pushed screens
51
+ Color.clear.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
52
+ }
53
+ if let toolkit {
54
+ ToolkitNavigationHeaderRight(
55
+ headerRight: toolkit.data,
56
+ tintColor: toolkit.tintColor ?? headerColor.tintIconColor,
57
+ vertical: true
58
+ )
59
+ }
60
+ }
61
+ .padding(isRight ? .leading : .trailing, Spacing.M)
62
+ .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
63
+ .background(
64
+ GeometryReader { proxy in
65
+ Color.clear
66
+ .onAppear { columnHeight = proxy.size.height }
67
+ .onChange(of: proxy.size.height) { columnHeight = $0 }
68
+ }
69
+ )
70
+ .padding(.top, top)
71
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .topTrailing : .topLeading)
72
+ .ignoresSafeArea()
73
+ }
74
+ }
75
+
76
+ /// Dark glass under light icons (banner headers), light glass otherwise.
77
+ private struct SideBarGlass<S: Shape>: ViewModifier {
78
+ let tintColor: Color?
79
+ let shape: S
80
+
81
+ private var isDark: Bool { tintColor == Colors.black01 || tintColor == .white }
82
+
83
+ func body(content: Content) -> some View {
84
+ if isDark {
85
+ content
86
+ .background(shape.fill(Colors.black20.opacity(0.5)))
87
+ .overlay(shape.stroke(Colors.black01.opacity(0.2), lineWidth: 0.5))
88
+ } else {
89
+ content
90
+ .background(
91
+ shape.fill(Colors.black01.opacity(0.85))
92
+ .shadow(color: Color.black.opacity(0.12), radius: 4, x: 0, y: 2)
93
+ )
94
+ .overlay(shape.stroke(Colors.black20.opacity(0.08), lineWidth: 0.5))
95
+ }
96
+ }
97
+ }
98
+
99
+ /// 48pt glass circle for a single side bar control.
100
+ struct SideBarButton: View {
101
+ let icon: String
102
+ var tintColor: Color?
103
+ var showBadge: Bool = false
104
+ var disabled: Bool = false
105
+ var accessibilityId: String?
106
+ let action: () -> Void
107
+
108
+ var body: some View {
109
+ SwiftUI.Button(action: { if !disabled { action() } }) {
110
+ Icon(source: icon, size: sideBarIconSize, color: tintColor ?? Colors.black17)
111
+ .frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
112
+ .modifier(SideBarGlass(tintColor: tintColor, shape: Circle()))
113
+ .overlay(alignment: .topTrailing) {
114
+ if showBadge {
115
+ BadgeDot(size: .small).offset(x: -Spacing.XXS, y: Spacing.XXS)
116
+ }
117
+ }
118
+ }
119
+ .buttonStyle(.plain)
120
+ .accessibilityIdentifier(accessibilityId ?? "")
121
+ }
122
+ }
123
+
124
+ /// 48pt wide glass capsule grouping related controls, one item per 50pt.
125
+ struct SideBarGroup<Content: View>: View {
126
+ var tintColor: Color?
127
+ let itemCount: Int
128
+ @ViewBuilder let content: () -> Content
129
+
130
+ var body: some View {
131
+ VStack(spacing: 0) { content() }
132
+ .frame(
133
+ width: DuoMetrics.controlSize,
134
+ height: itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
135
+ )
136
+ .modifier(SideBarGlass(tintColor: tintColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
137
+ }
138
+ }
139
+
140
+ struct SideBarGroupItem: View {
141
+ let icon: String
142
+ var tintColor: Color?
143
+ let accessibilityId: String
144
+ let action: () -> Void
145
+
146
+ var body: some View {
147
+ SwiftUI.Button(action: action) {
148
+ Icon(source: icon, size: sideBarIconSize, color: tintColor ?? Colors.black17)
149
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
150
+ .contentShape(Rectangle())
151
+ }
152
+ .buttonStyle(.plain)
153
+ .accessibilityIdentifier(accessibilityId)
154
+ }
155
+ }
@@ -102,8 +102,14 @@ public struct StackScreen<Content: View>: View {
102
102
  @State private var keyboardHeight: CGFloat = 0
103
103
  @State private var headerRightWidth: CGFloat = 0
104
104
  @State private var isBottomTabRoot: Bool = false
105
+ @State private var headerHide: CGFloat = 0
106
+ @State private var lastScrollOffset: CGFloat = 0
107
+ @State private var headerSettle: Task<Void, Never>?
105
108
 
106
109
  @Environment(\.applicationEnvironment) private var appEnv
110
+ @Environment(\.screenLayout) private var layout
111
+ @Environment(\.duoTabBarInsets) private var duoTabBarInsets
112
+ @State private var footerHeight: CGFloat = 0
107
113
 
108
114
  // Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
109
115
  // the once-computed push/back `action`).
@@ -125,6 +131,63 @@ public struct StackScreen<Content: View>: View {
125
131
 
126
132
  private static var headerHeight: CGFloat { 52 }
127
133
 
134
+ private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
135
+
136
+ private var showsSideBar: Bool {
137
+ layout.sideBar != nil && headerType != .none && !isBottomTabRoot && (goBack != nil || toolkit != nil)
138
+ }
139
+
140
+ /// iPhone Duo: content scrolls under a glass header instead of starting below it.
141
+ private var useDuoGlass: Bool {
142
+ layout.sideBar != nil && headerType != .none && animatedHeader == nil
143
+ && inputSearchProps == nil && !headerTransparent && !fullScreenContent
144
+ }
145
+
146
+ /// iPhone Duo: the header leaves with the content and slides back in on a reverse scroll
147
+ /// (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
148
+ private var useQuickReturn: Bool {
149
+ layout.sideBar != nil && scrollable && (useDuoGlass || animatedHeader != nil)
150
+ }
151
+
152
+ private func updateHeaderHide(_ offset: CGFloat) {
153
+ let delta = offset - lastScrollOffset
154
+ lastScrollOffset = offset
155
+ guard useQuickReturn, offset > 0 else {
156
+ if headerHide != 0 { headerHide = 0 }
157
+ return
158
+ }
159
+ headerHide = min(max(headerHide + delta, 0), layout.headerHeight)
160
+ settleHeaderWhenScrollStops()
161
+ }
162
+
163
+ /// A half-shown header settles fully in or out. SwiftUI before iOS 18 reports no scroll
164
+ /// phase, so the scroll counts as ended once the offset stops changing.
165
+ private func settleHeaderWhenScrollStops() {
166
+ headerSettle?.cancel()
167
+ headerSettle = Task { @MainActor in
168
+ try? await Task.sleep(nanoseconds: 150_000_000)
169
+ guard !Task.isCancelled else { return }
170
+ let height = layout.headerHeight
171
+ // within the first header height the header still sits on the content
172
+ guard scrollOffset >= height, headerHide > 0, headerHide < height else { return }
173
+ withAnimation(.easeOut(duration: 0.2)) {
174
+ headerHide = headerHide > height / 2 ? height : 0
175
+ }
176
+ }
177
+ }
178
+
179
+ /// What the footer, tab bar or home indicator takes from the bottom of the window, gap included.
180
+ private var bottomBarsHeight: CGFloat {
181
+ if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom + Spacing.XL }
182
+ if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve }
183
+ return NavigationFooter.navigationBarInset + Spacing.XL
184
+ }
185
+
186
+ /// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
187
+ private var keyboardReferenceHeight: CGFloat {
188
+ layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
189
+ }
190
+
128
191
  /// Extra top space reserved inside the content so the animated search bar has
129
192
  /// room below the header at scroll 0; it shrinks to 0 as the bar slides up.
130
193
  private var animatedSearchInset: CGFloat {
@@ -163,9 +226,18 @@ public struct StackScreen<Content: View>: View {
163
226
  hasFooter: footer != nil,
164
227
  scrollOffset: scrollOffset
165
228
  )
229
+ .padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
166
230
  }
167
231
 
168
232
  NavigationFooter(footerComponent: footer, keyboardSize: keyboardSize)
233
+ .padding(.bottom, duoTabBarInsets.overlay)
234
+ .background(
235
+ GeometryReader { proxy in
236
+ Color.clear
237
+ .onAppear { footerHeight = proxy.size.height }
238
+ .onChange(of: proxy.size.height) { footerHeight = $0 }
239
+ }
240
+ )
169
241
  }
170
242
 
171
243
  // Animated search bar overlay (mirrors Compose's SearchAnimated layer).
@@ -176,11 +248,29 @@ public struct StackScreen<Content: View>: View {
176
248
  hiddenBack: !isBack,
177
249
  hasHeaderRight: headerRight != nil,
178
250
  headerRightWidth: headerRightWidth,
179
- headerHeight: Self.headerHeight
251
+ headerHeight: Self.headerHeight,
252
+ useSideBar: layout.sideBar != nil,
253
+ hasToolkit: toolkit != nil
254
+ )
255
+ }
256
+
257
+ if showsSideBar, let sideBar = layout.sideBar {
258
+ HeaderSideBar(
259
+ sideBar: sideBar,
260
+ goBack: goBack,
261
+ toolkit: toolkit,
262
+ headerColor: getHeaderColor(
263
+ isAnimated: animatedHeader != nil,
264
+ isSurface: animatedHeader?.isSurface ?? true,
265
+ opacity: scrollOpacity,
266
+ tintColor: tintColor
267
+ ),
268
+ bottomInset: bottomBarsHeight
180
269
  )
181
270
  }
182
271
  }
183
272
  .ignoresSafeArea(.keyboard, edges: .bottom)
273
+ .onChange(of: scrollOffset) { updateHeaderHide($0) }
184
274
  .onPreferenceChange(HeaderRightWidthKey.self) { width in
185
275
  if headerRightWidth != width { headerRightWidth = width }
186
276
  }
@@ -191,7 +281,7 @@ public struct StackScreen<Content: View>: View {
191
281
  guard useAvoidKeyboard,
192
282
  let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
193
283
  withAnimation(.easeOut(duration: 0.25)) {
194
- keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
284
+ keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
195
285
  }
196
286
  }
197
287
  // Mirrors Compose's `hideKeyboardOnTap()`: a simultaneous gesture so
@@ -246,16 +336,27 @@ public struct StackScreen<Content: View>: View {
246
336
  scrollOffset: $scrollOffset,
247
337
  scrollToTopCallback: scrollToTopCallback,
248
338
  animatedSearchInset: animatedSearchInset,
339
+ bottomInset: duoTabBarInsets.overlay,
249
340
  content: { animatedHeroBody(animatedHeader) }
250
341
  )
251
342
  .edgesIgnoringSafeArea(.all)
252
343
 
253
344
  header
254
345
  .background(
255
- Colors.black01
256
- .opacity(scrollOpacity)
257
- .edgesIgnoringSafeArea(.top)
346
+ Group {
347
+ if layout.sideBar != nil {
348
+ // glass shows once the banner has scrolled under the header
349
+ Color.clear.modifier(DuoBarMaterial(
350
+ shape: Rectangle(),
351
+ visible: scrollOffset > heroGlassStart(animatedHeader)
352
+ ))
353
+ } else {
354
+ Colors.black01.opacity(scrollOpacity)
355
+ }
356
+ }
357
+ .edgesIgnoringSafeArea([.top, .horizontal])
258
358
  )
359
+ .offset(y: -headerHide)
259
360
  .zIndex(100)
260
361
  }
261
362
  } else if headerTransparent && fullScreenContent {
@@ -271,9 +372,36 @@ public struct StackScreen<Content: View>: View {
271
372
  )
272
373
  .zIndex(100)
273
374
 
274
- StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
375
+ StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: { duoInsetContent })
275
376
  .edgesIgnoringSafeArea(.all)
276
377
  }
378
+ } else if useDuoGlass {
379
+ ZStack(alignment: .top) {
380
+ DuoScrollGradient(
381
+ scrollOffset: scrollOffset,
382
+ headerHeight: layout.headerHeight,
383
+ extended: headerType == .extended,
384
+ followScroll: useQuickReturn
385
+ )
386
+
387
+ StackScreenContent(
388
+ scrollable: scrollable,
389
+ scrollOffset: $scrollOffset,
390
+ scrollToTopCallback: scrollToTopCallback,
391
+ topInset: layout.headerHeight - layout.insets.top,
392
+ bottomInset: duoTabBarInsets.overlay,
393
+ content: content
394
+ )
395
+
396
+ header
397
+ .background(
398
+ Color.clear
399
+ .modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
400
+ .edgesIgnoringSafeArea([.top, .horizontal])
401
+ )
402
+ .offset(y: -headerHide)
403
+ .zIndex(100)
404
+ }
277
405
  } else {
278
406
  header
279
407
  .background(
@@ -283,28 +411,40 @@ public struct StackScreen<Content: View>: View {
283
411
  headerTransparent: headerTransparent,
284
412
  fullScreenContent: fullScreenContent
285
413
  )
286
- .edgesIgnoringSafeArea(.top)
414
+ .edgesIgnoringSafeArea([.top, .horizontal])
287
415
  )
288
416
 
289
- StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
417
+ StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: content)
290
418
  }
291
419
  }
292
420
 
421
+ /// Edge-to-edge branches lay out outside the safe area; iPhone Duo content still keeps
422
+ /// clear of the side strips, as RN's Screen does.
423
+ private var duoInsetContent: some View {
424
+ content()
425
+ .padding(.leading, layout.insetLeft)
426
+ .padding(.trailing, layout.insetRight)
427
+ }
428
+
429
+ private func heroGlassStart(_ animatedHeader: AnimatedHeader) -> CGFloat {
430
+ max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
431
+ }
432
+
293
433
  /// Hero image + body for an animated header. The image fills the width at its
294
434
  /// aspect-ratio height; the body is offset below the header band so it overlaps
295
435
  /// the lower part of the image (mirrors Compose's image box + padded content).
296
436
  @ViewBuilder
297
437
  private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
298
- let width = UIScreen.main.bounds.width
438
+ let width = layout.width
299
439
  let imageHeight = width / animatedHeader.aspectRatio.value
300
- let contentTopInset = safeAreaTopInset() + HEADER_HEIGHT + layoutOffset
440
+ let contentTopInset = layout.headerHeight + layoutOffset
301
441
 
302
442
  ZStack(alignment: .top) {
303
443
  animatedHeader.composable(scrollOffset)
304
444
  .frame(width: width, height: imageHeight)
305
445
  .clipped()
306
446
 
307
- content()
447
+ duoInsetContent
308
448
  .padding(.top, contentTopInset)
309
449
  }
310
450
  .frame(maxWidth: .infinity, alignment: .top)
@@ -343,6 +483,10 @@ private struct StackScreenContent<Content: View>: View {
343
483
  /// Extra top space reserved for the animated search bar (mirrors Compose's
344
484
  /// MainContent animated top spacer). Zero for non-animated screens.
345
485
  var animatedSearchInset: CGFloat = 0
486
+ /// Room for a header the content scrolls under (iPhone Duo glass header).
487
+ var topInset: CGFloat = 0
488
+ /// Room for a bar that overlays the content (iPhone Duo floating tab bar).
489
+ var bottomInset: CGFloat = 0
346
490
  let content: () -> Content
347
491
 
348
492
  private let topAnchorID = "stack_screen_top_anchor"
@@ -369,13 +513,17 @@ private struct StackScreenContent<Content: View>: View {
369
513
  }
370
514
  })
371
515
 
372
- if animatedSearchInset > 0 {
373
- Spacer().frame(height: animatedSearchInset)
516
+ if animatedSearchInset + topInset > 0 {
517
+ Spacer().frame(height: animatedSearchInset + topInset)
374
518
  }
375
519
 
376
520
  VStack {
377
521
  content()
378
522
  }
523
+
524
+ if bottomInset > 0 {
525
+ Spacer().frame(height: bottomInset)
526
+ }
379
527
  }
380
528
  }
381
529
  .coordinateSpace(name: scrollSpace)
@@ -390,6 +538,8 @@ private struct StackScreenContent<Content: View>: View {
390
538
  }
391
539
  } else {
392
540
  content()
541
+ .padding(.top, topInset)
542
+ .padding(.bottom, bottomInset)
393
543
  }
394
544
  }
395
545
  }
@@ -407,16 +557,23 @@ private struct SearchAnimatedHeader: View {
407
557
  let hasHeaderRight: Bool
408
558
  let headerRightWidth: CGFloat
409
559
  let headerHeight: CGFloat
560
+ var useSideBar: Bool = false
561
+ var hasToolkit: Bool = false
562
+
563
+ @Environment(\.screenLayout) private var layout
410
564
 
411
565
  var body: some View {
412
566
  let scroll = max(scrollOffset, 0)
413
567
 
414
- // Insets are measured from the safe-area top (status bar handled by layout).
415
- let minTop: CGFloat = 0
416
- let maxTop = headerHeight
568
+ // Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
569
+ let rowTop = layout.headerStatusBarHeight - layout.insets.top
570
+ let minTop = rowTop + (layout.headerBarHeight - headerHeight) / 2
571
+ let maxTop = layout.headerHeight - layout.insets.top
417
572
  let minSide = Spacing.M
418
- let maxStart: CGFloat = hiddenBack ? Spacing.M : 48
419
- let maxEnd = headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
573
+ let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
574
+ let maxEnd: CGFloat = useSideBar && hasToolkit
575
+ ? Spacing.M
576
+ : headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
420
577
 
421
578
  let top = min(max(maxTop - scroll, minTop), maxTop)
422
579
  let start = min(max(minSide + scroll, minSide), maxStart)