@momo-kits/native-kits 0.167.1-duo.3 → 0.167.1-duo.4-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 (187) hide show
  1. package/CLAUDE.md +17 -8
  2. package/build.gradle.kts +11 -0
  3. package/compose/build.gradle.kts +190 -0
  4. package/compose/build.gradle.kts.backup +190 -0
  5. package/compose/compose.podspec +47 -0
  6. package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +19 -0
  7. package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +6 -0
  8. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +12 -0
  9. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +7 -0
  10. package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +151 -0
  11. package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +52 -0
  12. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +124 -0
  13. package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +183 -0
  14. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +207 -0
  15. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +39 -0
  16. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +86 -0
  17. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +76 -0
  18. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +76 -0
  19. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +293 -0
  20. package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +33 -0
  21. package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +831 -0
  22. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +269 -0
  23. package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +110 -0
  24. package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +378 -0
  25. package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +56 -0
  26. package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +69 -0
  27. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +157 -0
  28. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +82 -0
  29. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +32 -0
  30. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +338 -0
  31. package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +175 -0
  32. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +356 -0
  33. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +113 -0
  34. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +89 -0
  35. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +128 -0
  36. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +209 -0
  37. package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +542 -0
  38. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +49 -0
  39. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +76 -0
  40. package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +125 -0
  41. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +231 -0
  42. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +189 -0
  43. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +475 -0
  44. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +114 -0
  45. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +266 -0
  46. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +240 -0
  47. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +223 -0
  48. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +264 -0
  49. package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +185 -0
  50. package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +343 -0
  51. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +108 -0
  52. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +56 -0
  53. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +41 -0
  54. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +84 -0
  55. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +40 -0
  56. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +334 -0
  57. package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +98 -0
  58. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +310 -0
  59. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +59 -0
  60. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +87 -0
  61. package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +21 -0
  62. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +90 -0
  63. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +323 -0
  64. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +220 -0
  65. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +473 -0
  66. package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +122 -0
  67. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +199 -0
  68. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +91 -0
  69. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +552 -0
  70. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +88 -0
  71. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +125 -0
  72. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +207 -0
  73. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +498 -0
  74. package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +176 -0
  75. package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +182 -0
  76. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +193 -0
  77. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +29 -0
  78. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +234 -0
  79. package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +188 -0
  80. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +325 -0
  81. package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +16 -0
  82. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +18 -0
  83. package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +22 -0
  84. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +12 -0
  85. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +16 -0
  86. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +188 -0
  87. package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +297 -0
  88. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +54 -0
  89. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +50 -0
  90. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +157 -0
  91. package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +41 -0
  92. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +52 -0
  93. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +66 -0
  94. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +11 -0
  95. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +104 -0
  96. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +48 -0
  97. package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +51 -0
  98. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +299 -0
  99. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +58 -0
  100. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +129 -0
  101. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +106 -0
  102. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +205 -0
  103. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +393 -0
  104. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +8 -0
  105. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +622 -0
  106. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +148 -0
  107. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +35 -0
  108. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +175 -0
  109. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +232 -0
  110. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +86 -0
  111. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +169 -0
  112. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +381 -0
  113. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +83 -0
  114. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +105 -0
  115. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +74 -0
  116. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +347 -0
  117. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +37 -0
  118. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +363 -0
  119. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +169 -0
  120. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +132 -0
  121. package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +127 -0
  122. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +12 -0
  123. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +91 -0
  124. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +9 -0
  125. package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +71 -0
  126. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +1329 -0
  127. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +55 -0
  128. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +15 -0
  129. package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +118 -0
  130. package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +40 -0
  131. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +45 -0
  132. package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +32 -0
  133. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +38 -0
  134. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +66 -0
  135. package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +213 -0
  136. package/gradle/libs.versions.toml +69 -0
  137. package/gradle/wrapper/gradle-wrapper.jar +0 -0
  138. package/gradle/wrapper/gradle-wrapper.properties +8 -0
  139. package/gradle.properties +26 -0
  140. package/gradlew +252 -0
  141. package/gradlew.bat +94 -0
  142. package/ios/Application/ApplicationEnvironment.swift +6 -0
  143. package/ios/Application/Components.swift +1 -3
  144. package/ios/Application/HeaderRight.swift +19 -27
  145. package/ios/Application/Navigation/BottomTab/BottomTab.swift +9 -30
  146. package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +2 -5
  147. package/ios/Application/Navigation/HeaderUser.swift +1 -7
  148. package/ios/Application/Navigation/NavigationContainer.swift +45 -45
  149. package/ios/Application/Navigation/Navigator.swift +122 -29
  150. package/ios/Application/Navigation/NavigatorManager.swift +13 -6
  151. package/ios/Application/Navigation/Overplay/BottomSheet.swift +4 -41
  152. package/ios/Application/Navigation/Overplay/ModalScreen.swift +1 -5
  153. package/ios/Application/Navigation/component/Header.swift +4 -12
  154. package/ios/Application/Navigation/component/HeaderBackground.swift +2 -3
  155. package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
  156. package/ios/Application/Navigation/component/NavigationHeaderRight.swift +9 -61
  157. package/ios/Application/StackScreen.swift +18 -135
  158. package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
  159. package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
  160. package/ios/Extensions/Color++.swift +20 -9
  161. package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
  162. package/ios/Icon/Icon.swift +14 -22
  163. package/ios/Icon/Icons.swift +1329 -0
  164. package/ios/Image/Image.swift +16 -5
  165. package/ios/Lottie/LottieView.swift +4 -2
  166. package/ios/Popup/PopupDisplay.swift +68 -84
  167. package/ios/Popup/PopupNotify.swift +32 -43
  168. package/ios/Popup/PopupPromotion.swift +6 -18
  169. package/ios/Resources/dot_loading.json +558 -0
  170. package/ios/Resources/lottie_circle_loader.json +1 -0
  171. package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
  172. package/ios/native-kits.podspec +1 -1
  173. package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
  174. package/ios-demo/README.md +120 -0
  175. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +0 -1
  176. package/package.json +1 -1
  177. package/scripts/gen-ios-icons.mjs +36 -0
  178. package/settings.gradle.kts +64 -0
  179. package/.claude/scheduled_tasks.lock +0 -1
  180. package/.claude/settings.local.json +0 -185
  181. package/ios/Application/Layout/ScreenLayout.swift +0 -196
  182. package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +0 -275
  183. package/ios/Application/Navigation/component/DuoScrollGradient.swift +0 -55
  184. package/ios/Application/Navigation/component/SideBar.swift +0 -155
  185. package/ios/Popup/PopupLayout.swift +0 -45
  186. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +0 -76
  187. package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +0 -170
@@ -1,196 +0,0 @@
1
- //
2
- // ScreenLayout.swift
3
- // MoMoUIKits
4
- //
5
- // Port of momo-kits RN `useScreenLayout` (Layout/utils.ts) and `Application/device.ts`:
6
- // window size, safe-area insets, content width, header geometry and the iPhone Duo side bar.
7
- //
8
-
9
- import SwiftUI
10
- import UIKit
11
-
12
- public enum SideBarEdge: Equatable {
13
- case left, right
14
- }
15
-
16
- public struct SideBarLayout: Equatable {
17
- public let edge: SideBarEdge
18
- public let width: CGFloat
19
- /// Preferred window y of the first control; StackScreen still keeps the column above the bottom bars.
20
- public let top: CGFloat
21
- }
22
-
23
- public struct ScreenLayout: Equatable {
24
- public let width: CGFloat
25
- public let height: CGFloat
26
- public let isIPhoneDuo: Bool
27
- public let insets: EdgeInsets
28
- public let insetLeft: CGFloat
29
- public let insetRight: CGFloat
30
- public let contentWidth: CGFloat
31
- public let headerHeight: CGFloat
32
- /// 0 when the status bar is off the top edge, so the bar row fills the header and keeps the title centered.
33
- public let headerStatusBarHeight: CGFloat
34
- /// iPhone Duo moves back and toolkit into a column in the side safe area; nil while they stay in the bar.
35
- public let sideBar: SideBarLayout?
36
-
37
- public var headerBarHeight: CGFloat { headerHeight - headerStatusBarHeight }
38
- }
39
-
40
- // Measured by momo-kits RN (Layout/utils.ts) on the iOS 27.1 iPhone Duo simulator; keep both in sync.
41
- enum DuoMetrics {
42
- // Inner display is ~1.42, outer ~1.45; every slab iPhone is 1.77 or taller.
43
- static let maxAspectRatio: CGFloat = 1.6
44
- // Outer display is 466pt tall in landscape, inner 669pt.
45
- static let innerMinHeight: CGFloat = 600
46
- static let controlSize: CGFloat = 48
47
- static let itemPitch: CGFloat = 50
48
- static let firstItemTop: CGFloat = 170
49
- static let firstItemTopLandscape: CGFloat = 120
50
- // Widest short side of any single-display iPhone.
51
- static let iPhoneMaxShortSide: CGFloat = 440
52
- }
53
-
54
- enum HeaderMetrics {
55
- static let barHeight: CGFloat = HEADER_HEIGHT
56
- static let iPhoneMinHeight: CGFloat = 72
57
- // A smaller top inset means the status bar is not on the top edge.
58
- static let statusBarMinInset: CGFloat = 20
59
- }
60
-
61
- private var isPhoneIdiom: Bool { UIDevice.current.userInterfaceIdiom == .phone }
62
-
63
- private var keyWindow: UIWindow? {
64
- UIApplication.shared.connectedScenes
65
- .compactMap { ($0 as? UIWindowScene)?.keyWindow }
66
- .first
67
- }
68
-
69
- extension ScreenLayout {
70
- /// Snapshot of the key window; not reactive, so views read `\.screenLayout` instead.
71
- public static var current: ScreenLayout {
72
- guard let window = keyWindow else {
73
- return make(size: UIScreen.main.bounds.size, insets: EdgeInsets())
74
- }
75
- let inset = window.safeAreaInsets
76
- return make(
77
- size: window.bounds.size,
78
- insets: EdgeInsets(top: inset.top, leading: inset.left, bottom: inset.bottom, trailing: inset.right)
79
- )
80
- }
81
-
82
- static func make(size: CGSize, insets: EdgeInsets, isPhone: Bool = isPhoneIdiom) -> ScreenLayout {
83
- let width = size.width
84
- let height = size.height
85
- let (headerHeight, headerStatusBarHeight) = headerGeometry(topInset: insets.top, isPhone: isPhone)
86
- let isDuo = isPhone && isIPhoneDuoWindow(width: width, height: height, insets: insets)
87
- guard isDuo else {
88
- return ScreenLayout(
89
- width: width, height: height, isIPhoneDuo: false, insets: insets,
90
- insetLeft: 0, insetRight: 0, contentWidth: width,
91
- headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: nil
92
- )
93
- }
94
-
95
- var sideBar: SideBarLayout?
96
- let sideInset = max(insets.leading, insets.trailing)
97
- // narrower strips cannot host a control 12pt from the inner edge
98
- if sideInset >= DuoMetrics.controlSize + Spacing.M {
99
- var top = insets.top + HeaderMetrics.barHeight + Spacing.M
100
- if insets.top < HeaderMetrics.statusBarMinInset {
101
- // status bar lives in the strip: start under its cluster, as the native bar does
102
- let isInnerLandscape = width > height && height >= DuoMetrics.innerMinHeight
103
- top = isInnerLandscape ? DuoMetrics.firstItemTopLandscape : DuoMetrics.firstItemTop
104
- }
105
- sideBar = SideBarLayout(
106
- edge: insets.trailing >= insets.leading ? .right : .left,
107
- width: sideInset,
108
- top: top
109
- )
110
- }
111
-
112
- return ScreenLayout(
113
- width: width, height: height, isIPhoneDuo: true, insets: insets,
114
- insetLeft: insets.leading, insetRight: insets.trailing,
115
- contentWidth: width - insets.leading - insets.trailing,
116
- headerHeight: headerHeight, headerStatusBarHeight: headerStatusBarHeight, sideBar: sideBar
117
- )
118
- }
119
-
120
- private static func headerGeometry(topInset: CGFloat, isPhone: Bool) -> (CGFloat, CGFloat) {
121
- let height = topInset + HeaderMetrics.barHeight
122
- guard isPhone else { return (height, topInset) }
123
- return (
124
- max(height, HeaderMetrics.iPhoneMinHeight),
125
- topInset < HeaderMetrics.statusBarMinInset ? 0 : topInset
126
- )
127
- }
128
-
129
- // Slab iPhones report equal side insets in landscape; iPhone Duo moves the status bar to one side.
130
- private static func isIPhoneDuoWindow(width: CGFloat, height: CGFloat, insets: EdgeInsets) -> Bool {
131
- let shortSide = min(width, height)
132
- let isSquare = shortSide > 0 && max(width, height) / shortSide < DuoMetrics.maxAspectRatio
133
- return isSquare || insets.leading != insets.trailing
134
- }
135
-
136
- /// Below the side status bar: in portrait it is about twice as tall as its inset is wide.
137
- public var duoTopInset: CGFloat {
138
- max(insets.top, height > width ? max(insets.leading, insets.trailing) * 2 : 0)
139
- }
140
- }
141
-
142
- /// Port of RN `device.ts` `isIPhoneDuo()`: UIScreen stays on the outer display, so this
143
- /// still matches when Split View narrows the window. Used by popups and overlays.
144
- public func isIPhoneDuoDevice(windowSize: CGSize? = nil) -> Bool {
145
- guard isPhoneIdiom else { return false }
146
- let screen = UIScreen.main.bounds.size
147
- let window = windowSize ?? keyWindow?.bounds.size ?? screen
148
- return max(min(screen.width, screen.height), min(window.width, window.height)) > DuoMetrics.iPhoneMaxShortSide
149
- }
150
-
151
- // MARK: - Environment
152
-
153
- private struct ScreenLayoutKey: EnvironmentKey {
154
- // Computed so an un-injected subtree (bare UIHostingController) still reads the live window.
155
- static var defaultValue: ScreenLayout { .current }
156
- }
157
-
158
- public extension EnvironmentValues {
159
- var screenLayout: ScreenLayout {
160
- get { self[ScreenLayoutKey.self] }
161
- set { self[ScreenLayoutKey.self] = newValue }
162
- }
163
- }
164
-
165
- /// Re-reads the window whenever this view's size changes (rotation, fold/unfold, Split View).
166
- private struct ScreenLayoutReader: ViewModifier {
167
- @State private var layout: ScreenLayout = .current
168
-
169
- func body(content: Content) -> some View {
170
- content
171
- .environment(\.screenLayout, layout)
172
- .background(
173
- GeometryReader { proxy in
174
- Color.clear
175
- .onAppear { refresh() }
176
- .onChange(of: proxy.size) { _ in refresh() }
177
- .onChange(of: proxy.safeAreaInsets) { _ in refresh() }
178
- }
179
- )
180
- .onReceive(NotificationCenter.default.publisher(for: UIDevice.orientationDidChangeNotification)) { _ in
181
- refresh()
182
- }
183
- }
184
-
185
- private func refresh() {
186
- let next = ScreenLayout.current
187
- if next != layout { layout = next }
188
- }
189
- }
190
-
191
- public extension View {
192
- /// Provides `\.screenLayout`; NavigationContainer applies it, a bare UIHostingController root should too.
193
- func momoScreenLayout() -> some View {
194
- modifier(ScreenLayoutReader())
195
- }
196
- }
@@ -1,275 +0,0 @@
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 above it.
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
- extension ScreenLayout {
47
- func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool) -> DuoTabBarInsets {
48
- guard isIPhoneDuo else { return DuoTabBarInsets() }
49
- guard sideBar != nil else {
50
- return DuoTabBarInsets(overlay: duoFloatingTabBarHeight + Spacing.M)
51
- }
52
- let count = CGFloat(itemCount)
53
- let rail = railPadding * 2 + count * itemSize + max(count - 1, 0) * railGap
54
- let qr = hasFloatingButton ? Spacing.M + itemSize : 0
55
- return DuoTabBarInsets(sideBarReserve: rail + qr + Spacing.XL)
56
- }
57
- }
58
-
59
- /// Glass on iOS 26, a system blur before it (RN GlassView's fallback).
60
- struct DuoBarMaterial<S: Shape>: ViewModifier {
61
- let shape: S
62
- var visible: Bool = true
63
-
64
- func body(content: Content) -> some View {
65
- content.background(material.allowsHitTesting(false))
66
- }
67
-
68
- @ViewBuilder
69
- private var material: some View {
70
- #if compiler(>=6.2)
71
- if #available(iOS 26.0, *) {
72
- // glass materializes by toggling its effect, it does not fade with opacity
73
- Color.clear
74
- .glassEffect(visible ? .regular : .identity, in: shape)
75
- .shadow(color: Color.black.opacity(visible ? 0.16 : 0), radius: 16, x: 0, y: 6)
76
- .animation(.easeInOut(duration: 0.25), value: visible)
77
- } else {
78
- blur
79
- }
80
- #else
81
- blur
82
- #endif
83
- }
84
-
85
- private var blur: some View {
86
- shape.fill(.regularMaterial)
87
- .shadow(color: Color.black.opacity(0.16), radius: 16, x: 0, y: 6)
88
- .opacity(visible ? 1 : 0)
89
- .animation(.easeInOut(duration: 0.25), value: visible)
90
- }
91
- }
92
-
93
- struct DuoTabBar: View {
94
- let items: [BottomTabItem]
95
- let floatingButton: BottomTabFloatingButton?
96
- let selectedIndex: Int
97
- let onTabSelected: (Int) -> Void
98
-
99
- @Environment(\.screenLayout) private var layout
100
-
101
- var body: some View {
102
- if let sideBar = layout.sideBar {
103
- DuoRailTabBar(sideBar: sideBar, items: items, floatingButton: floatingButton,
104
- selectedIndex: selectedIndex, onTabSelected: onTabSelected)
105
- } else {
106
- DuoFloatingTabBar(items: items, floatingButton: floatingButton,
107
- selectedIndex: selectedIndex, onTabSelected: onTabSelected)
108
- }
109
- }
110
- }
111
-
112
- /// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E).
113
- private struct DuoRailTabBar: View {
114
- let sideBar: SideBarLayout
115
- let items: [BottomTabItem]
116
- let floatingButton: BottomTabFloatingButton?
117
- let selectedIndex: Int
118
- let onTabSelected: (Int) -> Void
119
-
120
- @Environment(\.appTheme) private var theme
121
-
122
- private var isRight: Bool { sideBar.edge == .right }
123
-
124
- var body: some View {
125
- VStack(spacing: Spacing.M) {
126
- ZStack(alignment: .top) {
127
- Capsule()
128
- .fill(theme.colors.background.selected)
129
- .frame(width: railBubbleWidth, height: railBubbleHeight)
130
- .offset(y: railPadding - (railBubbleHeight - itemSize) / 2
131
- + CGFloat(selectedIndex) * (itemSize + railGap))
132
- .animation(tabSpring, value: selectedIndex)
133
-
134
- VStack(spacing: railGap) {
135
- ForEach(items.indices, id: \.self) { index in
136
- SwiftUI.Button { onTabSelected(index) } label: {
137
- DuoTabIcon(item: items[index], selected: index == selectedIndex)
138
- .frame(width: itemSize, height: itemSize)
139
- .contentShape(Rectangle())
140
- }
141
- .buttonStyle(.plain)
142
- .accessibilityLabel(items[index].label)
143
- }
144
- }
145
- .padding(.vertical, railPadding)
146
- }
147
- .frame(width: itemSize)
148
- .modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
149
- .accessibilityIdentifier("bottom_tab_bar")
150
-
151
- if let floatingButton {
152
- DuoQrButton(data: floatingButton, size: itemSize)
153
- }
154
- }
155
- .padding(isRight ? .leading : .trailing, Spacing.M)
156
- .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
157
- .padding(.bottom, Spacing.XL)
158
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .bottomTrailing : .bottomLeading)
159
- .ignoresSafeArea()
160
- }
161
- }
162
-
163
- /// Floating labelled bar for the unfolded portrait display, QR in the middle cell (Figma MT1-M).
164
- private struct DuoFloatingTabBar: View {
165
- let items: [BottomTabItem]
166
- let floatingButton: BottomTabFloatingButton?
167
- let selectedIndex: Int
168
- let onTabSelected: (Int) -> Void
169
-
170
- @Environment(\.screenLayout) private var layout
171
- @Environment(\.appTheme) private var theme
172
-
173
- private var barWidth: CGFloat {
174
- let columnWidth = (layout.width - 2 * Spacing.M - (gridColumns - 1) * Spacing.M) / gridColumns
175
- return columnWidth * gridSpan + (gridSpan - 1) * Spacing.M
176
- }
177
-
178
- private var useQr: Bool { floatingButton != nil && items.count % 2 == 0 }
179
- private var middle: Int { items.count / 2 }
180
- private var cellCount: Int { items.count + (useQr ? 1 : 0) }
181
- private var cellWidth: CGFloat { (barWidth - floatingPadding * 2) / CGFloat(max(cellCount, 1)) }
182
- private var visualIndex: Int { useQr && selectedIndex >= middle ? selectedIndex + 1 : selectedIndex }
183
-
184
- var body: some View {
185
- ZStack(alignment: .leading) {
186
- Capsule()
187
- .fill(theme.colors.background.selected)
188
- .frame(width: cellWidth)
189
- .offset(x: CGFloat(visualIndex) * cellWidth)
190
- .animation(tabSpring, value: visualIndex)
191
-
192
- HStack(spacing: 0) {
193
- ForEach(0..<cellCount, id: \.self) { cell in
194
- if useQr && cell == middle, let floatingButton {
195
- DuoQrButton(data: floatingButton, size: floatingQrSize)
196
- .frame(width: cellWidth)
197
- } else {
198
- let index = useQr && cell > middle ? cell - 1 : cell
199
- tabCell(index)
200
- }
201
- }
202
- }
203
- }
204
- .padding(floatingPadding)
205
- .frame(width: barWidth, height: duoFloatingTabBarHeight)
206
- .modifier(DuoBarMaterial(shape: Capsule()))
207
- .accessibilityIdentifier("bottom_tab_bar")
208
- .padding(.bottom, layout.insets.bottom + Spacing.M)
209
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
210
- .ignoresSafeArea()
211
- }
212
-
213
- private func tabCell(_ index: Int) -> some View {
214
- let selected = index == selectedIndex
215
- return SwiftUI.Button { onTabSelected(index) } label: {
216
- VStack(spacing: Spacing.XXS) {
217
- DuoTabIcon(item: items[index], selected: selected)
218
- MomoText(
219
- items[index].label,
220
- typography: .labelXsMedium,
221
- color: selected ? theme.colors.primary : theme.colors.text.default,
222
- textAlign: .center,
223
- maxLines: 1
224
- )
225
- }
226
- .frame(width: cellWidth)
227
- .frame(maxHeight: .infinity)
228
- .contentShape(Rectangle())
229
- }
230
- .buttonStyle(.plain)
231
- .accessibilityLabel(items[index].label)
232
- }
233
- }
234
-
235
- private struct DuoTabIcon: View {
236
- let item: BottomTabItem
237
- let selected: Bool
238
-
239
- @Environment(\.appTheme) private var theme
240
-
241
- private var badgeLabel: String? {
242
- guard let label = item.badgeLabel else { return nil }
243
- if let n = Int(label), n == 0 { return nil }
244
- return label
245
- }
246
-
247
- var body: some View {
248
- Icon(source: item.icon, size: iconSize, color: selected ? theme.colors.primary : theme.colors.text.default)
249
- .overlay(alignment: .topTrailing) {
250
- if let badgeLabel, !badgeLabel.isEmpty {
251
- Badge(label: badgeLabel).offset(x: Spacing.S, y: -Spacing.XS)
252
- } else if badgeLabel != nil || item.showDot {
253
- BadgeDot(size: .small)
254
- }
255
- }
256
- }
257
- }
258
-
259
- private struct DuoQrButton: View {
260
- let data: BottomTabFloatingButton
261
- let size: CGFloat
262
-
263
- @Environment(\.appTheme) private var theme
264
-
265
- var body: some View {
266
- SwiftUI.Button(action: data.onPress) {
267
- Circle()
268
- .fill(theme.colors.primary)
269
- .frame(width: size, height: size)
270
- .overlay(Icon(source: data.icon, size: qrIconSize, color: Colors.black01))
271
- }
272
- .buttonStyle(.plain)
273
- .accessibilityLabel(data.label)
274
- }
275
- }
@@ -1,55 +0,0 @@
1
- //
2
- // DuoScrollGradient.swift
3
- // MoMoUIKits
4
- //
5
- // Port of RN `DuoScrollGradient` (Components/DuoGlassBackground.tsx): with the iPhone Duo
6
- // glass header the content scrolls under the bar, so the header gradient is drawn under it.
7
- //
8
-
9
- import SwiftUI
10
-
11
- // extended headers run their gradient this far below the header, as off Duo
12
- private let extendedGradientExtra: CGFloat = 52
13
- // scroll distance over which the extended gradient collapses to the header
14
- private let extendedCollapseDistance: CGFloat = 100
15
-
16
- struct DuoScrollGradient: View {
17
- let scrollOffset: CGFloat
18
- let headerHeight: CGFloat
19
- let extended: Bool
20
-
21
- @Environment(\.appTheme) private var theme
22
- @Environment(\.screenLayout) private var layout
23
-
24
- private var scroll: CGFloat { max(scrollOffset, 0) }
25
-
26
- private var gradient: LinearGradient {
27
- LinearGradient(
28
- colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
29
- startPoint: .top,
30
- endPoint: .bottom
31
- )
32
- }
33
-
34
- var body: some View {
35
- Group {
36
- if extended {
37
- // extended squeezes the whole fade into its box and fades out
38
- let progress = min(scroll / extendedCollapseDistance, 1)
39
- gradient
40
- .frame(height: headerHeight + extendedGradientExtra * (1 - progress))
41
- .opacity(1 - min(scroll / extendedGradientExtra, 1))
42
- } else {
43
- // default keeps the off-Duo 375:154 shape, the header clips it, and scrolls away
44
- gradient
45
- .frame(height: layout.width * 154 / 375)
46
- .frame(height: headerHeight, alignment: .top)
47
- .clipped()
48
- .offset(y: -scroll)
49
- }
50
- }
51
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
52
- .ignoresSafeArea()
53
- .allowsHitTesting(false)
54
- }
55
- }
@@ -1,155 +0,0 @@
1
- //
2
- // SideBar.swift
3
- // MoMoUIKits
4
- //
5
- // iPhone Duo: back and toolkit leave the header bar for a column in the side safe area,
6
- // as the system bars do. Port of RN `SideBarButton.tsx` + `useSideBarItemStyle`.
7
- //
8
-
9
- import SwiftUI
10
-
11
- private let sideBarIconSize: CGFloat = 24
12
-
13
- /// Toolkit data when `headerRight` renders the kit toolkit; nil for a custom header right.
14
- func toolkitHeaderRight(_ headerRight: (() -> any View)?) -> (data: HeaderRightData?, tintColor: Color?)? {
15
- guard let view = headerRight?() else { return nil }
16
- if let toolkit = view as? HeaderRight { return (toolkit.headerRight, toolkit.tintColor) }
17
- if let toolkit = view as? NavigationHeaderRight { return (toolkit.headerRight, toolkit.tintColor) }
18
- return nil
19
- }
20
-
21
- /// The back button and toolkit column, placed in window coordinates over the side strip.
22
- struct HeaderSideBar: View {
23
- let sideBar: SideBarLayout
24
- let goBack: (() -> Void)?
25
- let toolkit: (data: HeaderRightData?, tintColor: Color?)?
26
- let headerColor: HeaderColor
27
- /// What footer / tab bar / home indicator take from the bottom; the column stays above it.
28
- let bottomInset: CGFloat
29
-
30
- @Environment(\.screenLayout) private var layout
31
- @State private var columnHeight: CGFloat = 0
32
-
33
- private var isRight: Bool { sideBar.edge == .right }
34
-
35
- private var top: CGFloat {
36
- let maxY = layout.height - bottomInset - Spacing.XL - columnHeight
37
- return max(min(sideBar.top, maxY), 0)
38
- }
39
-
40
- var body: some View {
41
- VStack(alignment: isRight ? .leading : .trailing, spacing: Spacing.M) {
42
- if let goBack {
43
- SideBarButton(
44
- icon: "arrow-back",
45
- tintColor: headerColor.tintIconColor,
46
- accessibilityId: "btn_navigation_back",
47
- action: goBack
48
- )
49
- } else {
50
- // keep the slot so the toolkit does not jump between root and pushed screens
51
- Color.clear.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
52
- }
53
- if let toolkit {
54
- ToolkitNavigationHeaderRight(
55
- headerRight: toolkit.data,
56
- tintColor: toolkit.tintColor ?? headerColor.tintIconColor,
57
- vertical: true
58
- )
59
- }
60
- }
61
- .padding(isRight ? .leading : .trailing, Spacing.M)
62
- .frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
63
- .background(
64
- GeometryReader { proxy in
65
- Color.clear
66
- .onAppear { columnHeight = proxy.size.height }
67
- .onChange(of: proxy.size.height) { columnHeight = $0 }
68
- }
69
- )
70
- .padding(.top, top)
71
- .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .topTrailing : .topLeading)
72
- .ignoresSafeArea()
73
- }
74
- }
75
-
76
- /// Dark glass under light icons (banner headers), light glass otherwise.
77
- private struct SideBarGlass<S: Shape>: ViewModifier {
78
- let tintColor: Color?
79
- let shape: S
80
-
81
- private var isDark: Bool { tintColor == Colors.black01 || tintColor == .white }
82
-
83
- func body(content: Content) -> some View {
84
- if isDark {
85
- content
86
- .background(shape.fill(Colors.black20.opacity(0.5)))
87
- .overlay(shape.stroke(Colors.black01.opacity(0.2), lineWidth: 0.5))
88
- } else {
89
- content
90
- .background(
91
- shape.fill(Colors.black01.opacity(0.85))
92
- .shadow(color: Color.black.opacity(0.12), radius: 4, x: 0, y: 2)
93
- )
94
- .overlay(shape.stroke(Colors.black20.opacity(0.08), lineWidth: 0.5))
95
- }
96
- }
97
- }
98
-
99
- /// 48pt glass circle for a single side bar control.
100
- struct SideBarButton: View {
101
- let icon: String
102
- var tintColor: Color?
103
- var showBadge: Bool = false
104
- var disabled: Bool = false
105
- var accessibilityId: String?
106
- let action: () -> Void
107
-
108
- var body: some View {
109
- SwiftUI.Button(action: { if !disabled { action() } }) {
110
- Icon(source: icon, size: sideBarIconSize, color: tintColor ?? Colors.black17)
111
- .frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
112
- .modifier(SideBarGlass(tintColor: tintColor, shape: Circle()))
113
- .overlay(alignment: .topTrailing) {
114
- if showBadge {
115
- BadgeDot(size: .small).offset(x: -Spacing.XXS, y: Spacing.XXS)
116
- }
117
- }
118
- }
119
- .buttonStyle(.plain)
120
- .accessibilityIdentifier(accessibilityId ?? "")
121
- }
122
- }
123
-
124
- /// 48pt wide glass capsule grouping related controls, one item per 50pt.
125
- struct SideBarGroup<Content: View>: View {
126
- var tintColor: Color?
127
- let itemCount: Int
128
- @ViewBuilder let content: () -> Content
129
-
130
- var body: some View {
131
- VStack(spacing: 0) { content() }
132
- .frame(
133
- width: DuoMetrics.controlSize,
134
- height: itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
135
- )
136
- .modifier(SideBarGlass(tintColor: tintColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
137
- }
138
- }
139
-
140
- struct SideBarGroupItem: View {
141
- let icon: String
142
- var tintColor: Color?
143
- let accessibilityId: String
144
- let action: () -> Void
145
-
146
- var body: some View {
147
- SwiftUI.Button(action: action) {
148
- Icon(source: icon, size: sideBarIconSize, color: tintColor ?? Colors.black17)
149
- .frame(maxWidth: .infinity, maxHeight: .infinity)
150
- .contentShape(Rectangle())
151
- }
152
- .buttonStyle(.plain)
153
- .accessibilityIdentifier(accessibilityId)
154
- }
155
- }