@momo-kits/native-kits 0.167.1-beta.9-debug → 0.167.1-duo-search.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +36 -0
- package/ios/Application/ApplicationEnvironment.swift +6 -0
- package/ios/Application/Components.swift +1 -3
- package/ios/Application/HeaderRight.swift +32 -14
- package/ios/Application/Layout/ScreenLayout.swift +312 -0
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +67 -10
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
- package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/Application/Navigation/HeaderUser.swift +4 -4
- package/ios/Application/Navigation/NavigationContainer.swift +48 -42
- package/ios/Application/Navigation/Navigator.swift +122 -29
- package/ios/Application/Navigation/NavigatorManager.swift +13 -6
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +7 -0
- package/ios/Application/Navigation/component/DuoScrollGradient.swift +54 -0
- package/ios/Application/Navigation/component/Header.swift +11 -6
- package/ios/Application/Navigation/component/HeaderBackground.swift +3 -2
- package/ios/Application/Navigation/component/NavigationFooter.swift +1 -1
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +93 -9
- package/ios/Application/Navigation/component/SideBar.swift +155 -0
- package/ios/Application/StackScreen.swift +175 -18
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +59 -5
- package/ios/Colors+Radius+Spacing/Colors.swift +1 -1
- package/ios/Extensions/Color++.swift +20 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Icon/Icon.swift +14 -22
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/Image/Image.swift +16 -5
- package/ios/Lottie/LottieView.swift +4 -2
- package/ios/Popup/PopupDisplay.swift +84 -67
- package/ios/Popup/PopupLayout.swift +45 -0
- package/ios/Popup/PopupNotify.swift +44 -26
- package/ios/Popup/PopupPromotion.swift +18 -6
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/Template/TrustBanner/TrustBanner.swift +69 -31
- package/ios/Typography/FontScale.swift +3 -3
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +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/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/scripts/gen-ios-icons.mjs +36 -0
- package/build.gradle.kts +0 -11
- package/compose/build.gradle.kts +0 -190
- package/compose/build.gradle.kts.backup +0 -190
- 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 -151
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Context.kt +0 -124
- 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/LiteScreen.kt +0 -831
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Localize.kt +0 -269
- 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/WidgetContainer.kt +0 -56
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- 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 -82
- 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 -356
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +0 -113
- 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 -76
- 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 -475
- 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 -266
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +0 -240
- 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 -264
- 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 -328
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -97
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +0 -310
- 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 -323
- 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 -199
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +0 -91
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +0 -552
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +0 -88
- 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 -498
- 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 -234
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +0 -188
- 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 -16
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/KitDesignSystem.kt +0 -14
- 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 -221
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -270
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Card.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +0 -38
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Section.kt +0 -39
- 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 -299
- 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 -129
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +0 -106
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/NavigationContainer.kt +0 -201
- 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 -595
- 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 -175
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +0 -225
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/CurvedContainer.kt +0 -86
- 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 -358
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +0 -83
- 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 -75
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +0 -309
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -40
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +0 -364
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +0 -132
- 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 -71
- 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 -105
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +0 -40
- 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 -213
- 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/local.properties +0 -8
- package/settings.gradle.kts +0 -64
|
@@ -26,9 +26,12 @@ public struct NavigationHeaderRight: View {
|
|
|
26
26
|
struct ToolkitNavigationHeaderRight: View {
|
|
27
27
|
var headerRight: HeaderRightData?
|
|
28
28
|
var tintColor: Color?
|
|
29
|
+
/// iPhone Duo side bar column instead of the bar row.
|
|
30
|
+
var vertical: Bool = false
|
|
29
31
|
@State private var isFavorite: Bool = false
|
|
30
32
|
@State private var isLoading: Bool = false
|
|
31
33
|
@Environment(\.applicationEnvironment) private var environment
|
|
34
|
+
@Environment(\.duoBarCompression) private var compression
|
|
32
35
|
|
|
33
36
|
private func apiValue(_ value: Any?) -> Any {
|
|
34
37
|
value ?? NSNull()
|
|
@@ -142,15 +145,99 @@ struct ToolkitNavigationHeaderRight: View {
|
|
|
142
145
|
return NavigationButtonConfig(icon: icon, onPress: onClickHandler)
|
|
143
146
|
}
|
|
144
147
|
|
|
148
|
+
private var showHelpCenter: Bool {
|
|
149
|
+
environment.applicationContext != nil && showsHelpCenter(environment.applicationContext)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private var closeIcon: String { headerRight?.closeIcon ?? "16_basic_home" }
|
|
153
|
+
|
|
145
154
|
var body: some View {
|
|
155
|
+
Group {
|
|
156
|
+
if vertical {
|
|
157
|
+
verticalBody
|
|
158
|
+
} else {
|
|
159
|
+
horizontalBody
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
.onAppear {
|
|
163
|
+
loadFavoriteState()
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private var showBadge: Bool {
|
|
168
|
+
headerRight?.tools.contains { group in
|
|
169
|
+
group.items.contains { $0.showBadge }
|
|
170
|
+
} ?? false
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/// Back slot, the optional shortcut, then the help/home capsule.
|
|
174
|
+
private var fullToolbarHeight: CGFloat {
|
|
175
|
+
DuoMetrics.controlSize + Spacing.M
|
|
176
|
+
+ (shouldShowShortcut ? DuoMetrics.controlSize + Spacing.M : 0)
|
|
177
|
+
+ (showHelpCenter ? 2 * DuoMetrics.itemPitch : DuoMetrics.controlSize)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
@ViewBuilder
|
|
181
|
+
private var verticalBody: some View {
|
|
182
|
+
Group {
|
|
183
|
+
if compression.toolbarHidden {
|
|
184
|
+
// an open tab rail takes this slot for a moment
|
|
185
|
+
EmptyView()
|
|
186
|
+
} else if compression.toolbarCompressed {
|
|
187
|
+
// HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
|
|
188
|
+
SideBarButton(
|
|
189
|
+
icon: "navigation_menu",
|
|
190
|
+
tintColor: tintColor,
|
|
191
|
+
showBadge: showBadge,
|
|
192
|
+
accessibilityId: "btn_navigation_toolbar",
|
|
193
|
+
action: compression.expandToolbar
|
|
194
|
+
)
|
|
195
|
+
} else {
|
|
196
|
+
toolbarColumn
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// full height, so the tab bar compression decision matches this screen's toolkit
|
|
200
|
+
.onAppear { compression.setToolbarHeight(fullToolbarHeight) }
|
|
201
|
+
.onChange(of: fullToolbarHeight) { compression.setToolbarHeight($0) }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private var toolbarColumn: some View {
|
|
205
|
+
let navButtonConfig = getNavigationButtonConfig()
|
|
206
|
+
return VStack(spacing: Spacing.M) {
|
|
207
|
+
if shouldShowShortcut {
|
|
208
|
+
SideBarButton(
|
|
209
|
+
icon: navButtonConfig.icon,
|
|
210
|
+
tintColor: tintColor,
|
|
211
|
+
showBadge: showBadge,
|
|
212
|
+
disabled: isLoading,
|
|
213
|
+
action: navButtonConfig.onPress
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
SideBarGroup(tintColor: tintColor, itemCount: showHelpCenter ? 2 : 1) {
|
|
217
|
+
if showHelpCenter {
|
|
218
|
+
SideBarGroupItem(
|
|
219
|
+
icon: "help_center",
|
|
220
|
+
tintColor: tintColor,
|
|
221
|
+
accessibilityId: "btn_navigation_help_center",
|
|
222
|
+
action: onPressHelpCenter
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
SideBarGroupItem(
|
|
226
|
+
icon: closeIcon,
|
|
227
|
+
tintColor: tintColor,
|
|
228
|
+
accessibilityId: "btn_navigation_close",
|
|
229
|
+
action: onPressClose
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private var horizontalBody: some View {
|
|
146
236
|
let backgroundButtonColor: Color = tintColor == Colors.black01 ? Colors.black20.opacity(0.6) : Colors.black01.opacity(0.6)
|
|
147
237
|
let borderColor: Color = tintColor == Colors.black01 ? Colors.black01.opacity(0.2) : Colors.black20.opacity(0.2)
|
|
148
238
|
let navButtonConfig = getNavigationButtonConfig()
|
|
149
|
-
let showBadge = headerRight?.tools.contains { group in
|
|
150
|
-
group.items.contains { $0.showBadge }
|
|
151
|
-
} ?? false
|
|
152
239
|
|
|
153
|
-
HStack(alignment: .center, spacing: 0) {
|
|
240
|
+
return HStack(alignment: .center, spacing: 0) {
|
|
154
241
|
if shouldShowShortcut {
|
|
155
242
|
NavigationButton(
|
|
156
243
|
disabled: isLoading,
|
|
@@ -162,7 +249,7 @@ struct ToolkitNavigationHeaderRight: View {
|
|
|
162
249
|
}
|
|
163
250
|
|
|
164
251
|
HStack(alignment: .center, spacing: 0) {
|
|
165
|
-
if
|
|
252
|
+
if showHelpCenter {
|
|
166
253
|
Icon(source: "help_center", size: 20, color: tintColor ?? Colors.black17)
|
|
167
254
|
.padding(4)
|
|
168
255
|
.onTapGesture {
|
|
@@ -174,7 +261,7 @@ struct ToolkitNavigationHeaderRight: View {
|
|
|
174
261
|
.frame(width: 0.5, height: 12)
|
|
175
262
|
}
|
|
176
263
|
|
|
177
|
-
Icon(source:
|
|
264
|
+
Icon(source: closeIcon, size: 20, color: tintColor ?? Colors.black17)
|
|
178
265
|
.padding(4)
|
|
179
266
|
.onTapGesture {
|
|
180
267
|
onPressClose()
|
|
@@ -189,8 +276,5 @@ struct ToolkitNavigationHeaderRight: View {
|
|
|
189
276
|
)
|
|
190
277
|
.padding(.leading, Spacing.S)
|
|
191
278
|
}
|
|
192
|
-
.onAppear {
|
|
193
|
-
loadFavoriteState()
|
|
194
|
-
}
|
|
195
279
|
}
|
|
196
280
|
}
|
|
@@ -0,0 +1,155 @@
|
|
|
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, gap included; 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 - 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
|
+
}
|
|
@@ -102,8 +102,14 @@ public struct StackScreen<Content: View>: View {
|
|
|
102
102
|
@State private var keyboardHeight: CGFloat = 0
|
|
103
103
|
@State private var headerRightWidth: CGFloat = 0
|
|
104
104
|
@State private var isBottomTabRoot: Bool = false
|
|
105
|
+
@State private var headerHide: CGFloat = 0
|
|
106
|
+
@State private var lastScrollOffset: CGFloat = 0
|
|
107
|
+
@State private var headerSettle: Task<Void, Never>?
|
|
105
108
|
|
|
106
109
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
110
|
+
@Environment(\.screenLayout) private var layout
|
|
111
|
+
@Environment(\.duoTabBarInsets) private var duoTabBarInsets
|
|
112
|
+
@State private var footerHeight: CGFloat = 0
|
|
107
113
|
|
|
108
114
|
// Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
|
|
109
115
|
// the once-computed push/back `action`).
|
|
@@ -125,6 +131,63 @@ public struct StackScreen<Content: View>: View {
|
|
|
125
131
|
|
|
126
132
|
private static var headerHeight: CGFloat { 52 }
|
|
127
133
|
|
|
134
|
+
private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
|
|
135
|
+
|
|
136
|
+
private var showsSideBar: Bool {
|
|
137
|
+
layout.sideBar != nil && headerType != .none && !isBottomTabRoot && (goBack != nil || toolkit != nil)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/// iPhone Duo: content scrolls under a glass header instead of starting below it.
|
|
141
|
+
private var useDuoGlass: Bool {
|
|
142
|
+
layout.sideBar != nil && headerType != .none && animatedHeader == nil
|
|
143
|
+
&& inputSearchProps == nil && !headerTransparent && !fullScreenContent
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// iPhone Duo: the header leaves with the content and slides back in on a reverse scroll
|
|
147
|
+
/// (RN DuoQuickReturnHeader). The side bar is drawn apart from the header, so it stays put.
|
|
148
|
+
private var useQuickReturn: Bool {
|
|
149
|
+
layout.sideBar != nil && scrollable && (useDuoGlass || animatedHeader != nil)
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private func updateHeaderHide(_ offset: CGFloat) {
|
|
153
|
+
let delta = offset - lastScrollOffset
|
|
154
|
+
lastScrollOffset = offset
|
|
155
|
+
guard useQuickReturn, offset > 0 else {
|
|
156
|
+
if headerHide != 0 { headerHide = 0 }
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
headerHide = min(max(headerHide + delta, 0), layout.headerHeight)
|
|
160
|
+
settleHeaderWhenScrollStops()
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/// A half-shown header settles fully in or out. SwiftUI before iOS 18 reports no scroll
|
|
164
|
+
/// phase, so the scroll counts as ended once the offset stops changing.
|
|
165
|
+
private func settleHeaderWhenScrollStops() {
|
|
166
|
+
headerSettle?.cancel()
|
|
167
|
+
headerSettle = Task { @MainActor in
|
|
168
|
+
try? await Task.sleep(nanoseconds: 150_000_000)
|
|
169
|
+
guard !Task.isCancelled else { return }
|
|
170
|
+
let height = layout.headerHeight
|
|
171
|
+
// within the first header height the header still sits on the content
|
|
172
|
+
guard scrollOffset >= height, headerHide > 0, headerHide < height else { return }
|
|
173
|
+
withAnimation(.easeOut(duration: 0.2)) {
|
|
174
|
+
headerHide = headerHide > height / 2 ? height : 0
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/// What the footer, tab bar or home indicator takes from the bottom of the window, gap included.
|
|
180
|
+
private var bottomBarsHeight: CGFloat {
|
|
181
|
+
if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom + Spacing.XL }
|
|
182
|
+
if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve }
|
|
183
|
+
return NavigationFooter.navigationBarInset + Spacing.XL
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
|
|
187
|
+
private var keyboardReferenceHeight: CGFloat {
|
|
188
|
+
layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
|
|
189
|
+
}
|
|
190
|
+
|
|
128
191
|
/// Extra top space reserved inside the content so the animated search bar has
|
|
129
192
|
/// room below the header at scroll 0; it shrinks to 0 as the bar slides up.
|
|
130
193
|
private var animatedSearchInset: CGFloat {
|
|
@@ -163,9 +226,18 @@ public struct StackScreen<Content: View>: View {
|
|
|
163
226
|
hasFooter: footer != nil,
|
|
164
227
|
scrollOffset: scrollOffset
|
|
165
228
|
)
|
|
229
|
+
.padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
|
|
166
230
|
}
|
|
167
231
|
|
|
168
232
|
NavigationFooter(footerComponent: footer, keyboardSize: keyboardSize)
|
|
233
|
+
.padding(.bottom, duoTabBarInsets.overlay)
|
|
234
|
+
.background(
|
|
235
|
+
GeometryReader { proxy in
|
|
236
|
+
Color.clear
|
|
237
|
+
.onAppear { footerHeight = proxy.size.height }
|
|
238
|
+
.onChange(of: proxy.size.height) { footerHeight = $0 }
|
|
239
|
+
}
|
|
240
|
+
)
|
|
169
241
|
}
|
|
170
242
|
|
|
171
243
|
// Animated search bar overlay (mirrors Compose's SearchAnimated layer).
|
|
@@ -176,11 +248,29 @@ public struct StackScreen<Content: View>: View {
|
|
|
176
248
|
hiddenBack: !isBack,
|
|
177
249
|
hasHeaderRight: headerRight != nil,
|
|
178
250
|
headerRightWidth: headerRightWidth,
|
|
179
|
-
headerHeight: Self.headerHeight
|
|
251
|
+
headerHeight: Self.headerHeight,
|
|
252
|
+
useSideBar: layout.sideBar != nil,
|
|
253
|
+
hasToolkit: toolkit != nil
|
|
254
|
+
)
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if showsSideBar, let sideBar = layout.sideBar {
|
|
258
|
+
HeaderSideBar(
|
|
259
|
+
sideBar: sideBar,
|
|
260
|
+
goBack: goBack,
|
|
261
|
+
toolkit: toolkit,
|
|
262
|
+
headerColor: getHeaderColor(
|
|
263
|
+
isAnimated: animatedHeader != nil,
|
|
264
|
+
isSurface: animatedHeader?.isSurface ?? true,
|
|
265
|
+
opacity: scrollOpacity,
|
|
266
|
+
tintColor: tintColor
|
|
267
|
+
),
|
|
268
|
+
bottomInset: bottomBarsHeight
|
|
180
269
|
)
|
|
181
270
|
}
|
|
182
271
|
}
|
|
183
272
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
|
273
|
+
.onChange(of: scrollOffset) { updateHeaderHide($0) }
|
|
184
274
|
.onPreferenceChange(HeaderRightWidthKey.self) { width in
|
|
185
275
|
if headerRightWidth != width { headerRightWidth = width }
|
|
186
276
|
}
|
|
@@ -191,7 +281,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
191
281
|
guard useAvoidKeyboard,
|
|
192
282
|
let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
193
283
|
withAnimation(.easeOut(duration: 0.25)) {
|
|
194
|
-
keyboardHeight =
|
|
284
|
+
keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
|
|
195
285
|
}
|
|
196
286
|
}
|
|
197
287
|
// Mirrors Compose's `hideKeyboardOnTap()`: a simultaneous gesture so
|
|
@@ -246,16 +336,27 @@ public struct StackScreen<Content: View>: View {
|
|
|
246
336
|
scrollOffset: $scrollOffset,
|
|
247
337
|
scrollToTopCallback: scrollToTopCallback,
|
|
248
338
|
animatedSearchInset: animatedSearchInset,
|
|
339
|
+
bottomInset: duoTabBarInsets.overlay,
|
|
249
340
|
content: { animatedHeroBody(animatedHeader) }
|
|
250
341
|
)
|
|
251
342
|
.edgesIgnoringSafeArea(.all)
|
|
252
343
|
|
|
253
344
|
header
|
|
254
345
|
.background(
|
|
255
|
-
|
|
256
|
-
.
|
|
257
|
-
|
|
346
|
+
Group {
|
|
347
|
+
if layout.sideBar != nil {
|
|
348
|
+
// glass shows once the banner has scrolled under the header
|
|
349
|
+
Color.clear.modifier(DuoBarMaterial(
|
|
350
|
+
shape: Rectangle(),
|
|
351
|
+
visible: scrollOffset > heroGlassStart(animatedHeader)
|
|
352
|
+
))
|
|
353
|
+
} else {
|
|
354
|
+
Colors.black01.opacity(scrollOpacity)
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
258
358
|
)
|
|
359
|
+
.offset(y: -headerHide)
|
|
259
360
|
.zIndex(100)
|
|
260
361
|
}
|
|
261
362
|
} else if headerTransparent && fullScreenContent {
|
|
@@ -271,9 +372,36 @@ public struct StackScreen<Content: View>: View {
|
|
|
271
372
|
)
|
|
272
373
|
.zIndex(100)
|
|
273
374
|
|
|
274
|
-
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content:
|
|
375
|
+
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: { duoInsetContent })
|
|
275
376
|
.edgesIgnoringSafeArea(.all)
|
|
276
377
|
}
|
|
378
|
+
} else if useDuoGlass {
|
|
379
|
+
ZStack(alignment: .top) {
|
|
380
|
+
DuoScrollGradient(
|
|
381
|
+
scrollOffset: scrollOffset,
|
|
382
|
+
headerHeight: layout.headerHeight,
|
|
383
|
+
extended: headerType == .extended,
|
|
384
|
+
followScroll: useQuickReturn
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
StackScreenContent(
|
|
388
|
+
scrollable: scrollable,
|
|
389
|
+
scrollOffset: $scrollOffset,
|
|
390
|
+
scrollToTopCallback: scrollToTopCallback,
|
|
391
|
+
topInset: layout.headerHeight - layout.insets.top,
|
|
392
|
+
bottomInset: duoTabBarInsets.overlay,
|
|
393
|
+
content: content
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
header
|
|
397
|
+
.background(
|
|
398
|
+
Color.clear
|
|
399
|
+
.modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
|
|
400
|
+
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
401
|
+
)
|
|
402
|
+
.offset(y: -headerHide)
|
|
403
|
+
.zIndex(100)
|
|
404
|
+
}
|
|
277
405
|
} else {
|
|
278
406
|
header
|
|
279
407
|
.background(
|
|
@@ -283,28 +411,40 @@ public struct StackScreen<Content: View>: View {
|
|
|
283
411
|
headerTransparent: headerTransparent,
|
|
284
412
|
fullScreenContent: fullScreenContent
|
|
285
413
|
)
|
|
286
|
-
.edgesIgnoringSafeArea(.top)
|
|
414
|
+
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
287
415
|
)
|
|
288
416
|
|
|
289
|
-
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
|
|
417
|
+
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: content)
|
|
290
418
|
}
|
|
291
419
|
}
|
|
292
420
|
|
|
421
|
+
/// Edge-to-edge branches lay out outside the safe area; iPhone Duo content still keeps
|
|
422
|
+
/// clear of the side strips, as RN's Screen does.
|
|
423
|
+
private var duoInsetContent: some View {
|
|
424
|
+
content()
|
|
425
|
+
.padding(.leading, layout.insetLeft)
|
|
426
|
+
.padding(.trailing, layout.insetRight)
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
private func heroGlassStart(_ animatedHeader: AnimatedHeader) -> CGFloat {
|
|
430
|
+
max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
|
|
431
|
+
}
|
|
432
|
+
|
|
293
433
|
/// Hero image + body for an animated header. The image fills the width at its
|
|
294
434
|
/// aspect-ratio height; the body is offset below the header band so it overlaps
|
|
295
435
|
/// the lower part of the image (mirrors Compose's image box + padded content).
|
|
296
436
|
@ViewBuilder
|
|
297
437
|
private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
|
|
298
|
-
let width =
|
|
438
|
+
let width = layout.width
|
|
299
439
|
let imageHeight = width / animatedHeader.aspectRatio.value
|
|
300
|
-
let contentTopInset =
|
|
440
|
+
let contentTopInset = layout.headerHeight + layoutOffset
|
|
301
441
|
|
|
302
442
|
ZStack(alignment: .top) {
|
|
303
443
|
animatedHeader.composable(scrollOffset)
|
|
304
444
|
.frame(width: width, height: imageHeight)
|
|
305
445
|
.clipped()
|
|
306
446
|
|
|
307
|
-
|
|
447
|
+
duoInsetContent
|
|
308
448
|
.padding(.top, contentTopInset)
|
|
309
449
|
}
|
|
310
450
|
.frame(maxWidth: .infinity, alignment: .top)
|
|
@@ -343,6 +483,10 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
343
483
|
/// Extra top space reserved for the animated search bar (mirrors Compose's
|
|
344
484
|
/// MainContent animated top spacer). Zero for non-animated screens.
|
|
345
485
|
var animatedSearchInset: CGFloat = 0
|
|
486
|
+
/// Room for a header the content scrolls under (iPhone Duo glass header).
|
|
487
|
+
var topInset: CGFloat = 0
|
|
488
|
+
/// Room for a bar that overlays the content (iPhone Duo floating tab bar).
|
|
489
|
+
var bottomInset: CGFloat = 0
|
|
346
490
|
let content: () -> Content
|
|
347
491
|
|
|
348
492
|
private let topAnchorID = "stack_screen_top_anchor"
|
|
@@ -369,13 +513,17 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
369
513
|
}
|
|
370
514
|
})
|
|
371
515
|
|
|
372
|
-
if animatedSearchInset > 0 {
|
|
373
|
-
Spacer().frame(height: animatedSearchInset)
|
|
516
|
+
if animatedSearchInset + topInset > 0 {
|
|
517
|
+
Spacer().frame(height: animatedSearchInset + topInset)
|
|
374
518
|
}
|
|
375
519
|
|
|
376
520
|
VStack {
|
|
377
521
|
content()
|
|
378
522
|
}
|
|
523
|
+
|
|
524
|
+
if bottomInset > 0 {
|
|
525
|
+
Spacer().frame(height: bottomInset)
|
|
526
|
+
}
|
|
379
527
|
}
|
|
380
528
|
}
|
|
381
529
|
.coordinateSpace(name: scrollSpace)
|
|
@@ -390,6 +538,8 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
390
538
|
}
|
|
391
539
|
} else {
|
|
392
540
|
content()
|
|
541
|
+
.padding(.top, topInset)
|
|
542
|
+
.padding(.bottom, bottomInset)
|
|
393
543
|
}
|
|
394
544
|
}
|
|
395
545
|
}
|
|
@@ -407,16 +557,23 @@ private struct SearchAnimatedHeader: View {
|
|
|
407
557
|
let hasHeaderRight: Bool
|
|
408
558
|
let headerRightWidth: CGFloat
|
|
409
559
|
let headerHeight: CGFloat
|
|
560
|
+
var useSideBar: Bool = false
|
|
561
|
+
var hasToolkit: Bool = false
|
|
562
|
+
|
|
563
|
+
@Environment(\.screenLayout) private var layout
|
|
410
564
|
|
|
411
565
|
var body: some View {
|
|
412
566
|
let scroll = max(scrollOffset, 0)
|
|
413
567
|
|
|
414
|
-
// Insets are measured from the safe-area top
|
|
415
|
-
let
|
|
416
|
-
let
|
|
568
|
+
// Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
|
|
569
|
+
let rowTop = layout.headerStatusBarHeight - layout.insets.top
|
|
570
|
+
let minTop = rowTop + (layout.headerBarHeight - headerHeight) / 2
|
|
571
|
+
let maxTop = layout.headerHeight - layout.insets.top
|
|
417
572
|
let minSide = Spacing.M
|
|
418
|
-
let maxStart: CGFloat = hiddenBack ? Spacing.M : 48
|
|
419
|
-
let maxEnd =
|
|
573
|
+
let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
|
|
574
|
+
let maxEnd: CGFloat = useSideBar && hasToolkit
|
|
575
|
+
? Spacing.M
|
|
576
|
+
: headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
|
|
420
577
|
|
|
421
578
|
let top = min(max(maxTop - scroll, minTop), maxTop)
|
|
422
579
|
let start = min(max(minSide + scroll, minSide), maxStart)
|