@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 (194) hide show
  1. package/CLAUDE.md +36 -8
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +15 -1
  4. package/compose/build.gradle.kts.backup +14 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
  8. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  57. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
  58. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
  59. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  60. package/gradle.properties +1 -1
  61. package/ios/Application/ApplicationEnvironment.swift +17 -0
  62. package/ios/Application/MaxApi.swift +12 -3
  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/Colors+Radius+Spacing/Radius.swift +6 -0
  75. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  76. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  77. package/ios/Divider/Divider.swift +37 -4
  78. package/ios/Extensions/Color++.swift +38 -9
  79. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  80. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  81. package/ios/Icon/Icon.swift +21 -23
  82. package/ios/Icon/Icons.swift +1329 -0
  83. package/ios/IconButton/IconButton.swift +15 -7
  84. package/ios/Image/Image.swift +7 -23
  85. package/ios/Information/Information.swift +27 -9
  86. package/ios/Input/ErrorView.swift +2 -2
  87. package/ios/Input/Input.swift +160 -147
  88. package/ios/Input/InputPhoneNumber.swift +12 -9
  89. package/ios/Input/InputSearch.swift +102 -75
  90. package/ios/Input/InputTextArea.swift +128 -145
  91. package/ios/InputDropDown/InputDropDown.swift +10 -7
  92. package/ios/InputMoney/InputMoney.swift +33 -11
  93. package/ios/InputOTP/InputOTP.swift +22 -12
  94. package/ios/Layout/Card.swift +50 -0
  95. package/ios/Layout/GridContext.swift +187 -3
  96. package/ios/Layout/Item.swift +5 -4
  97. package/ios/Layout/ScreenLayout.swift +380 -0
  98. package/ios/Layout/Section.swift +4 -100
  99. package/ios/Loader/Loader.swift +5 -9
  100. package/ios/Lottie/LottieView.swift +14 -2
  101. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
  102. package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
  103. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
  104. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  105. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  106. package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
  107. package/ios/Navigation/Component/Header.swift +531 -0
  108. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  109. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  110. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  111. package/ios/Navigation/Component/HeaderRight.swift +377 -0
  112. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  113. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  114. package/ios/Navigation/Component/SideBar.swift +183 -0
  115. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
  116. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  117. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
  118. package/ios/Navigation/Navigation.swift +204 -0
  119. package/ios/Navigation/NavigationContainer.swift +194 -0
  120. package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
  121. package/ios/Navigation/StackScreen.swift +820 -0
  122. package/ios/Navigation/StackTransition.swift +400 -0
  123. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  124. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  125. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  126. package/ios/Pagination/PaginationScroll.swift +2 -2
  127. package/ios/Popup/PopupDisplay.swift +72 -136
  128. package/ios/Popup/PopupInput.swift +10 -13
  129. package/ios/Popup/PopupLayout.swift +140 -0
  130. package/ios/Popup/PopupNotify.swift +56 -73
  131. package/ios/Popup/PopupPromotion.swift +31 -47
  132. package/ios/PopupView/FullscreenPopup.swift +13 -19
  133. package/ios/PopupView/PopupView.swift +1 -1
  134. package/ios/PopupView/Utils++.swift +0 -8
  135. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  136. package/ios/Radio/Radio.swift +2 -1
  137. package/ios/Rating/Rating.swift +9 -1
  138. package/ios/Resources/dot_loading.json +558 -0
  139. package/ios/Resources/lottie_circle_loader.json +1 -0
  140. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  141. package/ios/Skeleton/Skeleton.swift +22 -27
  142. package/ios/Slider/Slider.swift +16 -18
  143. package/ios/Stepper/Stepper.swift +6 -1
  144. package/ios/Steps/Steps.swift +8 -6
  145. package/ios/SuggestAction/SuggestAction.swift +5 -0
  146. package/ios/Swipeable/SwipeCell.swift +10 -6
  147. package/ios/Switch/Switch.swift +22 -20
  148. package/ios/TabView/TabView.swift +102 -21
  149. package/ios/Tag/Tag.swift +15 -4
  150. package/ios/Template/Logo/Logo.swift +2 -1
  151. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  152. package/ios/Title/Title.swift +6 -17
  153. package/ios/Tooltip/Tooltip.swift +29 -14
  154. package/ios/Typography/Text.swift +61 -19
  155. package/ios/Uploader/Uploader.swift +3 -1
  156. package/ios/native-kits.podspec +2 -1
  157. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  158. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  159. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  160. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
  161. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  162. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  163. package/package.json +1 -1
  164. package/scripts/gen-ios-icons.mjs +36 -0
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  175. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  176. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  177. package/ios/Application/Components.swift +0 -270
  178. package/ios/Application/ComposeApi.swift +0 -22
  179. package/ios/Application/HeaderRight.swift +0 -306
  180. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  181. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  182. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  183. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  184. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  185. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  186. package/ios/Application/Navigation/component/Header.swift +0 -178
  187. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  188. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  189. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  190. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  191. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  192. package/ios/Application/Screen.swift +0 -250
  193. package/ios/Application/StackScreen.swift +0 -492
  194. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -0,0 +1,183 @@
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
+ layout: .sideBarColumn
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 glass capsule grouping related controls, one item per 50pt: vertical in the side bar,
146
+ /// horizontal on a header row.
147
+ struct SideBarGroup<Content: View>: View {
148
+ let headerColor: HeaderColor
149
+ let itemCount: Int
150
+ var horizontal: Bool = false
151
+ @ViewBuilder let content: () -> Content
152
+
153
+ var body: some View {
154
+ let length = itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
155
+ Group {
156
+ if horizontal {
157
+ HStack(spacing: 0) { content() }
158
+ .frame(width: length, height: DuoMetrics.controlSize)
159
+ } else {
160
+ VStack(spacing: 0) { content() }
161
+ .frame(width: DuoMetrics.controlSize, height: length)
162
+ }
163
+ }
164
+ .modifier(SideBarGlass(headerColor: headerColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
165
+ }
166
+ }
167
+
168
+ struct SideBarGroupItem: View {
169
+ let icon: String
170
+ let headerColor: HeaderColor
171
+ let accessibilityId: String
172
+ let action: () -> Void
173
+
174
+ var body: some View {
175
+ SwiftUI.Button(action: action) {
176
+ Icon(source: icon, size: sideBarIconSize, color: headerColor.tintIconColor)
177
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
178
+ .contentShape(Rectangle())
179
+ }
180
+ .buttonStyle(ActiveOpacityButtonStyle())
181
+ .accessibilityIdentifier(accessibilityId)
182
+ }
183
+ }
@@ -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
 
@@ -50,11 +49,10 @@ public struct SnackBar {
50
49
 
51
50
  /// Self-animating container. Slides the content up on appear, schedules an
52
51
  /// auto-dismiss after `duration`, and animates out before removing the overplay.
53
- @available(iOS 16.0, *)
54
52
  struct SnackBarView: View {
55
53
  let snackBar: SnackBar
56
- /// Bottom inset so the bar sits above the footer / home indicator
57
- /// (mirrors Compose's `footerHeight` offset).
54
+ /// Distance from the window bottom to the top of the screen's footer, or the navigation bar
55
+ /// inset without one (Compose's `footerHeight` offset).
58
56
  let bottomInset: CGFloat
59
57
 
60
58
  @EnvironmentObject private var navigator: Navigator
@@ -78,6 +76,7 @@ struct SnackBarView: View {
78
76
  .padding(.bottom, bottomInset)
79
77
  }
80
78
  .frame(maxWidth: .infinity, maxHeight: .infinity)
79
+ .ignoresSafeArea()
81
80
  .onAppear {
82
81
  guard !didAppear else { return }
83
82
  didAppear = true
@@ -0,0 +1,78 @@
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
+ // `started` / `progressed` / `completed` / `cancelled` scrub the pop transition with the finger;
8
+ // a host that only calls `pop` gets the regular animated back.
9
+ //
10
+
11
+ import UIKit
12
+
13
+ public final class HostBackGestureBridge {
14
+ public static let shared = HostBackGestureBridge()
15
+ private init() {}
16
+
17
+ // Weak: a container torn down without disappearing must not keep receiving backs.
18
+ private var navigators: [WeakNavigator] = []
19
+ private weak var gestureTarget: Navigator?
20
+
21
+ /// Set while a bottom tab other than the first is selected; the host shows its floating arrow.
22
+ public var shouldUseFloatingArrowBack = false
23
+
24
+ public func attach(_ navigator: Navigator) {
25
+ navigators.removeAll { $0.value == nil }
26
+ guard !navigators.contains(where: { $0.value === navigator }) else { return }
27
+ navigators.append(WeakNavigator(value: navigator))
28
+ }
29
+
30
+ public func detach(_ navigator: Navigator) {
31
+ navigators.removeAll { $0.value == nil || $0.value === navigator }
32
+ if gestureTarget === navigator { gestureTarget = nil }
33
+ }
34
+
35
+ /// Whether a kit stack is on screen at all.
36
+ public func checkCanPop() -> Bool { top() != nil }
37
+
38
+ /// Whether the top stack pops a screen itself; false means a back dismisses the mini app.
39
+ public func canPopInner() -> Bool { top()?.canPop() == true }
40
+
41
+ public func pop() { top()?.onBackSafe() }
42
+
43
+ public func started() {
44
+ guard let target = top() else { return }
45
+ gestureTarget = target
46
+ target.backStarted()
47
+ }
48
+
49
+ /// `progress` is the touch travel over the window width, 0...1.
50
+ public func progressed(_ progress: CGFloat) {
51
+ gestureTarget?.backProgressed(min(max(progress, 0), 1))
52
+ }
53
+
54
+ public func completed() {
55
+ (gestureTarget ?? top())?.backCompleted()
56
+ gestureTarget = nil
57
+ }
58
+
59
+ public func cancelled() {
60
+ gestureTarget?.backCancelled()
61
+ gestureTarget = nil
62
+ }
63
+
64
+ @available(*, deprecated, renamed: "attach")
65
+ public func push(_ navigator: Navigator) { attach(navigator) }
66
+
67
+ @available(*, deprecated, renamed: "detach")
68
+ public func remove(_ navigator: Navigator) { detach(navigator) }
69
+
70
+ private func top() -> Navigator? { navigators.last { $0.value != nil }?.value }
71
+ }
72
+
73
+ @available(*, deprecated, renamed: "HostBackGestureBridge")
74
+ public typealias NavigatorManager = HostBackGestureBridge
75
+
76
+ private struct WeakNavigator {
77
+ weak var value: Navigator?
78
+ }
@@ -9,13 +9,13 @@
9
9
 
10
10
  import SwiftUI
11
11
 
12
- @available(iOS 16.0, *)
13
12
  struct ModalScreen<Content: View>: View {
14
13
  let barrierDismissible: Bool
15
14
  let content: () -> Content
16
15
 
17
16
  @EnvironmentObject private var navigator: Navigator
18
17
  @Environment(\.applicationEnvironment) private var appEnv
18
+ @Environment(\.screenLayout) private var layout
19
19
 
20
20
  @State private var scrimOpacity: CGFloat = 0
21
21
  @State private var contentScale: CGFloat = 0.8
@@ -35,8 +35,11 @@ struct ModalScreen<Content: View>: View {
35
35
  content()
36
36
  .scaleEffect(contentScale)
37
37
  .opacity(contentOpacity)
38
+ // iPhone Duo: center below the side status bar and above the home indicator
39
+ .padding(.top, layout.isDuoDevice ? layout.duoTopInset : 0)
40
+ .padding(.bottom, layout.isDuoDevice ? layout.insets.bottom : 0)
38
41
  }
39
- // Center on the full screen like Compose; landscape safe areas are asymmetric (camera on one side).
42
+ // Full screen like RN's Modal, so asymmetric landscape insets don't shift the content sideways.
40
43
  .frame(maxWidth: .infinity, maxHeight: .infinity)
41
44
  .ignoresSafeArea(.container)
42
45
  .onAppear {
@@ -0,0 +1,204 @@
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
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
70
+ /// Drawn instead of `backgroundColor` over the whole window: under the header and the status bar,
71
+ /// across the iPhone Duo side strips, and not shrunk by the keyboard. Pair with `headerTransparent`.
72
+ public var background: (() -> AnyView)?
73
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
74
+ public var tintColor: Color?
75
+ public var scrollable: Bool
76
+ /// Compose's `ScrollData.scrollToTopCallback`, fired on a status-bar tap.
77
+ public var scrollToTopCallback: (() -> Void)?
78
+ public var useAvoidKeyboard: Bool
79
+ /// Compose's `KeyboardOptions.keyboardShouldPersistTaps`: a tap anywhere hides the keyboard.
80
+ public var keyboardShouldPersistTaps: Bool
81
+ public var headerBackProps: HeaderBackProps?
82
+ /// Toolkit data for the default header right (Compose `HeaderRight.Toolkit`). Use `headerRight`
83
+ /// for a custom view and nil for none.
84
+ public var headerRightData: HeaderRightData?
85
+ public var headerRight: (() -> any View)?
86
+ public var footer: (() -> AnyView)?
87
+ public var footerTransparent: Bool
88
+ public var footerUseMargin: Bool
89
+ public var animatedHeader: AnimatedHeader?
90
+ public var layoutOffset: CGFloat
91
+ public var inputSearchProps: InputSearchProps?
92
+ public var fabProps: FabProps?
93
+ public var onBackHandler: (() -> Void)?
94
+
95
+ public init(
96
+ screenName: String? = nil,
97
+ title: String = "Stack",
98
+ headerTitle: HeaderTitle? = nil,
99
+ titlePosition: TitlePosition = .left,
100
+ headerType: HeaderType = .default,
101
+ useAnimated: Bool = false,
102
+ headerTransparent: Bool = false,
103
+ fullScreenContent: Bool = false,
104
+ hiddenBack: Bool = false,
105
+ backgroundColor: Color? = nil,
106
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
107
+ background: (() -> AnyView)? = nil,
108
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
109
+ tintColor: Color? = Colors.black17,
110
+ scrollable: Bool = true,
111
+ scrollToTopCallback: (() -> Void)? = nil,
112
+ useAvoidKeyboard: Bool = true,
113
+ keyboardShouldPersistTaps: Bool = false,
114
+ headerBackProps: HeaderBackProps? = nil,
115
+ headerRightData: HeaderRightData? = nil,
116
+ headerRight: (() -> any View)? = { HeaderRight() },
117
+ footer: (() -> AnyView)? = nil,
118
+ footerTransparent: Bool = false,
119
+ footerUseMargin: Bool = true,
120
+ animatedHeader: AnimatedHeader? = nil,
121
+ layoutOffset: CGFloat = 56,
122
+ inputSearchProps: InputSearchProps? = nil,
123
+ fabProps: FabProps? = nil,
124
+ onBackHandler: (() -> Void)? = nil
125
+ ) {
126
+ self.screenName = screenName
127
+ self.title = title
128
+ self.headerTitle = headerTitle
129
+ self.titlePosition = titlePosition
130
+ self.headerType = headerType
131
+ self.useAnimated = useAnimated
132
+ self.headerTransparent = headerTransparent
133
+ self.fullScreenContent = fullScreenContent
134
+ self.hiddenBack = hiddenBack
135
+ self.backgroundColor = backgroundColor
136
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
137
+ self.background = background
138
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
139
+ self.tintColor = tintColor
140
+ self.scrollable = scrollable
141
+ self.scrollToTopCallback = scrollToTopCallback
142
+ self.useAvoidKeyboard = useAvoidKeyboard
143
+ self.keyboardShouldPersistTaps = keyboardShouldPersistTaps
144
+ self.headerBackProps = headerBackProps
145
+ self.headerRightData = headerRightData
146
+ self.headerRight = headerRight
147
+ self.footer = footer
148
+ self.footerTransparent = footerTransparent
149
+ self.footerUseMargin = footerUseMargin
150
+ self.animatedHeader = animatedHeader
151
+ self.layoutOffset = layoutOffset
152
+ self.inputSearchProps = inputSearchProps
153
+ self.fabProps = fabProps
154
+ self.onBackHandler = onBackHandler
155
+ }
156
+ }
157
+
158
+ extension NavigationOptions {
159
+ var resolvedHeaderRight: (() -> any View)? {
160
+ guard let headerRightData else { return headerRight }
161
+ return { HeaderRight(headerRight: headerRightData, tintColor: tintColor) }
162
+ }
163
+ }
164
+
165
+ /// `StackScreen` built from options, shared by stack screens and bottom-tab screens.
166
+ struct OptionsStackScreen: View {
167
+ let options: NavigationOptions
168
+ let screenName: String
169
+ let goBack: (() -> Void)?
170
+ let content: () -> AnyView
171
+
172
+ var body: some View {
173
+ StackScreen(
174
+ backgroundColor: options.backgroundColor,
175
+ // AI-GENERATED START: custom screen background view (Compose backgroundComponent)
176
+ background: options.background,
177
+ // AI-GENERATED END: custom screen background view (Compose backgroundComponent)
178
+ headerTransparent: options.headerTransparent,
179
+ fullScreenContent: options.fullScreenContent,
180
+ tintColor: options.tintColor ?? Colors.black17,
181
+ isBack: !options.hiddenBack,
182
+ headerType: options.headerType,
183
+ useAnimated: options.useAnimated,
184
+ title: options.title,
185
+ headerTitle: options.headerTitle,
186
+ titlePosition: options.titlePosition,
187
+ goBack: options.hiddenBack ? nil : goBack,
188
+ scrollable: options.scrollable,
189
+ scrollToTopCallback: options.scrollToTopCallback,
190
+ useAvoidKeyboard: options.useAvoidKeyboard,
191
+ keyboardShouldPersistTaps: options.keyboardShouldPersistTaps,
192
+ footer: options.footer,
193
+ footerTransparent: options.footerTransparent,
194
+ footerUseMargin: options.footerUseMargin,
195
+ headerRight: options.resolvedHeaderRight,
196
+ animatedHeader: options.animatedHeader,
197
+ layoutOffset: options.layoutOffset,
198
+ inputSearchProps: options.inputSearchProps,
199
+ fabProps: options.fabProps,
200
+ screenName: screenName,
201
+ content: content
202
+ )
203
+ }
204
+ }