@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
@@ -104,6 +104,9 @@ public struct StackScreen<Content: View>: View {
104
104
  @State private var isBottomTabRoot: Bool = false
105
105
 
106
106
  @Environment(\.applicationEnvironment) private var appEnv
107
+ @Environment(\.screenLayout) private var layout
108
+ @Environment(\.duoTabBarInsets) private var duoTabBarInsets
109
+ @State private var footerHeight: CGFloat = 0
107
110
 
108
111
  // Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
109
112
  // the once-computed push/back `action`).
@@ -125,6 +128,30 @@ public struct StackScreen<Content: View>: View {
125
128
 
126
129
  private static var headerHeight: CGFloat { 52 }
127
130
 
131
+ private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
132
+
133
+ private var showsSideBar: Bool {
134
+ layout.sideBar != nil && headerType != .none && !isBottomTabRoot && (goBack != nil || toolkit != nil)
135
+ }
136
+
137
+ /// iPhone Duo: content scrolls under a glass header instead of starting below it.
138
+ private var useDuoGlass: Bool {
139
+ layout.sideBar != nil && headerType != .none && animatedHeader == nil
140
+ && inputSearchProps == nil && !headerTransparent && !fullScreenContent
141
+ }
142
+
143
+ /// Height the footer, tab bar or home indicator takes from the bottom of the window.
144
+ private var bottomBarsHeight: CGFloat {
145
+ if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom }
146
+ if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve - Spacing.XL }
147
+ return NavigationFooter.navigationBarInset
148
+ }
149
+
150
+ /// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
151
+ private var keyboardReferenceHeight: CGFloat {
152
+ layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
153
+ }
154
+
128
155
  /// Extra top space reserved inside the content so the animated search bar has
129
156
  /// room below the header at scroll 0; it shrinks to 0 as the bar slides up.
130
157
  private var animatedSearchInset: CGFloat {
@@ -163,9 +190,18 @@ public struct StackScreen<Content: View>: View {
163
190
  hasFooter: footer != nil,
164
191
  scrollOffset: scrollOffset
165
192
  )
193
+ .padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
166
194
  }
167
195
 
168
196
  NavigationFooter(footerComponent: footer, keyboardSize: keyboardSize)
197
+ .padding(.bottom, duoTabBarInsets.overlay)
198
+ .background(
199
+ GeometryReader { proxy in
200
+ Color.clear
201
+ .onAppear { footerHeight = proxy.size.height }
202
+ .onChange(of: proxy.size.height) { footerHeight = $0 }
203
+ }
204
+ )
169
205
  }
170
206
 
171
207
  // Animated search bar overlay (mirrors Compose's SearchAnimated layer).
@@ -176,7 +212,24 @@ public struct StackScreen<Content: View>: View {
176
212
  hiddenBack: !isBack,
177
213
  hasHeaderRight: headerRight != nil,
178
214
  headerRightWidth: headerRightWidth,
179
- headerHeight: Self.headerHeight
215
+ headerHeight: Self.headerHeight,
216
+ useSideBar: layout.sideBar != nil,
217
+ hasToolkit: toolkit != nil
218
+ )
219
+ }
220
+
221
+ if showsSideBar, let sideBar = layout.sideBar {
222
+ HeaderSideBar(
223
+ sideBar: sideBar,
224
+ goBack: goBack,
225
+ toolkit: toolkit,
226
+ headerColor: getHeaderColor(
227
+ isAnimated: animatedHeader != nil,
228
+ isSurface: animatedHeader?.isSurface ?? true,
229
+ opacity: scrollOpacity,
230
+ tintColor: tintColor
231
+ ),
232
+ bottomInset: bottomBarsHeight
180
233
  )
181
234
  }
182
235
  }
@@ -191,7 +244,7 @@ public struct StackScreen<Content: View>: View {
191
244
  guard useAvoidKeyboard,
192
245
  let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
193
246
  withAnimation(.easeOut(duration: 0.25)) {
194
- keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
247
+ keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
195
248
  }
196
249
  }
197
250
  // Mirrors Compose's `hideKeyboardOnTap()`: a simultaneous gesture so
@@ -246,15 +299,25 @@ public struct StackScreen<Content: View>: View {
246
299
  scrollOffset: $scrollOffset,
247
300
  scrollToTopCallback: scrollToTopCallback,
248
301
  animatedSearchInset: animatedSearchInset,
302
+ bottomInset: duoTabBarInsets.overlay,
249
303
  content: { animatedHeroBody(animatedHeader) }
250
304
  )
251
305
  .edgesIgnoringSafeArea(.all)
252
306
 
253
307
  header
254
308
  .background(
255
- Colors.black01
256
- .opacity(scrollOpacity)
257
- .edgesIgnoringSafeArea(.top)
309
+ Group {
310
+ if layout.sideBar != nil {
311
+ // glass shows once the banner has scrolled under the header
312
+ Color.clear.modifier(DuoBarMaterial(
313
+ shape: Rectangle(),
314
+ visible: scrollOffset > heroGlassStart(animatedHeader)
315
+ ))
316
+ } else {
317
+ Colors.black01.opacity(scrollOpacity)
318
+ }
319
+ }
320
+ .edgesIgnoringSafeArea([.top, .horizontal])
258
321
  )
259
322
  .zIndex(100)
260
323
  }
@@ -271,9 +334,34 @@ public struct StackScreen<Content: View>: View {
271
334
  )
272
335
  .zIndex(100)
273
336
 
274
- StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
337
+ StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: { duoInsetContent })
275
338
  .edgesIgnoringSafeArea(.all)
276
339
  }
340
+ } else if useDuoGlass {
341
+ ZStack(alignment: .top) {
342
+ DuoScrollGradient(
343
+ scrollOffset: scrollOffset,
344
+ headerHeight: layout.headerHeight,
345
+ extended: headerType == .extended
346
+ )
347
+
348
+ StackScreenContent(
349
+ scrollable: scrollable,
350
+ scrollOffset: $scrollOffset,
351
+ scrollToTopCallback: scrollToTopCallback,
352
+ topInset: layout.headerHeight - layout.insets.top,
353
+ bottomInset: duoTabBarInsets.overlay,
354
+ content: content
355
+ )
356
+
357
+ header
358
+ .background(
359
+ Color.clear
360
+ .modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
361
+ .edgesIgnoringSafeArea([.top, .horizontal])
362
+ )
363
+ .zIndex(100)
364
+ }
277
365
  } else {
278
366
  header
279
367
  .background(
@@ -283,28 +371,40 @@ public struct StackScreen<Content: View>: View {
283
371
  headerTransparent: headerTransparent,
284
372
  fullScreenContent: fullScreenContent
285
373
  )
286
- .edgesIgnoringSafeArea(.top)
374
+ .edgesIgnoringSafeArea([.top, .horizontal])
287
375
  )
288
376
 
289
- StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
377
+ StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: content)
290
378
  }
291
379
  }
292
380
 
381
+ /// Edge-to-edge branches lay out outside the safe area; iPhone Duo content still keeps
382
+ /// clear of the side strips, as RN's Screen does.
383
+ private var duoInsetContent: some View {
384
+ content()
385
+ .padding(.leading, layout.insetLeft)
386
+ .padding(.trailing, layout.insetRight)
387
+ }
388
+
389
+ private func heroGlassStart(_ animatedHeader: AnimatedHeader) -> CGFloat {
390
+ max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
391
+ }
392
+
293
393
  /// Hero image + body for an animated header. The image fills the width at its
294
394
  /// aspect-ratio height; the body is offset below the header band so it overlaps
295
395
  /// the lower part of the image (mirrors Compose's image box + padded content).
296
396
  @ViewBuilder
297
397
  private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
298
- let width = UIScreen.main.bounds.width
398
+ let width = layout.width
299
399
  let imageHeight = width / animatedHeader.aspectRatio.value
300
- let contentTopInset = safeAreaTopInset() + HEADER_HEIGHT + layoutOffset
400
+ let contentTopInset = layout.headerHeight + layoutOffset
301
401
 
302
402
  ZStack(alignment: .top) {
303
403
  animatedHeader.composable(scrollOffset)
304
404
  .frame(width: width, height: imageHeight)
305
405
  .clipped()
306
406
 
307
- content()
407
+ duoInsetContent
308
408
  .padding(.top, contentTopInset)
309
409
  }
310
410
  .frame(maxWidth: .infinity, alignment: .top)
@@ -343,6 +443,10 @@ private struct StackScreenContent<Content: View>: View {
343
443
  /// Extra top space reserved for the animated search bar (mirrors Compose's
344
444
  /// MainContent animated top spacer). Zero for non-animated screens.
345
445
  var animatedSearchInset: CGFloat = 0
446
+ /// Room for a header the content scrolls under (iPhone Duo glass header).
447
+ var topInset: CGFloat = 0
448
+ /// Room for a bar that overlays the content (iPhone Duo floating tab bar).
449
+ var bottomInset: CGFloat = 0
346
450
  let content: () -> Content
347
451
 
348
452
  private let topAnchorID = "stack_screen_top_anchor"
@@ -369,13 +473,17 @@ private struct StackScreenContent<Content: View>: View {
369
473
  }
370
474
  })
371
475
 
372
- if animatedSearchInset > 0 {
373
- Spacer().frame(height: animatedSearchInset)
476
+ if animatedSearchInset + topInset > 0 {
477
+ Spacer().frame(height: animatedSearchInset + topInset)
374
478
  }
375
479
 
376
480
  VStack {
377
481
  content()
378
482
  }
483
+
484
+ if bottomInset > 0 {
485
+ Spacer().frame(height: bottomInset)
486
+ }
379
487
  }
380
488
  }
381
489
  .coordinateSpace(name: scrollSpace)
@@ -390,6 +498,8 @@ private struct StackScreenContent<Content: View>: View {
390
498
  }
391
499
  } else {
392
500
  content()
501
+ .padding(.top, topInset)
502
+ .padding(.bottom, bottomInset)
393
503
  }
394
504
  }
395
505
  }
@@ -407,16 +517,23 @@ private struct SearchAnimatedHeader: View {
407
517
  let hasHeaderRight: Bool
408
518
  let headerRightWidth: CGFloat
409
519
  let headerHeight: CGFloat
520
+ var useSideBar: Bool = false
521
+ var hasToolkit: Bool = false
522
+
523
+ @Environment(\.screenLayout) private var layout
410
524
 
411
525
  var body: some View {
412
526
  let scroll = max(scrollOffset, 0)
413
527
 
414
- // Insets are measured from the safe-area top (status bar handled by layout).
415
- let minTop: CGFloat = 0
416
- let maxTop = headerHeight
528
+ // Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
529
+ let rowTop = min(layout.headerStatusBarHeight - layout.insets.top, 0)
530
+ let minTop = rowTop + (layout.headerBarHeight - headerHeight) / 2
531
+ let maxTop = layout.headerHeight - layout.insets.top
417
532
  let minSide = Spacing.M
418
- let maxStart: CGFloat = hiddenBack ? Spacing.M : 48
419
- let maxEnd = headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
533
+ let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
534
+ let maxEnd: CGFloat = useSideBar && hasToolkit
535
+ ? Spacing.M
536
+ : headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
420
537
 
421
538
  let top = min(max(maxTop - scroll, minTop), maxTop)
422
539
  let start = min(max(minSide + scroll, minSide), maxStart)
@@ -55,6 +55,7 @@ extension View {
55
55
 
56
56
  public struct PopupDisplay: View {
57
57
  @Environment(\.localize) private var localize
58
+ @Environment(\.screenLayout) private var layout
58
59
 
59
60
  // MARK: Lifecycle
60
61
 
@@ -91,6 +92,7 @@ public struct PopupDisplay: View {
91
92
 
92
93
  @State private var textHeight: CGFloat = .zero
93
94
  @State private var isScrollable = false
95
+ @State private var actionsHeight: CGFloat = 0
94
96
 
95
97
  public var body: some View {
96
98
  var shouldUseColumn: Bool {
@@ -114,89 +116,103 @@ public struct PopupDisplay: View {
114
116
  }
115
117
 
116
118
  VStack(spacing: 0) {
117
- if(!url.isEmpty) {
118
- WebImage(url: URL(string: url), isAnimating: .constant(true))
119
- .resizable()
120
- .placeholder {
121
- Rectangle()
122
- .fill(Color.gray.opacity(0.3))
119
+ // scrolls when iPhone Duo landscape is too short; actions stay visible
120
+ PopupScrollBody(maxHeight: layout.popupMaxHeight.map { $0 - actionsHeight }) {
121
+ VStack(spacing: 0) {
122
+ if(!url.isEmpty) {
123
+ WebImage(url: URL(string: url), isAnimating: .constant(true))
124
+ .resizable()
125
+ .placeholder {
126
+ Rectangle()
127
+ .fill(Color.gray.opacity(0.3))
128
+ .aspectRatio(1.777, contentMode: .fit)
129
+ .frame(maxWidth: .infinity, maxHeight: 184)
130
+ }
123
131
  .aspectRatio(1.777, contentMode: .fit)
124
- .frame(maxWidth: .infinity, maxHeight: 184)
132
+ .frame(maxWidth: .infinity, alignment: .center)
133
+ .clipShape(RoundedCorner(radius: 15, corners: [.topLeft, .topRight]))
134
+ .clipped()
125
135
  }
126
- .aspectRatio(1.777, contentMode: .fit)
127
- .frame(maxWidth: .infinity, alignment: .center)
128
- .clipShape(RoundedCorner(radius: 15, corners: [.topLeft, .topRight]))
129
- .clipped()
130
- }
131
- VStack(alignment: .leading, spacing: 0) {
132
- MomoText(title, typography: .headerDefaultBold)
133
- .padding(.top, 24)
134
- .padding(.bottom, 8)
135
- .lineLimit(2)
136
- .accessibility(identifier: "title_popup_permission")
136
+ VStack(alignment: .leading, spacing: 0) {
137
+ MomoText(title, typography: .headerDefaultBold)
138
+ .padding(.top, 24)
139
+ .padding(.bottom, 8)
140
+ .lineLimit(2)
141
+ .accessibility(identifier: "title_popup_permission")
137
142
 
138
- Group {
139
- if isScrollable {
140
- ScrollView(showsIndicators: false) {
141
- MomoText(description, typography: .bodyDefaultRegular)
142
- .multilineTextAlignment(.leading)
143
- .background(GeometryReader { geo in
144
- Color.clear.onAppear { textHeight = geo.size.height }
145
- })
146
- .measureLineHeights(font: .body_default_regular,
147
- oneLine: $oneLineH,
148
- twoLines: $twoLineH)
149
- .accessibility(identifier: "description_popup_permission")
150
- }
151
- // Cap the visible height to ~8.5 lines
152
- .frame(height: min(maxHeight8_5, textHeight))
153
- } else {
154
- MomoText(description, typography: .bodyDefaultRegular)
155
- .multilineTextAlignment(.leading)
156
- .background(GeometryReader { geo in
157
- Color.clear.onAppear {
158
- textHeight = geo.size.height
159
- // Trigger scroll at ~8.5 lines
160
- isScrollable = textHeight > maxHeight8_5
143
+ Group {
144
+ if isScrollable {
145
+ ScrollView(showsIndicators: false) {
146
+ MomoText(description, typography: .bodyDefaultRegular)
147
+ .multilineTextAlignment(.leading)
148
+ .background(GeometryReader { geo in
149
+ Color.clear.onAppear { textHeight = geo.size.height }
150
+ })
151
+ .measureLineHeights(font: .body_default_regular,
152
+ oneLine: $oneLineH,
153
+ twoLines: $twoLineH)
154
+ .accessibility(identifier: "description_popup_permission")
161
155
  }
162
- })
163
- .measureLineHeights(font: .body_default_regular,
164
- oneLine: $oneLineH,
165
- twoLines: $twoLineH)
166
- .accessibility(identifier: "description_popup_permission")
167
- }
168
- }
169
- .padding(.bottom, 8)
170
-
171
- if(!errorCode.isEmpty) {
172
- MomoText(localize.translate("errorCode") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
173
- .lineLimit(1)
156
+ // Cap the visible height to ~8.5 lines
157
+ .frame(height: min(maxHeight8_5, textHeight))
158
+ } else {
159
+ MomoText(description, typography: .bodyDefaultRegular)
160
+ .multilineTextAlignment(.leading)
161
+ .background(GeometryReader { geo in
162
+ Color.clear.onAppear {
163
+ textHeight = geo.size.height
164
+ // Trigger scroll at ~8.5 lines
165
+ isScrollable = textHeight > maxHeight8_5
166
+ }
167
+ })
168
+ .measureLineHeights(font: .body_default_regular,
169
+ oneLine: $oneLineH,
170
+ twoLines: $twoLineH)
171
+ .accessibility(identifier: "description_popup_permission")
172
+ }
173
+ }
174
174
  .padding(.bottom, 8)
175
+
176
+ if(!errorCode.isEmpty) {
177
+ MomoText(localize.translate("errorCode") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
178
+ .lineLimit(1)
179
+ .padding(.bottom, 8)
180
+ }
181
+ }
182
+ .frame(maxWidth: .infinity, alignment: .leading)
183
+ .padding(.horizontal, 24)
184
+ .padding(.bottom, 16)
175
185
  }
176
186
  }
177
- .frame(maxWidth: .infinity, alignment: .leading)
178
- .padding(.horizontal, 24)
179
- .padding(.bottom, 16)
180
-
181
- if shouldUseColumn {
182
- if buttonType == .text {
183
- ColumnTextButtons
184
- } else {
185
- ColumnButtons
186
- }
187
- } else {
188
- if buttonType == .text {
189
- RowTextButtons
187
+
188
+ Group {
189
+ if shouldUseColumn {
190
+ if buttonType == .text {
191
+ ColumnTextButtons
192
+ } else {
193
+ ColumnButtons
194
+ }
190
195
  } else {
191
- RowButtons
196
+ if buttonType == .text {
197
+ RowTextButtons
198
+ } else {
199
+ RowButtons
200
+ }
192
201
  }
193
202
  }
203
+ .background(
204
+ GeometryReader { proxy in
205
+ Color.clear
206
+ .onAppear { actionsHeight = proxy.size.height }
207
+ .onChange(of: proxy.size.height) { actionsHeight = $0 }
208
+ }
209
+ )
194
210
  }
195
211
  .fixedSize(horizontal: false, vertical: true)
196
212
  }
197
213
  }
198
214
  .background(Color.white.cornerRadius(15))
199
- .frame(maxWidth: popupMaxWidth - 24)
215
+ .frame(maxWidth: layout.popupMaxWidth)
200
216
  .padding(.horizontal, 12)
201
217
  .accessibilityElement(children: .ignore)
202
218
  .accessibility(identifier: "popup_notify")
@@ -0,0 +1,45 @@
1
+ import SwiftUI
2
+
3
+ // Width of a system alert on the iPhone Duo inner display, in both orientations (RN Popup/utils.ts).
4
+ private let duoPopupMaxWidth: CGFloat = 440
5
+
6
+ extension ScreenLayout {
7
+ var isDuoDevice: Bool { isIPhoneDuoDevice(windowSize: CGSize(width: width, height: height)) }
8
+
9
+ /// Card width cap; other devices keep the full window width minus the popup's own padding.
10
+ var popupMaxWidth: CGFloat {
11
+ isDuoDevice ? min(width - Spacing.XL * 2, duoPopupMaxWidth) : .infinity
12
+ }
13
+
14
+ /// Room left for a popup on iPhone Duo, where landscape is too short for it; nil elsewhere.
15
+ var popupMaxHeight: CGFloat? {
16
+ guard isDuoDevice else { return nil }
17
+ return height - duoTopInset - insets.bottom - Spacing.XL * 2
18
+ }
19
+ }
20
+
21
+ /// Scrolls `content` once it is taller than `maxHeight`; otherwise sizes to it.
22
+ struct PopupScrollBody<Content: View>: View {
23
+ let maxHeight: CGFloat?
24
+ @ViewBuilder let content: () -> Content
25
+
26
+ @State private var contentHeight: CGFloat = 0
27
+
28
+ var body: some View {
29
+ if let maxHeight {
30
+ ScrollView(.vertical, showsIndicators: false) {
31
+ content()
32
+ .background(
33
+ GeometryReader { proxy in
34
+ Color.clear
35
+ .onAppear { contentHeight = proxy.size.height }
36
+ .onChange(of: proxy.size.height) { contentHeight = $0 }
37
+ }
38
+ )
39
+ }
40
+ .frame(height: min(contentHeight, max(maxHeight, 0)))
41
+ } else {
42
+ content()
43
+ }
44
+ }
45
+ }
@@ -3,12 +3,6 @@ import SwiftUI
3
3
 
4
4
  public typealias PopupActionDirection = PopupButtonDirection
5
5
 
6
- // Portrait width in landscape too, so aspect-ratio images don't push popups past the screen height.
7
- var popupMaxWidth: CGFloat {
8
- let size = UIScreen.main.bounds.size
9
- return min(size.width, size.height)
10
- }
11
-
12
6
  public struct PopupNotifyProps {
13
7
  public var image: String
14
8
  public var title: String
@@ -46,10 +40,12 @@ public struct PopupNotifyProps {
46
40
  public struct PopupNotify: View {
47
41
  @Environment(\.localize) private var localize
48
42
  @Environment(\.momoFontScale) private var fontScale
43
+ @Environment(\.screenLayout) private var layout
49
44
 
50
45
  public let props: PopupNotifyProps
51
46
 
52
47
  @State private var descriptionHeight: CGFloat = 0
48
+ @State private var actionsHeight: CGFloat = 0
53
49
 
54
50
  private var errorCodePrefix: String {
55
51
  localize.translate("errorCode")
@@ -77,37 +73,26 @@ public struct PopupNotify: View {
77
73
  public var body: some View {
78
74
  ZStack(alignment: .topTrailing) {
79
75
  VStack(spacing: 0) {
80
- if !props.image.isEmpty {
81
- popupImage
82
- }
83
-
84
- VStack(alignment: .leading, spacing: 0) {
85
- MomoText(
86
- props.title,
87
- typography: .headerDefaultBold,
88
- maxLines: 2,
89
- accessibilityId: "title_popup_permission"
90
- )
91
- .padding(.bottom, Spacing.S)
92
-
93
- descriptionContent
94
-
95
- if !props.error.isEmpty {
96
- MomoText(
97
- errorCodePrefix + props.error,
98
- typography: .descriptionXsRegular,
99
- color: Colors.black12,
100
- maxLines: 1
101
- )
102
- .padding(.top, Spacing.S)
76
+ // scrolls when iPhone Duo landscape is too short; actions stay visible
77
+ PopupScrollBody(maxHeight: layout.popupMaxHeight.map { $0 - actionsHeight }) {
78
+ VStack(spacing: 0) {
79
+ if !props.image.isEmpty {
80
+ popupImage
81
+ }
82
+ textContent
103
83
  }
104
84
  }
105
- .frame(maxWidth: .infinity, alignment: .leading)
106
- .padding(Spacing.XL)
107
85
 
108
86
  actionButtons
109
87
  .padding(.horizontal, Spacing.XL)
110
88
  .padding(.bottom, Spacing.XL)
89
+ .background(
90
+ GeometryReader { proxy in
91
+ Color.clear
92
+ .onAppear { actionsHeight = proxy.size.height }
93
+ .onChange(of: proxy.size.height) { actionsHeight = $0 }
94
+ }
95
+ )
111
96
  }
112
97
  .background(Colors.black01)
113
98
  .clipShape(RoundedRectangle(cornerRadius: Radius.L))
@@ -117,12 +102,38 @@ public struct PopupNotify: View {
117
102
  .offset(x: 11, y: -11)
118
103
  }
119
104
  }
120
- .frame(maxWidth: popupMaxWidth - Spacing.M * 2)
105
+ .frame(maxWidth: layout.popupMaxWidth)
121
106
  .padding(.horizontal, Spacing.M)
122
107
  .accessibilityElement(children: .ignore)
123
108
  .accessibilityIdentifier("popup_notify")
124
109
  }
125
110
 
111
+ private var textContent: some View {
112
+ VStack(alignment: .leading, spacing: 0) {
113
+ MomoText(
114
+ props.title,
115
+ typography: .headerDefaultBold,
116
+ maxLines: 2,
117
+ accessibilityId: "title_popup_permission"
118
+ )
119
+ .padding(.bottom, Spacing.S)
120
+
121
+ descriptionContent
122
+
123
+ if !props.error.isEmpty {
124
+ MomoText(
125
+ errorCodePrefix + props.error,
126
+ typography: .descriptionXsRegular,
127
+ color: Colors.black12,
128
+ maxLines: 1
129
+ )
130
+ .padding(.top, Spacing.S)
131
+ }
132
+ }
133
+ .frame(maxWidth: .infinity, alignment: .leading)
134
+ .padding(Spacing.XL)
135
+ }
136
+
126
137
  private var popupImage: some View {
127
138
  WebImage(url: URL(string: props.image), isAnimating: .constant(true))
128
139
  .resizable()
@@ -3,6 +3,7 @@ import SDWebImageSwiftUI
3
3
 
4
4
  public struct PopupPromotion: View {
5
5
  @Environment(\.appTheme) private var theme
6
+ @Environment(\.screenLayout) private var layout
6
7
 
7
8
  var imageUrl: String
8
9
  var onIconClose: (() -> Void)?
@@ -19,22 +20,33 @@ public struct PopupPromotion: View {
19
20
  self.onAction = onAction
20
21
  }
21
22
 
23
+ private static let aspectRatio: CGFloat = 0.72
24
+ // close button row under the image: stack spacing + padded 40pt button
25
+ private static let closeRowHeight: CGFloat = 8 + 40 + Spacing.S * 2
26
+
27
+ // fit the portrait image by height when iPhone Duo leaves too little room
28
+ private var imageWidth: CGFloat {
29
+ let width = min(layout.width - Spacing.XL * 2, layout.popupMaxWidth)
30
+ guard let maxHeight = layout.popupMaxHeight else { return width }
31
+ return min(width, (maxHeight - Self.closeRowHeight) * Self.aspectRatio)
32
+ }
33
+
22
34
  public var body: some View {
23
35
  VStack(spacing: 8) {
24
36
  if(imageUrl.isEmpty) {
25
- Icon(source: "media_fail", size: popupMaxWidth - Spacing.XL * 2)
37
+ Icon(source: "media_fail", size: imageWidth)
26
38
  }
27
39
  else {
28
40
  WebImage(url: URL(string: imageUrl))
29
41
  .resizable()
30
42
  .placeholder {
31
43
  Color.gray
32
- .aspectRatio(0.72, contentMode: .fit)
33
- .frame(maxWidth: popupMaxWidth - Spacing.XL * 2)
44
+ .aspectRatio(Self.aspectRatio, contentMode: .fit)
45
+ .frame(maxWidth: imageWidth)
34
46
  .clipped()
35
47
  }
36
- .aspectRatio(0.72, contentMode: .fit)
37
- .frame(maxWidth: popupMaxWidth - Spacing.XL * 2, alignment: .center)
48
+ .aspectRatio(Self.aspectRatio, contentMode: .fit)
49
+ .frame(maxWidth: imageWidth, alignment: .center)
38
50
  .clipped()
39
51
  .onTapGesture {
40
52
  onAction?()