@momo-kits/native-kits 0.167.1-duo.5-debug → 0.167.1-duo.6

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 (167) hide show
  1. package/CLAUDE.md +8 -3
  2. package/ios/Application/HeaderRight.swift +25 -12
  3. package/ios/Application/Layout/ScreenLayout.swift +196 -0
  4. package/ios/Application/Navigation/BottomTab/BottomTab.swift +30 -9
  5. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
  6. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +275 -0
  7. package/ios/Application/Navigation/HeaderUser.swift +4 -1
  8. package/ios/Application/Navigation/NavigationContainer.swift +3 -0
  9. package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
  10. package/ios/Application/Navigation/Overplay/ModalScreen.swift +5 -1
  11. package/ios/Application/Navigation/component/DuoScrollGradient.swift +55 -0
  12. package/ios/Application/Navigation/component/Header.swift +9 -3
  13. package/ios/Application/Navigation/component/HeaderBackground.swift +3 -2
  14. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  15. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +61 -9
  16. package/ios/Application/Navigation/component/SideBar.swift +155 -0
  17. package/ios/Application/StackScreen.swift +135 -18
  18. package/ios/Popup/PopupDisplay.swift +84 -68
  19. package/ios/Popup/PopupLayout.swift +45 -0
  20. package/ios/Popup/PopupNotify.swift +43 -32
  21. package/ios/Popup/PopupPromotion.swift +17 -5
  22. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +76 -0
  23. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  24. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +170 -0
  25. package/package.json +1 -1
  26. package/build.gradle.kts +0 -11
  27. package/compose/build.gradle.kts +0 -190
  28. package/compose/build.gradle.kts.backup +0 -190
  29. package/compose/compose.podspec +0 -47
  30. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  31. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  32. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  33. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  34. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -151
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -831
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -334
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -98
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -188
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -157
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -299
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -129
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -622
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -175
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -232
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -381
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -83
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -105
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -347
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -169
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  149. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -71
  150. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  151. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  152. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  153. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  154. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
  155. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  156. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  157. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  158. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  159. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -213
  160. package/gradle/libs.versions.toml +0 -69
  161. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  162. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  163. package/gradle.properties +0 -26
  164. package/gradlew +0 -252
  165. package/gradlew.bat +0 -94
  166. package/ios-demo/README.md +0 -120
  167. package/settings.gradle.kts +0 -64
@@ -123,6 +123,7 @@ public struct NavigationContainer<Initial: View>: View {
123
123
  private func overplayLayer(presentedId: Int?) -> some View {
124
124
  if let item = navigator.overplay, item.presentedId == presentedId {
125
125
  OverplayHost(item: item)
126
+ .momoScreenLayout()
126
127
  }
127
128
  }
128
129
 
@@ -170,6 +171,8 @@ private struct ScreenHost: View {
170
171
  screenName: screen.name,
171
172
  content: { screen.content() }
172
173
  )
174
+ // Per screen, not around the NavigationStack: a state-driven reader there stops path pushes rendering.
175
+ .momoScreenLayout()
173
176
  }
174
177
  }
175
178
 
@@ -45,6 +45,7 @@ struct BottomSheetView<Content: View>: View {
45
45
  @EnvironmentObject private var navigator: Navigator
46
46
  @Environment(\.appTheme) private var theme
47
47
  @Environment(\.applicationEnvironment) private var appEnv
48
+ @Environment(\.screenLayout) private var layout
48
49
 
49
50
  /// Vertical translation of the sheet; starts off-screen and animates to 0.
50
51
  @State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
@@ -66,7 +67,36 @@ struct BottomSheetView<Content: View>: View {
66
67
  /// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
67
68
  private let maxDynamicAlpha: CGFloat = 0.3
68
69
 
69
- private var screenHeight: CGFloat { UIScreen.main.bounds.height }
70
+ // UIScreen stays on the outer display on iPhone Duo, even when unfolded
71
+ private var screenHeight: CGFloat { isDuo ? layout.height : UIScreen.main.bounds.height }
72
+
73
+ private var isDuo: Bool { layout.isDuoDevice }
74
+ // iPhone Duo inner display: center the sheet instead of spanning the width
75
+ private var isDuoCentered: Bool { isDuo && layout.width >= 600 }
76
+
77
+ /// Keeps the portrait width in landscape, like system sheets.
78
+ private var duoCenteredWidth: CGFloat {
79
+ min(
80
+ min(layout.width, layout.height) - Spacing.L * 2,
81
+ layout.width - max(layout.insets.leading, layout.insets.trailing, Spacing.L) * 2
82
+ )
83
+ }
84
+
85
+ /// Landscape side buttons span the full height, so the content is inset; in portrait the
86
+ /// side status bar only covers the top corner.
87
+ private var sideInsetLeading: CGFloat {
88
+ isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.leading : 0
89
+ }
90
+
91
+ private var sideInsetTrailing: CGFloat {
92
+ isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.trailing : 0
93
+ }
94
+
95
+ /// The keyboard filler is part of the sheet, so capping the sheet shrinks the content by it.
96
+ private var duoMaxHeight: CGFloat? {
97
+ guard isDuo else { return nil }
98
+ return layout.height - max(layout.duoTopInset, Spacing.XL)
99
+ }
70
100
  private var bottomInset: CGFloat { safeAreaBottomInset() }
71
101
  /// While the keyboard is up, it already covers the home-indicator area, so it
72
102
  /// replaces (rather than adds to) the usual bottom-inset filler.
@@ -110,7 +140,7 @@ struct BottomSheetView<Content: View>: View {
110
140
  .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
111
141
  guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
112
142
  withAnimation(.easeOut(duration: 0.25)) {
113
- keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
143
+ keyboardHeight = screenHeight - endFrame.origin.y
114
144
  }
115
145
  }
116
146
  .onAppear {
@@ -138,6 +168,8 @@ struct BottomSheetView<Content: View>: View {
138
168
  headerView
139
169
  .frame(height: 56)
140
170
  .padding(.horizontal, Spacing.M)
171
+ .padding(.leading, sideInsetLeading)
172
+ .padding(.trailing, sideInsetTrailing)
141
173
  }
142
174
  .frame(maxWidth: .infinity)
143
175
  .frame(height: 72)
@@ -149,12 +181,17 @@ struct BottomSheetView<Content: View>: View {
149
181
  .frame(height: 1)
150
182
 
151
183
  content()
184
+ .padding(.leading, sideInsetLeading)
185
+ .padding(.trailing, sideInsetTrailing)
186
+ // a tall sheet on iPhone Duo keeps its header and gives the content what is left
187
+ .layoutPriority(-1)
152
188
 
153
189
  backgroundColor
154
190
  .frame(height: effectiveBottomInset)
155
191
  }
156
- .frame(maxWidth: .infinity)
157
- .frame(alignment: .bottom)
192
+ .frame(maxWidth: isDuoCentered ? duoCenteredWidth : .infinity)
193
+ .frame(maxHeight: duoMaxHeight, alignment: .bottom)
194
+ .clipped()
158
195
  .background(
159
196
  GeometryReader { geo in
160
197
  Color.clear.onAppear { sheetHeight = geo.size.height }
@@ -16,6 +16,7 @@ struct ModalScreen<Content: View>: View {
16
16
 
17
17
  @EnvironmentObject private var navigator: Navigator
18
18
  @Environment(\.applicationEnvironment) private var appEnv
19
+ @Environment(\.screenLayout) private var layout
19
20
 
20
21
  @State private var scrimOpacity: CGFloat = 0
21
22
  @State private var contentScale: CGFloat = 0.8
@@ -35,8 +36,11 @@ struct ModalScreen<Content: View>: View {
35
36
  content()
36
37
  .scaleEffect(contentScale)
37
38
  .opacity(contentOpacity)
39
+ // iPhone Duo: center below the side status bar and above the home indicator
40
+ .padding(.top, layout.isDuoDevice ? layout.duoTopInset : 0)
41
+ .padding(.bottom, layout.isDuoDevice ? layout.insets.bottom : 0)
38
42
  }
39
- // Center on the full screen like Compose; landscape safe areas are asymmetric (camera on one side).
43
+ // Full screen like RN's Modal, so asymmetric landscape insets don't shift the content sideways.
40
44
  .frame(maxWidth: .infinity, maxHeight: .infinity)
41
45
  .ignoresSafeArea(.container)
42
46
  .onAppear {
@@ -0,0 +1,55 @@
1
+ //
2
+ // DuoScrollGradient.swift
3
+ // MoMoUIKits
4
+ //
5
+ // Port of RN `DuoScrollGradient` (Components/DuoGlassBackground.tsx): with the iPhone Duo
6
+ // glass header the content scrolls under the bar, so the header gradient is drawn under it.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ // extended headers run their gradient this far below the header, as off Duo
12
+ private let extendedGradientExtra: CGFloat = 52
13
+ // scroll distance over which the extended gradient collapses to the header
14
+ private let extendedCollapseDistance: CGFloat = 100
15
+
16
+ struct DuoScrollGradient: View {
17
+ let scrollOffset: CGFloat
18
+ let headerHeight: CGFloat
19
+ let extended: Bool
20
+
21
+ @Environment(\.appTheme) private var theme
22
+ @Environment(\.screenLayout) private var layout
23
+
24
+ private var scroll: CGFloat { max(scrollOffset, 0) }
25
+
26
+ private var gradient: LinearGradient {
27
+ LinearGradient(
28
+ colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
29
+ startPoint: .top,
30
+ endPoint: .bottom
31
+ )
32
+ }
33
+
34
+ var body: some View {
35
+ Group {
36
+ if extended {
37
+ // extended squeezes the whole fade into its box and fades out
38
+ let progress = min(scroll / extendedCollapseDistance, 1)
39
+ gradient
40
+ .frame(height: headerHeight + extendedGradientExtra * (1 - progress))
41
+ .opacity(1 - min(scroll / extendedGradientExtra, 1))
42
+ } else {
43
+ // default keeps the off-Duo 375:154 shape, the header clips it, and scrolls away
44
+ gradient
45
+ .frame(height: layout.width * 154 / 375)
46
+ .frame(height: headerHeight, alignment: .top)
47
+ .clipped()
48
+ .offset(y: -scroll)
49
+ }
50
+ }
51
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
52
+ .ignoresSafeArea()
53
+ .allowsHitTesting(false)
54
+ }
55
+ }
@@ -60,7 +60,11 @@ public struct NavigationHeader: View {
60
60
  self.titleOpacity = titleOpacity
61
61
  }
62
62
 
63
+ @Environment(\.screenLayout) private var layout
64
+
63
65
  private var isAnimated: Bool { animatedHeader != nil }
66
+ /// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
67
+ private var useSideBar: Bool { layout.sideBar != nil }
64
68
  private var isSurface: Bool { animatedHeader?.isSurface ?? true }
65
69
 
66
70
  private var headerColor: HeaderColor {
@@ -79,7 +83,7 @@ public struct NavigationHeader: View {
79
83
  if headerType != .none {
80
84
  ZStack {
81
85
  HStack(spacing: 0) {
82
- if let goBack {
86
+ if let goBack, !useSideBar {
83
87
  BackButton(color: headerColor, onBack: goBack)
84
88
  Spacer().frame(width: Spacing.S)
85
89
  }
@@ -91,7 +95,7 @@ public struct NavigationHeader: View {
91
95
  .frame(maxWidth: .infinity, alignment: .leading)
92
96
  }
93
97
 
94
- if let headerRight {
98
+ if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
95
99
  AnyView(headerRight())
96
100
  .background(
97
101
  GeometryReader { geo in
@@ -107,8 +111,10 @@ public struct NavigationHeader: View {
107
111
  .padding(.horizontal, Spacing.M)
108
112
  }
109
113
  }
110
- .frame(height: HEADER_HEIGHT)
114
+ .frame(height: layout.headerBarHeight)
111
115
  .overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
116
+ // status bar off the top edge: the row spans the whole header from the window top
117
+ .padding(.top, min(layout.headerStatusBarHeight - layout.insets.top, 0))
112
118
  }
113
119
  }
114
120
 
@@ -43,10 +43,11 @@ public struct NavigationHeaderBackground: View {
43
43
  )
44
44
  }
45
45
 
46
+ @Environment(\.screenLayout) private var layout
47
+
46
48
  public var body: some View {
47
- let statusBarHeight = safeAreaTopInset()
48
49
  let opacity = max(0, 1 - scrollState / CGFloat(HEADER_HEIGHT))
49
- let minHeight = fullScreenContent ? 0 : statusBarHeight + HEADER_HEIGHT
50
+ let minHeight = fullScreenContent ? 0 : layout.headerHeight
50
51
  let maxHeight: CGFloat = 154
51
52
  let backgroundColor = headerTransparent ? Color.clear : Colors.black01
52
53
  let topColor = opacity == 0 ? backgroundColor : Color(hex: 0xFDCADE)
@@ -69,7 +69,7 @@ public struct NavigationFooter: View {
69
69
  .padding(.bottom, Self.bottomPadding)
70
70
  // Compose paints the surface all the way to the bottom of the screen; the
71
71
  // footer's own frame stops at the safe area, so only the fill bleeds down.
72
- .background(theme.colors.background.surface.ignoresSafeArea(.container, edges: .bottom))
72
+ .background(theme.colors.background.surface.ignoresSafeArea(.container, edges: [.bottom, .horizontal]))
73
73
  .overlay(alignment: .top) { shadow }
74
74
  .offset(y: Self.verticalOffset(keyboardSize: keyboardSize))
75
75
  }
@@ -26,6 +26,8 @@ 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
@@ -142,15 +144,68 @@ struct ToolkitNavigationHeaderRight: View {
142
144
  return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
143
145
  }
144
146
 
147
+ private var showHelpCenter: Bool {
148
+ environment.applicationContext != nil && showsHelpCenter(environment.applicationContext)
149
+ }
150
+
151
+ private var closeIcon: String { headerRight?.closeIcon ?? "16_basic_home" }
152
+
145
153
  var body: some View {
154
+ Group {
155
+ if vertical {
156
+ verticalBody
157
+ } else {
158
+ horizontalBody
159
+ }
160
+ }
161
+ .onAppear {
162
+ loadFavoriteState()
163
+ }
164
+ }
165
+
166
+ private var showBadge: Bool {
167
+ headerRight?.tools.contains { group in
168
+ group.items.contains { $0.showBadge }
169
+ } ?? false
170
+ }
171
+
172
+ private var verticalBody: some View {
173
+ let navButtonConfig = getNavigationButtonConfig()
174
+ return VStack(spacing: Spacing.M) {
175
+ if shouldShowShortcut {
176
+ SideBarButton(
177
+ icon: navButtonConfig.icon,
178
+ tintColor: tintColor,
179
+ showBadge: showBadge,
180
+ disabled: isLoading,
181
+ action: navButtonConfig.onPress
182
+ )
183
+ }
184
+ SideBarGroup(tintColor: tintColor, itemCount: showHelpCenter ? 2 : 1) {
185
+ if showHelpCenter {
186
+ SideBarGroupItem(
187
+ icon: "help_center",
188
+ tintColor: tintColor,
189
+ accessibilityId: "btn_navigation_help_center",
190
+ action: onPressHelpCenter
191
+ )
192
+ }
193
+ SideBarGroupItem(
194
+ icon: closeIcon,
195
+ tintColor: tintColor,
196
+ accessibilityId: "btn_navigation_close",
197
+ action: onPressClose
198
+ )
199
+ }
200
+ }
201
+ }
202
+
203
+ private var horizontalBody: some View {
146
204
  let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
147
205
  let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
148
206
  let navButtonConfig = getNavigationButtonConfig()
149
- let showBadge = headerRight?.tools.contains { group in
150
- group.items.contains { $0.showBadge }
151
- } ?? false
152
207
 
153
- HStack(alignment: .center, spacing: 0) {
208
+ return HStack(alignment: .center, spacing: 0) {
154
209
  if shouldShowShortcut {
155
210
  NavigationButton(
156
211
  disabled: isLoading,
@@ -162,7 +217,7 @@ struct ToolkitNavigationHeaderRight: View {
162
217
  }
163
218
 
164
219
  HStack(alignment: .center, spacing: 0) {
165
- if environment.applicationContext != nil {
220
+ if showHelpCenter {
166
221
  Icon(source: "help_center", size: 20, color: tintColor ?? Colors.black17)
167
222
  .padding(4)
168
223
  .onTapGesture {
@@ -174,7 +229,7 @@ struct ToolkitNavigationHeaderRight: View {
174
229
  .frame(width: 0.5, height: 12)
175
230
  }
176
231
 
177
- Icon(source: "16_basic_home", size: 20, color: tintColor ?? Colors.black17)
232
+ Icon(source: closeIcon, size: 20, color: tintColor ?? Colors.black17)
178
233
  .padding(4)
179
234
  .onTapGesture {
180
235
  onPressClose()
@@ -189,8 +244,5 @@ struct ToolkitNavigationHeaderRight: View {
189
244
  )
190
245
  .padding(.leading, Spacing.S)
191
246
  }
192
- .onAppear {
193
- loadFavoriteState()
194
- }
195
247
  }
196
248
  }
@@ -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; 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 - Spacing.XL - 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
+ }