@momo-kits/native-kits 0.167.1-duo.18-debug → 0.167.1-duo.19

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 (163) hide show
  1. package/CLAUDE.md +5 -3
  2. package/ios/Application/MaxApi.swift +12 -3
  3. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  4. package/ios/Layout/GridContext.swift +67 -3
  5. package/ios/Navigation/BottomSheet.swift +30 -4
  6. package/ios/Navigation/BottomTab/BottomTab.swift +0 -2
  7. package/ios/Navigation/Component/SnackBar.swift +0 -1
  8. package/ios/Navigation/HostBackGestureBridge.swift +27 -6
  9. package/ios/Navigation/ModalScreen.swift +0 -1
  10. package/ios/Navigation/Navigation.swift +14 -0
  11. package/ios/Navigation/NavigationContainer.swift +26 -56
  12. package/ios/Navigation/Navigator.swift +102 -165
  13. package/ios/Navigation/StackScreen.swift +19 -2
  14. package/ios/Navigation/StackTransition.swift +400 -0
  15. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  16. package/package.json +1 -1
  17. package/build.gradle.kts +0 -11
  18. package/compose/build.gradle.kts +0 -204
  19. package/compose/build.gradle.kts.backup +0 -204
  20. package/compose/compose.podspec +0 -47
  21. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  22. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  23. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  24. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  25. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
  26. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -916
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -81
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -357
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -115
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -86
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -478
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -265
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -235
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -265
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -308
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -330
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -207
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -92
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -560
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -86
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -501
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -236
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -180
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -20
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -194
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -283
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -309
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -713
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -218
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -231
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -447
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -103
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -379
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -39
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -192
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -133
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
  149. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -78
  150. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  151. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  152. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  153. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  154. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
  155. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -94
  156. package/gradle/libs.versions.toml +0 -69
  157. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  158. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  159. package/gradle.properties +0 -26
  160. package/gradlew +0 -252
  161. package/gradlew.bat +0 -94
  162. package/ios-demo/README.md +0 -120
  163. package/settings.gradle.kts +0 -64
@@ -99,36 +99,31 @@ public struct OverplayItem: Identifiable {
99
99
  public var footer: (() -> AnyView)? = nil
100
100
  // Snack-bar payload (mirrors Compose's SnackBar value).
101
101
  public var snackBar: SnackBar? = nil
102
- /// The presented screen it was shown over, nil for the main stack. Like Compose's overplay
103
- /// root id, it pins the item to that context instead of following later presents/dismisses.
104
- public internal(set) var presentedId: Int? = nil
102
+ /// The screen it was shown over (Compose's overplay root id): it renders inside that screen,
103
+ /// so a later push slides over it instead of re-mounting it on the new top.
104
+ public internal(set) var screenId: Int? = nil
105
+ }
106
+
107
+ // MARK: - Stack
108
+
109
+ /// One back-stack entry, root first. A presented screen is an entry like any other (Compose's
110
+ /// `DynamicDialogRoute` in the same NavHost): it rises instead of sliding in, and later pushes
111
+ /// stack on top of it.
112
+ struct StackEntry: Hashable, Identifiable {
113
+ let id: Int
114
+ let isDialog: Bool
105
115
  }
106
116
 
107
117
  // MARK: - Navigator
108
118
 
109
- @available(iOS 16.0, *)
110
119
  public final class Navigator: ObservableObject {
111
- @Published public var path: [DynamicScreenRoute] = [] {
112
- // Disposal hangs off the path so every route that leaves the stack — pop,
113
- // replace, reset — is released the same way, after its exit transition.
114
- didSet { disposeScreens(droppedFrom: oldValue, keeping: path) }
115
- }
116
- @Published public var presented: DynamicDialogRoute? {
117
- didSet { syncCover() }
118
- }
119
- /// Screens pushed while a screen is presented. Compose's dialog route is a plain back-stack
120
- /// entry, so later pushes stack on top of it; the cover hosts its own stack for the same result.
121
- @Published public var presentedPath: [DynamicScreenRoute] = [] {
122
- didSet { disposeScreens(droppedFrom: oldValue, keeping: presentedPath) }
120
+ @Published private(set) var entries: [StackEntry] = [] {
121
+ // Disposal hangs off the stack so every entry that leaves it — pop, replace, reset,
122
+ // dismiss — is released the same way, after its exit transition.
123
+ didSet { disposeScreens(droppedFrom: oldValue, keeping: entries) }
123
124
  }
124
125
  @Published public var overplay: OverplayItem?
125
126
 
126
- /// What the cover shows. It trails `presented` while the previous cover is still sliding out:
127
- /// SwiftUI can drop a presentation issued during another one's dismissal.
128
- @Published private(set) var coverRoute: DynamicDialogRoute?
129
- private var coverOnScreen = false
130
- private var presentationGeneration = 0
131
-
132
127
  /// Owned per-Navigator (mirrors Compose's `remember { DynamicScreenRegistry() }` +
133
128
  /// disposal): screens registered by this navigator are released via ARC when the
134
129
  /// navigator itself deallocates, instead of accumulating in a process-wide singleton.
@@ -140,7 +135,7 @@ public final class Navigator: ObservableObject {
140
135
  /// actually removed via `finishOverplayDismiss`.
141
136
  private var overplayDismissHandler: (() -> Void)?
142
137
 
143
- @Published public private(set) var rootRoute: DynamicScreenRoute
138
+ public var rootRoute: DynamicScreenRoute { DynamicScreenRoute(id: entries[0].id) }
144
139
  public var composeApi: KitComposeApi?
145
140
  /// Host dismiss fallback when there is no `composeApi` (Compose's `maxApi.dismiss`).
146
141
  public var maxApi: MaxApi?
@@ -150,6 +145,11 @@ public final class Navigator: ObservableObject {
150
145
  /// over the tabs still pops normally.
151
146
  var tabBackHandler: (screenId: Int, handler: () -> Void)?
152
147
 
148
+ /// The container's stack view, which plays (and lets the host gesture scrub) the transitions.
149
+ weak var stackController: StackController?
150
+ private var isSeekingBack = false
151
+ private var swipeHaptic = SwipeBackHaptic()
152
+
153
153
  public init(
154
154
  initialScreenName: String = "",
155
155
  initialContent: @escaping () -> AnyView,
@@ -157,22 +157,18 @@ public final class Navigator: ObservableObject {
157
157
  composeApi: KitComposeApi? = nil
158
158
  ) {
159
159
  self.composeApi = composeApi
160
- self.rootRoute = registry.register(
160
+ let root = registry.register(
161
161
  screenName: initialScreenName,
162
162
  content: initialContent,
163
163
  options: options
164
164
  )
165
+ entries = [StackEntry(id: root.id, isDialog: false)]
165
166
  }
166
167
 
167
168
  // MARK: Stack
168
169
 
169
170
  public func push<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
170
- let route = registry.register(
171
- screenName: options?.screenName ?? "",
172
- content: { AnyView(content()) },
173
- options: options
174
- )
175
- appendToActiveStack(route)
171
+ push(screenName: options?.screenName ?? "", content, options: options)
176
172
  }
177
173
 
178
174
  public func push<V: View>(
@@ -180,63 +176,26 @@ public final class Navigator: ObservableObject {
180
176
  @ViewBuilder _ content: @escaping () -> V,
181
177
  options: NavigationOptions? = nil
182
178
  ) {
183
- let route = registry.register(
184
- screenName: screenName,
185
- content: { AnyView(content()) },
186
- options: options
187
- )
188
- appendToActiveStack(route)
179
+ entries.append(register(screenName, content, options, isDialog: false))
189
180
  }
190
181
 
191
182
  public func replace<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
192
- guard removeLastFromActiveStack() else { return }
193
- push(content, options: options)
183
+ replace(screenName: options?.screenName ?? "", content, options: options)
194
184
  }
195
185
 
186
+ /// Compose pops the top entry (a presented one included) and navigates, so the replacement
187
+ /// always lands as a pushed screen; on the root it becomes the new root.
196
188
  public func replace<V: View>(
197
189
  screenName: String,
198
190
  @ViewBuilder _ content: @escaping () -> V,
199
191
  options: NavigationOptions? = nil
200
192
  ) {
201
- guard removeLastFromActiveStack() else { return }
202
- push(screenName: screenName, content, options: options)
203
- }
204
-
205
- /// The presented flow while one is up, the main stack otherwise.
206
- private func appendToActiveStack(_ route: DynamicScreenRoute) {
207
- if presented != nil {
208
- presentedPath.append(route)
209
- } else {
210
- path.append(route)
211
- }
212
- }
213
-
214
- private func removeLastFromActiveStack() -> Bool {
215
- if presented != nil {
216
- // Compose pops the dialog route itself, so the replacement lands on the main stack.
217
- if presentedPath.isEmpty {
218
- clearPresented()
219
- } else {
220
- presentedPath.removeLast()
221
- }
222
- } else {
223
- guard !path.isEmpty else { return false }
224
- path.removeLast()
225
- }
226
- return true
193
+ let entry = register(screenName, content, options, isDialog: false)
194
+ entries = Array(entries.dropLast()) + [entry]
227
195
  }
228
196
 
229
197
  public func reset<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
230
- let previousRoot = rootRoute.id
231
- clearPresented()
232
- path.removeAll()
233
- let route = registry.register(
234
- screenName: options?.screenName ?? "",
235
- content: { AnyView(content()) },
236
- options: options
237
- )
238
- rootRoute = route
239
- disposeScreens([previousRoot])
198
+ reset(screenName: options?.screenName ?? "", content, options: options)
240
199
  }
241
200
 
242
201
  public func reset<V: View>(
@@ -244,16 +203,21 @@ public final class Navigator: ObservableObject {
244
203
  @ViewBuilder _ content: @escaping () -> V,
245
204
  options: NavigationOptions? = nil
246
205
  ) {
247
- let previousRoot = rootRoute.id
248
- clearPresented()
249
- path.removeAll()
206
+ entries = [register(screenName, content, options, isDialog: false)]
207
+ }
208
+
209
+ private func register<V: View>(
210
+ _ screenName: String,
211
+ _ content: @escaping () -> V,
212
+ _ options: NavigationOptions?,
213
+ isDialog: Bool
214
+ ) -> StackEntry {
250
215
  let route = registry.register(
251
216
  screenName: screenName,
252
217
  content: { AnyView(content()) },
253
218
  options: options
254
219
  )
255
- rootRoute = route
256
- disposeScreens([previousRoot])
220
+ return StackEntry(id: route.id, isDialog: isDialog)
257
221
  }
258
222
 
259
223
  public func pop(_ count: Int = 1, callback: (() -> Void)? = nil) {
@@ -299,24 +263,15 @@ public final class Navigator: ObservableObject {
299
263
  }
300
264
  }
301
265
 
302
- /// Pops the topmost presented screen or stack entry. Returns `false` (and requests
303
- /// a host dismiss) when there is nothing left to pop.
266
+ /// Pops the top entry. Returns `false` (and requests a host dismiss) on the root.
304
267
  @discardableResult
305
268
  private func popScreen() -> Bool {
306
- if presented != nil {
307
- if presentedPath.isEmpty {
308
- clearPresented()
309
- } else {
310
- presentedPath.removeLast()
311
- }
312
- return true
313
- } else if !path.isEmpty {
314
- path.removeLast()
315
- return true
316
- } else {
269
+ guard canPop() else {
317
270
  requestHostDismiss()
318
271
  return false
319
272
  }
273
+ entries.removeLast()
274
+ return true
320
275
  }
321
276
 
322
277
  private func requestHostDismiss() {
@@ -327,26 +282,23 @@ public final class Navigator: ObservableObject {
327
282
  }
328
283
  }
329
284
 
330
- /// Whether the active stack has a screen to go back to (Compose's `canPop`); at the root a
331
- /// back dismisses the mini app instead.
285
+ /// Whether there is a screen to go back to (Compose's `canPop`); at the root a back
286
+ /// dismisses the mini app instead.
332
287
  public func canPop() -> Bool {
333
- presented != nil || !path.isEmpty
288
+ entries.count > 1
334
289
  }
335
290
 
336
- /// Empties both stacks; a presented screen stays up on its own root.
291
+ /// Back to the root of the active flow: the topmost presented screen while one is up,
292
+ /// the stack root otherwise.
337
293
  public func popToRoot() {
338
- presentedPath.removeAll()
339
- path.removeAll()
294
+ let root = entries.lastIndex(where: \.isDialog) ?? 0
295
+ entries = Array(entries[...root])
340
296
  }
341
297
 
342
- /// The entry a back action applies to — mirrors Compose's `currentScreen()`
343
- /// (`navController.currentBackStackEntry`): the presented flow sits on top of
344
- /// the stack, otherwise it is the last pushed screen, otherwise the root.
298
+ /// The entry a back action applies to — Compose's `currentScreen()`
299
+ /// (`navController.currentBackStackEntry`).
345
300
  private func currentScreen() -> DynamicScreen? {
346
- if let presented {
347
- return registry.getScreen(id: presentedPath.last?.id ?? presented.id)
348
- }
349
- return registry.getScreen(id: path.last?.id ?? rootRoute.id)
301
+ entries.last.flatMap { registry.getScreen(id: $0.id) }
350
302
  }
351
303
 
352
304
  public func onBackSafe(callback: (() -> Void)? = nil) {
@@ -402,12 +354,7 @@ public final class Navigator: ObservableObject {
402
354
  // MARK: Modal-style screen (vertical slide)
403
355
 
404
356
  public func present<V: View>(@ViewBuilder _ content: @escaping () -> V, options: NavigationOptions? = nil) {
405
- let route = registry.register(
406
- screenName: options?.screenName ?? "",
407
- content: { AnyView(content()) },
408
- options: options
409
- )
410
- presentScreen(route)
357
+ present(screenName: options?.screenName ?? "", content, options: options)
411
358
  }
412
359
 
413
360
  public func present<V: View>(
@@ -415,59 +362,55 @@ public final class Navigator: ObservableObject {
415
362
  @ViewBuilder _ content: @escaping () -> V,
416
363
  options: NavigationOptions? = nil
417
364
  ) {
418
- let route = registry.register(
419
- screenName: screenName,
420
- content: { AnyView(content()) },
421
- options: options
422
- )
423
- presentScreen(route)
365
+ entries.append(register(screenName, content, options, isDialog: true))
424
366
  }
425
367
 
368
+ /// Closes the topmost presented screen together with everything pushed over it.
426
369
  public func dismiss() {
427
- clearPresented()
370
+ guard let presented = entries.lastIndex(where: \.isDialog) else { return }
371
+ entries = Array(entries[..<presented])
428
372
  }
429
373
 
430
- /// One cover per container: presenting from a presented flow stacks onto that flow, the way
431
- /// Compose stacks a second dialog route.
432
- private func presentScreen(_ route: DynamicScreenRoute) {
433
- if presented != nil {
434
- presentedPath.append(route)
435
- } else {
436
- presentationGeneration += 1
437
- presentedPath = []
438
- presented = DynamicDialogRoute(id: route.id)
374
+ // MARK: Host back gesture (Compose's HostBackTarget)
375
+
376
+ /// Only a plain pop is scrubbed. A back the screen handles itself — tab back, custom
377
+ /// handler, prevent-back popup, an open overplay — or one closing a presented screen
378
+ /// waits for the release and goes through `onBackSafe`, as RN turns the gesture off there.
379
+ private var canSeekBack: Bool {
380
+ guard canPop(), overplay == nil, let top = entries.last, !top.isDialog else { return false }
381
+ if let tabBackHandler, tabBackHandler.screenId == -1 || tabBackHandler.screenId == top.id {
382
+ return false
439
383
  }
384
+ let options = registry.getScreen(id: top.id)?.options
385
+ return options?.onBackHandler == nil && options?.headerBackProps?.preventBack == nil
440
386
  }
441
387
 
442
- /// The cover keeps rendering its stack while it slides out, so the flow is torn down only
443
- /// once the exit animation is over.
444
- private func clearPresented() {
445
- guard let id = presented?.id else { return }
446
- presented = nil
447
- let generation = presentationGeneration
448
- DispatchQueue.main.asyncAfter(deadline: .now() + Self.presentedDisposeDelay) { [weak self] in
449
- guard let self, self.presentationGeneration == generation else { return }
450
- self.presentedPath.removeAll()
451
- }
452
- disposeScreens([id], after: Self.presentedDisposeDelay)
388
+ func backStarted() {
389
+ swipeHaptic.reset()
390
+ isSeekingBack = canSeekBack && stackController?.beginBackSeek() == true
453
391
  }
454
392
 
455
- private func syncCover() {
456
- if presented == nil {
457
- coverRoute = nil
458
- } else if !coverOnScreen || coverRoute != nil {
459
- coverRoute = presented
460
- }
393
+ func backProgressed(_ progress: CGFloat) {
394
+ guard isSeekingBack, let stackController else { return }
395
+ stackController.seekBack(to: progress)
396
+ swipeHaptic.update(travelled: progress * stackController.view.bounds.width, maxApi: maxApi)
461
397
  }
462
398
 
463
- /// Reported by the container from the cover's appear / disappear.
464
- func coverDidAppear() {
465
- coverOnScreen = true
399
+ func backCompleted() {
400
+ let wasSeeking = isSeekingBack
401
+ isSeekingBack = false
402
+ // A seek cut short by a programmatic change falls back to a regular back.
403
+ guard wasSeeking, stackController?.finishBackSeek() == true else {
404
+ onBackSafe()
405
+ return
406
+ }
407
+ entries.removeLast()
466
408
  }
467
409
 
468
- func coverDidDisappear() {
469
- coverOnScreen = false
470
- syncCover()
410
+ func backCancelled() {
411
+ guard isSeekingBack else { return }
412
+ isSeekingBack = false
413
+ stackController?.cancelBackSeek()
471
414
  }
472
415
 
473
416
  // MARK: Overplay
@@ -483,7 +426,7 @@ public final class Navigator: ObservableObject {
483
426
  content: { AnyView(content()) },
484
427
  barrierDismissible: barrierDismissible,
485
428
  onDismiss: onDismiss,
486
- presentedId: presented?.id
429
+ screenId: entries.last?.id
487
430
  )
488
431
  }
489
432
 
@@ -508,7 +451,7 @@ public final class Navigator: ObservableObject {
508
451
  draggable: draggable,
509
452
  useBottomInset: useBottomInset,
510
453
  footer: footer,
511
- presentedId: presented?.id
454
+ screenId: entries.last?.id
512
455
  )
513
456
  }
514
457
 
@@ -521,7 +464,7 @@ public final class Navigator: ObservableObject {
521
464
  barrierDismissible: false,
522
465
  onDismiss: onDismiss,
523
466
  snackBar: snackBar,
524
- presentedId: presented?.id
467
+ screenId: entries.last?.id
525
468
  )
526
469
  }
527
470
 
@@ -575,25 +518,19 @@ public final class Navigator: ObservableObject {
575
518
  /// after the component has taken itself down.
576
519
  private static let overplayDismissDelay: TimeInterval = 0.3
577
520
 
578
- /// A full-screen cover slides out slower than a stack pop.
579
- private static let presentedDisposeDelay: TimeInterval = 0.6
580
-
581
- private func disposeScreens(droppedFrom previous: [DynamicScreenRoute], keeping current: [DynamicScreenRoute]) {
521
+ private func disposeScreens(droppedFrom previous: [StackEntry], keeping current: [StackEntry]) {
582
522
  let kept = Set(current.map(\.id))
583
523
  disposeScreens(previous.map(\.id).filter { !kept.contains($0) })
584
524
  }
585
525
 
586
526
  /// Screens are released only once the pop transition has played out: the outgoing
587
- /// view is still on screen while it animates and the registry is what renders it —
588
- /// dropping it right away blanks the screen mid-transition. Liveness is re-checked
589
- /// when the timer fires, so a route pushed back before then keeps its screen.
590
- private func disposeScreens(_ ids: [Int], after delay: TimeInterval = Navigator.screenDisposeDelay) {
527
+ /// view is still on screen while it animates. Liveness is re-checked when the timer
528
+ /// fires, so an entry back on the stack by then keeps its screen.
529
+ private func disposeScreens(_ ids: [Int]) {
591
530
  guard !ids.isEmpty else { return }
592
- DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
531
+ DispatchQueue.main.asyncAfter(deadline: .now() + Self.screenDisposeDelay) { [weak self] in
593
532
  guard let self else { return }
594
- var live = Set(self.path.map(\.id)).union(self.presentedPath.map(\.id))
595
- live.insert(self.rootRoute.id)
596
- if let presented = self.presented { live.insert(presented.id) }
533
+ let live = Set(self.entries.map(\.id))
597
534
  for id in ids where !live.contains(id) {
598
535
  self.registry.unregister(id: id)
599
536
  }
@@ -13,6 +13,9 @@ public struct StackScreen<Content: View>: View {
13
13
  // MARK: - Properties
14
14
 
15
15
  var backgroundColor: Color?
16
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
17
+ var background: (() -> AnyView)?
18
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
16
19
  var headerTransparent: Bool = false
17
20
  var fullScreenContent: Bool = false
18
21
  var tintColor: Color? = Colors.black17
@@ -43,6 +46,9 @@ public struct StackScreen<Content: View>: View {
43
46
 
44
47
  public init(
45
48
  backgroundColor: Color? = nil,
49
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
50
+ background: (() -> AnyView)? = nil,
51
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
46
52
  headerTransparent: Bool = false,
47
53
  fullScreenContent: Bool = false,
48
54
  tintColor: Color = Colors.black17,
@@ -72,6 +78,9 @@ public struct StackScreen<Content: View>: View {
72
78
  @ViewBuilder content: @escaping () -> Content
73
79
  ) {
74
80
  self.backgroundColor = backgroundColor
81
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
82
+ self.background = background
83
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
75
84
  self.headerTransparent = headerTransparent
76
85
  self.fullScreenContent = fullScreenContent
77
86
  self.tintColor = tintColor
@@ -238,8 +247,16 @@ public struct StackScreen<Content: View>: View {
238
247
  public var body: some View {
239
248
  GeometryReader { _ in
240
249
  ZStack(alignment: .top) {
241
- (backgroundColor ?? theme.colors.background.default)
242
- .edgesIgnoringSafeArea(.all)
250
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
251
+ Group {
252
+ if let background {
253
+ background()
254
+ } else {
255
+ backgroundColor ?? theme.colors.background.default
256
+ }
257
+ }
258
+ .edgesIgnoringSafeArea(.all)
259
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
243
260
 
244
261
  VStack(spacing: 0) {
245
262
  ZStack(alignment: fabProps?.position == .center ? .bottom : .bottomTrailing) {