@momo-kits/native-kits 0.167.1-duo.21-debug → 0.167.1-duo.22
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.
- package/ios/Application/MaxApi.swift +20 -2
- package/ios/Input/InputSearch.swift +5 -2
- package/ios/Navigation/BottomSheet.swift +58 -37
- package/ios/Navigation/BottomTab/BottomTab.swift +165 -18
- package/ios/Navigation/BottomTab/BottomTabBar.swift +3 -0
- package/ios/Navigation/Component/FloatingButton.swift +29 -51
- package/ios/Navigation/Component/Header.swift +19 -5
- package/ios/Navigation/Component/HeaderRight.swift +48 -19
- package/ios/Navigation/Component/SnackBar.swift +3 -2
- package/ios/Navigation/ModalScreen.swift +2 -1
- package/ios/Navigation/NavigationContainer.swift +3 -2
- package/ios/Navigation/Navigator.swift +30 -16
- package/ios/Navigation/StackScreen.swift +151 -37
- package/package.json +1 -1
- package/build.gradle.kts +0 -11
- package/compose/build.gradle.kts +0 -204
- package/compose/build.gradle.kts.backup +0 -204
- package/compose/compose.podspec +0 -47
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +0 -19
- package/compose/src/androidMain/kotlin/vn/momo/kits/navigation/ScrollToTop.android.kt +0 -6
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/BackGesture.android.kt +0 -12
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/OsFontScale.android.kt +0 -7
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +0 -153
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +0 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +0 -951
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/WidgetContainer.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Avatar.kt +0 -157
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +0 -81
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeDot.kt +0 -32
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BadgeRibbon.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/BaselineView.kt +0 -175
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +0 -357
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -115
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CheckBox.kt +0 -89
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +0 -128
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Collapse.kt +0 -209
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/CupertinoOverscroll.kt +0 -542
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Divider.kt +0 -49
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/IconButton.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +0 -231
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +0 -189
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +0 -478
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputDropDown.kt +0 -114
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +0 -265
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -235
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +0 -223
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +0 -265
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +0 -185
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/LazyColumnWithBouncing.kt +0 -343
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Loader.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationDot.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationNumber.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationScroll.kt +0 -84
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PaginationWhiteDot.kt +0 -40
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +0 -361
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -108
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -308
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +0 -59
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Rating.kt +0 -87
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ScaleSizeScope.kt +0 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Skeleton.kt +0 -90
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +0 -330
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Stepper.kt +0 -220
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Steps.kt +0 -473
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/SuggestAction.kt +0 -122
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -92
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -560
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +0 -125
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Title.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +0 -501
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TrustBanner.kt +0 -176
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Uploader.kt +0 -182
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePicker.kt +0 -193
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerTypes.kt +0 -29
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +0 -236
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -180
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Colors.kt +0 -325
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +0 -20
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitFontScale.kt +0 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Radius.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Spacing.kt +0 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +0 -194
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -283
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -54
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -50
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +0 -309
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -41
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/AutomationId.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Clickable.kt +0 -66
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Conditional.kt +0 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/DeprecatedModifier.kt +0 -104
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Shadow.kt +0 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/modifier/Size.kt +0 -51
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +0 -338
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/HostBackGestureBridge.kt +0 -58
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +0 -137
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -116
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -205
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +0 -393
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ScrollToTop.kt +0 -8
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +0 -727
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackTransition.kt +0 -148
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/SwipeBackHaptic.kt +0 -35
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +0 -218
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -231
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +0 -98
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +0 -312
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/FloatingButton.kt +0 -169
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +0 -455
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -103
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +0 -106
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +0 -74
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -399
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -363
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +0 -192
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -133
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/tracking/ScreenTracker.kt +0 -127
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/BackGesture.kt +0 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ComposeLottieAnimation.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/OsFontScale.kt +0 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +0 -74
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +0 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Icons.kt +0 -1329
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Resources.kt +0 -55
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Tracking.kt +0 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +0 -118
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -78
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/HostBackGesture.ios.kt +0 -45
- package/compose/src/iosMain/kotlin/vn/momo/kits/navigation/ScrollToTop.ios.kt +0 -32
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/BackGesture.ios.kt +0 -38
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/OsFontScale.ios.kt +0 -66
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +0 -217
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +0 -94
- package/gradle/libs.versions.toml +0 -69
- package/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/gradle/wrapper/gradle-wrapper.properties +0 -8
- package/gradle.properties +0 -26
- package/gradlew +0 -252
- package/gradlew.bat +0 -94
- package/ios-demo/README.md +0 -120
- package/settings.gradle.kts +0 -64
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
// Swift mirror of the host-app bridge exposed on Kotlin as `vn.momo.maxapi.IMaxApi`
|
|
6
6
|
// (`LocalMaxApi`). `IMaxApi` itself is an external compiled dependency with no
|
|
7
7
|
// source in this repo, so this protocol intentionally exposes only the methods
|
|
8
|
-
// this kit's Kotlin source actually calls
|
|
9
|
-
//
|
|
8
|
+
// this kit's Kotlin source actually calls — not a full guess at IMaxApi's real surface.
|
|
9
|
+
// Callbacks carry IMaxApi's `["response": …]` payload.
|
|
10
10
|
//
|
|
11
11
|
|
|
12
12
|
import UIKit
|
|
@@ -17,6 +17,13 @@ public protocol MaxApi {
|
|
|
17
17
|
func getLanguage(callback: @escaping ([String: Any?]?) -> Void)
|
|
18
18
|
/// `type` is the IMaxApi vibration name (`"light"`, …).
|
|
19
19
|
func triggerEventVibration(_ type: String)
|
|
20
|
+
// Toolkit (Compose `HeaderRight.Toolkit`).
|
|
21
|
+
func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
22
|
+
func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
23
|
+
func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
24
|
+
func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void)
|
|
25
|
+
/// Back to the host home, closing every mini app.
|
|
26
|
+
func dismissAll(completion: (() -> Void)?)
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
public extension MaxApi {
|
|
@@ -24,4 +31,15 @@ public extension MaxApi {
|
|
|
24
31
|
func triggerEventVibration(_ type: String) {
|
|
25
32
|
UIImpactFeedbackGenerator(style: .light).impactOccurred()
|
|
26
33
|
}
|
|
34
|
+
|
|
35
|
+
// Hosts that don't bridge the toolkit keep it inert, as before these joined the protocol.
|
|
36
|
+
func isFavoriteApp(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
37
|
+
func onToolAction(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
38
|
+
func showTools(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
39
|
+
func showHelpCenter(_ params: [String: Any?], callback: @escaping ([String: Any?]?) -> Void) {}
|
|
40
|
+
|
|
41
|
+
/// Without a home to return to, closing this mini app is the closest thing.
|
|
42
|
+
func dismissAll(completion: (() -> Void)?) {
|
|
43
|
+
dismiss(completion: completion)
|
|
44
|
+
}
|
|
27
45
|
}
|
|
@@ -45,6 +45,8 @@ public struct InputSearchProps {
|
|
|
45
45
|
var placeholder: String = ""
|
|
46
46
|
var onChangeText: (String) -> Void = { _ in }
|
|
47
47
|
var onPressButtonText: () -> Void = {}
|
|
48
|
+
/// Without one, a header search's button text pops the screen (Compose `SearchAnimated`).
|
|
49
|
+
var hasButtonTextAction = false
|
|
48
50
|
var error: String = ""
|
|
49
51
|
var disabled: Bool = false
|
|
50
52
|
var icon: String = ""
|
|
@@ -70,7 +72,7 @@ public struct InputSearchProps {
|
|
|
70
72
|
showBorder: Bool = true,
|
|
71
73
|
placeholder: String = "",
|
|
72
74
|
onChangeText: @escaping (String) -> Void = { _ in },
|
|
73
|
-
onPressButtonText:
|
|
75
|
+
onPressButtonText: (() -> Void)? = nil,
|
|
74
76
|
error: String = "",
|
|
75
77
|
disabled: Bool = false,
|
|
76
78
|
icon: String = "",
|
|
@@ -93,7 +95,8 @@ public struct InputSearchProps {
|
|
|
93
95
|
self.showBorder = showBorder
|
|
94
96
|
self.placeholder = placeholder
|
|
95
97
|
self.onChangeText = onChangeText
|
|
96
|
-
self.onPressButtonText = onPressButtonText
|
|
98
|
+
self.onPressButtonText = onPressButtonText ?? {}
|
|
99
|
+
self.hasButtonTextAction = onPressButtonText != nil
|
|
97
100
|
self.error = error
|
|
98
101
|
self.disabled = disabled
|
|
99
102
|
self.icon = icon
|
|
@@ -36,6 +36,7 @@ public struct BottomSheetHeader {
|
|
|
36
36
|
// MARK: - Bottom sheet view
|
|
37
37
|
|
|
38
38
|
struct BottomSheetView<Content: View>: View {
|
|
39
|
+
let overplayId: UUID
|
|
39
40
|
let header: BottomSheetHeader
|
|
40
41
|
let isSurface: Bool
|
|
41
42
|
let barrierDismissible: Bool
|
|
@@ -56,6 +57,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
56
57
|
@State private var backgroundAlpha: CGFloat = 0
|
|
57
58
|
@State private var sheetHeight: CGFloat = 0
|
|
58
59
|
@State private var didAppear = false
|
|
60
|
+
@State private var isClosing = false
|
|
59
61
|
@State private var keyboardHeight: CGFloat = 0
|
|
60
62
|
/// Live drag offset, isolated from `sheetOffset` so a touch-move only invalidates
|
|
61
63
|
/// this gesture-scoped value instead of forcing the whole body (including
|
|
@@ -95,7 +97,6 @@ struct BottomSheetView<Content: View>: View {
|
|
|
95
97
|
isDuo && !isDuoCentered && layout.width > layout.height ? layout.insets.trailing : 0
|
|
96
98
|
}
|
|
97
99
|
|
|
98
|
-
/// The keyboard filler is part of the sheet, so capping the sheet shrinks the content by it.
|
|
99
100
|
private var maxHeight: CGFloat {
|
|
100
101
|
isDuo ? layout.height - max(layout.duoTopInset, Spacing.XL) : screenHeight - 90
|
|
101
102
|
}
|
|
@@ -160,43 +161,47 @@ struct BottomSheetView<Content: View>: View {
|
|
|
160
161
|
|
|
161
162
|
private var sheet: some View {
|
|
162
163
|
VStack(spacing: 0) {
|
|
163
|
-
// Drag handle + header (the draggable region).
|
|
164
164
|
VStack(spacing: 0) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
.
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
.frame(height:
|
|
172
|
-
|
|
165
|
+
// Drag handle + header (the draggable region).
|
|
166
|
+
VStack(spacing: 0) {
|
|
167
|
+
Spacer().frame(height: 8)
|
|
168
|
+
Capsule()
|
|
169
|
+
.fill(Colors.black06)
|
|
170
|
+
.frame(width: 40, height: 4)
|
|
171
|
+
Spacer().frame(height: 4)
|
|
172
|
+
headerView
|
|
173
|
+
.frame(height: 56)
|
|
174
|
+
.padding(.horizontal, Spacing.M)
|
|
175
|
+
.padding(.leading, sideInsetLeading)
|
|
176
|
+
.padding(.trailing, sideInsetTrailing)
|
|
177
|
+
}
|
|
178
|
+
.frame(maxWidth: .infinity)
|
|
179
|
+
.frame(height: 72)
|
|
180
|
+
.contentShape(Rectangle())
|
|
181
|
+
.gesture(dragGesture, including: draggable ? .all : .subviews)
|
|
182
|
+
|
|
183
|
+
Rectangle()
|
|
184
|
+
.fill(theme.colors.border.default)
|
|
185
|
+
.frame(height: 1)
|
|
186
|
+
|
|
187
|
+
content()
|
|
173
188
|
.padding(.leading, sideInsetLeading)
|
|
174
189
|
.padding(.trailing, sideInsetTrailing)
|
|
190
|
+
// a tall sheet keeps its header (and footer) and gives the content what is left
|
|
191
|
+
.layoutPriority(-1)
|
|
192
|
+
|
|
193
|
+
if let footer {
|
|
194
|
+
BottomSheetFooter(content: footer)
|
|
195
|
+
}
|
|
175
196
|
}
|
|
176
|
-
|
|
177
|
-
.
|
|
178
|
-
.
|
|
179
|
-
.gesture(dragGesture, including: draggable ? .all : .subviews)
|
|
180
|
-
|
|
181
|
-
Rectangle()
|
|
182
|
-
.fill(theme.colors.border.default)
|
|
183
|
-
.frame(height: 1)
|
|
184
|
-
|
|
185
|
-
content()
|
|
186
|
-
.padding(.leading, sideInsetLeading)
|
|
187
|
-
.padding(.trailing, sideInsetTrailing)
|
|
188
|
-
// a tall sheet keeps its header (and footer) and gives the content what is left
|
|
189
|
-
.layoutPriority(-1)
|
|
190
|
-
|
|
191
|
-
if let footer {
|
|
192
|
-
BottomSheetFooter(content: footer)
|
|
193
|
-
}
|
|
197
|
+
// The filler stays outside the cap, which shrinks by it instead: the sheet still ends at
|
|
198
|
+
// `maxHeight`, and the iOS 15 fallback never measures the keyboard as content.
|
|
199
|
+
.modifier(CapHeight(maxHeight: maxHeight - effectiveBottomInset))
|
|
194
200
|
|
|
195
201
|
(footer != nil ? theme.colors.background.surface : backgroundColor)
|
|
196
202
|
.frame(height: effectiveBottomInset)
|
|
197
203
|
}
|
|
198
204
|
.frame(maxWidth: isDuoCentered ? duoCenteredWidth : .infinity)
|
|
199
|
-
.modifier(CapHeight(maxHeight: maxHeight))
|
|
200
205
|
.clipped()
|
|
201
206
|
.background(
|
|
202
207
|
GeometryReader { geo in
|
|
@@ -241,14 +246,16 @@ struct BottomSheetView<Content: View>: View {
|
|
|
241
246
|
// same bottom-anchored top edge — is still in flight doesn't fight a
|
|
242
247
|
// moving target. Without this, the live drag value chases an animated
|
|
243
248
|
// one, producing a couple of corrective jumps before it settles.
|
|
244
|
-
|
|
249
|
+
// A closing sheet keeps going down.
|
|
250
|
+
if sheetOffset != 0 && !isClosing {
|
|
245
251
|
sheetOffset = 0
|
|
246
252
|
}
|
|
247
253
|
}
|
|
248
254
|
.updating($dragTranslation) { value, state, _ in
|
|
249
|
-
state = max(value.translation.height, 0)
|
|
255
|
+
state = isClosing ? 0 : max(value.translation.height, 0)
|
|
250
256
|
}
|
|
251
257
|
.onEnded { value in
|
|
258
|
+
guard !isClosing else { return }
|
|
252
259
|
let finalOffset = max(value.translation.height, 0)
|
|
253
260
|
// Capture into persisted state BEFORE @GestureState resets to 0,
|
|
254
261
|
// avoiding a one-frame snap back to rest before close()'s own
|
|
@@ -276,6 +283,8 @@ struct BottomSheetView<Content: View>: View {
|
|
|
276
283
|
/// Mirrors Compose's `closeEvent`: slide out over 200ms, then fade the scrim
|
|
277
284
|
/// out over 100ms only after the slide finishes, then dismiss.
|
|
278
285
|
private func close() {
|
|
286
|
+
guard !isClosing else { return }
|
|
287
|
+
isClosing = true
|
|
279
288
|
withAnimation(.timingCurve(0.3, 0, 0.8, 0.15, duration: 0.2)) {
|
|
280
289
|
sheetOffset = max(sheetHeight, screenHeight)
|
|
281
290
|
}
|
|
@@ -285,7 +294,7 @@ struct BottomSheetView<Content: View>: View {
|
|
|
285
294
|
}
|
|
286
295
|
}
|
|
287
296
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
288
|
-
navigator.finishOverplayDismiss()
|
|
297
|
+
navigator.finishOverplayDismiss(id: overplayId)
|
|
289
298
|
}
|
|
290
299
|
}
|
|
291
300
|
}
|
|
@@ -306,10 +315,12 @@ private struct CapHeight: ViewModifier {
|
|
|
306
315
|
}
|
|
307
316
|
|
|
308
317
|
/// iOS 15 has no `Layout`: the sheet hugs its ideal height until that outgrows the cap, then is
|
|
309
|
-
/// held at the cap
|
|
318
|
+
/// held at the cap. A capped sheet can't read its ideal height (that needs a second copy of the
|
|
319
|
+
/// content), so it is re-measured only when the cap grows, e.g. once the keyboard goes down.
|
|
310
320
|
private struct CapHeightFallback: ViewModifier {
|
|
311
321
|
let maxHeight: CGFloat
|
|
312
322
|
@State private var capped = false
|
|
323
|
+
@State private var idealHeight: CGFloat = 0
|
|
313
324
|
|
|
314
325
|
func body(content: Content) -> some View {
|
|
315
326
|
content
|
|
@@ -318,15 +329,25 @@ private struct CapHeightFallback: ViewModifier {
|
|
|
318
329
|
.background(
|
|
319
330
|
GeometryReader { proxy in
|
|
320
331
|
Color.clear
|
|
321
|
-
.onAppear {
|
|
322
|
-
.onChange(of: proxy.size.height) {
|
|
332
|
+
.onAppear { measure(proxy.size.height) }
|
|
333
|
+
.onChange(of: proxy.size.height) { measure($0) }
|
|
323
334
|
}
|
|
324
335
|
)
|
|
325
336
|
.frame(height: capped ? maxHeight : nil, alignment: .top)
|
|
337
|
+
.onChange(of: maxHeight) { cap in
|
|
338
|
+
if capped {
|
|
339
|
+
// the ideal height was over the old cap, so only a taller cap can release it
|
|
340
|
+
if cap > idealHeight { capped = false }
|
|
341
|
+
} else if idealHeight > cap {
|
|
342
|
+
capped = true
|
|
343
|
+
}
|
|
344
|
+
}
|
|
326
345
|
}
|
|
327
346
|
|
|
328
|
-
private func
|
|
329
|
-
|
|
347
|
+
private func measure(_ height: CGFloat) {
|
|
348
|
+
guard !capped else { return }
|
|
349
|
+
idealHeight = height
|
|
350
|
+
if height > maxHeight { capped = true }
|
|
330
351
|
}
|
|
331
352
|
}
|
|
332
353
|
|
|
@@ -8,11 +8,14 @@
|
|
|
8
8
|
//
|
|
9
9
|
|
|
10
10
|
import SwiftUI
|
|
11
|
+
import UIKit
|
|
11
12
|
|
|
12
13
|
public struct BottomTab: View {
|
|
13
14
|
private let items: [BottomTabItem]
|
|
14
15
|
private let floatingButton: BottomTabFloatingButton?
|
|
15
16
|
private let compressionBehavior: DuoBarCompressionBehavior
|
|
17
|
+
private let onTabPress: ((_ index: Int, _ switchTab: @escaping () -> Void) -> Void)?
|
|
18
|
+
private let backToInitialTab: Bool
|
|
16
19
|
|
|
17
20
|
@EnvironmentObject private var navigator: Navigator
|
|
18
21
|
/// The screen hosting the tabs (Compose's `LocalNavigation` in BottomTab).
|
|
@@ -20,21 +23,33 @@ public struct BottomTab: View {
|
|
|
20
23
|
@Environment(\.screenLayout) private var layout
|
|
21
24
|
@Environment(\.appTheme) private var theme
|
|
22
25
|
@StateObject private var tabNavigations = TabNavigations()
|
|
23
|
-
|
|
26
|
+
/// Compose's `startRoute`: picked once from `initialRouteName`.
|
|
27
|
+
@State private var startIndex: Int
|
|
28
|
+
@State private var selectedIndex: Int
|
|
24
29
|
@State private var toolbarHeight = DuoBarCompression.defaultToolbarHeight
|
|
25
30
|
@State private var expandedBar: DuoBar?
|
|
26
31
|
@State private var openedBar: DuoBar?
|
|
27
32
|
|
|
28
33
|
/// `compressionBehavior`: on iPhone Duo, when the side bar can't hold both bars, which one
|
|
29
34
|
/// starts compressed; tapping the compressed one swaps them.
|
|
35
|
+
/// `onTabPress` gets every tab press with the switch to perform, e.g. after a login gate.
|
|
36
|
+
/// `backToInitialTab`: back off another tab returns to the initial one before leaving the screen.
|
|
30
37
|
public init(
|
|
31
38
|
items: [BottomTabItem],
|
|
32
39
|
floatingButton: BottomTabFloatingButton? = nil,
|
|
33
|
-
compressionBehavior: DuoBarCompressionBehavior = .toolbar
|
|
40
|
+
compressionBehavior: DuoBarCompressionBehavior = .toolbar,
|
|
41
|
+
initialRouteName: String? = nil,
|
|
42
|
+
onTabPress: ((_ index: Int, _ switchTab: @escaping () -> Void) -> Void)? = nil,
|
|
43
|
+
backToInitialTab: Bool = true
|
|
34
44
|
) {
|
|
35
45
|
self.items = items
|
|
36
46
|
self.floatingButton = floatingButton
|
|
37
47
|
self.compressionBehavior = compressionBehavior
|
|
48
|
+
self.onTabPress = onTabPress
|
|
49
|
+
self.backToInitialTab = backToInitialTab
|
|
50
|
+
let start = items.firstIndex { $0.name == initialRouteName } ?? 0
|
|
51
|
+
_startIndex = State(initialValue: start)
|
|
52
|
+
_selectedIndex = State(initialValue: start)
|
|
38
53
|
}
|
|
39
54
|
|
|
40
55
|
private var bottomInset: CGFloat { safeAreaBottomInset() }
|
|
@@ -73,18 +88,12 @@ public struct BottomTab: View {
|
|
|
73
88
|
let compression = compression
|
|
74
89
|
ZStack(alignment: .bottom) {
|
|
75
90
|
// Tab content area, inset above the bar.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
.padding(.bottom, reservedBottom)
|
|
85
|
-
.environment(\.duoTabBarInsets, duoInsets(compression))
|
|
86
|
-
.environment(\.duoBarCompression, compression)
|
|
87
|
-
.animation(.easeInOut(duration: 0.2), value: selectedIndex)
|
|
91
|
+
TabHost(selectedIndex: selectedIndex, count: items.count) { index in AnyView(tabScreen(for: index)) }
|
|
92
|
+
// each tab hosts itself, so it needs the window's safe area to draw its header under the status bar
|
|
93
|
+
.ignoresSafeArea(.container, edges: [.top, .horizontal])
|
|
94
|
+
.padding(.bottom, reservedBottom)
|
|
95
|
+
.environment(\.duoTabBarInsets, duoInsets(compression))
|
|
96
|
+
.environment(\.duoBarCompression, compression)
|
|
88
97
|
|
|
89
98
|
if layout.isIPhoneDuo {
|
|
90
99
|
DuoTabBar(
|
|
@@ -128,14 +137,15 @@ public struct BottomTab: View {
|
|
|
128
137
|
}
|
|
129
138
|
|
|
130
139
|
private func selectTab(_ index: Int) {
|
|
131
|
-
if index != selectedIndex { selectedIndex = index }
|
|
140
|
+
let switchTab = { if index != selectedIndex { selectedIndex = index } }
|
|
141
|
+
if let onTabPress { onTabPress(index, switchTab) } else { switchTab() }
|
|
132
142
|
}
|
|
133
143
|
|
|
134
|
-
/// Back off the
|
|
144
|
+
/// Back off the initial tab returns to it; the host shows its floating arrow there (RN parity).
|
|
135
145
|
private func updateTabBack() {
|
|
136
|
-
let isFirstTab = selectedIndex ==
|
|
146
|
+
let isFirstTab = selectedIndex == startIndex || !backToInitialTab
|
|
137
147
|
HostBackGestureBridge.shared.shouldUseFloatingArrowBack = !isFirstTab
|
|
138
|
-
navigator.tabBackHandler = isFirstTab ? nil : (hostNavigation?.id ?? -1, { selectedIndex =
|
|
148
|
+
navigator.tabBackHandler = isFirstTab ? nil : (hostNavigation?.id ?? -1, { selectedIndex = startIndex })
|
|
139
149
|
}
|
|
140
150
|
|
|
141
151
|
private func tabScreen(for index: Int) -> some View {
|
|
@@ -176,6 +186,143 @@ private struct TabScreenHost: View {
|
|
|
176
186
|
}
|
|
177
187
|
}
|
|
178
188
|
|
|
189
|
+
// MARK: - Tab host
|
|
190
|
+
|
|
191
|
+
/// Compose's tab NavHost with `saveState` / `restoreState`. A tab gets its own UIHostingController
|
|
192
|
+
/// on first selection and keeps it, so its state and scroll survive a switch; while hidden it is
|
|
193
|
+
/// out of the window, so it gets onDisappear / onAppear (and re-runs screen tracking) like a NavHost
|
|
194
|
+
/// destination or a UITabBarController tab. Same scheme as `StackController`.
|
|
195
|
+
private struct TabHost: UIViewControllerRepresentable {
|
|
196
|
+
let selectedIndex: Int
|
|
197
|
+
let count: Int
|
|
198
|
+
let content: (Int) -> AnyView
|
|
199
|
+
|
|
200
|
+
func makeUIViewController(context: Context) -> TabController {
|
|
201
|
+
TabController()
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
func updateUIViewController(_ controller: TabController, context: Context) {
|
|
205
|
+
controller.update(selectedIndex: selectedIndex, count: count, environment: context.environment, content: content)
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private final class TabController: UIViewController {
|
|
210
|
+
private final class Tab {
|
|
211
|
+
let motion = TabMotion()
|
|
212
|
+
var host: UIHostingController<AnyView>!
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
private static let switchDuration: TimeInterval = 0.2
|
|
216
|
+
|
|
217
|
+
private var tabs: [Int: Tab] = [:]
|
|
218
|
+
private var selectedIndex: Int?
|
|
219
|
+
private var environment = EnvironmentValues()
|
|
220
|
+
private var content: (Int) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
221
|
+
|
|
222
|
+
override func viewDidLoad() {
|
|
223
|
+
super.viewDidLoad()
|
|
224
|
+
view.backgroundColor = .clear
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
override func viewDidLayoutSubviews() {
|
|
228
|
+
super.viewDidLayoutSubviews()
|
|
229
|
+
for subview in view.subviews { subview.frame = view.bounds }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
func update(
|
|
233
|
+
selectedIndex index: Int,
|
|
234
|
+
count: Int,
|
|
235
|
+
environment: EnvironmentValues,
|
|
236
|
+
content: @escaping (Int) -> AnyView
|
|
237
|
+
) {
|
|
238
|
+
self.environment = environment
|
|
239
|
+
self.content = content
|
|
240
|
+
for (stale, removed) in tabs where stale >= count {
|
|
241
|
+
tabs[stale] = nil
|
|
242
|
+
tearDown(removed)
|
|
243
|
+
}
|
|
244
|
+
if let selectedIndex, tabs[selectedIndex] == nil { self.selectedIndex = nil }
|
|
245
|
+
// hidden tabs too, so a tab coming back already shows the latest items and environment
|
|
246
|
+
for (tabIndex, existing) in tabs { existing.host.rootView = rootView(tabIndex, existing) }
|
|
247
|
+
guard (0..<count).contains(index), index != selectedIndex else { return }
|
|
248
|
+
|
|
249
|
+
let previous = selectedIndex.flatMap { tabs[$0] }
|
|
250
|
+
selectedIndex = index
|
|
251
|
+
let selected = tabs[index] ?? makeTab(index)
|
|
252
|
+
attach(selected)
|
|
253
|
+
|
|
254
|
+
guard let previous, view.window != nil else {
|
|
255
|
+
selected.motion.visible = true
|
|
256
|
+
previous.map(leave)
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
previous.host.view.isUserInteractionEnabled = false
|
|
260
|
+
// a frame at the start values first, so a tab created just now fades in too
|
|
261
|
+
DispatchQueue.main.async {
|
|
262
|
+
selected.motion.visible = true
|
|
263
|
+
previous.motion.visible = false
|
|
264
|
+
}
|
|
265
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + Self.switchDuration) { [weak self] in
|
|
266
|
+
self?.leave(previous)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
private func makeTab(_ index: Int) -> Tab {
|
|
271
|
+
let tab = Tab()
|
|
272
|
+
let host = UIHostingController(rootView: rootView(index, tab))
|
|
273
|
+
host.view.backgroundColor = .clear
|
|
274
|
+
addChild(host)
|
|
275
|
+
host.didMove(toParent: self)
|
|
276
|
+
tab.host = host
|
|
277
|
+
tabs[index] = tab
|
|
278
|
+
return tab
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private func rootView(_ index: Int, _ tab: Tab) -> AnyView {
|
|
282
|
+
AnyView(TabCard(motion: tab.motion, content: content(index)).environment(\.self, environment))
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
private func attach(_ tab: Tab) {
|
|
286
|
+
let hostView: UIView = tab.host.view
|
|
287
|
+
hostView.isUserInteractionEnabled = true
|
|
288
|
+
hostView.frame = view.bounds
|
|
289
|
+
if hostView.superview === view {
|
|
290
|
+
view.bringSubviewToFront(hostView)
|
|
291
|
+
} else {
|
|
292
|
+
view.addSubview(hostView)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/// Off the window once hidden; a tab selected again in the meantime stays.
|
|
297
|
+
private func leave(_ tab: Tab) {
|
|
298
|
+
guard selectedIndex.flatMap({ tabs[$0] }) !== tab else { return }
|
|
299
|
+
tab.host.view.removeFromSuperview()
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private func tearDown(_ tab: Tab) {
|
|
303
|
+
tab.host.willMove(toParent: nil)
|
|
304
|
+
tab.host.view.removeFromSuperview()
|
|
305
|
+
tab.host.removeFromParent()
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private final class TabMotion: ObservableObject {
|
|
310
|
+
@Published var visible = false
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/// The tab switch: 0.97 scale + fade over 200ms, both ways (Compose's tab enter / exit transitions).
|
|
314
|
+
private struct TabCard: View {
|
|
315
|
+
@ObservedObject var motion: TabMotion
|
|
316
|
+
let content: AnyView
|
|
317
|
+
|
|
318
|
+
var body: some View {
|
|
319
|
+
content
|
|
320
|
+
.opacity(motion.visible ? 1 : 0)
|
|
321
|
+
.scaleEffect(motion.visible ? 1 : 0.97)
|
|
322
|
+
.animation(.easeInOut(duration: 0.2), value: motion.visible)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
179
326
|
/// Bottom safe-area inset (home indicator).
|
|
180
327
|
private func safeAreaBottomInset() -> CGFloat {
|
|
181
328
|
let bottomInset = UIApplication.shared.connectedScenes
|
|
@@ -20,6 +20,7 @@ public struct BottomTabItem: Identifiable {
|
|
|
20
20
|
public let showDot: Bool
|
|
21
21
|
public let badgeLabel: String?
|
|
22
22
|
public let options: NavigationOptions?
|
|
23
|
+
public let initialParams: Any?
|
|
23
24
|
public let screen: () -> AnyView
|
|
24
25
|
|
|
25
26
|
public init(
|
|
@@ -29,6 +30,7 @@ public struct BottomTabItem: Identifiable {
|
|
|
29
30
|
showDot: Bool = false,
|
|
30
31
|
badgeLabel: String? = nil,
|
|
31
32
|
options: NavigationOptions? = nil,
|
|
33
|
+
initialParams: Any? = nil,
|
|
32
34
|
@ViewBuilder screen: @escaping () -> some View
|
|
33
35
|
) {
|
|
34
36
|
self.name = name
|
|
@@ -37,6 +39,7 @@ public struct BottomTabItem: Identifiable {
|
|
|
37
39
|
self.showDot = showDot
|
|
38
40
|
self.badgeLabel = badgeLabel
|
|
39
41
|
self.options = options
|
|
42
|
+
self.initialParams = initialParams
|
|
40
43
|
self.screen = { AnyView(screen()) }
|
|
41
44
|
}
|
|
42
45
|
}
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
//
|
|
8
8
|
|
|
9
9
|
import SwiftUI
|
|
10
|
-
import Combine
|
|
11
10
|
|
|
12
11
|
public enum FABSize {
|
|
13
12
|
case small
|
|
@@ -66,14 +65,10 @@ public struct FabProps {
|
|
|
66
65
|
}
|
|
67
66
|
|
|
68
67
|
public struct FloatingButton: View {
|
|
68
|
+
@State private var isExpanded: Bool = true
|
|
69
69
|
@State private var lastScrollOffset: CGFloat = 0
|
|
70
|
-
@State private var
|
|
71
|
-
|
|
72
|
-
@State private var showText: Bool = true
|
|
73
|
-
@State private var fullWidth: CGFloat? = nil
|
|
74
|
-
@State private var currentWidth: CGFloat = 0
|
|
75
|
-
@State private var lastDirection: String? = nil
|
|
76
|
-
|
|
70
|
+
@State private var settleScroll: Task<Void, Never>?
|
|
71
|
+
|
|
77
72
|
private let props: FabProps
|
|
78
73
|
private var keyboardOffset: CGFloat
|
|
79
74
|
private var scrollOffset: CGFloat
|
|
@@ -129,57 +124,40 @@ public struct FloatingButton: View {
|
|
|
129
124
|
.onTapGesture {
|
|
130
125
|
onClick?()
|
|
131
126
|
}
|
|
127
|
+
.accessibilityElement(children: .combine)
|
|
128
|
+
.accessibilityAddTraits(.isButton)
|
|
132
129
|
.padding(.bottom, bottomPadding)
|
|
133
130
|
.frame(alignment: position == .center ? .center : .trailing)
|
|
134
|
-
.onAppear {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
.onReceive(Just(scrollOffset)) { newValue in
|
|
139
|
-
let direction = newValue > internalScrollOffset ? "down" : "up"
|
|
131
|
+
.onAppear { lastScrollOffset = scrollOffset }
|
|
132
|
+
.onChange(of: scrollOffset, perform: updateExpanded)
|
|
133
|
+
.onDisappear { settleScroll?.cancel() }
|
|
134
|
+
}
|
|
140
135
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
isExpanded = true
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
}
|
|
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
|
|
158
148
|
}
|
|
159
149
|
}
|
|
160
|
-
|
|
150
|
+
|
|
161
151
|
private var content: some View {
|
|
162
152
|
HStack(spacing: 0) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
.frame(width: iconSize, height: iconSize)
|
|
166
|
-
.foregroundColor(iconColor)
|
|
167
|
-
|
|
168
|
-
if let label = label {
|
|
169
|
-
if showText {
|
|
170
|
-
Spacer().frame(width: 8)
|
|
171
|
-
MomoText(label, typography: .actionDefaultBold, color: .white, maxLines: 1)
|
|
172
|
-
.background(
|
|
173
|
-
GeometryReader { geo in
|
|
174
|
-
Color.clear.onAppear {
|
|
175
|
-
fullWidth = geo.size.width
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
)
|
|
179
|
-
.opacity(isExpanded ? 1 : 0)
|
|
180
|
-
}
|
|
181
|
-
}
|
|
153
|
+
// Compose `Icon`: an icon key or an https URL, tinted
|
|
154
|
+
Icon(source: icon ?? "", size: iconSize ?? defaultIconSize, color: iconColor)
|
|
182
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
|
+
}
|
|
183
161
|
}
|
|
184
162
|
}
|
|
185
163
|
}
|
|
@@ -273,11 +273,18 @@ public struct HeaderRightWidthKey: PreferenceKey {
|
|
|
273
273
|
}
|
|
274
274
|
|
|
275
275
|
/// Compose `SearchAnimated`: a header search fills from surface to the screen background over
|
|
276
|
-
/// the scroll that lifts it into the bar.
|
|
277
|
-
func headerSearchProps(
|
|
278
|
-
|
|
279
|
-
|
|
276
|
+
/// the scroll that lifts it into the bar, and its button text pops the screen.
|
|
277
|
+
func headerSearchProps(
|
|
278
|
+
_ props: InputSearchProps,
|
|
279
|
+
scroll: CGFloat,
|
|
280
|
+
layout: ScreenLayout,
|
|
281
|
+
theme: Theme,
|
|
282
|
+
pop: (() -> Void)?
|
|
283
|
+
) -> InputSearchProps {
|
|
280
284
|
var themed = props
|
|
285
|
+
if !props.hasButtonTextAction, let pop { themed.onPressButtonText = pop }
|
|
286
|
+
guard props.backgroundColor == nil else { return themed }
|
|
287
|
+
let collapsedTop = max(layout.headerStatusBarHeight + (layout.headerBarHeight - HEADER_HEIGHT) / 2, 1)
|
|
281
288
|
themed.backgroundColor = theme.colors.background.surface
|
|
282
289
|
.lerp(to: theme.colors.background.default, max(scroll, 0) / collapsedTop)
|
|
283
290
|
return themed
|
|
@@ -338,6 +345,7 @@ public struct NavigationHeader: View {
|
|
|
338
345
|
|
|
339
346
|
@Environment(\.screenLayout) private var layout
|
|
340
347
|
@Environment(\.appTheme) private var theme
|
|
348
|
+
@OptionalEnvironmentObject private var navigator: Navigator?
|
|
341
349
|
@State private var headerRightWidth: CGFloat = 0
|
|
342
350
|
|
|
343
351
|
/// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
|
|
@@ -437,7 +445,13 @@ public struct NavigationHeader: View {
|
|
|
437
445
|
@ViewBuilder
|
|
438
446
|
private func titleArea(centered: Bool) -> some View {
|
|
439
447
|
if let inputSearchProps {
|
|
440
|
-
InputSearch(props: headerSearchProps(
|
|
448
|
+
InputSearch(props: headerSearchProps(
|
|
449
|
+
inputSearchProps,
|
|
450
|
+
scroll: scrollState,
|
|
451
|
+
layout: layout,
|
|
452
|
+
theme: theme,
|
|
453
|
+
pop: navigator.map { navigator in { navigator.pop() } }
|
|
454
|
+
))
|
|
441
455
|
} else {
|
|
442
456
|
switch headerTitle {
|
|
443
457
|
case let .user(data)?:
|