@momo-kits/native-kits 0.167.1-duo.3 → 0.167.1-duo.4-debug

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/CLAUDE.md +17 -8
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +190 -0
  4. package/compose/build.gradle.kts.backup +190 -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 +151 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +334 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +98 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +157 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +622 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +175 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +381 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +83 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +347 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +169 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +71 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +213 -0
  136. package/gradle/libs.versions.toml +69 -0
  137. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  138. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  139. package/gradle.properties +26 -0
  140. package/gradlew +252 -0
  141. package/gradlew.bat +94 -0
  142. package/ios/Application/ApplicationEnvironment.swift +6 -0
  143. package/ios/Application/Components.swift +1 -3
  144. package/ios/Application/HeaderRight.swift +19 -27
  145. package/ios/Application/Navigation/BottomTab/BottomTab.swift +9 -30
  146. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +2 -5
  147. package/ios/Application/Navigation/HeaderUser.swift +1 -7
  148. package/ios/Application/Navigation/NavigationContainer.swift +45 -45
  149. package/ios/Application/Navigation/Navigator.swift +122 -29
  150. package/ios/Application/Navigation/NavigatorManager.swift +13 -6
  151. package/ios/Application/Navigation/Overplay/BottomSheet.swift +4 -41
  152. package/ios/Application/Navigation/Overplay/ModalScreen.swift +1 -5
  153. package/ios/Application/Navigation/component/Header.swift +4 -12
  154. package/ios/Application/Navigation/component/HeaderBackground.swift +2 -3
  155. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  156. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +9 -61
  157. package/ios/Application/StackScreen.swift +18 -135
  158. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
  159. package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
  160. package/ios/Extensions/Color++.swift +20 -9
  161. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  162. package/ios/Icon/Icon.swift +14 -22
  163. package/ios/Icon/Icons.swift +1329 -0
  164. package/ios/Image/Image.swift +16 -5
  165. package/ios/Lottie/LottieView.swift +4 -2
  166. package/ios/Popup/PopupDisplay.swift +68 -84
  167. package/ios/Popup/PopupNotify.swift +32 -43
  168. package/ios/Popup/PopupPromotion.swift +6 -18
  169. package/ios/Resources/dot_loading.json +558 -0
  170. package/ios/Resources/lottie_circle_loader.json +1 -0
  171. package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
  172. package/ios/native-kits.podspec +1 -1
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  174. package/ios-demo/README.md +120 -0
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +0 -1
  176. package/package.json +1 -1
  177. package/scripts/gen-ios-icons.mjs +36 -0
  178. package/settings.gradle.kts +64 -0
  179. package/.claude/scheduled_tasks.lock +0 -1
  180. package/.claude/settings.local.json +0 -185
  181. package/ios/Application/Layout/ScreenLayout.swift +0 -196
  182. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +0 -275
  183. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -55
  184. package/ios/Application/Navigation/component/SideBar.swift +0 -155
  185. package/ios/Popup/PopupLayout.swift +0 -45
  186. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +0 -76
  187. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +0 -170
@@ -10,7 +10,6 @@
10
10
 
11
11
  import SwiftUI
12
12
 
13
- @available(iOS 16.0, *)
14
13
  public struct HeaderUserView: View {
15
14
  private let data: HeaderUserData
16
15
 
@@ -20,11 +19,8 @@ public struct HeaderUserView: View {
20
19
 
21
20
  private let avatarSize: CGFloat = Spacing.XXL // 32
22
21
 
23
- @Environment(\.screenLayout) private var layout
24
-
25
- // back button and toolkit share the bar unless the iPhone Duo side bar holds them
26
22
  private var maxWidth: CGFloat {
27
- layout.sideBar != nil ? layout.contentWidth - Spacing.M * 2 : layout.width - 172
23
+ UIScreen.main.bounds.width - 172
28
24
  }
29
25
 
30
26
  public var body: some View {
@@ -108,7 +104,6 @@ public struct HeaderUserView: View {
108
104
 
109
105
  /// Mirrors Compose's `AvatarGroup`: lays out 1..4 avatars in a fixed collage,
110
106
  /// falls back to initials when empty, and shows a "+n" overflow tile beyond 4.
111
- @available(iOS 16.0, *)
112
107
  private struct AvatarGroup: View {
113
108
  let urls: [String]
114
109
  let title: String
@@ -190,7 +185,6 @@ private struct AvatarGroup: View {
190
185
  }
191
186
  }
192
187
 
193
- @available(iOS 16.0, *)
194
188
  private struct InitialsAvatar: View {
195
189
  let name: String
196
190
  let size: CGFloat
@@ -3,9 +3,8 @@
3
3
  // MoMoUIKits
4
4
  //
5
5
  // SwiftUI mirror of Compose's NavigationContainer.
6
- // Wraps NavigationStack, injects Navigator into the environment, and renders
7
- // every registered screen inside `Screen(options)` chrome — same auto-wrap
8
- // behavior as Compose's StackScreen.
6
+ // Wraps NavigationStack (iOS 16+), injects Navigator into the environment, and renders
7
+ // every registered screen inside `StackScreen` chrome built from its options.
9
8
  //
10
9
 
11
10
  import SwiftUI
@@ -55,41 +54,23 @@ public struct NavigationContainer<Initial: View>: View {
55
54
  }
56
55
 
57
56
  public var body: some View {
58
- NavigationStack(path: $navigator.path) {
59
- screenView(for: navigator.rootRoute)
60
- .navigationDestination(for: DynamicScreenRoute.self) { route in
61
- screenView(for: route)
62
- }
63
- }
64
- .environmentObject(navigator)
65
- .environmentObject(applicationEnvironment)
66
- .environment(\.applicationEnvironment, applicationEnvironment)
67
- // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
68
- .momoLocalize(localize)
69
- .momoFontScale(context: applicationEnvironment.applicationContext)
70
- // AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
71
- .fullScreenCover(item: $navigator.presented) { route in
72
- screenView(forDialog: route)
73
- .environmentObject(navigator)
74
- .environmentObject(applicationEnvironment)
75
- .environment(\.applicationEnvironment, applicationEnvironment)
76
- // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
77
- .momoLocalize(localize)
78
- .momoFontScale(context: applicationEnvironment.applicationContext)
79
- // AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
80
- }
81
- .overlay {
82
- if let item = navigator.overplay {
83
- OverplayHost(item: item)
84
- .environmentObject(navigator)
85
- .environmentObject(applicationEnvironment)
86
- .environment(\.applicationEnvironment, applicationEnvironment)
87
- // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
88
- .momoLocalize(localize)
89
- .momoFontScale(context: applicationEnvironment.applicationContext)
90
- // AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
91
- .momoScreenLayout()
92
- }
57
+ withKitEnvironment(
58
+ stack(path: $navigator.path, root: navigator.rootRoute)
59
+ .overlay { overplayLayer(presentedId: nil) }
60
+ )
61
+ .fullScreenCover(item: Binding(
62
+ get: { navigator.coverRoute },
63
+ // A close that bypasses the navigator (e.g. `dismiss()`) still tears the flow down.
64
+ // SwiftUI also writes nil after a navigator-driven close; by then a new screen may
65
+ // already be queued, so only a cover that is still showing counts.
66
+ set: { if $0 == nil, navigator.coverRoute != nil { navigator.dismiss() } }
67
+ )) { route in
68
+ withKitEnvironment(
69
+ stack(path: $navigator.presentedPath, root: DynamicScreenRoute(id: route.id))
70
+ .overlay { overplayLayer(presentedId: route.id) }
71
+ )
72
+ .onAppear { navigator.coverDidAppear() }
73
+ .onDisappear { navigator.coverDidDisappear() }
93
74
  }
94
75
  .onAppear {
95
76
  if navigator.composeApi == nil {
@@ -110,10 +91,23 @@ public struct NavigationContainer<Initial: View>: View {
110
91
  // AI-GENERATED END: demote the MaxApi language pull to a seed of last resort
111
92
  }
112
93
  .onDisappear {
94
+ // Our own cover hides the stack without leaving it; host back must still reach it.
95
+ guard navigator.presented == nil else { return }
113
96
  NavigatorManager.shared.remove(navigator)
114
97
  }
115
98
  }
116
99
 
100
+ private func stack(path: Binding<[DynamicScreenRoute]>, root: DynamicScreenRoute) -> some View {
101
+ NavigationStack(path: path) {
102
+ screenView(for: root)
103
+ // A reset swaps the root in place; without the id it would inherit the old root's state.
104
+ .id(root.id)
105
+ .navigationDestination(for: DynamicScreenRoute.self) { route in
106
+ screenView(for: route)
107
+ }
108
+ }
109
+ }
110
+
117
111
  @ViewBuilder
118
112
  private func screenView(for route: DynamicScreenRoute) -> some View {
119
113
  if let screen = navigator.registry.getScreen(id: route.id) {
@@ -124,14 +118,22 @@ public struct NavigationContainer<Initial: View>: View {
124
118
  }
125
119
  }
126
120
 
121
+ /// Each context draws only the overplays shown over it, so one never re-mounts elsewhere.
127
122
  @ViewBuilder
128
- private func screenView(forDialog route: DynamicDialogRoute) -> some View {
129
- if let screen = navigator.registry.getScreen(id: route.id) {
130
- ScreenHost(screen: screen, navigator: navigator)
131
- } else {
132
- EmptyView()
123
+ private func overplayLayer(presentedId: Int?) -> some View {
124
+ if let item = navigator.overplay, item.presentedId == presentedId {
125
+ OverplayHost(item: item)
133
126
  }
134
127
  }
128
+
129
+ private func withKitEnvironment<Content: View>(_ content: Content) -> some View {
130
+ content
131
+ .environmentObject(navigator)
132
+ .environmentObject(applicationEnvironment)
133
+ .environment(\.applicationEnvironment, applicationEnvironment)
134
+ .momoLocalize(localize)
135
+ .momoFontScale(context: applicationEnvironment.applicationContext)
136
+ }
135
137
  }
136
138
 
137
139
  // MARK: - ScreenHost
@@ -168,8 +170,6 @@ private struct ScreenHost: View {
168
170
  screenName: screen.name,
169
171
  content: { screen.content() }
170
172
  )
171
- // Per screen, not around the NavigationStack: a state-driven reader there stops path pushes rendering.
172
- .momoScreenLayout()
173
173
  }
174
174
  }
175
175
 
@@ -3,8 +3,8 @@
3
3
  // MoMoUIKits
4
4
  //
5
5
  // SwiftUI mirror of Compose's Navigator + DynamicScreenRegistry.
6
- // Screens are registered by Int id; the NavigationStack path stores routes
7
- // by id and looks up content from the registry on render.
6
+ // Screens are registered by Int id; the stack paths store routes
7
+ // by id and content is looked up from the registry on render.
8
8
  //
9
9
 
10
10
  import SwiftUI
@@ -83,6 +83,9 @@ public struct OverplayItem: Identifiable {
83
83
  public var header: BottomSheetHeader? = nil
84
84
  // Snack-bar payload (mirrors Compose's SnackBar value).
85
85
  public var snackBar: SnackBar? = nil
86
+ /// The presented screen it was shown over, nil for the main stack. Like Compose's overplay
87
+ /// root id, it pins the item to that context instead of following later presents/dismisses.
88
+ public internal(set) var presentedId: Int? = nil
86
89
  }
87
90
 
88
91
  // MARK: - Navigator
@@ -92,11 +95,24 @@ public final class Navigator: ObservableObject {
92
95
  @Published public var path: [DynamicScreenRoute] = [] {
93
96
  // Disposal hangs off the path so every route that leaves the stack — pop,
94
97
  // replace, reset — is released the same way, after its exit transition.
95
- didSet { disposeScreens(droppedFrom: oldValue) }
98
+ didSet { disposeScreens(droppedFrom: oldValue, keeping: path) }
99
+ }
100
+ @Published public var presented: DynamicDialogRoute? {
101
+ didSet { syncCover() }
102
+ }
103
+ /// Screens pushed while a screen is presented. Compose's dialog route is a plain back-stack
104
+ /// entry, so later pushes stack on top of it; the cover hosts its own stack for the same result.
105
+ @Published public var presentedPath: [DynamicScreenRoute] = [] {
106
+ didSet { disposeScreens(droppedFrom: oldValue, keeping: presentedPath) }
96
107
  }
97
- @Published public var presented: DynamicDialogRoute?
98
108
  @Published public var overplay: OverplayItem?
99
109
 
110
+ /// What the cover shows. It trails `presented` while the previous cover is still sliding out:
111
+ /// SwiftUI can drop a presentation issued during another one's dismissal.
112
+ @Published private(set) var coverRoute: DynamicDialogRoute?
113
+ private var coverOnScreen = false
114
+ private var presentationGeneration = 0
115
+
100
116
  /// Owned per-Navigator (mirrors Compose's `remember { DynamicScreenRegistry() }` +
101
117
  /// disposal): screens registered by this navigator are released via ARC when the
102
118
  /// navigator itself deallocates, instead of accumulating in a process-wide singleton.
@@ -108,7 +124,7 @@ public final class Navigator: ObservableObject {
108
124
  /// actually removed via `finishOverplayDismiss`.
109
125
  private var overplayDismissHandler: (() -> Void)?
110
126
 
111
- public private(set) var rootRoute: DynamicScreenRoute
127
+ @Published public private(set) var rootRoute: DynamicScreenRoute
112
128
  public var composeApi: KitComposeApi?
113
129
 
114
130
  public init(
@@ -133,7 +149,7 @@ public final class Navigator: ObservableObject {
133
149
  content: { AnyView(content()) },
134
150
  options: options
135
151
  )
136
- path.append(route)
152
+ appendToActiveStack(route)
137
153
  }
138
154
 
139
155
  public func push<V: View>(
@@ -146,12 +162,11 @@ public final class Navigator: ObservableObject {
146
162
  content: { AnyView(content()) },
147
163
  options: options
148
164
  )
149
- path.append(route)
165
+ appendToActiveStack(route)
150
166
  }
151
167
 
152
168
  public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
153
- guard !path.isEmpty else { return }
154
- path.removeLast()
169
+ guard removeLastFromActiveStack() else { return }
155
170
  push(content, options: options)
156
171
  }
157
172
 
@@ -160,13 +175,37 @@ public final class Navigator: ObservableObject {
160
175
  @ViewBuilder _ content: @escaping () -> V,
161
176
  options: NavigationOptions? = nil
162
177
  ) {
163
- guard !path.isEmpty else { return }
164
- path.removeLast()
178
+ guard removeLastFromActiveStack() else { return }
165
179
  push(screenName: screenName, content, options: options)
166
180
  }
167
181
 
182
+ /// The presented flow while one is up, the main stack otherwise.
183
+ private func appendToActiveStack(_ route: DynamicScreenRoute) {
184
+ if presented != nil {
185
+ presentedPath.append(route)
186
+ } else {
187
+ path.append(route)
188
+ }
189
+ }
190
+
191
+ private func removeLastFromActiveStack() -> Bool {
192
+ if presented != nil {
193
+ // Compose pops the dialog route itself, so the replacement lands on the main stack.
194
+ if presentedPath.isEmpty {
195
+ clearPresented()
196
+ } else {
197
+ presentedPath.removeLast()
198
+ }
199
+ } else {
200
+ guard !path.isEmpty else { return false }
201
+ path.removeLast()
202
+ }
203
+ return true
204
+ }
205
+
168
206
  public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
169
207
  let previousRoot = rootRoute.id
208
+ clearPresented()
170
209
  path.removeAll()
171
210
  let route = registry.register(
172
211
  screenName: options?.screenName ?? "",
@@ -183,6 +222,7 @@ public final class Navigator: ObservableObject {
183
222
  options: NavigationOptions? = nil
184
223
  ) {
185
224
  let previousRoot = rootRoute.id
225
+ clearPresented()
186
226
  path.removeAll()
187
227
  let route = registry.register(
188
228
  screenName: screenName,
@@ -241,9 +281,11 @@ public final class Navigator: ObservableObject {
241
281
  @discardableResult
242
282
  private func popScreen() -> Bool {
243
283
  if presented != nil {
244
- let dismissedId = presented?.id
245
- presented = nil
246
- if let id = dismissedId { disposeScreens([id]) }
284
+ if presentedPath.isEmpty {
285
+ clearPresented()
286
+ } else {
287
+ presentedPath.removeLast()
288
+ }
247
289
  return true
248
290
  } else if !path.isEmpty {
249
291
  path.removeLast()
@@ -254,15 +296,20 @@ public final class Navigator: ObservableObject {
254
296
  }
255
297
  }
256
298
 
299
+ /// Empties both stacks; a presented screen stays up on its own root.
257
300
  public func popToRoot() {
301
+ presentedPath.removeAll()
258
302
  path.removeAll()
259
303
  }
260
304
 
261
305
  /// The entry a back action applies to — mirrors Compose's `currentScreen()`
262
- /// (`navController.currentBackStackEntry`): a presented screen sits on top of
306
+ /// (`navController.currentBackStackEntry`): the presented flow sits on top of
263
307
  /// the stack, otherwise it is the last pushed screen, otherwise the root.
264
308
  private func currentScreen() -> DynamicScreen? {
265
- registry.getScreen(id: presented?.id ?? path.last?.id ?? rootRoute.id)
309
+ if let presented {
310
+ return registry.getScreen(id: presentedPath.last?.id ?? presented.id)
311
+ }
312
+ return registry.getScreen(id: path.last?.id ?? rootRoute.id)
266
313
  }
267
314
 
268
315
  public func onBackSafe(callback: (() -> Void)? = nil) {
@@ -318,7 +365,7 @@ public final class Navigator: ObservableObject {
318
365
  content: { AnyView(content()) },
319
366
  options: options
320
367
  )
321
- presented = DynamicDialogRoute(id: route.id)
368
+ presentScreen(route)
322
369
  }
323
370
 
324
371
  public func present<V: View>(
@@ -331,14 +378,54 @@ public final class Navigator: ObservableObject {
331
378
  content: { AnyView(content()) },
332
379
  options: options
333
380
  )
334
- presented = DynamicDialogRoute(id: route.id)
381
+ presentScreen(route)
335
382
  }
336
383
 
337
384
  public func dismiss() {
338
- if let id = presented?.id {
339
- registry.unregister(id: id)
385
+ clearPresented()
386
+ }
387
+
388
+ /// One cover per container: presenting from a presented flow stacks onto that flow, the way
389
+ /// Compose stacks a second dialog route.
390
+ private func presentScreen(_ route: DynamicScreenRoute) {
391
+ if presented != nil {
392
+ presentedPath.append(route)
393
+ } else {
394
+ presentationGeneration += 1
395
+ presentedPath = []
396
+ presented = DynamicDialogRoute(id: route.id)
340
397
  }
398
+ }
399
+
400
+ /// The cover keeps rendering its stack while it slides out, so the flow is torn down only
401
+ /// once the exit animation is over.
402
+ private func clearPresented() {
403
+ guard let id = presented?.id else { return }
341
404
  presented = nil
405
+ let generation = presentationGeneration
406
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.presentedDisposeDelay) { [weak self] in
407
+ guard let self, self.presentationGeneration == generation else { return }
408
+ self.presentedPath.removeAll()
409
+ }
410
+ disposeScreens([id], after: Self.presentedDisposeDelay)
411
+ }
412
+
413
+ private func syncCover() {
414
+ if presented == nil {
415
+ coverRoute = nil
416
+ } else if !coverOnScreen || coverRoute != nil {
417
+ coverRoute = presented
418
+ }
419
+ }
420
+
421
+ /// Reported by the container from the cover's appear / disappear.
422
+ func coverDidAppear() {
423
+ coverOnScreen = true
424
+ }
425
+
426
+ func coverDidDisappear() {
427
+ coverOnScreen = false
428
+ syncCover()
342
429
  }
343
430
 
344
431
  // MARK: Overplay
@@ -353,7 +440,8 @@ public final class Navigator: ObservableObject {
353
440
  type: .modal,
354
441
  content: { AnyView(content()) },
355
442
  barrierDismissible: barrierDismissible,
356
- onDismiss: onDismiss
443
+ onDismiss: onDismiss,
444
+ presentedId: presented?.id
357
445
  )
358
446
  }
359
447
 
@@ -371,7 +459,8 @@ public final class Navigator: ObservableObject {
371
459
  barrierDismissible: barrierDismissible,
372
460
  onDismiss: onDismiss,
373
461
  isSurface: isSurface,
374
- header: header
462
+ header: header,
463
+ presentedId: presented?.id
375
464
  )
376
465
  }
377
466
 
@@ -383,7 +472,8 @@ public final class Navigator: ObservableObject {
383
472
  content: { AnyView(EmptyView()) },
384
473
  barrierDismissible: false,
385
474
  onDismiss: onDismiss,
386
- snackBar: snackBar
475
+ snackBar: snackBar,
476
+ presentedId: presented?.id
387
477
  )
388
478
  }
389
479
 
@@ -429,20 +519,23 @@ public final class Navigator: ObservableObject {
429
519
  /// after the component has taken itself down.
430
520
  private static let overplayDismissDelay: TimeInterval = 0.3
431
521
 
432
- private func disposeScreens(droppedFrom previous: [DynamicScreenRoute]) {
433
- let live = Set(path.map(\.id))
434
- disposeScreens(previous.map(\.id).filter { !live.contains($0) })
522
+ /// A full-screen cover slides out slower than a stack pop.
523
+ private static let presentedDisposeDelay: TimeInterval = 0.6
524
+
525
+ private func disposeScreens(droppedFrom previous: [DynamicScreenRoute], keeping current: [DynamicScreenRoute]) {
526
+ let kept = Set(current.map(\.id))
527
+ disposeScreens(previous.map(\.id).filter { !kept.contains($0) })
435
528
  }
436
529
 
437
530
  /// Screens are released only once the pop transition has played out: the outgoing
438
531
  /// view is still on screen while it animates and the registry is what renders it —
439
532
  /// dropping it right away blanks the screen mid-transition. Liveness is re-checked
440
533
  /// when the timer fires, so a route pushed back before then keeps its screen.
441
- private func disposeScreens(_ ids: [Int]) {
534
+ private func disposeScreens(_ ids: [Int], after delay: TimeInterval = Navigator.screenDisposeDelay) {
442
535
  guard !ids.isEmpty else { return }
443
- DispatchQueue.main.asyncAfter(deadline: .now() + Self.screenDisposeDelay) { [weak self] in
536
+ DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
444
537
  guard let self else { return }
445
- var live = Set(self.path.map(\.id))
538
+ var live = Set(self.path.map(\.id)).union(self.presentedPath.map(\.id))
446
539
  live.insert(self.rootRoute.id)
447
540
  if let presented = self.presented { live.insert(presented.id) }
448
541
  for id in ids where !live.contains(id) {
@@ -5,18 +5,25 @@ public final class NavigatorManager {
5
5
  public static let shared = NavigatorManager()
6
6
  private init() {}
7
7
 
8
- private var navigators: [Navigator] = []
8
+ // Weak: a container torn down while its cover is up never reports its disappearance.
9
+ private var navigators: [WeakNavigator] = []
9
10
 
10
11
  public func push(_ navigator: Navigator) {
11
- guard !navigators.contains(where: { $0 === navigator }) else { return }
12
- navigators.append(navigator)
12
+ navigators.removeAll { $0.value == nil }
13
+ guard !navigators.contains(where: { $0.value === navigator }) else { return }
14
+ navigators.append(WeakNavigator(value: navigator))
13
15
  }
14
16
 
15
17
  public func remove(_ navigator: Navigator) {
16
- navigators.removeAll { $0 === navigator }
18
+ navigators.removeAll { $0.value == nil || $0.value === navigator }
17
19
  }
18
20
 
19
- public func checkCanPop() -> Bool { !navigators.isEmpty }
21
+ public func checkCanPop() -> Bool { navigators.contains { $0.value != nil } }
20
22
 
21
- public func pop() { navigators.last?.onBackSafe() }
23
+ public func pop() { navigators.last { $0.value != nil }?.value?.onBackSafe() }
24
+ }
25
+
26
+ @available(iOS 16.0, *)
27
+ private struct WeakNavigator {
28
+ weak var value: Navigator?
22
29
  }
@@ -45,7 +45,6 @@ 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
49
48
 
50
49
  /// Vertical translation of the sheet; starts off-screen and animates to 0.
51
50
  @State private var sheetOffset: CGFloat = UIScreen.main.bounds.height
@@ -67,36 +66,7 @@ struct BottomSheetView<Content: View>: View {
67
66
  /// steady-state (fully open) scrim peaks at 0.6, matching ModalScreen's peak.
68
67
  private let maxDynamicAlpha: CGFloat = 0.3
69
68
 
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
- }
69
+ private var screenHeight: CGFloat { UIScreen.main.bounds.height }
100
70
  private var bottomInset: CGFloat { safeAreaBottomInset() }
101
71
  /// While the keyboard is up, it already covers the home-indicator area, so it
102
72
  /// replaces (rather than adds to) the usual bottom-inset filler.
@@ -140,7 +110,7 @@ struct BottomSheetView<Content: View>: View {
140
110
  .onReceive(NotificationCenter.default.publisher(for: UIResponder.keyboardWillChangeFrameNotification)) { notification in
141
111
  guard let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
142
112
  withAnimation(.easeOut(duration: 0.25)) {
143
- keyboardHeight = screenHeight - endFrame.origin.y
113
+ keyboardHeight = UIScreen.main.bounds.height - endFrame.origin.y
144
114
  }
145
115
  }
146
116
  .onAppear {
@@ -168,8 +138,6 @@ struct BottomSheetView<Content: View>: View {
168
138
  headerView
169
139
  .frame(height: 56)
170
140
  .padding(.horizontal, Spacing.M)
171
- .padding(.leading, sideInsetLeading)
172
- .padding(.trailing, sideInsetTrailing)
173
141
  }
174
142
  .frame(maxWidth: .infinity)
175
143
  .frame(height: 72)
@@ -181,17 +149,12 @@ struct BottomSheetView<Content: View>: View {
181
149
  .frame(height: 1)
182
150
 
183
151
  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)
188
152
 
189
153
  backgroundColor
190
154
  .frame(height: effectiveBottomInset)
191
155
  }
192
- .frame(maxWidth: isDuoCentered ? duoCenteredWidth : .infinity)
193
- .frame(maxHeight: duoMaxHeight, alignment: .bottom)
194
- .clipped()
156
+ .frame(maxWidth: .infinity)
157
+ .frame(alignment: .bottom)
195
158
  .background(
196
159
  GeometryReader { geo in
197
160
  Color.clear.onAppear { sheetHeight = geo.size.height }
@@ -16,7 +16,6 @@ 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
20
19
 
21
20
  @State private var scrimOpacity: CGFloat = 0
22
21
  @State private var contentScale: CGFloat = 0.8
@@ -36,11 +35,8 @@ struct ModalScreen<Content: View>: View {
36
35
  content()
37
36
  .scaleEffect(contentScale)
38
37
  .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)
42
38
  }
43
- // Full screen like RN's Modal, so asymmetric landscape insets don't shift the content sideways.
39
+ // Center on the full screen like Compose; landscape safe areas are asymmetric (camera on one side).
44
40
  .frame(maxWidth: .infinity, maxHeight: .infinity)
45
41
  .ignoresSafeArea(.container)
46
42
  .onAppear {
@@ -60,11 +60,7 @@ public struct NavigationHeader: View {
60
60
  self.titleOpacity = titleOpacity
61
61
  }
62
62
 
63
- @Environment(\.screenLayout) private var layout
64
-
65
63
  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 }
68
64
  private var isSurface: Bool { animatedHeader?.isSurface ?? true }
69
65
 
70
66
  private var headerColor: HeaderColor {
@@ -83,7 +79,7 @@ public struct NavigationHeader: View {
83
79
  if headerType != .none {
84
80
  ZStack {
85
81
  HStack(spacing: 0) {
86
- if let goBack, !useSideBar {
82
+ if let goBack {
87
83
  BackButton(color: headerColor, onBack: goBack)
88
84
  Spacer().frame(width: Spacing.S)
89
85
  }
@@ -95,7 +91,7 @@ public struct NavigationHeader: View {
95
91
  .frame(maxWidth: .infinity, alignment: .leading)
96
92
  }
97
93
 
98
- if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
94
+ if let headerRight {
99
95
  AnyView(headerRight())
100
96
  .background(
101
97
  GeometryReader { geo in
@@ -111,10 +107,8 @@ public struct NavigationHeader: View {
111
107
  .padding(.horizontal, Spacing.M)
112
108
  }
113
109
  }
114
- .frame(height: layout.headerBarHeight)
110
+ .frame(height: HEADER_HEIGHT)
115
111
  .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))
118
112
  }
119
113
  }
120
114
 
@@ -125,9 +119,7 @@ public struct NavigationHeader: View {
125
119
  if let inputSearchProps {
126
120
  InputSearch(props: inputSearchProps)
127
121
  } else if case let .user(userData)? = headerTitle {
128
- if #available(iOS 16.0, *) {
129
- HeaderUserView(userData)
130
- }
122
+ HeaderUserView(userData)
131
123
  } else {
132
124
  MomoText(
133
125
  headerTitle?.defaultTitle ?? title,
@@ -43,11 +43,10 @@ public struct NavigationHeaderBackground: View {
43
43
  )
44
44
  }
45
45
 
46
- @Environment(\.screenLayout) private var layout
47
-
48
46
  public var body: some View {
47
+ let statusBarHeight = safeAreaTopInset()
49
48
  let opacity = max(0, 1 - scrollState / CGFloat(HEADER_HEIGHT))
50
- let minHeight = fullScreenContent ? 0 : layout.headerHeight
49
+ let minHeight = fullScreenContent ? 0 : statusBarHeight + HEADER_HEIGHT
51
50
  let maxHeight: CGFloat = 154
52
51
  let backgroundColor = headerTransparent ? Color.clear : Colors.black01
53
52
  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, .horizontal]))
72
+ .background(theme.colors.background.surface.ignoresSafeArea(.container, edges: .bottom))
73
73
  .overlay(alignment: .top) { shadow }
74
74
  .offset(y: Self.verticalOffset(keyboardSize: keyboardSize))
75
75
  }