@momo-kits/native-kits 0.167.1-sizescale.2-debug → 0.168.1-beta.1-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 (223) hide show
  1. package/CLAUDE.md +45 -3
  2. package/README.md +2 -2
  3. package/compose/build.gradle.kts +9 -1
  4. package/compose/build.gradle.kts.backup +8 -0
  5. package/compose/compose.podspec +1 -1
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +3 -4
  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 +353 -113
  9. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +2 -2
  10. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +16 -9
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +6 -3
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +11 -15
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +7 -23
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +13 -11
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +21 -9
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +309 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +11 -9
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +8 -7
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +134 -81
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +312 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +126 -29
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +116 -26
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +2 -3
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
  65. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +1 -3
  66. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +10 -11
  67. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
  68. package/gradle/libs.versions.toml +1 -1
  69. package/gradle.properties +1 -1
  70. package/ios/Application/ApplicationEnvironment.swift +17 -0
  71. package/ios/Application/MaxApi.swift +30 -3
  72. package/ios/Avatar/Avatar.swift +4 -3
  73. package/ios/Badge/Badge.swift +6 -4
  74. package/ios/Badge/BadgeDot.swift +1 -1
  75. package/ios/Badge/BadgeRibbon.swift +0 -1
  76. package/ios/Button/Button.swift +15 -12
  77. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
  78. package/ios/Carousel/Carousel.swift +29 -12
  79. package/ios/Checkbox/Checkbox.swift +7 -3
  80. package/ios/Chip/Chip.swift +5 -4
  81. package/ios/Collapse/Collapse.swift +70 -19
  82. package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
  83. package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
  84. package/ios/DateTimePicker/DateTimePicker.swift +54 -20
  85. package/ios/DateTimePicker/WheelPicker.swift +103 -105
  86. package/ios/Divider/Divider.swift +37 -4
  87. package/ios/Extensions/Color++.swift +38 -9
  88. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  89. package/ios/Extensions/SkeletonUICompat.swift +11 -0
  90. package/ios/Icon/Icon.swift +21 -23
  91. package/ios/Icon/Icons.swift +1329 -0
  92. package/ios/IconButton/IconButton.swift +15 -7
  93. package/ios/Image/Image.swift +7 -23
  94. package/ios/Information/Information.swift +27 -9
  95. package/ios/Input/ErrorView.swift +2 -2
  96. package/ios/Input/Input.swift +160 -147
  97. package/ios/Input/InputPhoneNumber.swift +12 -9
  98. package/ios/Input/InputSearch.swift +107 -77
  99. package/ios/Input/InputTextArea.swift +128 -145
  100. package/ios/InputDropDown/InputDropDown.swift +10 -7
  101. package/ios/InputMoney/InputMoney.swift +33 -11
  102. package/ios/InputOTP/InputOTP.swift +22 -12
  103. package/ios/Layout/Card.swift +50 -0
  104. package/ios/Layout/GridContext.swift +187 -3
  105. package/ios/Layout/Item.swift +5 -4
  106. package/ios/Layout/ScreenLayout.swift +380 -0
  107. package/ios/Layout/Section.swift +4 -100
  108. package/ios/Loader/Loader.swift +5 -9
  109. package/ios/Lottie/LottieView.swift +14 -2
  110. package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
  111. package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
  112. package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
  113. package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
  114. package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
  115. package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
  116. package/ios/Navigation/Component/Header.swift +545 -0
  117. package/ios/Navigation/Component/HeaderBackground.swift +133 -0
  118. package/ios/Navigation/Component/HeaderJourney.swift +73 -0
  119. package/ios/Navigation/Component/HeaderLocation.swift +48 -0
  120. package/ios/Navigation/Component/HeaderRight.swift +425 -0
  121. package/ios/Navigation/Component/HeaderTitle.swift +36 -0
  122. package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
  123. package/ios/Navigation/Component/SideBar.swift +183 -0
  124. package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
  125. package/ios/Navigation/HostBackGestureBridge.swift +78 -0
  126. package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
  127. package/ios/Navigation/Navigation.swift +204 -0
  128. package/ios/Navigation/NavigationContainer.swift +195 -0
  129. package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
  130. package/ios/Navigation/StackScreen.swift +934 -0
  131. package/ios/Navigation/StackTransition.swift +400 -0
  132. package/ios/Navigation/SwipeBackHaptic.swift +33 -0
  133. package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
  134. package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
  135. package/ios/Pagination/PaginationScroll.swift +2 -2
  136. package/ios/Popup/PopupDisplay.swift +72 -135
  137. package/ios/Popup/PopupInput.swift +10 -13
  138. package/ios/Popup/PopupLayout.swift +140 -0
  139. package/ios/Popup/PopupNotify.swift +59 -69
  140. package/ios/Popup/PopupPromotion.swift +31 -47
  141. package/ios/PopupView/FullscreenPopup.swift +13 -19
  142. package/ios/PopupView/PopupView.swift +1 -1
  143. package/ios/PopupView/Utils++.swift +0 -8
  144. package/ios/ProgressInfo/ProgressInfo.swift +4 -2
  145. package/ios/Radio/Radio.swift +2 -1
  146. package/ios/Rating/Rating.swift +9 -1
  147. package/ios/Resources/dot_loading.json +558 -0
  148. package/ios/Resources/lottie_circle_loader.json +1 -0
  149. package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
  150. package/ios/Skeleton/Skeleton.swift +22 -27
  151. package/ios/Slider/Slider.swift +16 -18
  152. package/ios/Stepper/Stepper.swift +6 -1
  153. package/ios/Steps/Steps.swift +8 -6
  154. package/ios/SuggestAction/SuggestAction.swift +5 -0
  155. package/ios/Swipeable/SwipeCell.swift +10 -6
  156. package/ios/Switch/Switch.swift +22 -20
  157. package/ios/TabView/TabView.swift +102 -21
  158. package/ios/Tag/Tag.swift +15 -4
  159. package/ios/Template/Logo/Logo.swift +2 -1
  160. package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
  161. package/ios/Title/Title.swift +6 -17
  162. package/ios/Tooltip/Tooltip.swift +29 -14
  163. package/ios/Typography/FontScale.swift +3 -3
  164. package/ios/Typography/Text.swift +81 -19
  165. package/ios/Uploader/Uploader.swift +3 -1
  166. package/ios/native-kits.podspec +2 -1
  167. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  168. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
  169. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
  170. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
  171. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
  172. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
  173. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
  174. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
  176. package/local.properties +2 -2
  177. package/package.json +1 -1
  178. package/scripts/gen-ios-icons.mjs +36 -0
  179. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
  180. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
  181. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
  182. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
  183. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
  184. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
  185. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
  186. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
  187. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
  188. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
  189. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
  190. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
  191. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitSizeScale.kt +0 -56
  192. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Example.xcscheme +0 -32
  193. package/example/ios/Example.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -14
  194. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/UserInterfaceState.xcuserstate +0 -0
  195. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/WorkspaceSettings.xcsettings +0 -16
  196. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcdebugger/Breakpoints_v2.xcbkptlist +0 -6
  197. package/example/ios/Example.xcworkspace/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -5
  198. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/MoMoUIKits.xcscheme +0 -58
  199. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/Pods-Example.xcscheme +0 -58
  200. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImage.xcscheme +0 -58
  201. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SDWebImageSwiftUI.xcscheme +0 -58
  202. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/SkeletonUI.xcscheme +0 -58
  203. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios-LottiePrivacyInfo.xcscheme +0 -58
  204. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/lottie-ios.xcscheme +0 -58
  205. package/example/ios/Pods/Pods.xcodeproj/xcuserdata/sonnguyen.xcuserdatad/xcschemes/xcschememanagement.plist +0 -60
  206. package/ios/Application/Components.swift +0 -270
  207. package/ios/Application/ComposeApi.swift +0 -22
  208. package/ios/Application/HeaderRight.swift +0 -306
  209. package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
  210. package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
  211. package/ios/Application/Navigation/HeaderTitle.swift +0 -57
  212. package/ios/Application/Navigation/NavigationContainer.swift +0 -213
  213. package/ios/Application/Navigation/NavigationOptions.swift +0 -109
  214. package/ios/Application/Navigation/NavigatorManager.swift +0 -22
  215. package/ios/Application/Navigation/component/Header.swift +0 -178
  216. package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
  217. package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
  218. package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
  219. package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
  220. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
  221. package/ios/Application/Screen.swift +0 -250
  222. package/ios/Application/StackScreen.swift +0 -492
  223. /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
@@ -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
+ }
@@ -1,5 +1,12 @@
1
+ //
2
+ // FloatingButton.swift
3
+ // MoMoUIKits
4
+ //
5
+ // SwiftUI mirror of Compose's `navigation/component/FloatingButton.kt`: the screen FAB, whose
6
+ // label collapses while the content scrolls down.
7
+ //
8
+
1
9
  import SwiftUI
2
- import Combine
3
10
 
4
11
  public enum FABSize {
5
12
  case small
@@ -58,14 +65,10 @@ public struct FabProps {
58
65
  }
59
66
 
60
67
  public struct FloatingButton: View {
68
+ @State private var isExpanded: Bool = true
61
69
  @State private var lastScrollOffset: CGFloat = 0
62
- @State private var isExpanded: Bool = false
63
- @State private var internalScrollOffset: CGFloat = 0
64
- @State private var showText: Bool = true
65
- @State private var fullWidth: CGFloat? = nil
66
- @State private var currentWidth: CGFloat = 0
67
- @State private var lastDirection: String? = nil
68
-
70
+ @State private var settleScroll: Task<Void, Never>?
71
+
69
72
  private let props: FabProps
70
73
  private var keyboardOffset: CGFloat
71
74
  private var scrollOffset: CGFloat
@@ -111,64 +114,50 @@ public struct FloatingButton: View {
111
114
  content
112
115
  }
113
116
  }
114
- .onTapGesture {
115
- onClick?()
116
- }
117
117
  .padding(Spacing.M)
118
118
  .frame(height: height + (Spacing.M * 2))
119
119
  .background(containerColor)
120
120
  .cornerRadius(100)
121
+ // Compose FloatingActionButton elevation 4dp
122
+ .shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 2)
123
+ .contentShape(Capsule())
124
+ .onTapGesture {
125
+ onClick?()
126
+ }
127
+ .accessibilityElement(children: .combine)
128
+ .accessibilityAddTraits(.isButton)
121
129
  .padding(.bottom, bottomPadding)
122
130
  .frame(alignment: position == .center ? .center : .trailing)
123
- .onAppear {
124
- internalScrollOffset = scrollOffset
125
- isExpanded = label != nil
126
- }
127
- .onReceive(Just(scrollOffset)) { newValue in
128
- let direction = newValue > internalScrollOffset ? "down" : "up"
131
+ .onAppear { lastScrollOffset = scrollOffset }
132
+ .onChange(of: scrollOffset, perform: updateExpanded)
133
+ .onDisappear { settleScroll?.cancel() }
134
+ }
129
135
 
130
- if direction != lastDirection {
131
- lastDirection = direction
132
- if direction == "down" {
133
- withAnimation(.easeOut(duration: 0.1)) {
134
- isExpanded = false
135
- }
136
- DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
137
- showText = false
138
- }
139
- } else {
140
- showText = true
141
- withAnimation(.easeIn(duration: 0.1)) {
142
- isExpanded = true
143
- }
144
-
145
- }
146
- }
136
+ /// Compose's `collectLatest { isExpanded = new <= last; delay(100); last = new }`: the label
137
+ /// shows while the content scrolls back up, and the reference only moves once the scroll pauses.
138
+ private func updateExpanded(_ offset: CGFloat) {
139
+ let expanded = offset <= lastScrollOffset
140
+ if expanded != isExpanded {
141
+ withAnimation(.linear(duration: 0.1)) { isExpanded = expanded }
142
+ }
143
+ settleScroll?.cancel()
144
+ settleScroll = Task { @MainActor in
145
+ try? await Task.sleep(nanoseconds: 100_000_000)
146
+ guard !Task.isCancelled else { return }
147
+ lastScrollOffset = offset
147
148
  }
148
149
  }
149
-
150
+
150
151
  private var content: some View {
151
152
  HStack(spacing: 0) {
152
- ImageView(icon ?? "")
153
- .scaledToFit()
154
- .frame(width: iconSize, height: iconSize)
155
- .foregroundColor(iconColor)
156
-
157
- if let label = label {
158
- if showText {
159
- Spacer().frame(width: 8)
160
- MomoText(label, typography: .actionDefaultBold, color: .white, maxLines: 1)
161
- .background(
162
- GeometryReader { geo in
163
- Color.clear.onAppear {
164
- fullWidth = geo.size.width
165
- }
166
- }
167
- )
168
- .opacity(isExpanded ? 1 : 0)
169
- }
170
- }
153
+ // Compose `Icon`: an icon key or an https URL, tinted
154
+ Icon(source: icon ?? "", size: iconSize ?? defaultIconSize, color: iconColor)
171
155
 
156
+ if let label, isExpanded {
157
+ MomoText(label, typography: .actionDefaultBold, color: .white, maxLines: 1)
158
+ .padding(.leading, 12)
159
+ .transition(.opacity.combined(with: .move(edge: .leading)))
160
+ }
172
161
  }
173
162
  }
174
163
  }