@momo-kits/native-kits 0.167.1-beta.9-debug → 0.167.1-duo-search.1

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 (189) hide show
  1. package/CLAUDE.md +36 -0
  2. package/ios/Application/ApplicationEnvironment.swift +6 -0
  3. package/ios/Application/Components.swift +1 -3
  4. package/ios/Application/HeaderRight.swift +32 -14
  5. package/ios/Application/Layout/ScreenLayout.swift +312 -0
  6. package/ios/Application/Navigation/BottomTab/BottomTab.swift +67 -10
  7. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
  8. package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  9. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +304 -0
  10. package/ios/Application/Navigation/HeaderUser.swift +4 -4
  11. package/ios/Application/Navigation/NavigationContainer.swift +48 -42
  12. package/ios/Application/Navigation/Navigator.swift +122 -29
  13. package/ios/Application/Navigation/NavigatorManager.swift +13 -6
  14. package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
  15. package/ios/Application/Navigation/Overplay/ModalScreen.swift +7 -0
  16. package/ios/Application/Navigation/component/DuoScrollGradient.swift +54 -0
  17. package/ios/Application/Navigation/component/Header.swift +11 -6
  18. package/ios/Application/Navigation/component/HeaderBackground.swift +3 -2
  19. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  20. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +93 -9
  21. package/ios/Application/Navigation/component/SideBar.swift +155 -0
  22. package/ios/Application/StackScreen.swift +175 -18
  23. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
  24. package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
  25. package/ios/Extensions/Color++.swift +20 -9
  26. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  27. package/ios/Icon/Icon.swift +14 -22
  28. package/ios/Icon/Icons.swift +1329 -0
  29. package/ios/Image/Image.swift +16 -5
  30. package/ios/Lottie/LottieView.swift +4 -2
  31. package/ios/Popup/PopupDisplay.swift +84 -67
  32. package/ios/Popup/PopupLayout.swift +45 -0
  33. package/ios/Popup/PopupNotify.swift +44 -26
  34. package/ios/Popup/PopupPromotion.swift +18 -6
  35. package/ios/Resources/dot_loading.json +558 -0
  36. package/ios/Resources/lottie_circle_loader.json +1 -0
  37. package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
  38. package/ios/Typography/FontScale.swift +3 -3
  39. package/ios/native-kits.podspec +1 -1
  40. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  41. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  42. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  43. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  44. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  45. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  46. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  47. package/package.json +1 -1
  48. package/scripts/gen-ios-icons.mjs +36 -0
  49. package/build.gradle.kts +0 -11
  50. package/compose/build.gradle.kts +0 -190
  51. package/compose/build.gradle.kts.backup +0 -190
  52. package/compose/compose.podspec +0 -47
  53. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
  54. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
  55. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
  56. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
  57. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -151
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -831
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -82
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -356
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -76
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -475
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -266
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -264
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -328
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -97
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -323
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -199
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -498
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -234
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -16
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -14
  130. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
  131. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
  132. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
  133. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -221
  134. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -270
  135. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -52
  136. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -38
  137. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -39
  138. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
  139. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
  140. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
  141. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
  142. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
  143. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
  144. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -299
  145. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
  146. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -129
  147. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
  148. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -201
  149. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
  150. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
  151. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -595
  152. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
  153. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
  154. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -175
  155. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -225
  156. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
  157. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
  158. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -358
  159. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -83
  160. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
  161. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -75
  162. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -309
  163. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -40
  164. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -364
  165. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
  166. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
  167. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
  168. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
  169. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
  170. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -71
  171. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
  172. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
  173. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
  174. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -105
  175. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
  176. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
  177. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
  178. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
  179. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
  180. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -213
  181. package/gradle/libs.versions.toml +0 -69
  182. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  183. package/gradle/wrapper/gradle-wrapper.properties +0 -8
  184. package/gradle.properties +0 -26
  185. package/gradlew +0 -252
  186. package/gradlew.bat +0 -94
  187. package/ios-demo/README.md +0 -120
  188. package/local.properties +0 -8
  189. package/settings.gradle.kts +0 -64
@@ -0,0 +1,105 @@
1
+ //
2
+ // DuoBarCompression.swift
3
+ // MoMoUIKits
4
+ //
5
+ // Port of RN `BottomTab/DuoBarCompression.tsx` (HIG, Designing for iPhone Duo: Vertical controls):
6
+ // keeps the side bar's toolbar and tab bar clear of each other.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ /// iPhone Duo: which side bar bar starts compressed when the strip cannot hold both whole.
12
+ public enum DuoBarCompressionBehavior {
13
+ case toolbar, tabBar
14
+ }
15
+
16
+ enum DuoBar {
17
+ case toolbar, tabBar
18
+ }
19
+
20
+ struct DuoBarCompression {
21
+ var toolbarCompressed = false
22
+ var tabBarCompressed = false
23
+ /// The open rail needs the toolbar button slot too: only Back stays.
24
+ var toolbarHidden = false
25
+ var expandToolbar: () -> Void = {}
26
+ var expandTabBar: () -> Void = {}
27
+ /// A rail opened from the compressed state closes once a tab is picked.
28
+ var onTabSelected: () -> Void = {}
29
+ /// Side bar toolkits report their full height so the tab bar knows what is left.
30
+ var setToolbarHeight: (CGFloat) -> Void = { _ in }
31
+
32
+ /// Space kept between the top and the bottom groups (HIG Vertical controls).
33
+ static let groupGap = Spacing.M
34
+ /// Back, then the right toolkit: help + home capsule.
35
+ static let defaultToolbarHeight = DuoMetrics.controlSize + Spacing.M + 2 * DuoMetrics.itemPitch
36
+ /// Back, then the single button the toolbar folds into.
37
+ private static let compressedToolbarHeight = 2 * DuoMetrics.controlSize + Spacing.M
38
+ /// Back alone, while a rail opened from the compressed state takes the toolbar slot.
39
+ private static let backOnlyHeight = DuoMetrics.controlSize
40
+
41
+ /// - both fit: both stay whole
42
+ /// - one fits whole beside the other compressed: one is compressed, tapping it swaps them
43
+ /// - otherwise both rest compressed; tapping one opens it for a moment (the rail closes
44
+ /// again once a tab is picked)
45
+ static func resolve(
46
+ layout: ScreenLayout,
47
+ tabCount: Int,
48
+ hasQr: Bool,
49
+ toolbarHeight: CGFloat,
50
+ expanded: Binding<DuoBar>,
51
+ opened: Binding<DuoBar?>,
52
+ setToolbarHeight: @escaping (CGFloat) -> Void
53
+ ) -> DuoBarCompression {
54
+ guard let sideBar = layout.sideBar else { return DuoBarCompression() }
55
+ let room = layout.height - sideBar.bottom - sideBar.top - groupGap
56
+ let tabBar = duoRailHeight(itemCount: tabCount, hasQr: hasQr)
57
+ let compressedTabBar = duoRailHeight(itemCount: 1, hasQr: hasQr, compressed: true)
58
+ func fits(_ toolbar: CGFloat, _ tabs: CGFloat) -> Bool { toolbar + tabs <= room }
59
+
60
+ if fits(toolbarHeight, tabBar) {
61
+ return DuoBarCompression(setToolbarHeight: setToolbarHeight)
62
+ }
63
+ if fits(compressedToolbarHeight, tabBar) && fits(toolbarHeight, compressedTabBar) {
64
+ return DuoBarCompression(
65
+ toolbarCompressed: expanded.wrappedValue == .tabBar,
66
+ tabBarCompressed: expanded.wrappedValue == .toolbar,
67
+ expandToolbar: { expanded.wrappedValue = .toolbar },
68
+ expandTabBar: { expanded.wrappedValue = .tabBar },
69
+ setToolbarHeight: setToolbarHeight
70
+ )
71
+ }
72
+
73
+ let expandToolbar = { opened.wrappedValue = .toolbar }
74
+ let expandTabBar = { opened.wrappedValue = .tabBar }
75
+ if opened.wrappedValue == .tabBar && fits(backOnlyHeight, tabBar) {
76
+ return DuoBarCompression(
77
+ toolbarCompressed: true,
78
+ tabBarCompressed: false,
79
+ toolbarHidden: true,
80
+ expandToolbar: expandToolbar,
81
+ expandTabBar: expandTabBar,
82
+ onTabSelected: { opened.wrappedValue = nil },
83
+ setToolbarHeight: setToolbarHeight
84
+ )
85
+ }
86
+ return DuoBarCompression(
87
+ toolbarCompressed: opened.wrappedValue != .toolbar,
88
+ tabBarCompressed: true,
89
+ expandToolbar: expandToolbar,
90
+ expandTabBar: expandTabBar,
91
+ setToolbarHeight: setToolbarHeight
92
+ )
93
+ }
94
+ }
95
+
96
+ private struct DuoBarCompressionKey: EnvironmentKey {
97
+ static let defaultValue = DuoBarCompression()
98
+ }
99
+
100
+ extension EnvironmentValues {
101
+ var duoBarCompression: DuoBarCompression {
102
+ get { self[DuoBarCompressionKey.self] }
103
+ set { self[DuoBarCompressionKey.self] = newValue }
104
+ }
105
+ }
@@ -0,0 +1,304 @@
1
+ //
2
+ // DuoTabBar.swift
3
+ // MoMoUIKits
4
+ //
5
+ // iPhone Duo tab bar, port of RN `BottomTab/DuoTabBar.tsx` (Figma Fold-Design-System MT1/MT2):
6
+ // a glass rail in the side bar, or a floating glass bar when unfolded portrait.
7
+ //
8
+
9
+ import SwiftUI
10
+
11
+ private let itemSize = DuoMetrics.controlSize
12
+ private let iconSize: CGFloat = 28
13
+ private let qrIconSize: CGFloat = 24
14
+ private let railPadding: CGFloat = 6
15
+ private let railGap = Spacing.XXS
16
+ // selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
17
+ private let railBubbleWidth: CGFloat = 44
18
+ private let railBubbleHeight: CGFloat = 58
19
+ let duoFloatingTabBarHeight: CGFloat = 62
20
+ private let floatingPadding = Spacing.XS
21
+ private let floatingQrSize: CGFloat = 44
22
+ // floating bar spans 6 of the 8 portrait grid columns, like the content
23
+ private let gridColumns: CGFloat = 8
24
+ private let gridSpan: CGFloat = 6
25
+ private let tabSpring = Animation.interpolatingSpring(mass: 0.8, stiffness: 220, damping: 18)
26
+
27
+ /// Room tab screens leave for the Duo tab bar, provided by BottomTab to its tab StackScreens.
28
+ struct DuoTabBarInsets: Equatable {
29
+ /// Floating bar overlays the content: scroll content, footer and FAB keep clear of it.
30
+ var overlay: CGFloat = 0
31
+ /// Rail sits at the bottom of the side strip: the header side bar stays this far above the window bottom.
32
+ var sideBarReserve: CGFloat = 0
33
+ }
34
+
35
+ private struct DuoTabBarInsetsKey: EnvironmentKey {
36
+ static let defaultValue = DuoTabBarInsets()
37
+ }
38
+
39
+ extension EnvironmentValues {
40
+ var duoTabBarInsets: DuoTabBarInsets {
41
+ get { self[DuoTabBarInsetsKey.self] }
42
+ set { self[DuoTabBarInsetsKey.self] = newValue }
43
+ }
44
+ }
45
+
46
+ /// Rail plus the QR button under it; compressed, the rail is a single button.
47
+ func duoRailHeight(itemCount: Int, hasQr: Bool, compressed: Bool = false) -> CGFloat {
48
+ let count = CGFloat(itemCount)
49
+ let rail = compressed ? itemSize : railPadding * 2 + count * itemSize + max(count - 1, 0) * railGap
50
+ return rail + (hasQr ? Spacing.M + itemSize : 0)
51
+ }
52
+
53
+ extension ScreenLayout {
54
+ func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool, tabBarCompressed: Bool = false) -> DuoTabBarInsets {
55
+ guard isIPhoneDuo else { return DuoTabBarInsets() }
56
+ guard let sideBar else {
57
+ return DuoTabBarInsets(overlay: duoFloatingTabBarHeight + Spacing.M)
58
+ }
59
+ let rail = duoRailHeight(itemCount: itemCount, hasQr: hasFloatingButton, compressed: tabBarCompressed)
60
+ return DuoTabBarInsets(sideBarReserve: sideBar.bottom + rail + DuoBarCompression.groupGap)
61
+ }
62
+ }
63
+
64
+ /// Glass on iOS 26, a system blur before it (RN GlassView's fallback).
65
+ struct DuoBarMaterial<S: Shape>: ViewModifier {
66
+ let shape: S
67
+ var visible: Bool = true
68
+
69
+ func body(content: Content) -> some View {
70
+ content.background(material.allowsHitTesting(false))
71
+ }
72
+
73
+ @ViewBuilder
74
+ private var material: some View {
75
+ #if compiler(>=6.2)
76
+ if #available(iOS 26.0, *) {
77
+ // glass materializes by toggling its effect, it does not fade with opacity
78
+ Color.clear
79
+ .glassEffect(visible ? .regular : .identity, in: shape)
80
+ .shadow(color: Color.black.opacity(visible ? 0.16 : 0), radius: 16, x: 0, y: 6)
81
+ .animation(.easeInOut(duration: 0.25), value: visible)
82
+ } else {
83
+ blur
84
+ }
85
+ #else
86
+ blur
87
+ #endif
88
+ }
89
+
90
+ private var blur: some View {
91
+ shape.fill(.regularMaterial)
92
+ .shadow(color: Color.black.opacity(0.16), radius: 16, x: 0, y: 6)
93
+ .opacity(visible ? 1 : 0)
94
+ .animation(.easeInOut(duration: 0.25), value: visible)
95
+ }
96
+ }
97
+
98
+ struct DuoTabBar: View {
99
+ let items: [BottomTabItem]
100
+ let floatingButton: BottomTabFloatingButton?
101
+ let selectedIndex: Int
102
+ let onTabSelected: (Int) -> Void
103
+ var compression = DuoBarCompression()
104
+
105
+ @Environment(\.screenLayout) private var layout
106
+
107
+ var body: some View {
108
+ if let sideBar = layout.sideBar {
109
+ DuoRailTabBar(sideBar: sideBar, items: items, floatingButton: floatingButton,
110
+ selectedIndex: selectedIndex, onTabSelected: onTabSelected, compression: compression)
111
+ } else {
112
+ DuoFloatingTabBar(items: items, floatingButton: floatingButton,
113
+ selectedIndex: selectedIndex, onTabSelected: onTabSelected)
114
+ }
115
+ }
116
+ }
117
+
118
+ /// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E).
119
+ /// When the side bar is short it can shrink to one button showing the current tab
120
+ /// (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar.
121
+ private struct DuoRailTabBar: View {
122
+ let sideBar: SideBarLayout
123
+ let items: [BottomTabItem]
124
+ let floatingButton: BottomTabFloatingButton?
125
+ let selectedIndex: Int
126
+ let onTabSelected: (Int) -> Void
127
+ let compression: DuoBarCompression
128
+
129
+ @Environment(\.appTheme) private var theme
130
+
131
+ private var isRight: Bool { sideBar.edge == .right }
132
+
133
+ var body: some View {
134
+ VStack(spacing: Spacing.M) {
135
+ if compression.tabBarCompressed, items.indices.contains(selectedIndex) {
136
+ SwiftUI.Button(action: compression.expandTabBar) {
137
+ DuoTabIcon(item: items[selectedIndex], selected: true)
138
+ .frame(width: itemSize, height: itemSize)
139
+ .contentShape(Circle())
140
+ }
141
+ .buttonStyle(.plain)
142
+ .modifier(DuoBarMaterial(shape: Circle()))
143
+ .accessibilityLabel(items[selectedIndex].label)
144
+ .accessibilityIdentifier("bottom_tab_bar")
145
+ } else {
146
+ rail
147
+ }
148
+
149
+ if let floatingButton {
150
+ DuoQrButton(data: floatingButton, size: itemSize)
151
+ }
152
+ }
153
+ .padding(isRight ? .leading : .trailing, Spacing.M)
154
+ .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
155
+ .padding(.bottom, sideBar.bottom)
156
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .bottomTrailing : .bottomLeading)
157
+ .ignoresSafeArea()
158
+ }
159
+
160
+ /// Glass column of tab items with the sliding selection bubble.
161
+ private var rail: some View {
162
+ ZStack(alignment: .top) {
163
+ Capsule()
164
+ .fill(theme.colors.background.selected)
165
+ .frame(width: railBubbleWidth, height: railBubbleHeight)
166
+ .offset(y: railPadding - (railBubbleHeight - itemSize) / 2
167
+ + CGFloat(selectedIndex) * (itemSize + railGap))
168
+ .animation(tabSpring, value: selectedIndex)
169
+
170
+ VStack(spacing: railGap) {
171
+ ForEach(items.indices, id: \.self) { index in
172
+ SwiftUI.Button {
173
+ onTabSelected(index)
174
+ compression.onTabSelected()
175
+ } label: {
176
+ DuoTabIcon(item: items[index], selected: index == selectedIndex)
177
+ .frame(width: itemSize, height: itemSize)
178
+ .contentShape(Rectangle())
179
+ }
180
+ .buttonStyle(.plain)
181
+ .accessibilityLabel(items[index].label)
182
+ }
183
+ }
184
+ .padding(.vertical, railPadding)
185
+ }
186
+ .frame(width: itemSize)
187
+ .modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
188
+ .accessibilityIdentifier("bottom_tab_bar")
189
+ }
190
+ }
191
+
192
+ /// Floating labelled bar for the unfolded portrait display, QR in the middle cell (Figma MT1-M).
193
+ private struct DuoFloatingTabBar: View {
194
+ let items: [BottomTabItem]
195
+ let floatingButton: BottomTabFloatingButton?
196
+ let selectedIndex: Int
197
+ let onTabSelected: (Int) -> Void
198
+
199
+ @Environment(\.screenLayout) private var layout
200
+ @Environment(\.appTheme) private var theme
201
+
202
+ private var barWidth: CGFloat {
203
+ let columnWidth = (layout.width - 2 * Spacing.M - (gridColumns - 1) * Spacing.M) / gridColumns
204
+ return columnWidth * gridSpan + (gridSpan - 1) * Spacing.M
205
+ }
206
+
207
+ private var useQr: Bool { floatingButton != nil && items.count % 2 == 0 }
208
+ private var middle: Int { items.count / 2 }
209
+ private var cellCount: Int { items.count + (useQr ? 1 : 0) }
210
+ private var cellWidth: CGFloat { (barWidth - floatingPadding * 2) / CGFloat(max(cellCount, 1)) }
211
+ private var visualIndex: Int { useQr && selectedIndex >= middle ? selectedIndex + 1 : selectedIndex }
212
+
213
+ var body: some View {
214
+ ZStack(alignment: .leading) {
215
+ Capsule()
216
+ .fill(theme.colors.background.selected)
217
+ .frame(width: cellWidth)
218
+ .offset(x: CGFloat(visualIndex) * cellWidth)
219
+ .animation(tabSpring, value: visualIndex)
220
+
221
+ HStack(spacing: 0) {
222
+ ForEach(0..<cellCount, id: \.self) { cell in
223
+ if useQr && cell == middle, let floatingButton {
224
+ DuoQrButton(data: floatingButton, size: floatingQrSize)
225
+ .frame(width: cellWidth)
226
+ } else {
227
+ let index = useQr && cell > middle ? cell - 1 : cell
228
+ tabCell(index)
229
+ }
230
+ }
231
+ }
232
+ }
233
+ .padding(floatingPadding)
234
+ .frame(width: barWidth, height: duoFloatingTabBarHeight)
235
+ .modifier(DuoBarMaterial(shape: Capsule()))
236
+ .accessibilityIdentifier("bottom_tab_bar")
237
+ .padding(.bottom, layout.insets.bottom + Spacing.M)
238
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
239
+ .ignoresSafeArea()
240
+ }
241
+
242
+ private func tabCell(_ index: Int) -> some View {
243
+ let selected = index == selectedIndex
244
+ return SwiftUI.Button { onTabSelected(index) } label: {
245
+ VStack(spacing: Spacing.XXS) {
246
+ DuoTabIcon(item: items[index], selected: selected)
247
+ MomoText(
248
+ items[index].label,
249
+ typography: .labelXsMedium,
250
+ color: selected ? theme.colors.primary : theme.colors.text.default,
251
+ textAlign: .center,
252
+ maxLines: 1
253
+ )
254
+ }
255
+ .frame(width: cellWidth)
256
+ .frame(maxHeight: .infinity)
257
+ .contentShape(Rectangle())
258
+ }
259
+ .buttonStyle(.plain)
260
+ .accessibilityLabel(items[index].label)
261
+ }
262
+ }
263
+
264
+ private struct DuoTabIcon: View {
265
+ let item: BottomTabItem
266
+ let selected: Bool
267
+
268
+ @Environment(\.appTheme) private var theme
269
+
270
+ private var badgeLabel: String? {
271
+ guard let label = item.badgeLabel else { return nil }
272
+ if let n = Int(label), n == 0 { return nil }
273
+ return label
274
+ }
275
+
276
+ var body: some View {
277
+ Icon(source: item.icon, size: iconSize, color: selected ? theme.colors.primary : theme.colors.text.default)
278
+ .overlay(alignment: .topTrailing) {
279
+ if let badgeLabel, !badgeLabel.isEmpty {
280
+ Badge(label: badgeLabel).offset(x: Spacing.S, y: -Spacing.XS)
281
+ } else if badgeLabel != nil || item.showDot {
282
+ BadgeDot(size: .small)
283
+ }
284
+ }
285
+ }
286
+ }
287
+
288
+ private struct DuoQrButton: View {
289
+ let data: BottomTabFloatingButton
290
+ let size: CGFloat
291
+
292
+ @Environment(\.appTheme) private var theme
293
+
294
+ var body: some View {
295
+ SwiftUI.Button(action: data.onPress) {
296
+ Circle()
297
+ .fill(theme.colors.primary)
298
+ .frame(width: size, height: size)
299
+ .overlay(Icon(source: data.icon, size: qrIconSize, color: Colors.black01))
300
+ }
301
+ .buttonStyle(.plain)
302
+ .accessibilityLabel(data.label)
303
+ }
304
+ }
@@ -10,7 +10,6 @@
10
10
 
11
11
  import SwiftUI
12
12
 
13
- @available(iOS 16.0, *)
14
13
  public struct HeaderUserView: View {
15
14
  private let data: HeaderUserData
16
15
 
@@ -20,8 +19,11 @@ public struct HeaderUserView: View {
20
19
 
21
20
  private let avatarSize: CGFloat = Spacing.XXL // 32
22
21
 
22
+ @Environment(\.screenLayout) private var layout
23
+
24
+ // back button and toolkit share the bar unless the iPhone Duo side bar holds them
23
25
  private var maxWidth: CGFloat {
24
- UIScreen.main.bounds.width - 172
26
+ layout.sideBar != nil ? layout.contentWidth - Spacing.M * 2 : layout.width - 172
25
27
  }
26
28
 
27
29
  public var body: some View {
@@ -105,7 +107,6 @@ public struct HeaderUserView: View {
105
107
 
106
108
  /// Mirrors Compose's `AvatarGroup`: lays out 1..4 avatars in a fixed collage,
107
109
  /// falls back to initials when empty, and shows a "+n" overflow tile beyond 4.
108
- @available(iOS 16.0, *)
109
110
  private struct AvatarGroup: View {
110
111
  let urls: [String]
111
112
  let title: String
@@ -187,7 +188,6 @@ private struct AvatarGroup: View {
187
188
  }
188
189
  }
189
190
 
190
- @available(iOS 16.0, *)
191
191
  private struct InitialsAvatar: View {
192
192
  let name: String
193
193
  let size: CGFloat
@@ -3,9 +3,8 @@
3
3
  // MoMoUIKits
4
4
  //
5
5
  // SwiftUI mirror of Compose's NavigationContainer.
6
- // Wraps NavigationStack, injects Navigator into the environment, and renders
7
- // every registered screen inside `Screen(options)` chrome — same auto-wrap
8
- // behavior as Compose's StackScreen.
6
+ // Wraps NavigationStack (iOS 16+), injects Navigator into the environment, and renders
7
+ // every registered screen inside `StackScreen` chrome built from its options.
9
8
  //
10
9
 
11
10
  import SwiftUI
@@ -55,40 +54,23 @@ public struct NavigationContainer<Initial: View>: View {
55
54
  }
56
55
 
57
56
  public var body: some View {
58
- NavigationStack(path: $navigator.path) {
59
- screenView(for: navigator.rootRoute)
60
- .navigationDestination(for: DynamicScreenRoute.self) { route in
61
- screenView(for: route)
62
- }
63
- }
64
- .environmentObject(navigator)
65
- .environmentObject(applicationEnvironment)
66
- .environment(\.applicationEnvironment, applicationEnvironment)
67
- // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
68
- .momoLocalize(localize)
69
- .momoFontScale(context: applicationEnvironment.applicationContext)
70
- // AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
71
- .fullScreenCover(item: $navigator.presented) { route in
72
- screenView(forDialog: route)
73
- .environmentObject(navigator)
74
- .environmentObject(applicationEnvironment)
75
- .environment(\.applicationEnvironment, applicationEnvironment)
76
- // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
77
- .momoLocalize(localize)
78
- .momoFontScale(context: applicationEnvironment.applicationContext)
79
- // AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
80
- }
81
- .overlay {
82
- if let item = navigator.overplay {
83
- OverplayHost(item: item)
84
- .environmentObject(navigator)
85
- .environmentObject(applicationEnvironment)
86
- .environment(\.applicationEnvironment, applicationEnvironment)
87
- // AI-GENERATED START: route Localize + font-scale injection through the observing modifiers
88
- .momoLocalize(localize)
89
- .momoFontScale(context: applicationEnvironment.applicationContext)
90
- // AI-GENERATED END: route Localize + font-scale injection through the observing modifiers
91
- }
57
+ withKitEnvironment(
58
+ stack(path: $navigator.path, root: navigator.rootRoute)
59
+ .overlay { overplayLayer(presentedId: nil) }
60
+ )
61
+ .fullScreenCover(item: Binding(
62
+ get: { navigator.coverRoute },
63
+ // A close that bypasses the navigator (e.g. `dismiss()`) still tears the flow down.
64
+ // SwiftUI also writes nil after a navigator-driven close; by then a new screen may
65
+ // already be queued, so only a cover that is still showing counts.
66
+ set: { if $0 == nil, navigator.coverRoute != nil { navigator.dismiss() } }
67
+ )) { route in
68
+ withKitEnvironment(
69
+ stack(path: $navigator.presentedPath, root: DynamicScreenRoute(id: route.id))
70
+ .overlay { overplayLayer(presentedId: route.id) }
71
+ )
72
+ .onAppear { navigator.coverDidAppear() }
73
+ .onDisappear { navigator.coverDidDisappear() }
92
74
  }
93
75
  .onAppear {
94
76
  if navigator.composeApi == nil {
@@ -109,10 +91,23 @@ public struct NavigationContainer<Initial: View>: View {
109
91
  // AI-GENERATED END: demote the MaxApi language pull to a seed of last resort
110
92
  }
111
93
  .onDisappear {
94
+ // Our own cover hides the stack without leaving it; host back must still reach it.
95
+ guard navigator.presented == nil else { return }
112
96
  NavigatorManager.shared.remove(navigator)
113
97
  }
114
98
  }
115
99
 
100
+ private func stack(path: Binding<[DynamicScreenRoute]>, root: DynamicScreenRoute) -> some View {
101
+ NavigationStack(path: path) {
102
+ screenView(for: root)
103
+ // A reset swaps the root in place; without the id it would inherit the old root's state.
104
+ .id(root.id)
105
+ .navigationDestination(for: DynamicScreenRoute.self) { route in
106
+ screenView(for: route)
107
+ }
108
+ }
109
+ }
110
+
116
111
  @ViewBuilder
117
112
  private func screenView(for route: DynamicScreenRoute) -> some View {
118
113
  if let screen = navigator.registry.getScreen(id: route.id) {
@@ -123,14 +118,23 @@ public struct NavigationContainer<Initial: View>: View {
123
118
  }
124
119
  }
125
120
 
121
+ /// Each context draws only the overplays shown over it, so one never re-mounts elsewhere.
126
122
  @ViewBuilder
127
- private func screenView(forDialog route: DynamicDialogRoute) -> some View {
128
- if let screen = navigator.registry.getScreen(id: route.id) {
129
- ScreenHost(screen: screen, navigator: navigator)
130
- } else {
131
- EmptyView()
123
+ private func overplayLayer(presentedId: Int?) -> some View {
124
+ if let item = navigator.overplay, item.presentedId == presentedId {
125
+ OverplayHost(item: item)
126
+ .momoScreenLayout()
132
127
  }
133
128
  }
129
+
130
+ private func withKitEnvironment<Content: View>(_ content: Content) -> some View {
131
+ content
132
+ .environmentObject(navigator)
133
+ .environmentObject(applicationEnvironment)
134
+ .environment(\.applicationEnvironment, applicationEnvironment)
135
+ .momoLocalize(localize)
136
+ .momoFontScale(context: applicationEnvironment.applicationContext)
137
+ }
134
138
  }
135
139
 
136
140
  // MARK: - ScreenHost
@@ -167,6 +171,8 @@ private struct ScreenHost: View {
167
171
  screenName: screen.name,
168
172
  content: { screen.content() }
169
173
  )
174
+ // Per screen, not around the NavigationStack: a state-driven reader there stops path pushes rendering.
175
+ .momoScreenLayout()
170
176
  }
171
177
  }
172
178