@momo-kits/native-kits 0.167.1-duo.7 → 0.167.1-duo.8-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. package/CLAUDE.md +7 -4
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +204 -0
  4. package/compose/build.gradle.kts.backup +204 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +153 -0
  11. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +361 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +108 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +338 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +137 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +708 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +203 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +408 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +92 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +366 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +74 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  136. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  137. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  138. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  139. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +217 -0
  140. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  141. package/gradle/libs.versions.toml +69 -0
  142. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  143. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  144. package/gradle.properties +26 -0
  145. package/gradlew +252 -0
  146. package/gradlew.bat +94 -0
  147. package/ios/Application/Layout/ScreenLayout.swift +122 -6
  148. package/ios/Application/Navigation/BottomTab/BottomTab.swift +41 -5
  149. package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  150. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +60 -31
  151. package/ios/Application/Navigation/component/DuoScrollGradient.swift +18 -19
  152. package/ios/Application/Navigation/component/Header.swift +3 -2
  153. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +32 -0
  154. package/ios/Application/Navigation/component/SideBar.swift +2 -2
  155. package/ios/Application/StackScreen.swift +46 -6
  156. package/ios-demo/README.md +120 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +7 -3
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +3 -0
  159. package/package.json +1 -1
  160. package/settings.gradle.kts +64 -0
@@ -18,6 +18,8 @@ public struct SideBarLayout: Equatable {
18
18
  public let width: CGFloat
19
19
  /// Preferred window y of the first control; StackScreen still keeps the column above the bottom bars.
20
20
  public let top: CGFloat
21
+ /// Room kept free above the window bottom, clear of a camera at the bottom end of the strip.
22
+ public let bottom: CGFloat
21
23
  }
22
24
 
23
25
  public struct ScreenLayout: Equatable {
@@ -47,6 +49,13 @@ enum DuoMetrics {
47
49
  static let itemPitch: CGFloat = 50
48
50
  static let firstItemTop: CGFloat = 170
49
51
  static let firstItemTopLandscape: CGFloat = 120
52
+ // Outer display in portrait: the header is 4pt taller than the iPhone floor and its title
53
+ // lines up with the camera punchout (y 24–66 at the top of the strip).
54
+ static let outerHeaderHeight: CGFloat = 76
55
+ static let outerCameraCenterY: CGFloat = 45
56
+ // Outer display in landscape has no status bar in the strip: controls keep this far from
57
+ // both ends unless a reserved region (the camera at either end) asks for more.
58
+ static let edgeInset: CGFloat = 24
50
59
  // Widest short side of any single-display iPhone.
51
60
  static let iPhoneMaxShortSide: CGFloat = 440
52
61
  }
@@ -79,10 +88,15 @@ extension ScreenLayout {
79
88
  )
80
89
  }
81
90
 
82
- static func make(size: CGSize, insets: EdgeInsets, isPhone: Bool = isPhoneIdiom) -> ScreenLayout {
91
+ static func make(
92
+ size: CGSize,
93
+ insets: EdgeInsets,
94
+ reservedRegions: [CGRect] = ReservedRegions.current,
95
+ isPhone: Bool = isPhoneIdiom
96
+ ) -> ScreenLayout {
83
97
  let width = size.width
84
98
  let height = size.height
85
- let (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
99
+ var (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
86
100
  let isDuo = isPhone && isIPhoneDuoWindow(width: width, height: height, insets: insets)
87
101
  guard isDuo else {
88
102
  return ScreenLayout(
@@ -99,13 +113,32 @@ extension ScreenLayout {
99
113
  var top = insets.top + HeaderMetrics.barHeight + Spacing.M
100
114
  if insets.top < HeaderMetrics.statusBarMinInset {
101
115
  // status bar lives in the strip: start under its cluster, as the native bar does
102
- let isInnerLandscape = width > height && height >= DuoMetrics.innerMinHeight
103
- top = isInnerLandscape ? DuoMetrics.firstItemTopLandscape : DuoMetrics.firstItemTop
116
+ if width <= height {
117
+ top = DuoMetrics.firstItemTop
118
+ } else if height >= DuoMetrics.innerMinHeight {
119
+ top = DuoMetrics.firstItemTopLandscape
120
+ } else {
121
+ // outer display in landscape: no status bar in the strip
122
+ top = DuoMetrics.edgeInset
123
+ }
124
+
125
+ if height > width && width < DuoMetrics.innerMinHeight {
126
+ // outer display: the row starts below the top so the title centers on the camera
127
+ headerHeight = DuoMetrics.outerHeaderHeight
128
+ headerStatusBarHeight = DuoMetrics.outerCameraCenterY * 2 - DuoMetrics.outerHeaderHeight
129
+ }
104
130
  }
131
+
132
+ let edge: SideBarEdge = insets.trailing >= insets.leading ? .right : .left
133
+ let stripLeft = edge == .right ? width - sideInset : 0
134
+ // clear of the camera and of the status bar cluster, which grows for calls and Live
135
+ // Activities; region frames already include the system's margin
136
+ let occlusion = stripOcclusion(reservedRegions, stripLeft: stripLeft, stripRight: stripLeft + sideInset, windowHeight: height)
105
137
  sideBar = SideBarLayout(
106
- edge: insets.trailing >= insets.leading ? .right : .left,
138
+ edge: edge,
107
139
  width: sideInset,
108
- top: top
140
+ top: max(top, occlusion.top),
141
+ bottom: max(DuoMetrics.edgeInset, occlusion.bottom)
109
142
  )
110
143
  }
111
144
 
@@ -126,6 +159,24 @@ extension ScreenLayout {
126
159
  )
127
160
  }
128
161
 
162
+ /// How far system regions reach into a side strip from its top and from its bottom end
163
+ /// (e.g. the camera, which sits at either end depending on the rotation).
164
+ private static func stripOcclusion(
165
+ _ regions: [CGRect],
166
+ stripLeft: CGFloat,
167
+ stripRight: CGFloat,
168
+ windowHeight: CGFloat
169
+ ) -> (top: CGFloat, bottom: CGFloat) {
170
+ regions.reduce(into: (top: CGFloat(0), bottom: CGFloat(0))) { occlusion, region in
171
+ guard region.minX < stripRight && region.maxX > stripLeft else { return }
172
+ if region.midY < windowHeight / 2 {
173
+ occlusion.top = max(occlusion.top, region.maxY)
174
+ } else {
175
+ occlusion.bottom = max(occlusion.bottom, windowHeight - region.minY)
176
+ }
177
+ }
178
+ }
179
+
129
180
  // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
130
181
  private static func isIPhoneDuoWindow(width: CGFloat, height: CGFloat, insets: EdgeInsets) -> Bool {
131
182
  let shortSide = min(width, height)
@@ -148,6 +199,70 @@ public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
148
199
  return max(min(screen.width, screen.height), min(window.width, window.height)) > DuoMetrics.iPhoneMaxShortSide
149
200
  }
150
201
 
202
+ // MARK: - Reserved regions
203
+
204
+ /// System occlusion regions (iOS 27.1+) in window coordinates: the iPhone Duo camera, and the
205
+ /// status bar cluster as it grows for a call or a Live Activity. Empty before iOS 27.1.
206
+ enum ReservedRegions {
207
+ // last report of the probe, so `ScreenLayout.current` includes it
208
+ static var current: [CGRect] = []
209
+ }
210
+
211
+ /// Invisible probe: reading the regions in layoutSubviews makes UIKit run it again when they
212
+ /// change, so nothing polls. Port of RN `@momo-kits/reserved-regions`.
213
+ private final class ReservedRegionsProbe: UIView {
214
+ var onChange: (() -> Void)?
215
+ private var last: [CGRect]?
216
+
217
+ override init(frame: CGRect) {
218
+ super.init(frame: frame)
219
+ isUserInteractionEnabled = false
220
+ }
221
+
222
+ required init?(coder: NSCoder) { nil }
223
+
224
+ override func layoutSubviews() {
225
+ super.layoutSubviews()
226
+ let regions = occlusionRegions()
227
+ guard regions != last else { return }
228
+ last = regions
229
+ ReservedRegions.current = regions
230
+ // layout pass: publish on the next turn instead of mutating SwiftUI state mid-update
231
+ DispatchQueue.main.async { [weak self] in self?.onChange?() }
232
+ }
233
+
234
+ override func didMoveToWindow() {
235
+ super.didMoveToWindow()
236
+ setNeedsLayout()
237
+ }
238
+
239
+ // The API is newer than the SDKs hosts may build with, so it is reached through the runtime.
240
+ private func occlusionRegions() -> [CGRect] {
241
+ let regionsOfKind = NSSelectorFromString("reservedRegionsOfKind:")
242
+ guard window != nil, responds(to: regionsOfKind),
243
+ let kindType = NSClassFromString("UIViewReservedRegionKind") as? NSObject.Type,
244
+ let kind = (kindType as AnyObject).perform(NSSelectorFromString("occlusionRegionKind"))?.takeUnretainedValue(),
245
+ let regions = perform(regionsOfKind, with: kind)?.takeUnretainedValue() as? [NSObject]
246
+ else { return [] }
247
+ return regions.compactMap { ($0.value(forKey: "frame") as? NSValue)?.cgRectValue }
248
+ .map { convert($0, to: nil) }
249
+ }
250
+ }
251
+
252
+ private struct ReservedRegionsProbeView: UIViewRepresentable {
253
+ let onChange: () -> Void
254
+
255
+ func makeUIView(context: Context) -> ReservedRegionsProbe {
256
+ let probe = ReservedRegionsProbe()
257
+ probe.onChange = onChange
258
+ return probe
259
+ }
260
+
261
+ func updateUIView(_ probe: ReservedRegionsProbe, context: Context) {
262
+ probe.onChange = onChange
263
+ }
264
+ }
265
+
151
266
  // MARK: - Environment
152
267
 
153
268
  private struct ScreenLayoutKey: EnvironmentKey {
@@ -177,6 +292,7 @@ private struct ScreenLayoutReader: ViewModifier {
177
292
  .onChange(of: proxy.safeAreaInsets) { _ in refresh() }
178
293
  }
179
294
  )
295
+ .background(ReservedRegionsProbeView { refresh() }.ignoresSafeArea())
180
296
  .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
181
297
  refresh()
182
298
  }
@@ -14,14 +14,25 @@ import SwiftUI
14
14
  public struct BottomTab: View {
15
15
  private let items: [BottomTabItem]
16
16
  private let floatingButton: BottomTabFloatingButton?
17
+ private let compressionBehavior: DuoBarCompressionBehavior
17
18
 
18
19
  @EnvironmentObject private var navigator: Navigator
19
20
  @Environment(\.screenLayout) private var layout
20
21
  @State private var selectedIndex: Int = 0
22
+ @State private var toolbarHeight = DuoBarCompression.defaultToolbarHeight
23
+ @State private var expandedBar: DuoBar?
24
+ @State private var openedBar: DuoBar?
21
25
 
22
- public init(items: [BottomTabItem], floatingButton: BottomTabFloatingButton? = nil) {
26
+ /// `compressionBehavior`: on iPhone Duo, when the side bar can't hold both bars, which one
27
+ /// starts compressed; tapping the compressed one swaps them.
28
+ public init(
29
+ items: [BottomTabItem],
30
+ floatingButton: BottomTabFloatingButton? = nil,
31
+ compressionBehavior: DuoBarCompressionBehavior = .toolbar
32
+ ) {
23
33
  self.items = items
24
34
  self.floatingButton = floatingButton
35
+ self.compressionBehavior = compressionBehavior
25
36
  }
26
37
 
27
38
  private var bottomInset: CGFloat { safeAreaBottomInset() }
@@ -30,11 +41,34 @@ public struct BottomTab: View {
30
41
  layout.isIPhoneDuo ? layout.insets.bottom : BOTTOM_TAB_BAR_HEIGHT + bottomInset + Spacing.S
31
42
  }
32
43
 
33
- private var duoInsets: DuoTabBarInsets {
34
- layout.duoTabBarInsets(itemCount: items.count, hasFloatingButton: floatingButton != nil)
44
+ private var compression: DuoBarCompression {
45
+ DuoBarCompression.resolve(
46
+ layout: layout,
47
+ tabCount: items.count,
48
+ hasQr: floatingButton != nil,
49
+ toolbarHeight: toolbarHeight,
50
+ // 'toolbar' compression keeps the tab bar whole until the toolbar is tapped
51
+ expanded: Binding(
52
+ get: { expandedBar ?? (compressionBehavior == .toolbar ? .tabBar : .toolbar) },
53
+ set: { expandedBar = $0 }
54
+ ),
55
+ opened: $openedBar,
56
+ setToolbarHeight: { height in
57
+ if toolbarHeight != height { toolbarHeight = height }
58
+ }
59
+ )
60
+ }
61
+
62
+ private func duoInsets(_ compression: DuoBarCompression) -> DuoTabBarInsets {
63
+ layout.duoTabBarInsets(
64
+ itemCount: items.count,
65
+ hasFloatingButton: floatingButton != nil,
66
+ tabBarCompressed: compression.tabBarCompressed
67
+ )
35
68
  }
36
69
 
37
70
  public var body: some View {
71
+ let compression = compression
38
72
  ZStack(alignment: .bottom) {
39
73
  // Tab content area, inset above the bar.
40
74
  ZStack {
@@ -46,7 +80,8 @@ public struct BottomTab: View {
46
80
  }
47
81
  }
48
82
  .padding(.bottom, reservedBottom)
49
- .environment(\.duoTabBarInsets, duoInsets)
83
+ .environment(\.duoTabBarInsets, duoInsets(compression))
84
+ .environment(\.duoBarCompression, compression)
50
85
  .animation(.easeInOut(duration: 0.2), value: selectedIndex)
51
86
 
52
87
  if layout.isIPhoneDuo {
@@ -54,7 +89,8 @@ public struct BottomTab: View {
54
89
  items: items,
55
90
  floatingButton: floatingButton,
56
91
  selectedIndex: selectedIndex,
57
- onTabSelected: selectTab
92
+ onTabSelected: selectTab,
93
+ compression: compression
58
94
  )
59
95
  } else {
60
96
  // Tab bar + home-indicator filler.
@@ -0,0 +1,105 @@
1
+ //
2
+ // DuoBarCompression.swift
3
+ // MoMoUIKits
4
+ //
5
+ // Port of RN `BottomTab/DuoBarCompression.tsx` (HIG, Designing for iPhone Duo: Vertical controls):
6
+ // keeps the side bar's toolbar and tab bar clear of each other.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ /// iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
12
+ public enum DuoBarCompressionBehavior {
13
+ case toolbar, tabBar
14
+ }
15
+
16
+ enum DuoBar {
17
+ case toolbar, tabBar
18
+ }
19
+
20
+ struct DuoBarCompression {
21
+ var toolbarCompressed = false
22
+ var tabBarCompressed = false
23
+ /// The open rail needs the toolbar button slot too: only Back stays.
24
+ var toolbarHidden = false
25
+ var expandToolbar: () -> Void = {}
26
+ var expandTabBar: () -> Void = {}
27
+ /// A rail opened from the compressed state closes once a tab is picked.
28
+ var onTabSelected: () -> Void = {}
29
+ /// Side bar toolkits report their full height so the tab bar knows what is left.
30
+ var setToolbarHeight: (CGFloat) -> Void = { _ in }
31
+
32
+ /// Space kept between the top and the bottom groups (HIG Vertical controls).
33
+ static let groupGap = Spacing.M
34
+ /// Back, then the right toolkit: help + home capsule.
35
+ static let defaultToolbarHeight = DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch
36
+ /// Back, then the single button the toolbar folds into.
37
+ private static let compressedToolbarHeight = 2 * DuoMetrics.controlSize + Spacing.M
38
+ /// Back alone, while a rail opened from the compressed state takes the toolbar slot.
39
+ private static let backOnlyHeight = DuoMetrics.controlSize
40
+
41
+ /// - both fit: both stay whole
42
+ /// - one fits whole beside the other compressed: one is compressed, tapping it swaps them
43
+ /// - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
44
+ /// again once a tab is picked)
45
+ static func resolve(
46
+ layout: ScreenLayout,
47
+ tabCount: Int,
48
+ hasQr: Bool,
49
+ toolbarHeight: CGFloat,
50
+ expanded: Binding<DuoBar>,
51
+ opened: Binding<DuoBar?>,
52
+ setToolbarHeight: @escaping (CGFloat) -> Void
53
+ ) -> DuoBarCompression {
54
+ guard let sideBar = layout.sideBar else { return DuoBarCompression() }
55
+ let room = layout.height - sideBar.bottom - sideBar.top - groupGap
56
+ let tabBar = duoRailHeight(itemCount: tabCount, hasQr: hasQr)
57
+ let compressedTabBar = duoRailHeight(itemCount: 1, hasQr: hasQr, compressed: true)
58
+ func fits(_ toolbar: CGFloat, _ tabs: CGFloat) -> Bool { toolbar + tabs <= room }
59
+
60
+ if fits(toolbarHeight, tabBar) {
61
+ return DuoBarCompression(setToolbarHeight: setToolbarHeight)
62
+ }
63
+ if fits(compressedToolbarHeight, tabBar) && fits(toolbarHeight, compressedTabBar) {
64
+ return DuoBarCompression(
65
+ toolbarCompressed: expanded.wrappedValue == .tabBar,
66
+ tabBarCompressed: expanded.wrappedValue == .toolbar,
67
+ expandToolbar: { expanded.wrappedValue = .toolbar },
68
+ expandTabBar: { expanded.wrappedValue = .tabBar },
69
+ setToolbarHeight: setToolbarHeight
70
+ )
71
+ }
72
+
73
+ let expandToolbar = { opened.wrappedValue = .toolbar }
74
+ let expandTabBar = { opened.wrappedValue = .tabBar }
75
+ if opened.wrappedValue == .tabBar && fits(backOnlyHeight, tabBar) {
76
+ return DuoBarCompression(
77
+ toolbarCompressed: true,
78
+ tabBarCompressed: false,
79
+ toolbarHidden: true,
80
+ expandToolbar: expandToolbar,
81
+ expandTabBar: expandTabBar,
82
+ onTabSelected: { opened.wrappedValue = nil },
83
+ setToolbarHeight: setToolbarHeight
84
+ )
85
+ }
86
+ return DuoBarCompression(
87
+ toolbarCompressed: opened.wrappedValue != .toolbar,
88
+ tabBarCompressed: true,
89
+ expandToolbar: expandToolbar,
90
+ expandTabBar: expandTabBar,
91
+ setToolbarHeight: setToolbarHeight
92
+ )
93
+ }
94
+ }
95
+
96
+ private struct DuoBarCompressionKey: EnvironmentKey {
97
+ static let defaultValue = DuoBarCompression()
98
+ }
99
+
100
+ extension EnvironmentValues {
101
+ var duoBarCompression: DuoBarCompression {
102
+ get { self[DuoBarCompressionKey.self] }
103
+ set { self[DuoBarCompressionKey.self] = newValue }
104
+ }
105
+ }
@@ -28,7 +28,7 @@ private let tabSpring = Animation.interpolatingSpring(mass: 0.8, stiffness: 220,
28
28
  struct DuoTabBarInsets: Equatable {
29
29
  /// Floating bar overlays the content: scroll content, footer and FAB keep clear of it.
30
30
  var overlay: CGFloat = 0
31
- /// Rail sits at the bottom of the side strip: the header side bar stays above it.
31
+ /// Rail sits at the bottom of the side strip: the header side bar stays this far above the window bottom.
32
32
  var sideBarReserve: CGFloat = 0
33
33
  }
34
34
 
@@ -43,16 +43,21 @@ extension EnvironmentValues {
43
43
  }
44
44
  }
45
45
 
46
+ /// Rail plus the QR button under it; compressed, the rail is a single button.
47
+ func duoRailHeight(itemCount: Int, hasQr: Bool, compressed: Bool = false) -> CGFloat {
48
+ let count = CGFloat(itemCount)
49
+ let rail = compressed ? itemSize : railPadding * 2 + count * itemSize + max(count - 1, 0) * railGap
50
+ return rail + (hasQr ? Spacing.M + itemSize : 0)
51
+ }
52
+
46
53
  extension ScreenLayout {
47
- func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool) -> DuoTabBarInsets {
54
+ func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool, tabBarCompressed: Bool = false) -> DuoTabBarInsets {
48
55
  guard isIPhoneDuo else { return DuoTabBarInsets() }
49
- guard sideBar != nil else {
56
+ guard let sideBar else {
50
57
  return DuoTabBarInsets(overlay: duoFloatingTabBarHeight + Spacing.M)
51
58
  }
52
- let count = CGFloat(itemCount)
53
- let rail = railPadding * 2 + count * itemSize + max(count - 1, 0) * railGap
54
- let qr = hasFloatingButton ? Spacing.M + itemSize : 0
55
- return DuoTabBarInsets(sideBarReserve: rail + qr + Spacing.XL)
59
+ let rail = duoRailHeight(itemCount: itemCount, hasQr: hasFloatingButton, compressed: tabBarCompressed)
60
+ return DuoTabBarInsets(sideBarReserve: sideBar.bottom + rail + DuoBarCompression.groupGap)
56
61
  }
57
62
  }
58
63
 
@@ -95,13 +100,14 @@ struct DuoTabBar: View {
95
100
  let floatingButton: BottomTabFloatingButton?
96
101
  let selectedIndex: Int
97
102
  let onTabSelected: (Int) -> Void
103
+ var compression = DuoBarCompression()
98
104
 
99
105
  @Environment(\.screenLayout) private var layout
100
106
 
101
107
  var body: some View {
102
108
  if let sideBar = layout.sideBar {
103
109
  DuoRailTabBar(sideBar: sideBar, items: items, floatingButton: floatingButton,
104
- selectedIndex: selectedIndex, onTabSelected: onTabSelected)
110
+ selectedIndex: selectedIndex, onTabSelected: onTabSelected, compression: compression)
105
111
  } else {
106
112
  DuoFloatingTabBar(items: items, floatingButton: floatingButton,
107
113
  selectedIndex: selectedIndex, onTabSelected: onTabSelected)
@@ -110,12 +116,15 @@ struct DuoTabBar: View {
110
116
  }
111
117
 
112
118
  /// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E).
119
+ /// When the side bar is short it can shrink to one button showing the current tab
120
+ /// (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar.
113
121
  private struct DuoRailTabBar: View {
114
122
  let sideBar: SideBarLayout
115
123
  let items: [BottomTabItem]
116
124
  let floatingButton: BottomTabFloatingButton?
117
125
  let selectedIndex: Int
118
126
  let onTabSelected: (Int) -> Void
127
+ let compression: DuoBarCompression
119
128
 
120
129
  @Environment(\.appTheme) private var theme
121
130
 
@@ -123,30 +132,19 @@ private struct DuoRailTabBar: View {
123
132
 
124
133
  var body: some View {
125
134
  VStack(spacing: Spacing.M) {
126
- ZStack(alignment: .top) {
127
- Capsule()
128
- .fill(theme.colors.background.selected)
129
- .frame(width: railBubbleWidth, height: railBubbleHeight)
130
- .offset(y: railPadding - (railBubbleHeight - itemSize) / 2
131
- + CGFloat(selectedIndex) * (itemSize + railGap))
132
- .animation(tabSpring, value: selectedIndex)
133
-
134
- VStack(spacing: railGap) {
135
- ForEach(items.indices, id: \.self) { index in
136
- SwiftUI.Button { onTabSelected(index) } label: {
137
- DuoTabIcon(item: items[index], selected: index == selectedIndex)
138
- .frame(width: itemSize, height: itemSize)
139
- .contentShape(Rectangle())
140
- }
141
- .buttonStyle(.plain)
142
- .accessibilityLabel(items[index].label)
143
- }
135
+ if compression.tabBarCompressed, items.indices.contains(selectedIndex) {
136
+ SwiftUI.Button(action: compression.expandTabBar) {
137
+ DuoTabIcon(item: items[selectedIndex], selected: true)
138
+ .frame(width: itemSize, height: itemSize)
139
+ .contentShape(Circle())
144
140
  }
145
- .padding(.vertical, railPadding)
141
+ .buttonStyle(.plain)
142
+ .modifier(DuoBarMaterial(shape: Circle()))
143
+ .accessibilityLabel(items[selectedIndex].label)
144
+ .accessibilityIdentifier("bottom_tab_bar")
145
+ } else {
146
+ rail
146
147
  }
147
- .frame(width: itemSize)
148
- .modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
149
- .accessibilityIdentifier("bottom_tab_bar")
150
148
 
151
149
  if let floatingButton {
152
150
  DuoQrButton(data: floatingButton, size: itemSize)
@@ -154,10 +152,41 @@ private struct DuoRailTabBar: View {
154
152
  }
155
153
  .padding(isRight ? .leading : .trailing, Spacing.M)
156
154
  .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
157
- .padding(.bottom, Spacing.XL)
155
+ .padding(.bottom, sideBar.bottom)
158
156
  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .bottomTrailing : .bottomLeading)
159
157
  .ignoresSafeArea()
160
158
  }
159
+
160
+ /// Glass column of tab items with the sliding selection bubble.
161
+ private var rail: some View {
162
+ ZStack(alignment: .top) {
163
+ Capsule()
164
+ .fill(theme.colors.background.selected)
165
+ .frame(width: railBubbleWidth, height: railBubbleHeight)
166
+ .offset(y: railPadding - (railBubbleHeight - itemSize) / 2
167
+ + CGFloat(selectedIndex) * (itemSize + railGap))
168
+ .animation(tabSpring, value: selectedIndex)
169
+
170
+ VStack(spacing: railGap) {
171
+ ForEach(items.indices, id: \.self) { index in
172
+ SwiftUI.Button {
173
+ onTabSelected(index)
174
+ compression.onTabSelected()
175
+ } label: {
176
+ DuoTabIcon(item: items[index], selected: index == selectedIndex)
177
+ .frame(width: itemSize, height: itemSize)
178
+ .contentShape(Rectangle())
179
+ }
180
+ .buttonStyle(.plain)
181
+ .accessibilityLabel(items[index].label)
182
+ }
183
+ }
184
+ .padding(.vertical, railPadding)
185
+ }
186
+ .frame(width: itemSize)
187
+ .modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
188
+ .accessibilityIdentifier("bottom_tab_bar")
189
+ }
161
190
  }
162
191
 
163
192
  /// Floating labelled bar for the unfolded portrait display, QR in the middle cell (Figma MT1-M).
@@ -17,39 +17,38 @@ struct DuoScrollGradient: View {
17
17
  let scrollOffset: CGFloat
18
18
  let headerHeight: CGFloat
19
19
  let extended: Bool
20
+ /// Quick return header: the gradient leaves with the content instead of fading.
21
+ var followScroll = false
20
22
 
21
23
  @Environment(\.appTheme) private var theme
22
- @Environment(\.screenLayout) private var layout
23
24
 
24
25
  private var scroll: CGFloat { max(scrollOffset, 0) }
25
26
 
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
27
  var body: some View {
35
28
  Group {
36
- if extended {
37
- // extended squeezes the whole fade into its box and fades out
38
- let progress = min(scroll / extendedCollapseDistance, 1)
29
+ if followScroll {
39
30
  gradient
40
- .frame(height: headerHeight + extendedGradientExtra * (1 - progress))
41
- .opacity(1 - min(scroll / extendedGradientExtra, 1))
31
+ .frame(height: headerHeight + (extended ? extendedGradientExtra : 0))
32
+ .offset(y: -scroll)
42
33
  } else {
43
- // default keeps the off-Duo 375:154 shape, the header clips it, and scrolls away
34
+ // fades out on scroll as the extended header does off Duo; extended also collapses
35
+ let progress = min(scroll / extendedCollapseDistance, 1)
44
36
  gradient
45
- .frame(height: layout.width * 154 / 375)
46
- .frame(height: headerHeight, alignment: .top)
47
- .clipped()
48
- .offset(y: -scroll)
37
+ .frame(height: headerHeight + (extended ? extendedGradientExtra * (1 - progress) : 0))
38
+ .opacity(1 - min(scroll / extendedGradientExtra, 1))
49
39
  }
50
40
  }
51
41
  .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
52
42
  .ignoresSafeArea()
53
43
  .allowsHitTesting(false)
54
44
  }
45
+
46
+ /// The fade spans the gradient box.
47
+ private var gradient: LinearGradient {
48
+ LinearGradient(
49
+ colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
50
+ startPoint: .top,
51
+ endPoint: .bottom
52
+ )
53
+ }
55
54
  }
@@ -113,8 +113,9 @@ public struct NavigationHeader: View {
113
113
  }
114
114
  .frame(height: layout.headerBarHeight)
115
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))
116
+ // the row starts headerStatusBarHeight below the window top: 0 with the status bar off
117
+ // the top edge, lower on the iPhone Duo outer display to center on the camera
118
+ .padding(.top, layout.headerStatusBarHeight - layout.insets.top)
118
119
  }
119
120
  }
120
121
 
@@ -31,6 +31,7 @@ struct ToolkitNavigationHeaderRight: View {
31
31
  @State private var isFavorite: Bool = false
32
32
  @State private var isLoading: Bool = false
33
33
  @Environment(\.applicationEnvironment) private var environment
34
+ @Environment(\.duoBarCompression) private var compression
34
35
 
35
36
  private func apiValue(_ value: Any?) -> Any {
36
37
  value ?? NSNull()
@@ -169,7 +170,38 @@ struct ToolkitNavigationHeaderRight: View {
169
170
  } ?? false
170
171
  }
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
172
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 {
173
205
  let navButtonConfig = getNavigationButtonConfig()
174
206
  return VStack(spacing: Spacing.M) {
175
207
  if shouldShowShortcut {
@@ -24,7 +24,7 @@ struct HeaderSideBar: View {
24
24
  let goBack: (() -> Void)?
25
25
  let toolkit: (data: HeaderRightData?, tintColor: Color?)?
26
26
  let headerColor: HeaderColor
27
- /// What footer / tab bar / home indicator take from the bottom; the column stays above it.
27
+ /// What footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
28
28
  let bottomInset: CGFloat
29
29
 
30
30
  @Environment(\.screenLayout) private var layout
@@ -33,7 +33,7 @@ struct HeaderSideBar: View {
33
33
  private var isRight: Bool { sideBar.edge == .right }
34
34
 
35
35
  private var top: CGFloat {
36
- let maxY = layout.height - bottomInset - Spacing.XL - columnHeight
36
+ let maxY = layout.height - bottomInset - columnHeight
37
37
  return max(min(sideBar.top, maxY), 0)
38
38
  }
39
39