@momo-kits/native-kits 0.167.1-duo.7-debug → 0.167.1-duo.8-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 +7 -4
- package/compose/build.gradle.kts +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +56 -11
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +55 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +14 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +98 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +71 -40
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +11 -27
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +19 -7
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +20 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +86 -0
- package/ios/Application/Layout/ScreenLayout.swift +122 -6
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +41 -5
- package/ios/Application/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Application/Navigation/BottomTab/DuoTabBar.swift +60 -31
- package/ios/Application/Navigation/component/DuoScrollGradient.swift +18 -19
- package/ios/Application/Navigation/component/Header.swift +3 -2
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +32 -0
- package/ios/Application/Navigation/component/SideBar.swift +2 -2
- package/ios/Application/StackScreen.swift +46 -6
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +7 -3
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +3 -0
- package/package.json +1 -1
|
@@ -28,7 +28,7 @@ private let tabSpring = Animation.interpolatingSpring(mass: 0.8, stiffness: 220,
|
|
|
28
28
|
struct DuoTabBarInsets: Equatable {
|
|
29
29
|
/// Floating bar overlays the content: scroll content, footer and FAB keep clear of it.
|
|
30
30
|
var overlay: CGFloat = 0
|
|
31
|
-
/// Rail sits at the bottom of the side strip: the header side bar stays above
|
|
31
|
+
/// Rail sits at the bottom of the side strip: the header side bar stays this far above the window bottom.
|
|
32
32
|
var sideBarReserve: CGFloat = 0
|
|
33
33
|
}
|
|
34
34
|
|
|
@@ -43,16 +43,21 @@ extension EnvironmentValues {
|
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
/// Rail plus the QR button under it; compressed, the rail is a single button.
|
|
47
|
+
func duoRailHeight(itemCount: Int, hasQr: Bool, compressed: Bool = false) -> CGFloat {
|
|
48
|
+
let count = CGFloat(itemCount)
|
|
49
|
+
let rail = compressed ? itemSize : railPadding * 2 + count * itemSize + max(count - 1, 0) * railGap
|
|
50
|
+
return rail + (hasQr ? Spacing.M + itemSize : 0)
|
|
51
|
+
}
|
|
52
|
+
|
|
46
53
|
extension ScreenLayout {
|
|
47
|
-
func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool) -> DuoTabBarInsets {
|
|
54
|
+
func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool, tabBarCompressed: Bool = false) -> DuoTabBarInsets {
|
|
48
55
|
guard isIPhoneDuo else { return DuoTabBarInsets() }
|
|
49
|
-
guard sideBar
|
|
56
|
+
guard let sideBar else {
|
|
50
57
|
return DuoTabBarInsets(overlay: duoFloatingTabBarHeight + Spacing.M)
|
|
51
58
|
}
|
|
52
|
-
let
|
|
53
|
-
|
|
54
|
-
let qr = hasFloatingButton ? Spacing.M + itemSize : 0
|
|
55
|
-
return DuoTabBarInsets(sideBarReserve: rail + qr + Spacing.XL)
|
|
59
|
+
let rail = duoRailHeight(itemCount: itemCount, hasQr: hasFloatingButton, compressed: tabBarCompressed)
|
|
60
|
+
return DuoTabBarInsets(sideBarReserve: sideBar.bottom + rail + DuoBarCompression.groupGap)
|
|
56
61
|
}
|
|
57
62
|
}
|
|
58
63
|
|
|
@@ -95,13 +100,14 @@ struct DuoTabBar: View {
|
|
|
95
100
|
let floatingButton: BottomTabFloatingButton?
|
|
96
101
|
let selectedIndex: Int
|
|
97
102
|
let onTabSelected: (Int) -> Void
|
|
103
|
+
var compression = DuoBarCompression()
|
|
98
104
|
|
|
99
105
|
@Environment(\.screenLayout) private var layout
|
|
100
106
|
|
|
101
107
|
var body: some View {
|
|
102
108
|
if let sideBar = layout.sideBar {
|
|
103
109
|
DuoRailTabBar(sideBar: sideBar, items: items, floatingButton: floatingButton,
|
|
104
|
-
selectedIndex: selectedIndex, onTabSelected: onTabSelected)
|
|
110
|
+
selectedIndex: selectedIndex, onTabSelected: onTabSelected, compression: compression)
|
|
105
111
|
} else {
|
|
106
112
|
DuoFloatingTabBar(items: items, floatingButton: floatingButton,
|
|
107
113
|
selectedIndex: selectedIndex, onTabSelected: onTabSelected)
|
|
@@ -110,12 +116,15 @@ struct DuoTabBar: View {
|
|
|
110
116
|
}
|
|
111
117
|
|
|
112
118
|
/// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E).
|
|
119
|
+
/// When the side bar is short it can shrink to one button showing the current tab
|
|
120
|
+
/// (HIG "Tab bar compressed"); tapping it expands the rail and compresses the toolbar.
|
|
113
121
|
private struct DuoRailTabBar: View {
|
|
114
122
|
let sideBar: SideBarLayout
|
|
115
123
|
let items: [BottomTabItem]
|
|
116
124
|
let floatingButton: BottomTabFloatingButton?
|
|
117
125
|
let selectedIndex: Int
|
|
118
126
|
let onTabSelected: (Int) -> Void
|
|
127
|
+
let compression: DuoBarCompression
|
|
119
128
|
|
|
120
129
|
@Environment(\.appTheme) private var theme
|
|
121
130
|
|
|
@@ -123,30 +132,19 @@ private struct DuoRailTabBar: View {
|
|
|
123
132
|
|
|
124
133
|
var body: some View {
|
|
125
134
|
VStack(spacing: Spacing.M) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
+ CGFloat(selectedIndex) * (itemSize + railGap))
|
|
132
|
-
.animation(tabSpring, value: selectedIndex)
|
|
133
|
-
|
|
134
|
-
VStack(spacing: railGap) {
|
|
135
|
-
ForEach(items.indices, id: \.self) { index in
|
|
136
|
-
SwiftUI.Button { onTabSelected(index) } label: {
|
|
137
|
-
DuoTabIcon(item: items[index], selected: index == selectedIndex)
|
|
138
|
-
.frame(width: itemSize, height: itemSize)
|
|
139
|
-
.contentShape(Rectangle())
|
|
140
|
-
}
|
|
141
|
-
.buttonStyle(.plain)
|
|
142
|
-
.accessibilityLabel(items[index].label)
|
|
143
|
-
}
|
|
135
|
+
if compression.tabBarCompressed, items.indices.contains(selectedIndex) {
|
|
136
|
+
SwiftUI.Button(action: compression.expandTabBar) {
|
|
137
|
+
DuoTabIcon(item: items[selectedIndex], selected: true)
|
|
138
|
+
.frame(width: itemSize, height: itemSize)
|
|
139
|
+
.contentShape(Circle())
|
|
144
140
|
}
|
|
145
|
-
.
|
|
141
|
+
.buttonStyle(.plain)
|
|
142
|
+
.modifier(DuoBarMaterial(shape: Circle()))
|
|
143
|
+
.accessibilityLabel(items[selectedIndex].label)
|
|
144
|
+
.accessibilityIdentifier("bottom_tab_bar")
|
|
145
|
+
} else {
|
|
146
|
+
rail
|
|
146
147
|
}
|
|
147
|
-
.frame(width: itemSize)
|
|
148
|
-
.modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
|
|
149
|
-
.accessibilityIdentifier("bottom_tab_bar")
|
|
150
148
|
|
|
151
149
|
if let floatingButton {
|
|
152
150
|
DuoQrButton(data: floatingButton, size: itemSize)
|
|
@@ -154,10 +152,41 @@ private struct DuoRailTabBar: View {
|
|
|
154
152
|
}
|
|
155
153
|
.padding(isRight ? .leading : .trailing, Spacing.M)
|
|
156
154
|
.frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
|
|
157
|
-
.padding(.bottom,
|
|
155
|
+
.padding(.bottom, sideBar.bottom)
|
|
158
156
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .bottomTrailing : .bottomLeading)
|
|
159
157
|
.ignoresSafeArea()
|
|
160
158
|
}
|
|
159
|
+
|
|
160
|
+
/// Glass column of tab items with the sliding selection bubble.
|
|
161
|
+
private var rail: some View {
|
|
162
|
+
ZStack(alignment: .top) {
|
|
163
|
+
Capsule()
|
|
164
|
+
.fill(theme.colors.background.selected)
|
|
165
|
+
.frame(width: railBubbleWidth, height: railBubbleHeight)
|
|
166
|
+
.offset(y: railPadding - (railBubbleHeight - itemSize) / 2
|
|
167
|
+
+ CGFloat(selectedIndex) * (itemSize + railGap))
|
|
168
|
+
.animation(tabSpring, value: selectedIndex)
|
|
169
|
+
|
|
170
|
+
VStack(spacing: railGap) {
|
|
171
|
+
ForEach(items.indices, id: \.self) { index in
|
|
172
|
+
SwiftUI.Button {
|
|
173
|
+
onTabSelected(index)
|
|
174
|
+
compression.onTabSelected()
|
|
175
|
+
} label: {
|
|
176
|
+
DuoTabIcon(item: items[index], selected: index == selectedIndex)
|
|
177
|
+
.frame(width: itemSize, height: itemSize)
|
|
178
|
+
.contentShape(Rectangle())
|
|
179
|
+
}
|
|
180
|
+
.buttonStyle(.plain)
|
|
181
|
+
.accessibilityLabel(items[index].label)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
.padding(.vertical, railPadding)
|
|
185
|
+
}
|
|
186
|
+
.frame(width: itemSize)
|
|
187
|
+
.modifier(DuoBarMaterial(shape: RoundedRectangle(cornerRadius: Radius.XL)))
|
|
188
|
+
.accessibilityIdentifier("bottom_tab_bar")
|
|
189
|
+
}
|
|
161
190
|
}
|
|
162
191
|
|
|
163
192
|
/// Floating labelled bar for the unfolded portrait display, QR in the middle cell (Figma MT1-M).
|
|
@@ -17,39 +17,38 @@ struct DuoScrollGradient: View {
|
|
|
17
17
|
let scrollOffset: CGFloat
|
|
18
18
|
let headerHeight: CGFloat
|
|
19
19
|
let extended: Bool
|
|
20
|
+
/// Quick return header: the gradient leaves with the content instead of fading.
|
|
21
|
+
var followScroll = false
|
|
20
22
|
|
|
21
23
|
@Environment(\.appTheme) private var theme
|
|
22
|
-
@Environment(\.screenLayout) private var layout
|
|
23
24
|
|
|
24
25
|
private var scroll: CGFloat { max(scrollOffset, 0) }
|
|
25
26
|
|
|
26
|
-
private var gradient: LinearGradient {
|
|
27
|
-
LinearGradient(
|
|
28
|
-
colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
|
|
29
|
-
startPoint: .top,
|
|
30
|
-
endPoint: .bottom
|
|
31
|
-
)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
27
|
var body: some View {
|
|
35
28
|
Group {
|
|
36
|
-
if
|
|
37
|
-
// extended squeezes the whole fade into its box and fades out
|
|
38
|
-
let progress = min(scroll / extendedCollapseDistance, 1)
|
|
29
|
+
if followScroll {
|
|
39
30
|
gradient
|
|
40
|
-
.frame(height: headerHeight +
|
|
41
|
-
.
|
|
31
|
+
.frame(height: headerHeight + (extended ? extendedGradientExtra : 0))
|
|
32
|
+
.offset(y: -scroll)
|
|
42
33
|
} else {
|
|
43
|
-
//
|
|
34
|
+
// fades out on scroll as the extended header does off Duo; extended also collapses
|
|
35
|
+
let progress = min(scroll / extendedCollapseDistance, 1)
|
|
44
36
|
gradient
|
|
45
|
-
.frame(height:
|
|
46
|
-
.
|
|
47
|
-
.clipped()
|
|
48
|
-
.offset(y: -scroll)
|
|
37
|
+
.frame(height: headerHeight + (extended ? extendedGradientExtra * (1 - progress) : 0))
|
|
38
|
+
.opacity(1 - min(scroll / extendedGradientExtra, 1))
|
|
49
39
|
}
|
|
50
40
|
}
|
|
51
41
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
|
|
52
42
|
.ignoresSafeArea()
|
|
53
43
|
.allowsHitTesting(false)
|
|
54
44
|
}
|
|
45
|
+
|
|
46
|
+
/// The fade spans the gradient box.
|
|
47
|
+
private var gradient: LinearGradient {
|
|
48
|
+
LinearGradient(
|
|
49
|
+
colors: [theme.colors.gradient, theme.colors.gradient.opacity(0)],
|
|
50
|
+
startPoint: .top,
|
|
51
|
+
endPoint: .bottom
|
|
52
|
+
)
|
|
53
|
+
}
|
|
55
54
|
}
|
|
@@ -113,8 +113,9 @@ public struct NavigationHeader: View {
|
|
|
113
113
|
}
|
|
114
114
|
.frame(height: layout.headerBarHeight)
|
|
115
115
|
.overlay(alignment: .bottom) { VerticalShadow(opacity: opacity) }
|
|
116
|
-
//
|
|
117
|
-
|
|
116
|
+
// the row starts headerStatusBarHeight below the window top: 0 with the status bar off
|
|
117
|
+
// the top edge, lower on the iPhone Duo outer display to center on the camera
|
|
118
|
+
.padding(.top, layout.headerStatusBarHeight - layout.insets.top)
|
|
118
119
|
}
|
|
119
120
|
}
|
|
120
121
|
|
|
@@ -31,6 +31,7 @@ struct ToolkitNavigationHeaderRight: View {
|
|
|
31
31
|
@State private var isFavorite: Bool = false
|
|
32
32
|
@State private var isLoading: Bool = false
|
|
33
33
|
@Environment(\.applicationEnvironment) private var environment
|
|
34
|
+
@Environment(\.duoBarCompression) private var compression
|
|
34
35
|
|
|
35
36
|
private func apiValue(_ value: Any?) -> Any {
|
|
36
37
|
value ?? NSNull()
|
|
@@ -169,7 +170,38 @@ struct ToolkitNavigationHeaderRight: View {
|
|
|
169
170
|
} ?? false
|
|
170
171
|
}
|
|
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
|
|
172
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 {
|
|
173
205
|
let navButtonConfig = getNavigationButtonConfig()
|
|
174
206
|
return VStack(spacing: Spacing.M) {
|
|
175
207
|
if shouldShowShortcut {
|
|
@@ -24,7 +24,7 @@ struct HeaderSideBar: View {
|
|
|
24
24
|
let goBack: (() -> Void)?
|
|
25
25
|
let toolkit: (data: HeaderRightData?, tintColor: Color?)?
|
|
26
26
|
let headerColor: HeaderColor
|
|
27
|
-
/// What footer / tab bar / home indicator take from the bottom; the column stays above it.
|
|
27
|
+
/// What footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
|
|
28
28
|
let bottomInset: CGFloat
|
|
29
29
|
|
|
30
30
|
@Environment(\.screenLayout) private var layout
|
|
@@ -33,7 +33,7 @@ struct HeaderSideBar: View {
|
|
|
33
33
|
private var isRight: Bool { sideBar.edge == .right }
|
|
34
34
|
|
|
35
35
|
private var top: CGFloat {
|
|
36
|
-
let maxY = layout.height - bottomInset -
|
|
36
|
+
let maxY = layout.height - bottomInset - columnHeight
|
|
37
37
|
return max(min(sideBar.top, maxY), 0)
|
|
38
38
|
}
|
|
39
39
|
|
|
@@ -102,6 +102,9 @@ 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
|
|
107
110
|
@Environment(\.screenLayout) private var layout
|
|
@@ -140,11 +143,44 @@ public struct StackScreen<Content: View>: View {
|
|
|
140
143
|
&& inputSearchProps == nil && !headerTransparent && !fullScreenContent
|
|
141
144
|
}
|
|
142
145
|
|
|
143
|
-
///
|
|
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.
|
|
144
180
|
private var bottomBarsHeight: CGFloat {
|
|
145
|
-
if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom }
|
|
146
|
-
if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve
|
|
147
|
-
return NavigationFooter.navigationBarInset
|
|
181
|
+
if footer != nil { return footerHeight + NavigationFooter.safeAreaBottom + Spacing.XL }
|
|
182
|
+
if duoTabBarInsets.sideBarReserve > 0 { return duoTabBarInsets.sideBarReserve }
|
|
183
|
+
return NavigationFooter.navigationBarInset + Spacing.XL
|
|
148
184
|
}
|
|
149
185
|
|
|
150
186
|
/// Keyboard frames are in screen coordinates; UIScreen stays on the outer display on iPhone Duo.
|
|
@@ -234,6 +270,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
234
270
|
}
|
|
235
271
|
}
|
|
236
272
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
|
273
|
+
.onChange(of: scrollOffset) { updateHeaderHide($0) }
|
|
237
274
|
.onPreferenceChange(HeaderRightWidthKey.self) { width in
|
|
238
275
|
if headerRightWidth != width { headerRightWidth = width }
|
|
239
276
|
}
|
|
@@ -319,6 +356,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
319
356
|
}
|
|
320
357
|
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
321
358
|
)
|
|
359
|
+
.offset(y: -headerHide)
|
|
322
360
|
.zIndex(100)
|
|
323
361
|
}
|
|
324
362
|
} else if headerTransparent && fullScreenContent {
|
|
@@ -342,7 +380,8 @@ public struct StackScreen<Content: View>: View {
|
|
|
342
380
|
DuoScrollGradient(
|
|
343
381
|
scrollOffset: scrollOffset,
|
|
344
382
|
headerHeight: layout.headerHeight,
|
|
345
|
-
extended: headerType == .extended
|
|
383
|
+
extended: headerType == .extended,
|
|
384
|
+
followScroll: useQuickReturn
|
|
346
385
|
)
|
|
347
386
|
|
|
348
387
|
StackScreenContent(
|
|
@@ -360,6 +399,7 @@ public struct StackScreen<Content: View>: View {
|
|
|
360
399
|
.modifier(DuoBarMaterial(shape: Rectangle(), visible: scrollOffset > 0))
|
|
361
400
|
.edgesIgnoringSafeArea([.top, .horizontal])
|
|
362
401
|
)
|
|
402
|
+
.offset(y: -headerHide)
|
|
363
403
|
.zIndex(100)
|
|
364
404
|
}
|
|
365
405
|
} else {
|
|
@@ -526,7 +566,7 @@ private struct SearchAnimatedHeader: View {
|
|
|
526
566
|
let scroll = max(scrollOffset, 0)
|
|
527
567
|
|
|
528
568
|
// Insets are measured from the safe-area top; collapsed, the search is centered in the bar row.
|
|
529
|
-
let rowTop =
|
|
569
|
+
let rowTop = layout.headerStatusBarHeight - layout.insets.top
|
|
530
570
|
let minTop = rowTop + (layout.headerBarHeight - headerHeight) / 2
|
|
531
571
|
let maxTop = layout.headerHeight - layout.insets.top
|
|
532
572
|
let minSide = Spacing.M
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
//
|
|
5
5
|
// Mirrors RN `app/screens/IPhoneDuo/DuoTabUsage`: glass rail in the Duo side bar, floating
|
|
6
6
|
// glass bar when unfolded portrait. `useShortcut` gives the tallest side bar header, for the
|
|
7
|
-
// overlap review
|
|
7
|
+
// overlap review; `compressTabBar` folds the tab bar instead of the toolbar when the side bar
|
|
8
|
+
// is short (HIG "Tab bar compressed").
|
|
8
9
|
//
|
|
9
10
|
|
|
10
11
|
import MoMoUIKits
|
|
@@ -15,10 +16,11 @@ private let history = (1...18).map { "Giao dịch #\($0)" }
|
|
|
15
16
|
|
|
16
17
|
struct DuoTabDemo: View {
|
|
17
18
|
let useShortcut: Bool
|
|
19
|
+
var compressTabBar = false
|
|
18
20
|
|
|
19
21
|
var body: some View {
|
|
20
22
|
if #available(iOS 16.0, *) {
|
|
21
|
-
DuoTabDemoContent(useShortcut: useShortcut)
|
|
23
|
+
DuoTabDemoContent(useShortcut: useShortcut, compressTabBar: compressTabBar)
|
|
22
24
|
} else {
|
|
23
25
|
Text("Requires iOS 16+").padding()
|
|
24
26
|
}
|
|
@@ -28,6 +30,7 @@ struct DuoTabDemo: View {
|
|
|
28
30
|
@available(iOS 16.0, *)
|
|
29
31
|
private struct DuoTabDemoContent: View {
|
|
30
32
|
let useShortcut: Bool
|
|
33
|
+
let compressTabBar: Bool
|
|
31
34
|
|
|
32
35
|
var body: some View {
|
|
33
36
|
BottomTab(
|
|
@@ -37,7 +40,8 @@ private struct DuoTabDemoContent: View {
|
|
|
37
40
|
tab("DuoHistory", label: "Lịch sử GD", icon: "time_clock_reset", title: "Lịch sử giao dịch", badge: "3"),
|
|
38
41
|
tab("DuoMe", label: "Tôi", icon: "basic_person", title: "Tôi"),
|
|
39
42
|
],
|
|
40
|
-
floatingButton: BottomTabFloatingButton(icon: "navigation_full_screen", label: "Quét mọi QR", onPress: {})
|
|
43
|
+
floatingButton: BottomTabFloatingButton(icon: "navigation_full_screen", label: "Quét mọi QR", onPress: {}),
|
|
44
|
+
compressionBehavior: compressTabBar ? .tabBar : .toolbar
|
|
41
45
|
)
|
|
42
46
|
}
|
|
43
47
|
|
|
@@ -53,6 +53,9 @@ private struct IPhoneDuoDemoContent: View {
|
|
|
53
53
|
demoButton("Glass tab bar + useShortcut") {
|
|
54
54
|
navigator.push({ DuoTabDemo(useShortcut: true) }, options: tabOptions)
|
|
55
55
|
}
|
|
56
|
+
demoButton("Glass tab bar + compress tab bar") {
|
|
57
|
+
navigator.push({ DuoTabDemo(useShortcut: true, compressTabBar: true) }, options: tabOptions)
|
|
58
|
+
}
|
|
56
59
|
demoButton("Bottom sheet: iPhone Duo safe area") {
|
|
57
60
|
navigator.showBottomSheet({ BottomSheetSafeArea() }, isSurface: true, header: .title("Safe area"))
|
|
58
61
|
}
|