@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.3
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/scheduled_tasks.lock +1 -0
- package/.claude/settings.local.json +185 -0
- package/CLAUDE.md +8 -3
- package/ios/Application/HeaderRight.swift +25 -12
- package/ios/Application/Layout/ScreenLayout.swift +196 -0
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +30 -9
- package/ios/Application/Navigation/BottomTab/BottomTabBar.swift +5 -2
- package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +275 -0
- package/ios/Application/Navigation/HeaderUser.swift +4 -1
- package/ios/Application/Navigation/NavigationContainer.swift +3 -0
- package/ios/Application/Navigation/Overplay/BottomSheet.swift +41 -4
- package/ios/Application/Navigation/Overplay/ModalScreen.swift +5 -1
- package/ios/Application/Navigation/component/DuoScrollGradient.swift +55 -0
- package/ios/Application/Navigation/component/Header.swift +9 -3
- 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 +61 -9
- package/ios/Application/Navigation/component/SideBar.swift +155 -0
- package/ios/Application/StackScreen.swift +135 -18
- package/ios/Popup/PopupDisplay.swift +84 -68
- package/ios/Popup/PopupLayout.swift +45 -0
- package/ios/Popup/PopupNotify.swift +43 -32
- package/ios/Popup/PopupPromotion.swift +17 -5
- package/ios/native-kits.podspec +1 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +76 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +170 -0
- package/package.json +1 -1
- 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 -334
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +0 -98
- 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 -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 -188
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +0 -297
- 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 -157
- 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 -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 -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 -622
- 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 -232
- 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 -381
- 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 -105
- 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 -347
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +0 -37
- 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 -169
- 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 -118
- 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/settings.gradle.kts +0 -64
|
@@ -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; the column stays above it.
|
|
28
|
+
let bottomInset: CGFloat
|
|
29
|
+
|
|
30
|
+
@Environment(\.screenLayout) private var layout
|
|
31
|
+
@State private var columnHeight: CGFloat = 0
|
|
32
|
+
|
|
33
|
+
private var isRight: Bool { sideBar.edge == .right }
|
|
34
|
+
|
|
35
|
+
private var top: CGFloat {
|
|
36
|
+
let maxY = layout.height - bottomInset - Spacing.XL - columnHeight
|
|
37
|
+
return max(min(sideBar.top, maxY), 0)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var body: some View {
|
|
41
|
+
VStack(alignment: isRight ? .leading : .trailing, spacing: Spacing.M) {
|
|
42
|
+
if let goBack {
|
|
43
|
+
SideBarButton(
|
|
44
|
+
icon: "arrow-back",
|
|
45
|
+
tintColor: headerColor.tintIconColor,
|
|
46
|
+
accessibilityId: "btn_navigation_back",
|
|
47
|
+
action: goBack
|
|
48
|
+
)
|
|
49
|
+
} else {
|
|
50
|
+
// keep the slot so the toolkit does not jump between root and pushed screens
|
|
51
|
+
Color.clear.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
|
|
52
|
+
}
|
|
53
|
+
if let toolkit {
|
|
54
|
+
ToolkitNavigationHeaderRight(
|
|
55
|
+
headerRight: toolkit.data,
|
|
56
|
+
tintColor: toolkit.tintColor ?? headerColor.tintIconColor,
|
|
57
|
+
vertical: true
|
|
58
|
+
)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
.padding(isRight ? .leading : .trailing, Spacing.M)
|
|
62
|
+
.frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
|
|
63
|
+
.background(
|
|
64
|
+
GeometryReader { proxy in
|
|
65
|
+
Color.clear
|
|
66
|
+
.onAppear { columnHeight = proxy.size.height }
|
|
67
|
+
.onChange(of: proxy.size.height) { columnHeight = $0 }
|
|
68
|
+
}
|
|
69
|
+
)
|
|
70
|
+
.padding(.top, top)
|
|
71
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .topTrailing : .topLeading)
|
|
72
|
+
.ignoresSafeArea()
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Dark glass under light icons (banner headers), light glass otherwise.
|
|
77
|
+
private struct SideBarGlass<S: Shape>: ViewModifier {
|
|
78
|
+
let tintColor: Color?
|
|
79
|
+
let shape: S
|
|
80
|
+
|
|
81
|
+
private var isDark: Bool { tintColor == Colors.black01 || tintColor == .white }
|
|
82
|
+
|
|
83
|
+
func body(content: Content) -> some View {
|
|
84
|
+
if isDark {
|
|
85
|
+
content
|
|
86
|
+
.background(shape.fill(Colors.black20.opacity(0.5)))
|
|
87
|
+
.overlay(shape.stroke(Colors.black01.opacity(0.2), lineWidth: 0.5))
|
|
88
|
+
} else {
|
|
89
|
+
content
|
|
90
|
+
.background(
|
|
91
|
+
shape.fill(Colors.black01.opacity(0.85))
|
|
92
|
+
.shadow(color: Color.black.opacity(0.12), radius: 4, x: 0, y: 2)
|
|
93
|
+
)
|
|
94
|
+
.overlay(shape.stroke(Colors.black20.opacity(0.08), lineWidth: 0.5))
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// 48pt glass circle for a single side bar control.
|
|
100
|
+
struct SideBarButton: View {
|
|
101
|
+
let icon: String
|
|
102
|
+
var tintColor: Color?
|
|
103
|
+
var showBadge: Bool = false
|
|
104
|
+
var disabled: Bool = false
|
|
105
|
+
var accessibilityId: String?
|
|
106
|
+
let action: () -> Void
|
|
107
|
+
|
|
108
|
+
var body: some View {
|
|
109
|
+
SwiftUI.Button(action: { if !disabled { action() } }) {
|
|
110
|
+
Icon(source: icon, size: sideBarIconSize, color: tintColor ?? Colors.black17)
|
|
111
|
+
.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
|
|
112
|
+
.modifier(SideBarGlass(tintColor: tintColor, shape: Circle()))
|
|
113
|
+
.overlay(alignment: .topTrailing) {
|
|
114
|
+
if showBadge {
|
|
115
|
+
BadgeDot(size: .small).offset(x: -Spacing.XXS, y: Spacing.XXS)
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
.buttonStyle(.plain)
|
|
120
|
+
.accessibilityIdentifier(accessibilityId ?? "")
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/// 48pt wide glass capsule grouping related controls, one item per 50pt.
|
|
125
|
+
struct SideBarGroup<Content: View>: View {
|
|
126
|
+
var tintColor: Color?
|
|
127
|
+
let itemCount: Int
|
|
128
|
+
@ViewBuilder let content: () -> Content
|
|
129
|
+
|
|
130
|
+
var body: some View {
|
|
131
|
+
VStack(spacing: 0) { content() }
|
|
132
|
+
.frame(
|
|
133
|
+
width: DuoMetrics.controlSize,
|
|
134
|
+
height: itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
|
|
135
|
+
)
|
|
136
|
+
.modifier(SideBarGlass(tintColor: tintColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
struct SideBarGroupItem: View {
|
|
141
|
+
let icon: String
|
|
142
|
+
var tintColor: Color?
|
|
143
|
+
let accessibilityId: String
|
|
144
|
+
let action: () -> Void
|
|
145
|
+
|
|
146
|
+
var body: some View {
|
|
147
|
+
SwiftUI.Button(action: action) {
|
|
148
|
+
Icon(source: icon, size: sideBarIconSize, color: tintColor ?? Colors.black17)
|
|
149
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
150
|
+
.contentShape(Rectangle())
|
|
151
|
+
}
|
|
152
|
+
.buttonStyle(.plain)
|
|
153
|
+
.accessibilityIdentifier(accessibilityId)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
@@ -104,6 +104,9 @@ public struct StackScreen<Content: View>: View {
|
|
|
104
104
|
@State private var isBottomTabRoot: Bool = false
|
|
105
105
|
|
|
106
106
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
107
|
+
@Environment(\.screenLayout) private var layout
|
|
108
|
+
@Environment(\.duoTabBarInsets) private var duoTabBarInsets
|
|
109
|
+
@State private var footerHeight: CGFloat = 0
|
|
107
110
|
|
|
108
111
|
// Auto-tracking state (mirrors Compose's `remember { ScreenTrackingState() }` +
|
|
109
112
|
// the once-computed push/back `action`).
|
|
@@ -125,6 +128,30 @@ public struct StackScreen<Content: View>: View {
|
|
|
125
128
|
|
|
126
129
|
private static var headerHeight: CGFloat { 52 }
|
|
127
130
|
|
|
131
|
+
private var toolkit: (data: HeaderRightData?, tintColor: Color?)? { toolkitHeaderRight(headerRight) }
|
|
132
|
+
|
|
133
|
+
private var showsSideBar: Bool {
|
|
134
|
+
layout.sideBar != nil && headerType != .none && !isBottomTabRoot && (goBack != nil || toolkit != nil)
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// iPhone Duo: content scrolls under a glass header instead of starting below it.
|
|
138
|
+
private var useDuoGlass: Bool {
|
|
139
|
+
layout.sideBar != nil && headerType != .none && animatedHeader == nil
|
|
140
|
+
&& inputSearchProps == nil && !headerTransparent && !fullScreenContent
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Height the footer, tab bar or home indicator takes from the bottom of the window.
|
|
144
|
+
private var bottomBarsHeight: CGFloat {
|
|
145
|
+
if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom }
|
|
146
|
+
if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve - Spacing.XL }
|
|
147
|
+
return NavigationFooter.navigationBarInset
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
|
|
151
|
+
private var keyboardReferenceHeight: CGFloat {
|
|
152
|
+
layout.isIPhoneDuo ? layout.height : UIScreen.main.bounds.height
|
|
153
|
+
}
|
|
154
|
+
|
|
128
155
|
/// Extra top space reserved inside the content so the animated search bar has
|
|
129
156
|
/// room below the header at scroll 0; it shrinks to 0 as the bar slides up.
|
|
130
157
|
private var animatedSearchInset: CGFloat {
|
|
@@ -163,9 +190,18 @@ public struct StackScreen<Content: View>: View {
|
|
|
163
190
|
hasFooter: footer != nil,
|
|
164
191
|
scrollOffset: scrollOffset
|
|
165
192
|
)
|
|
193
|
+
.padding(.bottom, footer == nil ? duoTabBarInsets.overlay : 0)
|
|
166
194
|
}
|
|
167
195
|
|
|
168
196
|
NavigationFooter(footerComponent: footer, keyboardSize: keyboardSize)
|
|
197
|
+
.padding(.bottom, duoTabBarInsets.overlay)
|
|
198
|
+
.background(
|
|
199
|
+
GeometryReader { proxy in
|
|
200
|
+
Color.clear
|
|
201
|
+
.onAppear { footerHeight = proxy.size.height }
|
|
202
|
+
.onChange(of: proxy.size.height) { footerHeight = $0 }
|
|
203
|
+
}
|
|
204
|
+
)
|
|
169
205
|
}
|
|
170
206
|
|
|
171
207
|
// Animated search bar overlay (mirrors Compose's SearchAnimated layer).
|
|
@@ -176,7 +212,24 @@ public struct StackScreen<Content: View>: View {
|
|
|
176
212
|
hiddenBack: !isBack,
|
|
177
213
|
hasHeaderRight: headerRight != nil,
|
|
178
214
|
headerRightWidth: headerRightWidth,
|
|
179
|
-
headerHeight: Self.headerHeight
|
|
215
|
+
headerHeight: Self.headerHeight,
|
|
216
|
+
useSideBar: layout.sideBar != nil,
|
|
217
|
+
hasToolkit: toolkit != nil
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if showsSideBar, let sideBar = layout.sideBar {
|
|
222
|
+
HeaderSideBar(
|
|
223
|
+
sideBar: sideBar,
|
|
224
|
+
goBack: goBack,
|
|
225
|
+
toolkit: toolkit,
|
|
226
|
+
headerColor: getHeaderColor(
|
|
227
|
+
isAnimated: animatedHeader != nil,
|
|
228
|
+
isSurface: animatedHeader?.isSurface ?? true,
|
|
229
|
+
opacity: scrollOpacity,
|
|
230
|
+
tintColor: tintColor
|
|
231
|
+
),
|
|
232
|
+
bottomInset: bottomBarsHeight
|
|
180
233
|
)
|
|
181
234
|
}
|
|
182
235
|
}
|
|
@@ -191,7 +244,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
191
244
|
guard useAvoidKeyboard,
|
|
192
245
|
let endFrame = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? CGRect else { return }
|
|
193
246
|
withAnimation(.easeOut(duration: 0.25)) {
|
|
194
|
-
keyboardHeight =
|
|
247
|
+
keyboardHeight = keyboardReferenceHeight - endFrame.origin.y
|
|
195
248
|
}
|
|
196
249
|
}
|
|
197
250
|
// Mirrors Compose's `hideKeyboardOnTap()`: a simultaneous gesture so
|
|
@@ -246,15 +299,25 @@ public struct StackScreen<Content: View>: View {
|
|
|
246
299
|
scrollOffset: $scrollOffset,
|
|
247
300
|
scrollToTopCallback: scrollToTopCallback,
|
|
248
301
|
animatedSearchInset: animatedSearchInset,
|
|
302
|
+
bottomInset: duoTabBarInsets.overlay,
|
|
249
303
|
content: { animatedHeroBody(animatedHeader) }
|
|
250
304
|
)
|
|
251
305
|
.edgesIgnoringSafeArea(.all)
|
|
252
306
|
|
|
253
307
|
header
|
|
254
308
|
.background(
|
|
255
|
-
|
|
256
|
-
.
|
|
257
|
-
|
|
309
|
+
Group {
|
|
310
|
+
if layout.sideBar != nil {
|
|
311
|
+
// glass shows once the banner has scrolled under the header
|
|
312
|
+
Color.clear.modifier(DuoBarMaterial(
|
|
313
|
+
shape: Rectangle(),
|
|
314
|
+
visible: scrollOffset > heroGlassStart(animatedHeader)
|
|
315
|
+
))
|
|
316
|
+
} else {
|
|
317
|
+
Colors.black01.opacity(scrollOpacity)
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
258
321
|
)
|
|
259
322
|
.zIndex(100)
|
|
260
323
|
}
|
|
@@ -271,9 +334,34 @@ public struct StackScreen<Content: View>: View {
|
|
|
271
334
|
)
|
|
272
335
|
.zIndex(100)
|
|
273
336
|
|
|
274
|
-
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content:
|
|
337
|
+
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: { duoInsetContent })
|
|
275
338
|
.edgesIgnoringSafeArea(.all)
|
|
276
339
|
}
|
|
340
|
+
} else if useDuoGlass {
|
|
341
|
+
ZStack(alignment: .top) {
|
|
342
|
+
DuoScrollGradient(
|
|
343
|
+
scrollOffset: scrollOffset,
|
|
344
|
+
headerHeight: layout.headerHeight,
|
|
345
|
+
extended: headerType == .extended
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
StackScreenContent(
|
|
349
|
+
scrollable: scrollable,
|
|
350
|
+
scrollOffset: $scrollOffset,
|
|
351
|
+
scrollToTopCallback: scrollToTopCallback,
|
|
352
|
+
topInset: layout.headerHeight - layout.insets.top,
|
|
353
|
+
bottomInset: duoTabBarInsets.overlay,
|
|
354
|
+
content: content
|
|
355
|
+
)
|
|
356
|
+
|
|
357
|
+
header
|
|
358
|
+
.background(
|
|
359
|
+
Color.clear
|
|
360
|
+
.modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
|
|
361
|
+
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
362
|
+
)
|
|
363
|
+
.zIndex(100)
|
|
364
|
+
}
|
|
277
365
|
} else {
|
|
278
366
|
header
|
|
279
367
|
.background(
|
|
@@ -283,28 +371,40 @@ public struct StackScreen<Content: View>: View {
|
|
|
283
371
|
headerTransparent: headerTransparent,
|
|
284
372
|
fullScreenContent: fullScreenContent
|
|
285
373
|
)
|
|
286
|
-
.edgesIgnoringSafeArea(.top)
|
|
374
|
+
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
287
375
|
)
|
|
288
376
|
|
|
289
|
-
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, content: content)
|
|
377
|
+
StackScreenContent(scrollable: scrollable, scrollOffset: $scrollOffset, scrollToTopCallback: scrollToTopCallback, animatedSearchInset: animatedSearchInset, bottomInset: duoTabBarInsets.overlay, content: content)
|
|
290
378
|
}
|
|
291
379
|
}
|
|
292
380
|
|
|
381
|
+
/// Edge-to-edge branches lay out outside the safe area; iPhone Duo content still keeps
|
|
382
|
+
/// clear of the side strips, as RN's Screen does.
|
|
383
|
+
private var duoInsetContent: some View {
|
|
384
|
+
content()
|
|
385
|
+
.padding(.leading, layout.insetLeft)
|
|
386
|
+
.padding(.trailing, layout.insetRight)
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
private func heroGlassStart(_ animatedHeader: AnimatedHeader) -> CGFloat {
|
|
390
|
+
max(layout.width / animatedHeader.aspectRatio.value - layout.headerHeight, 0)
|
|
391
|
+
}
|
|
392
|
+
|
|
293
393
|
/// Hero image + body for an animated header. The image fills the width at its
|
|
294
394
|
/// aspect-ratio height; the body is offset below the header band so it overlaps
|
|
295
395
|
/// the lower part of the image (mirrors Compose's image box + padded content).
|
|
296
396
|
@ViewBuilder
|
|
297
397
|
private func animatedHeroBody(_ animatedHeader: AnimatedHeader) -> some View {
|
|
298
|
-
let width =
|
|
398
|
+
let width = layout.width
|
|
299
399
|
let imageHeight = width / animatedHeader.aspectRatio.value
|
|
300
|
-
let contentTopInset =
|
|
400
|
+
let contentTopInset = layout.headerHeight + layoutOffset
|
|
301
401
|
|
|
302
402
|
ZStack(alignment: .top) {
|
|
303
403
|
animatedHeader.composable(scrollOffset)
|
|
304
404
|
.frame(width: width, height: imageHeight)
|
|
305
405
|
.clipped()
|
|
306
406
|
|
|
307
|
-
|
|
407
|
+
duoInsetContent
|
|
308
408
|
.padding(.top, contentTopInset)
|
|
309
409
|
}
|
|
310
410
|
.frame(maxWidth: .infinity, alignment: .top)
|
|
@@ -343,6 +443,10 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
343
443
|
/// Extra top space reserved for the animated search bar (mirrors Compose's
|
|
344
444
|
/// MainContent animated top spacer). Zero for non-animated screens.
|
|
345
445
|
var animatedSearchInset: CGFloat = 0
|
|
446
|
+
/// Room for a header the content scrolls under (iPhone Duo glass header).
|
|
447
|
+
var topInset: CGFloat = 0
|
|
448
|
+
/// Room for a bar that overlays the content (iPhone Duo floating tab bar).
|
|
449
|
+
var bottomInset: CGFloat = 0
|
|
346
450
|
let content: () -> Content
|
|
347
451
|
|
|
348
452
|
private let topAnchorID = "stack_screen_top_anchor"
|
|
@@ -369,13 +473,17 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
369
473
|
}
|
|
370
474
|
})
|
|
371
475
|
|
|
372
|
-
if animatedSearchInset > 0 {
|
|
373
|
-
Spacer().frame(height: animatedSearchInset)
|
|
476
|
+
if animatedSearchInset + topInset > 0 {
|
|
477
|
+
Spacer().frame(height: animatedSearchInset + topInset)
|
|
374
478
|
}
|
|
375
479
|
|
|
376
480
|
VStack {
|
|
377
481
|
content()
|
|
378
482
|
}
|
|
483
|
+
|
|
484
|
+
if bottomInset > 0 {
|
|
485
|
+
Spacer().frame(height: bottomInset)
|
|
486
|
+
}
|
|
379
487
|
}
|
|
380
488
|
}
|
|
381
489
|
.coordinateSpace(name: scrollSpace)
|
|
@@ -390,6 +498,8 @@ private struct StackScreenContent<Content: View>: View {
|
|
|
390
498
|
}
|
|
391
499
|
} else {
|
|
392
500
|
content()
|
|
501
|
+
.padding(.top, topInset)
|
|
502
|
+
.padding(.bottom, bottomInset)
|
|
393
503
|
}
|
|
394
504
|
}
|
|
395
505
|
}
|
|
@@ -407,16 +517,23 @@ private struct SearchAnimatedHeader: View {
|
|
|
407
517
|
let hasHeaderRight: Bool
|
|
408
518
|
let headerRightWidth: CGFloat
|
|
409
519
|
let headerHeight: CGFloat
|
|
520
|
+
var useSideBar: Bool = false
|
|
521
|
+
var hasToolkit: Bool = false
|
|
522
|
+
|
|
523
|
+
@Environment(\.screenLayout) private var layout
|
|
410
524
|
|
|
411
525
|
var body: some View {
|
|
412
526
|
let scroll = max(scrollOffset, 0)
|
|
413
527
|
|
|
414
|
-
// Insets are measured from the safe-area top
|
|
415
|
-
let
|
|
416
|
-
let
|
|
528
|
+
// Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
|
|
529
|
+
let rowTop = min(layout.headerStatusBarHeight - layout.insets.top, 0)
|
|
530
|
+
let minTop = rowTop + (layout.headerBarHeight - headerHeight) / 2
|
|
531
|
+
let maxTop = layout.headerHeight - layout.insets.top
|
|
417
532
|
let minSide = Spacing.M
|
|
418
|
-
let maxStart: CGFloat = hiddenBack ? Spacing.M : 48
|
|
419
|
-
let maxEnd =
|
|
533
|
+
let maxStart: CGFloat = hiddenBack || useSideBar ? Spacing.M : 48
|
|
534
|
+
let maxEnd: CGFloat = useSideBar && hasToolkit
|
|
535
|
+
? Spacing.M
|
|
536
|
+
: headerRightWidth + (hasHeaderRight ? Spacing.M * 2 : Spacing.M)
|
|
420
537
|
|
|
421
538
|
let top = min(max(maxTop - scroll, minTop), maxTop)
|
|
422
539
|
let start = min(max(minSide + scroll, minSide), maxStart)
|
|
@@ -55,6 +55,7 @@ extension View {
|
|
|
55
55
|
|
|
56
56
|
public struct PopupDisplay: View {
|
|
57
57
|
@Environment(\.localize) private var localize
|
|
58
|
+
@Environment(\.screenLayout) private var layout
|
|
58
59
|
|
|
59
60
|
// MARK: Lifecycle
|
|
60
61
|
|
|
@@ -91,6 +92,7 @@ public struct PopupDisplay: View {
|
|
|
91
92
|
|
|
92
93
|
@State private var textHeight: CGFloat = .zero
|
|
93
94
|
@State private var isScrollable = false
|
|
95
|
+
@State private var actionsHeight: CGFloat = 0
|
|
94
96
|
|
|
95
97
|
public var body: some View {
|
|
96
98
|
var shouldUseColumn: Bool {
|
|
@@ -114,89 +116,103 @@ public struct PopupDisplay: View {
|
|
|
114
116
|
}
|
|
115
117
|
|
|
116
118
|
VStack(spacing: 0) {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
.
|
|
121
|
-
|
|
122
|
-
.
|
|
119
|
+
// scrolls when iPhone Duo landscape is too short; actions stay visible
|
|
120
|
+
PopupScrollBody(maxHeight: layout.popupMaxHeight.map { $0 - actionsHeight }) {
|
|
121
|
+
VStack(spacing: 0) {
|
|
122
|
+
if(!url.isEmpty) {
|
|
123
|
+
WebImage(url: URL(string: url), isAnimating: .constant(true))
|
|
124
|
+
.resizable()
|
|
125
|
+
.placeholder {
|
|
126
|
+
Rectangle()
|
|
127
|
+
.fill(Color.gray.opacity(0.3))
|
|
128
|
+
.aspectRatio(1.777, contentMode: .fit)
|
|
129
|
+
.frame(maxWidth: .infinity, maxHeight: 184)
|
|
130
|
+
}
|
|
123
131
|
.aspectRatio(1.777, contentMode: .fit)
|
|
124
|
-
.frame(maxWidth: .infinity,
|
|
132
|
+
.frame(maxWidth: .infinity, alignment: .center)
|
|
133
|
+
.clipShape(RoundedCorner(radius: 15, corners: [.topLeft, .topRight]))
|
|
134
|
+
.clipped()
|
|
125
135
|
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
MomoText(title, typography: .headerDefaultBold)
|
|
133
|
-
.padding(.top, 24)
|
|
134
|
-
.padding(.bottom, 8)
|
|
135
|
-
.lineLimit(2)
|
|
136
|
-
.accessibility(identifier: "title_popup_permission")
|
|
136
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
137
|
+
MomoText(title, typography: .headerDefaultBold)
|
|
138
|
+
.padding(.top, 24)
|
|
139
|
+
.padding(.bottom, 8)
|
|
140
|
+
.lineLimit(2)
|
|
141
|
+
.accessibility(identifier: "title_popup_permission")
|
|
137
142
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
// Cap the visible height to ~8.5 lines
|
|
152
|
-
.frame(height: min(maxHeight8_5, textHeight))
|
|
153
|
-
} else {
|
|
154
|
-
MomoText(description, typography: .bodyDefaultRegular)
|
|
155
|
-
.multilineTextAlignment(.leading)
|
|
156
|
-
.background(GeometryReader { geo in
|
|
157
|
-
Color.clear.onAppear {
|
|
158
|
-
textHeight = geo.size.height
|
|
159
|
-
// Trigger scroll at ~8.5 lines
|
|
160
|
-
isScrollable = textHeight > maxHeight8_5
|
|
143
|
+
Group {
|
|
144
|
+
if isScrollable {
|
|
145
|
+
ScrollView(showsIndicators: false) {
|
|
146
|
+
MomoText(description, typography: .bodyDefaultRegular)
|
|
147
|
+
.multilineTextAlignment(.leading)
|
|
148
|
+
.background(GeometryReader { geo in
|
|
149
|
+
Color.clear.onAppear { textHeight = geo.size.height }
|
|
150
|
+
})
|
|
151
|
+
.measureLineHeights(font: .body_default_regular,
|
|
152
|
+
oneLine: $oneLineH,
|
|
153
|
+
twoLines: $twoLineH)
|
|
154
|
+
.accessibility(identifier: "description_popup_permission")
|
|
161
155
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
156
|
+
// Cap the visible height to ~8.5 lines
|
|
157
|
+
.frame(height: min(maxHeight8_5, textHeight))
|
|
158
|
+
} else {
|
|
159
|
+
MomoText(description, typography: .bodyDefaultRegular)
|
|
160
|
+
.multilineTextAlignment(.leading)
|
|
161
|
+
.background(GeometryReader { geo in
|
|
162
|
+
Color.clear.onAppear {
|
|
163
|
+
textHeight = geo.size.height
|
|
164
|
+
// Trigger scroll at ~8.5 lines
|
|
165
|
+
isScrollable = textHeight > maxHeight8_5
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
.measureLineHeights(font: .body_default_regular,
|
|
169
|
+
oneLine: $oneLineH,
|
|
170
|
+
twoLines: $twoLineH)
|
|
171
|
+
.accessibility(identifier: "description_popup_permission")
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
174
|
.padding(.bottom, 8)
|
|
175
|
+
|
|
176
|
+
if(!errorCode.isEmpty) {
|
|
177
|
+
MomoText(localize.translate("errorCode") + errorCode, typography: .descriptionXsRegular, color: Colors.black12)
|
|
178
|
+
.lineLimit(1)
|
|
179
|
+
.padding(.bottom, 8)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
.frame(maxWidth: .infinity, alignment: .leading)
|
|
183
|
+
.padding(.horizontal, 24)
|
|
184
|
+
.padding(.bottom, 16)
|
|
175
185
|
}
|
|
176
186
|
}
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
ColumnButtons
|
|
186
|
-
}
|
|
187
|
-
} else {
|
|
188
|
-
if buttonType == .text {
|
|
189
|
-
RowTextButtons
|
|
187
|
+
|
|
188
|
+
Group {
|
|
189
|
+
if shouldUseColumn {
|
|
190
|
+
if buttonType == .text {
|
|
191
|
+
ColumnTextButtons
|
|
192
|
+
} else {
|
|
193
|
+
ColumnButtons
|
|
194
|
+
}
|
|
190
195
|
} else {
|
|
191
|
-
|
|
196
|
+
if buttonType == .text {
|
|
197
|
+
RowTextButtons
|
|
198
|
+
} else {
|
|
199
|
+
RowButtons
|
|
200
|
+
}
|
|
192
201
|
}
|
|
193
202
|
}
|
|
203
|
+
.background(
|
|
204
|
+
GeometryReader { proxy in
|
|
205
|
+
Color.clear
|
|
206
|
+
.onAppear { actionsHeight = proxy.size.height }
|
|
207
|
+
.onChange(of: proxy.size.height) { actionsHeight = $0 }
|
|
208
|
+
}
|
|
209
|
+
)
|
|
194
210
|
}
|
|
195
211
|
.fixedSize(horizontal: false, vertical: true)
|
|
196
212
|
}
|
|
197
213
|
}
|
|
198
214
|
.background(Color.white.cornerRadius(15))
|
|
199
|
-
.frame(maxWidth: popupMaxWidth
|
|
215
|
+
.frame(maxWidth: layout.popupMaxWidth)
|
|
200
216
|
.padding(.horizontal, 12)
|
|
201
217
|
.accessibilityElement(children: .ignore)
|
|
202
218
|
.accessibility(identifier: "popup_notify")
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import SwiftUI
|
|
2
|
+
|
|
3
|
+
// Width of a system alert on the iPhone Duo inner display, in both orientations (RN Popup/utils.ts).
|
|
4
|
+
private let duoPopupMaxWidth: CGFloat = 440
|
|
5
|
+
|
|
6
|
+
extension ScreenLayout {
|
|
7
|
+
var isDuoDevice: Bool { isIPhoneDuoDevice(windowSize: CGSize(width: width, height: height)) }
|
|
8
|
+
|
|
9
|
+
/// Card width cap; other devices keep the full window width minus the popup's own padding.
|
|
10
|
+
var popupMaxWidth: CGFloat {
|
|
11
|
+
isDuoDevice ? min(width - Spacing.XL * 2, duoPopupMaxWidth) : .infinity
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/// Room left for a popup on iPhone Duo, where landscape is too short for it; nil elsewhere.
|
|
15
|
+
var popupMaxHeight: CGFloat? {
|
|
16
|
+
guard isDuoDevice else { return nil }
|
|
17
|
+
return height - duoTopInset - insets.bottom - Spacing.XL * 2
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/// Scrolls `content` once it is taller than `maxHeight`; otherwise sizes to it.
|
|
22
|
+
struct PopupScrollBody<Content: View>: View {
|
|
23
|
+
let maxHeight: CGFloat?
|
|
24
|
+
@ViewBuilder let content: () -> Content
|
|
25
|
+
|
|
26
|
+
@State private var contentHeight: CGFloat = 0
|
|
27
|
+
|
|
28
|
+
var body: some View {
|
|
29
|
+
if let maxHeight {
|
|
30
|
+
ScrollView(.vertical, showsIndicators: false) {
|
|
31
|
+
content()
|
|
32
|
+
.background(
|
|
33
|
+
GeometryReader { proxy in
|
|
34
|
+
Color.clear
|
|
35
|
+
.onAppear { contentHeight = proxy.size.height }
|
|
36
|
+
.onChange(of: proxy.size.height) { contentHeight = $0 }
|
|
37
|
+
}
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
.frame(height: min(contentHeight, max(maxHeight, 0)))
|
|
41
|
+
} else {
|
|
42
|
+
content()
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|