@momo-kits/native-kits 0.300.3-debug → 0.300.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.
- package/CLAUDE.md +51 -7
- package/README.md +2 -2
- package/compose/build.gradle.kts +10 -2
- package/compose/build.gradle.kts.backup +8 -0
- package/compose/compose.podspec +1 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +55 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +6 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +138 -0
- package/gradle/libs.versions.toml +3 -14
- package/gradle/wrapper/gradle-wrapper.properties +0 -2
- package/gradle.properties +3 -5
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +8 -2
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +415 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +7 -3
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- 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/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- package/ios/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,545 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Header.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/component/Header.kt`: the header bar (back button,
|
|
6
|
+
// title, header right), its colors, opacity and the header models.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
/// Header bar height, mirrors Compose's `HEADER_HEIGHT`.
|
|
12
|
+
public let HEADER_HEIGHT: CGFloat = 52
|
|
13
|
+
|
|
14
|
+
/// Room the back button and toolkit take from the bar (Compose `HEADER_ACTIONS_WIDTH`).
|
|
15
|
+
let headerActionsWidth: CGFloat = 172
|
|
16
|
+
// Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
|
|
17
|
+
private let statusBarRowActionsExtra: CGFloat = 55
|
|
18
|
+
|
|
19
|
+
public enum TitlePosition {
|
|
20
|
+
case left, center
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
public enum HeaderType {
|
|
24
|
+
case `default`, extended, none
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Mirrors Compose's `HeaderType.Animated`; `layoutOffset` lives on `NavigationOptions`.
|
|
28
|
+
public struct AnimatedHeader {
|
|
29
|
+
public var aspectRatio: AnimatedHeaderRatio = .ratio16_9
|
|
30
|
+
public var isSurface: Bool = true
|
|
31
|
+
public var composable: (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
32
|
+
|
|
33
|
+
public init(
|
|
34
|
+
aspectRatio: AnimatedHeaderRatio = .ratio16_9,
|
|
35
|
+
isSurface: Bool = true,
|
|
36
|
+
composable: @escaping (_ scrollState: CGFloat) -> AnyView = { _ in AnyView(EmptyView()) }
|
|
37
|
+
) {
|
|
38
|
+
self.aspectRatio = aspectRatio
|
|
39
|
+
self.isSurface = isSurface
|
|
40
|
+
self.composable = composable
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// MARK: - Header title
|
|
45
|
+
|
|
46
|
+
/// Mirrors Compose's `HeaderTitle.User`.
|
|
47
|
+
public struct HeaderUserData {
|
|
48
|
+
public var title: String
|
|
49
|
+
public var subTitle: String?
|
|
50
|
+
public var image: [String]?
|
|
51
|
+
public var dotColor: Color?
|
|
52
|
+
public var tintColor: Color?
|
|
53
|
+
public var onPress: (() -> Void)?
|
|
54
|
+
public var icons: [String]
|
|
55
|
+
public var isLoading: Bool
|
|
56
|
+
|
|
57
|
+
public init(
|
|
58
|
+
title: String,
|
|
59
|
+
subTitle: String? = nil,
|
|
60
|
+
image: [String]? = nil,
|
|
61
|
+
dotColor: Color? = nil,
|
|
62
|
+
tintColor: Color? = nil,
|
|
63
|
+
onPress: (() -> Void)? = nil,
|
|
64
|
+
icons: [String] = [],
|
|
65
|
+
isLoading: Bool = false
|
|
66
|
+
) {
|
|
67
|
+
self.title = title
|
|
68
|
+
self.subTitle = subTitle
|
|
69
|
+
self.image = image
|
|
70
|
+
self.dotColor = dotColor
|
|
71
|
+
self.tintColor = tintColor
|
|
72
|
+
self.onPress = onPress
|
|
73
|
+
self.icons = icons
|
|
74
|
+
self.isLoading = isLoading
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Mirrors Compose's `HeaderTitle.Location`. Colors are CSS hex strings, as in RN.
|
|
79
|
+
public struct HeaderLocationData {
|
|
80
|
+
public var description: String?
|
|
81
|
+
public var location: String
|
|
82
|
+
public var tintColor: String?
|
|
83
|
+
public var onPress: (() -> Void)?
|
|
84
|
+
public var isLoading: Bool
|
|
85
|
+
|
|
86
|
+
public init(
|
|
87
|
+
description: String? = nil,
|
|
88
|
+
location: String,
|
|
89
|
+
tintColor: String? = nil,
|
|
90
|
+
onPress: (() -> Void)? = nil,
|
|
91
|
+
isLoading: Bool = false
|
|
92
|
+
) {
|
|
93
|
+
self.description = description
|
|
94
|
+
self.location = location
|
|
95
|
+
self.tintColor = tintColor
|
|
96
|
+
self.onPress = onPress
|
|
97
|
+
self.isLoading = isLoading
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Mirrors Compose's `HeaderTitle.Journey`. Colors are CSS hex strings, as in RN.
|
|
102
|
+
public struct HeaderJourneyData {
|
|
103
|
+
public var start: String
|
|
104
|
+
public var end: String?
|
|
105
|
+
public var description: String?
|
|
106
|
+
public var icon: String
|
|
107
|
+
public var iconColor: String?
|
|
108
|
+
public var tintColor: String?
|
|
109
|
+
public var onPress: (() -> Void)?
|
|
110
|
+
public var isLoading: Bool
|
|
111
|
+
|
|
112
|
+
public init(
|
|
113
|
+
start: String,
|
|
114
|
+
end: String? = nil,
|
|
115
|
+
description: String? = nil,
|
|
116
|
+
icon: String,
|
|
117
|
+
iconColor: String? = nil,
|
|
118
|
+
tintColor: String? = nil,
|
|
119
|
+
onPress: (() -> Void)? = nil,
|
|
120
|
+
isLoading: Bool = false
|
|
121
|
+
) {
|
|
122
|
+
self.start = start
|
|
123
|
+
self.end = end
|
|
124
|
+
self.description = description
|
|
125
|
+
self.icon = icon
|
|
126
|
+
self.iconColor = iconColor
|
|
127
|
+
self.tintColor = tintColor
|
|
128
|
+
self.onPress = onPress
|
|
129
|
+
self.isLoading = isLoading
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/// Mirrors Compose's `sealed class HeaderTitle`.
|
|
134
|
+
public enum HeaderTitle {
|
|
135
|
+
case `default`(String)
|
|
136
|
+
case user(HeaderUserData)
|
|
137
|
+
case location(HeaderLocationData)
|
|
138
|
+
case journey(HeaderJourneyData)
|
|
139
|
+
|
|
140
|
+
/// Plain title text when this is a `.default` title.
|
|
141
|
+
public var defaultTitle: String? {
|
|
142
|
+
if case let .default(text) = self { return text }
|
|
143
|
+
return nil
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// MARK: - Back props
|
|
148
|
+
|
|
149
|
+
/// Compose's `PopupNotifyProps` as consumed by `headerBackProps.preventBack`.
|
|
150
|
+
public struct PreventBackProps {
|
|
151
|
+
public var image: String
|
|
152
|
+
public var title: String
|
|
153
|
+
public var description: String
|
|
154
|
+
public var error: String
|
|
155
|
+
public var primary: PopupAction?
|
|
156
|
+
public var secondary: PopupAction?
|
|
157
|
+
public var buttonDirection: PopupButtonDirection
|
|
158
|
+
public var isShowCloseIcon: Bool
|
|
159
|
+
public var onIconClose: (() -> Void)?
|
|
160
|
+
|
|
161
|
+
public init(
|
|
162
|
+
image: String = "",
|
|
163
|
+
title: String = "",
|
|
164
|
+
description: String = "",
|
|
165
|
+
error: String = "",
|
|
166
|
+
primary: PopupAction? = nil,
|
|
167
|
+
secondary: PopupAction? = nil,
|
|
168
|
+
buttonDirection: PopupButtonDirection = .row,
|
|
169
|
+
isShowCloseIcon: Bool = true,
|
|
170
|
+
onIconClose: (() -> Void)? = nil
|
|
171
|
+
) {
|
|
172
|
+
self.image = image
|
|
173
|
+
self.title = title
|
|
174
|
+
self.description = description
|
|
175
|
+
self.error = error
|
|
176
|
+
self.primary = primary
|
|
177
|
+
self.secondary = secondary
|
|
178
|
+
self.buttonDirection = buttonDirection
|
|
179
|
+
self.isShowCloseIcon = isShowCloseIcon
|
|
180
|
+
self.onIconClose = onIconClose
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/// Mirrors Compose's `HeaderBackProps`: with `preventBack`, `Navigator.onBackSafe` asks first.
|
|
185
|
+
public struct HeaderBackProps {
|
|
186
|
+
public var preventBack: PreventBackProps?
|
|
187
|
+
|
|
188
|
+
public init(preventBack: PreventBackProps? = nil) {
|
|
189
|
+
self.preventBack = preventBack
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// MARK: - Header colors
|
|
194
|
+
|
|
195
|
+
/// Mirrors Compose's `HeaderColor`.
|
|
196
|
+
public struct HeaderColor: Equatable {
|
|
197
|
+
public let tintIconColor: Color
|
|
198
|
+
public let backgroundButton: Color
|
|
199
|
+
public let borderColor: Color
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Mirrors Compose's `getHeaderColor`; an animated header is a non-nil `AnimatedHeader` here.
|
|
203
|
+
func getHeaderColor(
|
|
204
|
+
isAnimated: Bool,
|
|
205
|
+
isSurface: Bool,
|
|
206
|
+
opacity: CGFloat,
|
|
207
|
+
tintColor: Color?,
|
|
208
|
+
defaultColor: Color = Colors.black17
|
|
209
|
+
) -> HeaderColor {
|
|
210
|
+
if isAnimated {
|
|
211
|
+
if opacity >= 1 || !isSurface {
|
|
212
|
+
return HeaderColor(
|
|
213
|
+
tintIconColor: Colors.black17,
|
|
214
|
+
backgroundButton: Colors.black01.opacity(0.6),
|
|
215
|
+
borderColor: Colors.black20.opacity(0.2)
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
return HeaderColor(
|
|
219
|
+
tintIconColor: Colors.black01,
|
|
220
|
+
backgroundButton: Colors.black20.opacity(0.6),
|
|
221
|
+
borderColor: .clear
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// SwiftUI's `.white` is not equal to `Colors.black01`; both must get the dark button, as on the side bar
|
|
226
|
+
if tintColor == Colors.black01 || tintColor == .white {
|
|
227
|
+
return HeaderColor(
|
|
228
|
+
tintIconColor: Colors.black01,
|
|
229
|
+
backgroundButton: Colors.black20.opacity(0.6),
|
|
230
|
+
borderColor: Colors.black01.opacity(0.2)
|
|
231
|
+
)
|
|
232
|
+
}
|
|
233
|
+
return HeaderColor(
|
|
234
|
+
tintIconColor: tintColor ?? defaultColor,
|
|
235
|
+
backgroundButton: Colors.black01.opacity(0.6),
|
|
236
|
+
borderColor: Colors.black20.opacity(0.2)
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// Mirrors Compose's `headerOpacity()`: the bar turns opaque over one header height of scroll,
|
|
241
|
+
/// over `layoutOffset` for an animated header, and never for a transparent one.
|
|
242
|
+
func headerOpacity(
|
|
243
|
+
scrollOffset: CGFloat,
|
|
244
|
+
isAnimated: Bool,
|
|
245
|
+
layoutOffset: CGFloat,
|
|
246
|
+
transparent: Bool
|
|
247
|
+
) -> CGFloat {
|
|
248
|
+
if transparent { return 0 }
|
|
249
|
+
let distance = isAnimated ? layoutOffset : HEADER_HEIGHT
|
|
250
|
+
if distance <= 0 { return scrollOffset > 0 ? 1 : 0 }
|
|
251
|
+
return min(max(scrollOffset / distance, 0), 1)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
private struct HeaderColorKey: EnvironmentKey {
|
|
255
|
+
static let defaultValue: HeaderColor? = nil
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
extension EnvironmentValues {
|
|
259
|
+
/// The enclosing header's colors, so a toolkit follows the bar over a banner image.
|
|
260
|
+
var headerColor: HeaderColor? {
|
|
261
|
+
get { self[HeaderColorKey.self] }
|
|
262
|
+
set { self[HeaderColorKey.self] = newValue }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/// Measured width of the header-right area, read by the animated search for its trailing inset
|
|
267
|
+
/// (Compose's `LocalHeaderRightWidthPx`).
|
|
268
|
+
public struct HeaderRightWidthKey: PreferenceKey {
|
|
269
|
+
public static var defaultValue: CGFloat = 0
|
|
270
|
+
public static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) {
|
|
271
|
+
value = max(value, nextValue())
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/// Compose `SearchAnimated`: a header search fills from surface to the screen background over
|
|
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 {
|
|
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)
|
|
288
|
+
themed.backgroundColor = theme.colors.background.surface
|
|
289
|
+
.lerp(to: theme.colors.background.default, max(scroll, 0) / collapsedTop)
|
|
290
|
+
return themed
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// Max title width: back button and toolkit share the bar unless the iPhone Duo side bar holds them.
|
|
294
|
+
func headerTitleMaxWidth(_ layout: ScreenLayout) -> CGFloat {
|
|
295
|
+
let clearance = layout.statusBarClearance.map { $0.left + $0.right } ?? 0
|
|
296
|
+
if layout.sideBar != nil { return layout.contentWidth - Spacing.M * 2 - clearance }
|
|
297
|
+
return layout.width - headerActionsWidth - (layout.statusBarClearance != nil ? statusBarRowActionsExtra : 0) - clearance
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// MARK: - Header
|
|
301
|
+
|
|
302
|
+
/// Mirrors Compose's `Header`.
|
|
303
|
+
public struct NavigationHeader: View {
|
|
304
|
+
var headerType: HeaderType = .default
|
|
305
|
+
var titlePosition: TitlePosition
|
|
306
|
+
var title: String
|
|
307
|
+
var headerTitle: HeaderTitle?
|
|
308
|
+
var headerRight: (() -> any View)?
|
|
309
|
+
var goBack: (() -> Void)?
|
|
310
|
+
var opacity: CGFloat = 1
|
|
311
|
+
var animatedHeader: AnimatedHeader?
|
|
312
|
+
var scrollState: CGFloat = 0
|
|
313
|
+
var inputSearchProps: InputSearchProps?
|
|
314
|
+
var tintColor: Color?
|
|
315
|
+
/// The animated search fades the title out as it rises into the bar.
|
|
316
|
+
var titleOpacity: CGFloat = 1
|
|
317
|
+
|
|
318
|
+
public init(
|
|
319
|
+
headerType: HeaderType = .default,
|
|
320
|
+
titlePosition: TitlePosition,
|
|
321
|
+
title: String,
|
|
322
|
+
headerTitle: HeaderTitle? = nil,
|
|
323
|
+
headerRight: (() -> any View)? = { HeaderRight() },
|
|
324
|
+
goBack: (() -> Void)? = nil,
|
|
325
|
+
opacity: CGFloat = 1,
|
|
326
|
+
animatedHeader: AnimatedHeader? = nil,
|
|
327
|
+
scrollState: CGFloat = 0,
|
|
328
|
+
inputSearchProps: InputSearchProps? = nil,
|
|
329
|
+
tintColor: Color? = nil,
|
|
330
|
+
titleOpacity: CGFloat = 1
|
|
331
|
+
) {
|
|
332
|
+
self.headerType = headerType
|
|
333
|
+
self.titlePosition = titlePosition
|
|
334
|
+
self.title = title
|
|
335
|
+
self.headerTitle = headerTitle
|
|
336
|
+
self.headerRight = headerRight
|
|
337
|
+
self.goBack = goBack
|
|
338
|
+
self.opacity = opacity
|
|
339
|
+
self.animatedHeader = animatedHeader
|
|
340
|
+
self.scrollState = scrollState
|
|
341
|
+
self.inputSearchProps = inputSearchProps
|
|
342
|
+
self.tintColor = tintColor
|
|
343
|
+
self.titleOpacity = titleOpacity
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
@Environment(\.screenLayout) private var layout
|
|
347
|
+
@Environment(\.appTheme) private var theme
|
|
348
|
+
@OptionalEnvironmentObject private var navigator: Navigator?
|
|
349
|
+
@State private var headerRightWidth: CGFloat = 0
|
|
350
|
+
|
|
351
|
+
/// iPhone Duo side bar holds back and the toolkit; a custom header right stays in the bar.
|
|
352
|
+
private var useSideBar: Bool { layout.sideBar != nil }
|
|
353
|
+
|
|
354
|
+
/// iPhone Duo inner portrait: the row sits on the status bar, between its clusters, with 48pt controls.
|
|
355
|
+
private var statusBarRow: Bool { layout.statusBarClearance != nil && !useSideBar }
|
|
356
|
+
|
|
357
|
+
/// Max centered title width; on the status bar row the clusters are lopsided, so both sides keep the wider one's room.
|
|
358
|
+
private var centerTitleMaxWidth: CGFloat {
|
|
359
|
+
guard let clearance = layout.statusBarClearance else { return (layout.contentWidth - Spacing.M * 2) / 2 }
|
|
360
|
+
let back = goBack == nil || useSideBar ? 0 : DuoMetrics.controlSize + Spacing.S
|
|
361
|
+
let inset = max(clearance.left + back, clearance.right + headerRightWidth) + Spacing.S
|
|
362
|
+
return max(layout.contentWidth - (Spacing.M + inset) * 2, 0)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
private var headerColor: HeaderColor {
|
|
366
|
+
getHeaderColor(
|
|
367
|
+
isAnimated: animatedHeader != nil,
|
|
368
|
+
isSurface: animatedHeader?.isSurface ?? true,
|
|
369
|
+
opacity: opacity,
|
|
370
|
+
tintColor: tintColor
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/// A centered title overlays the whole bar so it stays screen-centered.
|
|
375
|
+
private var centerTitle: Bool {
|
|
376
|
+
guard inputSearchProps == nil else { return false }
|
|
377
|
+
return titlePosition == .center && (headerTitle == nil || headerTitle?.defaultTitle != nil)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
public var body: some View {
|
|
381
|
+
if headerType != .none {
|
|
382
|
+
ZStack {
|
|
383
|
+
HStack(spacing: 0) {
|
|
384
|
+
if let goBack, !useSideBar {
|
|
385
|
+
if statusBarRow {
|
|
386
|
+
SideBarButton(
|
|
387
|
+
icon: "arrow-back",
|
|
388
|
+
headerColor: headerColor,
|
|
389
|
+
accessibilityId: "btn_navigation_back",
|
|
390
|
+
action: goBack
|
|
391
|
+
)
|
|
392
|
+
} else {
|
|
393
|
+
BackButton(color: headerColor, onBack: goBack)
|
|
394
|
+
}
|
|
395
|
+
Spacer().frame(width: Spacing.S)
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if centerTitle {
|
|
399
|
+
Spacer()
|
|
400
|
+
} else {
|
|
401
|
+
titleArea(centered: false)
|
|
402
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
if let headerRight, !(useSideBar && toolkitHeaderRight(headerRight) != nil) {
|
|
406
|
+
Group {
|
|
407
|
+
if statusBarRow, let toolkit = toolkitHeaderRight(headerRight) {
|
|
408
|
+
Toolkit(
|
|
409
|
+
headerRight: toolkit.data ?? HeaderRightData(),
|
|
410
|
+
tintColor: toolkit.tintColor,
|
|
411
|
+
layout: .sideBarRow
|
|
412
|
+
)
|
|
413
|
+
} else {
|
|
414
|
+
AnyView(headerRight())
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
.environment(\.headerColor, headerColor)
|
|
418
|
+
.background(
|
|
419
|
+
GeometryReader { geo in
|
|
420
|
+
Color.clear.preference(key: HeaderRightWidthKey.self, value: geo.size.width)
|
|
421
|
+
}
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
.padding(.leading, Spacing.M + (layout.statusBarClearance?.left ?? 0))
|
|
426
|
+
.padding(.trailing, Spacing.M + (layout.statusBarClearance?.right ?? 0))
|
|
427
|
+
|
|
428
|
+
if centerTitle {
|
|
429
|
+
titleArea(centered: true)
|
|
430
|
+
// Compose caps a centered title at half the bar
|
|
431
|
+
.frame(maxWidth: centerTitleMaxWidth)
|
|
432
|
+
.padding(.horizontal, Spacing.M)
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
.onPreferenceChange(HeaderRightWidthKey.self) { headerRightWidth = $0 }
|
|
436
|
+
.frame(height: layout.headerBarHeight)
|
|
437
|
+
.overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
|
|
438
|
+
// the row starts headerStatusBarHeight below the window top: 0 with the status bar off
|
|
439
|
+
// the top edge, lower on the iPhone Duo outer display to center on the camera
|
|
440
|
+
.padding(.top, layout.headerStatusBarHeight - layout.insets.top)
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// Mirrors Compose's `HeaderContent`; a header search replaces the title.
|
|
445
|
+
@ViewBuilder
|
|
446
|
+
private func titleArea(centered: Bool) -> some View {
|
|
447
|
+
if let inputSearchProps {
|
|
448
|
+
InputSearch(props: headerSearchProps(
|
|
449
|
+
inputSearchProps,
|
|
450
|
+
scroll: scrollState,
|
|
451
|
+
layout: layout,
|
|
452
|
+
theme: theme,
|
|
453
|
+
pop: navigator.map { navigator in { navigator.pop() } }
|
|
454
|
+
))
|
|
455
|
+
} else {
|
|
456
|
+
switch headerTitle {
|
|
457
|
+
case let .user(data)?:
|
|
458
|
+
HeaderUserView(data)
|
|
459
|
+
case let .location(data)?:
|
|
460
|
+
HeaderLocationView(data, tintColor: headerColor.tintIconColor)
|
|
461
|
+
case let .journey(data)?:
|
|
462
|
+
HeaderJourneyView(data, tintColor: headerColor.tintIconColor)
|
|
463
|
+
case let .default(text)?:
|
|
464
|
+
HeaderTitleText(text, color: headerColor.tintIconColor, centered: centered)
|
|
465
|
+
.opacity(titleOpacity)
|
|
466
|
+
case nil:
|
|
467
|
+
HeaderTitleText(title, color: headerColor.tintIconColor, centered: centered)
|
|
468
|
+
.opacity(titleOpacity)
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// MARK: - Bar background
|
|
475
|
+
|
|
476
|
+
/// Compose `Header`'s own background: surface (or the theme's header image) fading in with the
|
|
477
|
+
/// scroll; a transparent header stays clear.
|
|
478
|
+
struct HeaderBarBackground: View {
|
|
479
|
+
let opacity: CGFloat
|
|
480
|
+
var transparent: Bool = false
|
|
481
|
+
|
|
482
|
+
@Environment(\.appTheme) private var theme
|
|
483
|
+
@Environment(\.screenLayout) private var layout
|
|
484
|
+
|
|
485
|
+
var body: some View {
|
|
486
|
+
Group {
|
|
487
|
+
if transparent {
|
|
488
|
+
Color.clear
|
|
489
|
+
} else if theme.assets.isHeaderImage, let image = theme.assets.headerBackground {
|
|
490
|
+
ZStack(alignment: .top) {
|
|
491
|
+
theme.colors.background.surface
|
|
492
|
+
ImageView(image, contentMode: .fill, loading: false)
|
|
493
|
+
.frame(height: HEADER_BACKGROUND_HEIGHT)
|
|
494
|
+
}
|
|
495
|
+
.clipped()
|
|
496
|
+
.opacity(opacity)
|
|
497
|
+
} else {
|
|
498
|
+
theme.colors.background.surface.opacity(opacity)
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
.frame(height: layout.headerHeight)
|
|
502
|
+
.allowsHitTesting(false)
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// MARK: - Back button
|
|
507
|
+
|
|
508
|
+
/// Mirrors Compose's `BackButton`: a 28pt circle with the back arrow.
|
|
509
|
+
private struct BackButton: View {
|
|
510
|
+
let color: HeaderColor
|
|
511
|
+
let onBack: () -> Void
|
|
512
|
+
|
|
513
|
+
var body: some View {
|
|
514
|
+
SwiftUI.Button(action: onBack) {
|
|
515
|
+
Circle()
|
|
516
|
+
.fill(color.backgroundButton)
|
|
517
|
+
.overlay(Circle().stroke(color.borderColor, lineWidth: 0.2))
|
|
518
|
+
.frame(width: 28, height: 28)
|
|
519
|
+
.overlay(Icon(source: "arrow-back", size: 20, color: color.tintIconColor))
|
|
520
|
+
}
|
|
521
|
+
.buttonStyle(ActiveOpacityButtonStyle())
|
|
522
|
+
.accessibilityIdentifier("btn_navigation_back")
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// MARK: - Vertical shadow
|
|
527
|
+
|
|
528
|
+
/// Mirrors Compose's `VerticalShadow`: shown once the bar is fully opaque.
|
|
529
|
+
private struct VerticalShadow: View {
|
|
530
|
+
let opacity: CGFloat
|
|
531
|
+
|
|
532
|
+
var body: some View {
|
|
533
|
+
if opacity >= 1 {
|
|
534
|
+
LinearGradient(
|
|
535
|
+
gradient: Gradient(colors: [Color.black.opacity(0.05), .clear]),
|
|
536
|
+
startPoint: .top,
|
|
537
|
+
endPoint: .bottom
|
|
538
|
+
)
|
|
539
|
+
.frame(height: 6)
|
|
540
|
+
.frame(maxWidth: .infinity)
|
|
541
|
+
.offset(y: 6)
|
|
542
|
+
.allowsHitTesting(false)
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HeaderBackground.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/component/HeaderBackground.kt`: the band behind the
|
|
6
|
+
// header — the pink gradient (or the theme's header image) fading into the screen background.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
public enum AnimatedHeaderRatio {
|
|
12
|
+
case ratio16_9
|
|
13
|
+
case ratio1_1
|
|
14
|
+
case ratio3_2
|
|
15
|
+
|
|
16
|
+
var value: CGFloat {
|
|
17
|
+
switch self {
|
|
18
|
+
case .ratio16_9: return 16.0 / 9.0
|
|
19
|
+
case .ratio1_1: return 1.0
|
|
20
|
+
case .ratio3_2: return 3.0 / 2.0
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let HEADER_BACKGROUND_HEIGHT: CGFloat = 154
|
|
26
|
+
|
|
27
|
+
/// Lay it out top-aligned: an extended band runs past the header into the content.
|
|
28
|
+
public struct NavigationHeaderBackground: View {
|
|
29
|
+
var headerType: HeaderType = .default
|
|
30
|
+
var scrollState: CGFloat
|
|
31
|
+
var headerTransparent: Bool = false
|
|
32
|
+
var fullScreenContent: Bool = false
|
|
33
|
+
/// The screen background the band fades into; the theme's `background.default` when nil.
|
|
34
|
+
var backgroundColor: Color?
|
|
35
|
+
|
|
36
|
+
public init(
|
|
37
|
+
headerType: HeaderType = .default,
|
|
38
|
+
scrollState: CGFloat = 0,
|
|
39
|
+
headerTransparent: Bool = false,
|
|
40
|
+
fullScreenContent: Bool = false,
|
|
41
|
+
backgroundColor: Color? = nil
|
|
42
|
+
) {
|
|
43
|
+
self.headerType = headerType
|
|
44
|
+
self.scrollState = scrollState
|
|
45
|
+
self.headerTransparent = headerTransparent
|
|
46
|
+
self.fullScreenContent = fullScreenContent
|
|
47
|
+
self.backgroundColor = backgroundColor
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Environment(\.screenLayout) private var layout
|
|
51
|
+
@Environment(\.appTheme) private var theme
|
|
52
|
+
|
|
53
|
+
public var body: some View {
|
|
54
|
+
let opacity = max(0, 1 - scrollState / HEADER_HEIGHT)
|
|
55
|
+
let background = backgroundColor ?? theme.colors.background.default
|
|
56
|
+
let topColor = opacity == 0 ? background : Color(hex: 0xFDCADE)
|
|
57
|
+
let minHeight = fullScreenContent ? 0 : layout.headerHeight
|
|
58
|
+
let isHeaderImage = theme.assets.isHeaderImage
|
|
59
|
+
|
|
60
|
+
let height: CGFloat = {
|
|
61
|
+
if headerTransparent { return 0 }
|
|
62
|
+
switch headerType {
|
|
63
|
+
case .default: return minHeight
|
|
64
|
+
// a header image collapses to the bar once scrolled, the gradient keeps its band
|
|
65
|
+
case .extended: return isHeaderImage && opacity == 0 ? minHeight : HEADER_BACKGROUND_HEIGHT
|
|
66
|
+
case .none: return 0
|
|
67
|
+
}
|
|
68
|
+
}()
|
|
69
|
+
|
|
70
|
+
ZStack(alignment: .top) {
|
|
71
|
+
background
|
|
72
|
+
if isHeaderImage, let image = theme.assets.headerBackground {
|
|
73
|
+
ImageView(image, contentMode: .fill, loading: false)
|
|
74
|
+
.frame(height: HEADER_BACKGROUND_HEIGHT)
|
|
75
|
+
} else {
|
|
76
|
+
LinearGradient(colors: [topColor, background], startPoint: .top, endPoint: .bottom)
|
|
77
|
+
.frame(height: HEADER_BACKGROUND_HEIGHT)
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
.frame(height: height, alignment: .top)
|
|
81
|
+
.clipped()
|
|
82
|
+
.allowsHitTesting(false)
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// MARK: - iPhone Duo glass header gradient
|
|
87
|
+
|
|
88
|
+
// extended headers run their gradient this far below the header, as off Duo
|
|
89
|
+
private let duoGradientExtra: CGFloat = 52
|
|
90
|
+
// scroll distance over which the extended gradient collapses to the header
|
|
91
|
+
private let duoExtendedCollapseDistance: CGFloat = 100
|
|
92
|
+
|
|
93
|
+
// scroll distance over which the bar fades in
|
|
94
|
+
private let duoBarFadeDistance: CGFloat = 16
|
|
95
|
+
|
|
96
|
+
/// Port of RN `DuoGlassBackground`: translucent surface with a hairline, shown once the content
|
|
97
|
+
/// scrolls `fadeStart` under the header.
|
|
98
|
+
struct DuoHeaderBar: View {
|
|
99
|
+
let scrollOffset: CGFloat
|
|
100
|
+
var fadeStart: CGFloat = 0
|
|
101
|
+
|
|
102
|
+
@Environment(\.appTheme) private var theme
|
|
103
|
+
|
|
104
|
+
var body: some View {
|
|
105
|
+
theme.colors.background.surface.opacity(0.8)
|
|
106
|
+
.overlay(alignment: .bottom) {
|
|
107
|
+
theme.colors.border.default.frame(height: 0.5)
|
|
108
|
+
}
|
|
109
|
+
.opacity(min(max((scrollOffset - fadeStart) / duoBarFadeDistance, 0), 1))
|
|
110
|
+
.allowsHitTesting(false)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Port of RN `DuoScrollGradient`: with the glass header the content scrolls under the bar, so the
|
|
115
|
+
/// header gradient is drawn under it. Compose draws this inside `HeaderBackground`.
|
|
116
|
+
struct DuoScrollGradient: View {
|
|
117
|
+
let scrollOffset: CGFloat
|
|
118
|
+
let headerHeight: CGFloat
|
|
119
|
+
let extended: Bool
|
|
120
|
+
/// Quick return header: the gradient leaves with the content instead of fading.
|
|
121
|
+
var followScroll = false
|
|
122
|
+
|
|
123
|
+
@Environment(\.appTheme) private var theme
|
|
124
|
+
|
|
125
|
+
private var scroll: CGFloat { max(scrollOffset, 0) }
|
|
126
|
+
|
|
127
|
+
var body: some View {
|
|
128
|
+
Group {
|
|
129
|
+
if followScroll {
|
|
130
|
+
gradient
|
|
131
|
+
.frame(height: headerHeight + (extended ? duoGradientExtra : 0))
|
|
132
|
+
.offset(y: -scroll)
|
|
133
|
+
} else {
|
|
134
|
+
// fades out on scroll as the extended header does off Duo; extended also collapses
|
|
135
|
+
let progress = min(scroll / duoExtendedCollapseDistance, 1)
|
|
136
|
+
gradient
|
|
137
|
+
.frame(height: headerHeight + (extended ? duoGradientExtra * (1 - progress) : 0))
|
|
138
|
+
.opacity(Double(1 - min(scroll / duoGradientExtra, 1)))
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
142
|
+
.ignoresSafeArea()
|
|
143
|
+
.allowsHitTesting(false)
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// The fade spans the gradient box.
|
|
147
|
+
private var gradient: LinearGradient {
|
|
148
|
+
LinearGradient(
|
|
149
|
+
colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
|
|
150
|
+
startPoint: .top,
|
|
151
|
+
endPoint: .bottom
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|