@momo-kits/native-kits 0.167.1-duo.1-debug → 0.167.1-duo.10-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 (177) hide show
  1. package/CLAUDE.md +32 -0
  2. package/compose/build.gradle.kts +15 -1
  3. package/compose/build.gradle.kts.backup +14 -0
  4. package/compose/compose.podspec +1 -1
  5. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  7. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +1 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +232 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +4 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +143 -31
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +100 -66
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +22 -16
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +66 -18
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +23 -14
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +81 -24
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +185 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +4 -1
  56. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
  58. package/gradle.properties +1 -1
  59. package/ios/Application/ApplicationEnvironment.swift +17 -0
  60. package/ios/Application/ComposeApi.swift +2 -1
  61. package/ios/Application/{Components.swift → LegacyHeader.swift} +7 -65
  62. package/ios/Application/Screen.swift +3 -2
  63. package/ios/Avatar/Avatar.swift +4 -3
  64. package/ios/Badge/Badge.swift +6 -4
  65. package/ios/Badge/BadgeDot.swift +1 -1
  66. package/ios/Badge/BadgeRibbon.swift +0 -1
  67. package/ios/Button/Button.swift +15 -12
  68. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  69. package/ios/Carousel/Carousel.swift +29 -12
  70. package/ios/Checkbox/Checkbox.swift +7 -3
  71. package/ios/Chip/Chip.swift +5 -4
  72. package/ios/Collapse/Collapse.swift +70 -19
  73. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  74. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  75. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  76. package/ios/Divider/Divider.swift +37 -4
  77. package/ios/Extensions/Color++.swift +38 -9
  78. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  79. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  80. package/ios/Icon/Icon.swift +21 -23
  81. package/ios/Icon/Icons.swift +1329 -0
  82. package/ios/IconButton/IconButton.swift +15 -7
  83. package/ios/Image/Image.swift +7 -23
  84. package/ios/Information/Information.swift +27 -9
  85. package/ios/Input/ErrorView.swift +2 -2
  86. package/ios/Input/Input.swift +160 -147
  87. package/ios/Input/InputPhoneNumber.swift +12 -9
  88. package/ios/Input/InputSearch.swift +102 -75
  89. package/ios/Input/InputTextArea.swift +128 -145
  90. package/ios/InputDropDown/InputDropDown.swift +10 -7
  91. package/ios/InputMoney/InputMoney.swift +33 -11
  92. package/ios/InputOTP/InputOTP.swift +22 -12
  93. package/ios/Layout/Card.swift +50 -0
  94. package/ios/Layout/GridContext.swift +123 -3
  95. package/ios/Layout/Item.swift +5 -4
  96. package/ios/Layout/ScreenLayout.swift +312 -0
  97. package/ios/Layout/Section.swift +4 -100
  98. package/ios/Loader/Loader.swift +5 -9
  99. package/ios/Lottie/LottieView.swift +14 -2
  100. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +97 -8
  101. package/ios/Navigation/BottomTab/BottomTab.swift +187 -0
  102. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  103. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  104. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  105. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  106. package/ios/Navigation/Component/Header.swift +493 -0
  107. package/ios/Navigation/Component/HeaderBackground.swift +134 -0
  108. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  109. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  110. package/ios/Navigation/Component/HeaderRight.swift +386 -0
  111. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  112. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  113. package/ios/Navigation/Component/SideBar.swift +176 -0
  114. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -5
  115. package/ios/Navigation/HostBackGestureBridge.swift +57 -0
  116. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -1
  117. package/ios/Navigation/Navigation.swift +190 -0
  118. package/ios/{Application/Navigation → Navigation}/NavigationContainer.swift +92 -81
  119. package/ios/{Application/Navigation → Navigation}/Navigator.swift +185 -36
  120. package/ios/Navigation/StackScreen.swift +782 -0
  121. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  122. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  123. package/ios/Pagination/PaginationScroll.swift +2 -2
  124. package/ios/Popup/PopupDisplay.swift +72 -136
  125. package/ios/Popup/PopupInput.swift +10 -13
  126. package/ios/Popup/PopupLayout.swift +140 -0
  127. package/ios/Popup/PopupNotify.swift +56 -73
  128. package/ios/Popup/PopupPromotion.swift +31 -47
  129. package/ios/PopupView/FullscreenPopup.swift +13 -19
  130. package/ios/PopupView/PopupView.swift +1 -1
  131. package/ios/PopupView/Utils++.swift +0 -8
  132. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  133. package/ios/Radio/Radio.swift +2 -1
  134. package/ios/Rating/Rating.swift +9 -1
  135. package/ios/Resources/dot_loading.json +558 -0
  136. package/ios/Resources/lottie_circle_loader.json +1 -0
  137. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  138. package/ios/Skeleton/Skeleton.swift +22 -27
  139. package/ios/Slider/Slider.swift +16 -18
  140. package/ios/Stepper/Stepper.swift +6 -1
  141. package/ios/Steps/Steps.swift +8 -6
  142. package/ios/SuggestAction/SuggestAction.swift +5 -0
  143. package/ios/Swipeable/SwipeCell.swift +10 -6
  144. package/ios/Switch/Switch.swift +22 -20
  145. package/ios/TabView/TabView.swift +102 -21
  146. package/ios/Tag/Tag.swift +15 -4
  147. package/ios/Template/Logo/Logo.swift +2 -1
  148. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  149. package/ios/Title/Title.swift +6 -17
  150. package/ios/Tooltip/Tooltip.swift +29 -14
  151. package/ios/Typography/Text.swift +61 -19
  152. package/ios/Uploader/Uploader.swift +3 -1
  153. package/ios/native-kits.podspec +2 -1
  154. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  155. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  156. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  157. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  160. package/package.json +1 -1
  161. package/scripts/gen-ios-icons.mjs +36 -0
  162. package/.claude/settings.local.json +0 -185
  163. package/ios/Application/HeaderRight.swift +0 -306
  164. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  165. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  166. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  167. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  168. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  169. package/ios/Application/Navigation/component/Header.swift +0 -178
  170. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  171. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  172. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  173. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  174. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  175. package/ios/Application/StackScreen.swift +0 -492
  176. package/local.properties +0 -8
  177. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,176 @@
1
+ //
2
+ // SideBar.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/SideBar.kt`. iPhone Duo: back and toolkit
6
+ // leave the header bar for a column in the side safe area, as the system bars do.
7
+ // Port of RN `SideBarButton.tsx` + `useSideBarItemStyle`.
8
+ //
9
+
10
+ import SwiftUI
11
+
12
+ private let sideBarIconSize: CGFloat = 24
13
+
14
+ /// The back button and toolkit column, placed in window coordinates over the side strip.
15
+ struct HeaderSideBar: View {
16
+ let sideBar: SideBarLayout
17
+ let goBack: (() -> Void)?
18
+ let toolkit: (data: HeaderRightData?, tintColor: Color?)?
19
+ let headerColor: HeaderColor
20
+ /// What footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
21
+ let bottomInset: CGFloat
22
+
23
+ @Environment(\.screenLayout) private var layout
24
+ @Environment(\.applicationEnvironment) private var environment
25
+ @Environment(\.duoBarCompression) private var compression
26
+ @State private var columnHeight: CGFloat = 0
27
+
28
+ private var isRight: Bool { sideBar.edge == .right }
29
+
30
+ private var top: CGFloat {
31
+ let maxY = layout.height - bottomInset - columnHeight
32
+ return max(min(sideBar.top, maxY), 0)
33
+ }
34
+
35
+ /// Full height, so the tab bar compression decision follows this screen's toolkit.
36
+ private var fullHeight: CGFloat {
37
+ let toolkitHeight = toolkit.map {
38
+ Spacing.M + toolkitColumnHeight($0.data ?? HeaderRightData(), context: environment.applicationContext)
39
+ }
40
+ return DuoMetrics.controlSize + (toolkitHeight ?? 0)
41
+ }
42
+
43
+ var body: some View {
44
+ VStack(alignment: isRight ? .leading : .trailing, spacing: Spacing.M) {
45
+ if let goBack {
46
+ SideBarButton(
47
+ icon: "arrow-back",
48
+ headerColor: headerColor,
49
+ accessibilityId: "btn_navigation_back",
50
+ action: goBack
51
+ )
52
+ } else {
53
+ // keep the slot so the toolkit does not jump between root and pushed screens
54
+ Color.clear.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
55
+ }
56
+ if let toolkit {
57
+ if compression.toolbarHidden {
58
+ // an open tab rail takes this slot for a moment
59
+ EmptyView()
60
+ } else if compression.toolbarCompressed {
61
+ // HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
62
+ SideBarButton(
63
+ icon: "navigation_menu",
64
+ headerColor: headerColor,
65
+ showBadge: toolkit.data?.showBadge ?? false,
66
+ accessibilityId: "btn_navigation_toolbar",
67
+ action: compression.expandToolbar
68
+ )
69
+ } else {
70
+ Toolkit(
71
+ headerRight: toolkit.data ?? HeaderRightData(),
72
+ tintColor: toolkit.tintColor,
73
+ vertical: true
74
+ )
75
+ .environment(\.headerColor, headerColor)
76
+ }
77
+ }
78
+ }
79
+ .padding(isRight ? .leading : .trailing, Spacing.M)
80
+ .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
81
+ .background(
82
+ GeometryReader { proxy in
83
+ Color.clear
84
+ .onAppear { columnHeight = proxy.size.height }
85
+ .onChange(of: proxy.size.height) { columnHeight = $0 }
86
+ }
87
+ )
88
+ .padding(.top, top)
89
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .topTrailing : .topLeading)
90
+ .ignoresSafeArea()
91
+ .onAppear { compression.setToolbarHeight(fullHeight) }
92
+ .onChange(of: fullHeight) { compression.setToolbarHeight($0) }
93
+ }
94
+ }
95
+
96
+ /// Dark glass under light icons (banner headers), light glass otherwise.
97
+ private struct SideBarGlass<S: Shape>: ViewModifier {
98
+ let headerColor: HeaderColor
99
+ let shape: S
100
+
101
+ private var isDark: Bool { headerColor.tintIconColor == Colors.black01 || headerColor.tintIconColor == .white }
102
+
103
+ func body(content: Content) -> some View {
104
+ if isDark {
105
+ content
106
+ .background(shape.fill(Colors.black20.opacity(0.5)))
107
+ .overlay(shape.stroke(Colors.black01.opacity(0.2), lineWidth: 0.5))
108
+ } else {
109
+ content
110
+ .background(
111
+ shape.fill(Colors.black01.opacity(0.85))
112
+ .shadow(color: Color.black.opacity(0.12), radius: 4, x: 0, y: 2)
113
+ )
114
+ .overlay(shape.stroke(Colors.black20.opacity(0.08), lineWidth: 0.5))
115
+ }
116
+ }
117
+ }
118
+
119
+ /// 48pt glass circle for a single side bar control.
120
+ struct SideBarButton: View {
121
+ let icon: String
122
+ let headerColor: HeaderColor
123
+ var showBadge: Bool = false
124
+ var disabled: Bool = false
125
+ var accessibilityId: String?
126
+ let action: () -> Void
127
+
128
+ var body: some View {
129
+ SwiftUI.Button(action: action) {
130
+ Icon(source: icon, size: sideBarIconSize, color: headerColor.tintIconColor)
131
+ .frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
132
+ .modifier(SideBarGlass(headerColor: headerColor, shape: Circle()))
133
+ .overlay(alignment: .topTrailing) {
134
+ if showBadge {
135
+ BadgeDot(size: .small).offset(x: -Spacing.XXS, y: Spacing.XXS)
136
+ }
137
+ }
138
+ }
139
+ .buttonStyle(ActiveOpacityButtonStyle())
140
+ .disabled(disabled)
141
+ .accessibilityIdentifier(accessibilityId ?? "")
142
+ }
143
+ }
144
+
145
+ /// 48pt wide glass capsule grouping related controls, one item per 50pt.
146
+ struct SideBarGroup<Content: View>: View {
147
+ let headerColor: HeaderColor
148
+ let itemCount: Int
149
+ @ViewBuilder let content: () -> Content
150
+
151
+ var body: some View {
152
+ VStack(spacing: 0) { content() }
153
+ .frame(
154
+ width: DuoMetrics.controlSize,
155
+ height: itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
156
+ )
157
+ .modifier(SideBarGlass(headerColor: headerColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
158
+ }
159
+ }
160
+
161
+ struct SideBarGroupItem: View {
162
+ let icon: String
163
+ let headerColor: HeaderColor
164
+ let accessibilityId: String
165
+ let action: () -> Void
166
+
167
+ var body: some View {
168
+ SwiftUI.Button(action: action) {
169
+ Icon(source: icon, size: sideBarIconSize, color: headerColor.tintIconColor)
170
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
171
+ .contentShape(Rectangle())
172
+ }
173
+ .buttonStyle(ActiveOpacityButtonStyle())
174
+ .accessibilityIdentifier(accessibilityId)
175
+ }
176
+ }
@@ -2,9 +2,8 @@
2
2
  // SnackBar.swift
3
3
  // MoMoUIKits
4
4
  //
5
- // SwiftUI mirror of Compose's `SnackBar` (navigation/component/SnackBar.kt).
6
- // A snack bar slides up from the bottom (just above the footer / home
7
- // indicator), stays for `duration`, then slides back down and is removed.
5
+ // SwiftUI mirror of Compose's `navigation/component/SnackBar.kt`: slides up above the screen's
6
+ // footer (or the home indicator), stays for `duration`, then slides back down.
8
7
  // `Navigator.showSnackBar(_:)` drives it through the overplay host.
9
8
  //
10
9
 
@@ -53,8 +52,8 @@ public struct SnackBar {
53
52
  @available(iOS 16.0, *)
54
53
  struct SnackBarView: View {
55
54
  let snackBar: SnackBar
56
- /// Bottom inset so the bar sits above the footer / home indicator
57
- /// (mirrors Compose's `footerHeight` offset).
55
+ /// Distance from the window bottom to the top of the screen's footer, or the navigation bar
56
+ /// inset without one (Compose's `footerHeight` offset).
58
57
  let bottomInset: CGFloat
59
58
 
60
59
  @EnvironmentObject private var navigator: Navigator
@@ -78,6 +77,7 @@ struct SnackBarView: View {
78
77
  .padding(.bottom, bottomInset)
79
78
  }
80
79
  .frame(maxWidth: .infinity, maxHeight: .infinity)
80
+ .ignoresSafeArea()
81
81
  .onAppear {
82
82
  guard !didAppear else { return }
83
83
  didAppear = true
@@ -0,0 +1,57 @@
1
+ //
2
+ // HostBackGestureBridge.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/HostBackGestureBridge.kt`: the host owns the edge-pan
6
+ // recogniser and routes a back into the topmost kit stack. NavigationContainer attaches itself.
7
+ // The kit stack is a NavigationStack, so there is no seekable transition: the host only pops.
8
+ //
9
+
10
+ import Foundation
11
+
12
+ @available(iOS 16.0, *)
13
+ public final class HostBackGestureBridge {
14
+ public static let shared = HostBackGestureBridge()
15
+ private init() {}
16
+
17
+ // Weak: a container torn down while its cover is up never reports its disappearance.
18
+ private var navigators: [WeakNavigator] = []
19
+
20
+ /// Set while a bottom tab other than the first is selected; the host shows its floating arrow.
21
+ public var shouldUseFloatingArrowBack = false
22
+
23
+ public func attach(_ navigator: Navigator) {
24
+ navigators.removeAll { $0.value == nil }
25
+ guard !navigators.contains(where: { $0.value === navigator }) else { return }
26
+ navigators.append(WeakNavigator(value: navigator))
27
+ }
28
+
29
+ public func detach(_ navigator: Navigator) {
30
+ navigators.removeAll { $0.value == nil || $0.value === navigator }
31
+ }
32
+
33
+ /// Whether a kit stack is on screen at all.
34
+ public func checkCanPop() -> Bool { top() != nil }
35
+
36
+ /// Whether the top stack pops a screen itself; false means a back dismisses the mini app.
37
+ public func canPopInner() -> Bool { top()?.canPop() == true }
38
+
39
+ public func pop() { top()?.onBackSafe() }
40
+
41
+ @available(*, deprecated, renamed: "attach")
42
+ public func push(_ navigator: Navigator) { attach(navigator) }
43
+
44
+ @available(*, deprecated, renamed: "detach")
45
+ public func remove(_ navigator: Navigator) { detach(navigator) }
46
+
47
+ private func top() -> Navigator? { navigators.last { $0.value != nil }?.value }
48
+ }
49
+
50
+ @available(iOS 16.0, *)
51
+ @available(*, deprecated, renamed: "HostBackGestureBridge")
52
+ public typealias NavigatorManager = HostBackGestureBridge
53
+
54
+ @available(iOS 16.0, *)
55
+ private struct WeakNavigator {
56
+ weak var value: Navigator?
57
+ }
@@ -16,6 +16,7 @@ struct ModalScreen<Content: View>: View {
16
16
 
17
17
  @EnvironmentObject private var navigator: Navigator
18
18
  @Environment(\.applicationEnvironment) private var appEnv
19
+ @Environment(\.screenLayout) private var layout
19
20
 
20
21
  @State private var scrimOpacity: CGFloat = 0
21
22
  @State private var contentScale: CGFloat = 0.8
@@ -35,8 +36,11 @@ struct ModalScreen<Content: View>: View {
35
36
  content()
36
37
  .scaleEffect(contentScale)
37
38
  .opacity(contentOpacity)
39
+ // iPhone Duo: center below the side status bar and above the home indicator
40
+ .padding(.top, layout.isDuoDevice ? layout.duoTopInset : 0)
41
+ .padding(.bottom, layout.isDuoDevice ? layout.insets.bottom : 0)
38
42
  }
39
- // Center on the full screen like Compose; landscape safe areas are asymmetric (camera on one side).
43
+ // Full screen like RN's Modal, so asymmetric landscape insets don't shift the content sideways.
40
44
  .frame(maxWidth: .infinity, maxHeight: .infinity)
41
45
  .ignoresSafeArea(.container)
42
46
  .onAppear {
@@ -0,0 +1,190 @@
1
+ //
2
+ // Navigation.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/Navigation.kt`: a screen's `NavigationOptions` and the
6
+ // per-screen `Navigation` that updates them at runtime.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ /// Per-screen handle (Compose `LocalNavigation`). Read it inside a screen with
12
+ /// `@EnvironmentObject var navigation: Navigation` and call `setOptions` to change the header,
13
+ /// footer or back handling after the screen is shown.
14
+ public final class Navigation: ObservableObject {
15
+ public let id: Int
16
+ public let bottomTabIndex: Int
17
+ @Published public private(set) var options: NavigationOptions
18
+
19
+ private weak var registry: DynamicScreenRegistry?
20
+ /// What the footer (or navigation bar) takes from the window bottom; lifts snack bars.
21
+ var bottomObstruction: CGFloat = 0
22
+
23
+ init(
24
+ id: Int = -1,
25
+ bottomTabIndex: Int = -1,
26
+ options: NavigationOptions?,
27
+ registry: DynamicScreenRegistry? = nil
28
+ ) {
29
+ self.id = id
30
+ self.bottomTabIndex = bottomTabIndex
31
+ self.options = options ?? NavigationOptions()
32
+ self.registry = registry
33
+ if id != -1 { registry?.bind(self) }
34
+ }
35
+
36
+ public func setOptions(_ options: NavigationOptions) {
37
+ update(options)
38
+ }
39
+
40
+ /// Changes only what the closure touches, like Compose's named-argument `setOptions`.
41
+ public func setOptions(_ transform: (inout NavigationOptions) -> Void) {
42
+ var updated = options
43
+ transform(&updated)
44
+ update(updated)
45
+ }
46
+
47
+ private func update(_ updated: NavigationOptions) {
48
+ options = updated
49
+ // back handling reads the registry copy
50
+ if id != -1 { registry?.setOptions(id: id, options: updated) }
51
+ }
52
+ }
53
+
54
+ public struct NavigationOptions {
55
+ public var screenName: String?
56
+ public var title: String
57
+ /// Rich header title (Compose `headerTitle`); `title` is used when nil.
58
+ public var headerTitle: HeaderTitle?
59
+ public var titlePosition: TitlePosition
60
+ public var headerType: HeaderType
61
+ /// Compose's `HeaderType.Default/Extended(useAnimated:)`: with `inputSearchProps`, the search
62
+ /// rises into the header on scroll.
63
+ public var useAnimated: Bool
64
+ /// Compose's `HeaderType.Transparent`; with `fullScreenContent` the content starts under it.
65
+ public var headerTransparent: Bool
66
+ public var fullScreenContent: Bool
67
+ public var hiddenBack: Bool
68
+ public var backgroundColor: Color?
69
+ public var tintColor: Color?
70
+ public var scrollable: Bool
71
+ /// Compose's `ScrollData.scrollToTopCallback`, fired on a status-bar tap.
72
+ public var scrollToTopCallback: (() -> Void)?
73
+ public var useAvoidKeyboard: Bool
74
+ /// Compose's `KeyboardOptions.keyboardShouldPersistTaps`: a tap anywhere hides the keyboard.
75
+ public var keyboardShouldPersistTaps: Bool
76
+ public var headerBackProps: HeaderBackProps?
77
+ /// Toolkit data for the default header right (Compose `HeaderRight.Toolkit`). Use `headerRight`
78
+ /// for a custom view and nil for none.
79
+ public var headerRightData: HeaderRightData?
80
+ public var headerRight: (() -> any View)?
81
+ public var footer: (() -> AnyView)?
82
+ public var footerTransparent: Bool
83
+ public var footerUseMargin: Bool
84
+ public var animatedHeader: AnimatedHeader?
85
+ public var layoutOffset: CGFloat
86
+ public var inputSearchProps: InputSearchProps?
87
+ public var fabProps: FabProps?
88
+ public var onBackHandler: (() -> Void)?
89
+
90
+ public init(
91
+ screenName: String? = nil,
92
+ title: String = "Stack",
93
+ headerTitle: HeaderTitle? = nil,
94
+ titlePosition: TitlePosition = .left,
95
+ headerType: HeaderType = .default,
96
+ useAnimated: Bool = false,
97
+ headerTransparent: Bool = false,
98
+ fullScreenContent: Bool = false,
99
+ hiddenBack: Bool = false,
100
+ backgroundColor: Color? = nil,
101
+ tintColor: Color? = Colors.black17,
102
+ scrollable: Bool = true,
103
+ scrollToTopCallback: (() -> Void)? = nil,
104
+ useAvoidKeyboard: Bool = true,
105
+ keyboardShouldPersistTaps: Bool = false,
106
+ headerBackProps: HeaderBackProps? = nil,
107
+ headerRightData: HeaderRightData? = nil,
108
+ headerRight: (() -> any View)? = { HeaderRight() },
109
+ footer: (() -> AnyView)? = nil,
110
+ footerTransparent: Bool = false,
111
+ footerUseMargin: Bool = true,
112
+ animatedHeader: AnimatedHeader? = nil,
113
+ layoutOffset: CGFloat = 56,
114
+ inputSearchProps: InputSearchProps? = nil,
115
+ fabProps: FabProps? = nil,
116
+ onBackHandler: (() -> Void)? = nil
117
+ ) {
118
+ self.screenName = screenName
119
+ self.title = title
120
+ self.headerTitle = headerTitle
121
+ self.titlePosition = titlePosition
122
+ self.headerType = headerType
123
+ self.useAnimated = useAnimated
124
+ self.headerTransparent = headerTransparent
125
+ self.fullScreenContent = fullScreenContent
126
+ self.hiddenBack = hiddenBack
127
+ self.backgroundColor = backgroundColor
128
+ self.tintColor = tintColor
129
+ self.scrollable = scrollable
130
+ self.scrollToTopCallback = scrollToTopCallback
131
+ self.useAvoidKeyboard = useAvoidKeyboard
132
+ self.keyboardShouldPersistTaps = keyboardShouldPersistTaps
133
+ self.headerBackProps = headerBackProps
134
+ self.headerRightData = headerRightData
135
+ self.headerRight = headerRight
136
+ self.footer = footer
137
+ self.footerTransparent = footerTransparent
138
+ self.footerUseMargin = footerUseMargin
139
+ self.animatedHeader = animatedHeader
140
+ self.layoutOffset = layoutOffset
141
+ self.inputSearchProps = inputSearchProps
142
+ self.fabProps = fabProps
143
+ self.onBackHandler = onBackHandler
144
+ }
145
+ }
146
+
147
+ extension NavigationOptions {
148
+ var resolvedHeaderRight: (() -> any View)? {
149
+ guard let headerRightData else { return headerRight }
150
+ return { HeaderRight(headerRight: headerRightData, tintColor: tintColor) }
151
+ }
152
+ }
153
+
154
+ /// `StackScreen` built from options, shared by stack screens and bottom-tab screens.
155
+ struct OptionsStackScreen: View {
156
+ let options: NavigationOptions
157
+ let screenName: String
158
+ let goBack: (() -> Void)?
159
+ let content: () -> AnyView
160
+
161
+ var body: some View {
162
+ StackScreen(
163
+ backgroundColor: options.backgroundColor,
164
+ headerTransparent: options.headerTransparent,
165
+ fullScreenContent: options.fullScreenContent,
166
+ tintColor: options.tintColor ?? Colors.black17,
167
+ isBack: !options.hiddenBack,
168
+ headerType: options.headerType,
169
+ useAnimated: options.useAnimated,
170
+ title: options.title,
171
+ headerTitle: options.headerTitle,
172
+ titlePosition: options.titlePosition,
173
+ goBack: options.hiddenBack ? nil : goBack,
174
+ scrollable: options.scrollable,
175
+ scrollToTopCallback: options.scrollToTopCallback,
176
+ useAvoidKeyboard: options.useAvoidKeyboard,
177
+ keyboardShouldPersistTaps: options.keyboardShouldPersistTaps,
178
+ footer: options.footer,
179
+ footerTransparent: options.footerTransparent,
180
+ footerUseMargin: options.footerUseMargin,
181
+ headerRight: options.resolvedHeaderRight,
182
+ animatedHeader: options.animatedHeader,
183
+ layoutOffset: options.layoutOffset,
184
+ inputSearchProps: options.inputSearchProps,
185
+ fabProps: options.fabProps,
186
+ screenName: screenName,
187
+ content: content
188
+ )
189
+ }
190
+ }