@momo-kits/native-kits 0.167.1-duo.2-debug → 0.167.1-duo.20-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 +36 -8
- package/README.md +2 -2
- package/compose/build.gradle.kts +15 -1
- package/compose/build.gradle.kts.backup +14 -0
- package/compose/compose.podspec +1 -1
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/Platform.android.kt +2 -0
- package/compose/src/androidMain/kotlin/vn/momo/kits/platform/ReservedRegions.android.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +168 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Badge.kt +20 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Button.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Carousel.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Chip.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Icon.kt +24 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Image.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Information.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Input.kt +9 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputMoney.kt +11 -12
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputOTP.kt +4 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputPhoneNumber.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputSearch.kt +10 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/InputTextArea.kt +6 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupNotify.kt +53 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/ProgressInfo.kt +7 -9
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Radio.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Slider.kt +13 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Swipe.kt +13 -5
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Switch.kt +3 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/TabView.kt +14 -6
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tag.kt +19 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Text.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/Tooltip.kt +5 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/DateTimePickerUtils.kt +3 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/datetimepicker/WheelPicker.kt +7 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/HostFont.kt +4 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Theme.kt +10 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +3 -17
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +173 -21
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/BottomSheet.kt +43 -4
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/ModalScreen.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigation.kt +10 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +153 -48
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +114 -71
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +8 -9
- 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 +312 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +84 -18
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +33 -13
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +51 -19
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +4 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +46 -23
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +13 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +38 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/Platform.ios.kt +4 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/platform/ReservedRegions.ios.kt +94 -0
- package/gradle.properties +1 -1
- package/ios/Application/ApplicationEnvironment.swift +17 -0
- package/ios/Application/MaxApi.swift +12 -3
- package/ios/Avatar/Avatar.swift +4 -3
- package/ios/Badge/Badge.swift +6 -4
- package/ios/Badge/BadgeDot.swift +1 -1
- package/ios/Badge/BadgeRibbon.swift +0 -1
- package/ios/Button/Button.swift +15 -12
- package/ios/CalculatorKeyboard/CalculatorKeyboard.swift +64 -18
- package/ios/Carousel/Carousel.swift +29 -12
- package/ios/Checkbox/Checkbox.swift +7 -3
- package/ios/Chip/Chip.swift +5 -4
- package/ios/Collapse/Collapse.swift +70 -19
- package/ios/Colors+Radius+Spacing/Colors.swift +10 -1
- package/ios/Colors+Radius+Spacing/Radius.swift +6 -0
- package/ios/DateTimePicker/DateTimePicker.swift +54 -20
- package/ios/DateTimePicker/WheelPicker.swift +103 -105
- package/ios/Divider/Divider.swift +37 -4
- package/ios/Extensions/Color++.swift +38 -9
- package/ios/Extensions/OptionalEnvironmentObject.swift +12 -0
- package/ios/Extensions/SkeletonUICompat.swift +11 -0
- package/ios/Icon/Icon.swift +21 -23
- package/ios/Icon/Icons.swift +1329 -0
- package/ios/IconButton/IconButton.swift +15 -7
- package/ios/Image/Image.swift +7 -23
- package/ios/Information/Information.swift +27 -9
- package/ios/Input/ErrorView.swift +2 -2
- package/ios/Input/Input.swift +160 -147
- package/ios/Input/InputPhoneNumber.swift +12 -9
- package/ios/Input/InputSearch.swift +102 -75
- package/ios/Input/InputTextArea.swift +128 -145
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +33 -11
- package/ios/InputOTP/InputOTP.swift +22 -12
- package/ios/Layout/Card.swift +50 -0
- package/ios/Layout/GridContext.swift +187 -3
- package/ios/Layout/Item.swift +5 -4
- package/ios/Layout/ScreenLayout.swift +380 -0
- package/ios/Layout/Section.swift +4 -100
- package/ios/Loader/Loader.swift +5 -9
- package/ios/Lottie/LottieView.swift +14 -2
- package/ios/{Application/Navigation/Overplay → Navigation}/BottomSheet.swift +128 -9
- package/ios/Navigation/BottomTab/BottomTab.swift +185 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +15 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +105 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +304 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +14 -3
- package/ios/Navigation/Component/Header.swift +531 -0
- package/ios/Navigation/Component/HeaderBackground.swift +133 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +377 -0
- package/ios/Navigation/Component/HeaderTitle.swift +36 -0
- package/ios/{Application/Navigation → Navigation/Component}/HeaderUser.swift +21 -24
- package/ios/Navigation/Component/SideBar.swift +183 -0
- package/ios/{Application/Navigation/Overplay → Navigation/Component}/SnackBar.swift +5 -6
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +5 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +194 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +177 -91
- package/ios/Navigation/StackScreen.swift +820 -0
- package/ios/Navigation/StackTransition.swift +400 -0
- package/ios/Navigation/SwipeBackHaptic.swift +33 -0
- package/ios/{Application/Navigation → Navigation}/Tracking/ScreenTracker.swift +12 -5
- package/ios/OTPKeyboard/OTPKeyboard.swift +8 -11
- package/ios/Pagination/PaginationScroll.swift +2 -2
- package/ios/Popup/PopupDisplay.swift +72 -136
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +140 -0
- package/ios/Popup/PopupNotify.swift +56 -73
- package/ios/Popup/PopupPromotion.swift +31 -47
- package/ios/PopupView/FullscreenPopup.swift +13 -19
- package/ios/PopupView/PopupView.swift +1 -1
- package/ios/PopupView/Utils++.swift +0 -8
- package/ios/ProgressInfo/ProgressInfo.swift +4 -2
- package/ios/Radio/Radio.swift +2 -1
- package/ios/Rating/Rating.swift +9 -1
- package/ios/Resources/dot_loading.json +558 -0
- package/ios/Resources/lottie_circle_loader.json +1 -0
- package/ios/ScrollIndicator/ScrollIndicator.swift +11 -10
- package/ios/Skeleton/Skeleton.swift +22 -27
- package/ios/Slider/Slider.swift +16 -18
- package/ios/Stepper/Stepper.swift +6 -1
- package/ios/Steps/Steps.swift +8 -6
- package/ios/SuggestAction/SuggestAction.swift +5 -0
- package/ios/Swipeable/SwipeCell.swift +10 -6
- package/ios/Switch/Switch.swift +22 -20
- package/ios/TabView/TabView.swift +102 -21
- package/ios/Tag/Tag.swift +15 -4
- package/ios/Template/Logo/Logo.swift +2 -1
- package/ios/Template/TrustBanner/TrustBanner.swift +106 -54
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/Text.swift +61 -19
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -1
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/ContentView.swift +1 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/DuoTabDemo.swift +80 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/Home.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/package.json +1 -1
- package/scripts/gen-ios-icons.mjs +36 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/AnimationSearchInput.kt +0 -52
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/FloatingButton.kt +0 -183
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Header.kt +0 -207
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderAnimated.kt +0 -39
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderBackground.kt +0 -86
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderDefault.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderExtended.kt +0 -76
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderRight.kt +0 -293
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/HeaderTitle.kt +0 -33
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/NavigationContainer.kt +0 -110
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/Screen.kt +0 -378
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/useHeaderSearchAnimation.kt +0 -69
- package/ios/Application/Components.swift +0 -270
- package/ios/Application/ComposeApi.swift +0 -22
- package/ios/Application/HeaderRight.swift +0 -306
- package/ios/Application/Navigation/BottomTab/BottomTab.swift +0 -104
- package/ios/Application/Navigation/HeaderBackProps.swift +0 -66
- package/ios/Application/Navigation/HeaderTitle.swift +0 -57
- package/ios/Application/Navigation/NavigationContainer.swift +0 -213
- package/ios/Application/Navigation/NavigationOptions.swift +0 -109
- package/ios/Application/Navigation/NavigatorManager.swift +0 -22
- package/ios/Application/Navigation/component/Header.swift +0 -178
- package/ios/Application/Navigation/component/HeaderBackground.swift +0 -83
- package/ios/Application/Navigation/component/HeaderColor.swift +0 -65
- package/ios/Application/Navigation/component/HeaderRightWidthKey.swift +0 -11
- package/ios/Application/Navigation/component/NavigationFooter.swift +0 -90
- package/ios/Application/Navigation/component/NavigationHeaderRight.swift +0 -196
- package/ios/Application/Screen.swift +0 -250
- package/ios/Application/StackScreen.swift +0 -492
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
//
|
|
2
|
+
// SideBar.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/component/SideBar.kt`. iPhone Duo: back and toolkit
|
|
6
|
+
// leave the header bar for a column in the side safe area, as the system bars do.
|
|
7
|
+
// Port of RN `SideBarButton.tsx` + `useSideBarItemStyle`.
|
|
8
|
+
//
|
|
9
|
+
|
|
10
|
+
import SwiftUI
|
|
11
|
+
|
|
12
|
+
private let sideBarIconSize: CGFloat = 24
|
|
13
|
+
|
|
14
|
+
/// The back button and toolkit column, placed in window coordinates over the side strip.
|
|
15
|
+
struct HeaderSideBar: View {
|
|
16
|
+
let sideBar: SideBarLayout
|
|
17
|
+
let goBack: (() -> Void)?
|
|
18
|
+
let toolkit: (data: HeaderRightData?, tintColor: Color?)?
|
|
19
|
+
let headerColor: HeaderColor
|
|
20
|
+
/// What footer / tab bar / home indicator take from the bottom, gap included; the column stays above it.
|
|
21
|
+
let bottomInset: CGFloat
|
|
22
|
+
|
|
23
|
+
@Environment(\.screenLayout) private var layout
|
|
24
|
+
@Environment(\.applicationEnvironment) private var environment
|
|
25
|
+
@Environment(\.duoBarCompression) private var compression
|
|
26
|
+
@State private var columnHeight: CGFloat = 0
|
|
27
|
+
|
|
28
|
+
private var isRight: Bool { sideBar.edge == .right }
|
|
29
|
+
|
|
30
|
+
private var top: CGFloat {
|
|
31
|
+
let maxY = layout.height - bottomInset - columnHeight
|
|
32
|
+
return max(min(sideBar.top, maxY), 0)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Full height, so the tab bar compression decision follows this screen's toolkit.
|
|
36
|
+
private var fullHeight: CGFloat {
|
|
37
|
+
let toolkitHeight = toolkit.map {
|
|
38
|
+
Spacing.M + toolkitColumnHeight($0.data ?? HeaderRightData(), context: environment.applicationContext)
|
|
39
|
+
}
|
|
40
|
+
return DuoMetrics.controlSize + (toolkitHeight ?? 0)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
var body: some View {
|
|
44
|
+
VStack(alignment: isRight ? .leading : .trailing, spacing: Spacing.M) {
|
|
45
|
+
if let goBack {
|
|
46
|
+
SideBarButton(
|
|
47
|
+
icon: "arrow-back",
|
|
48
|
+
headerColor: headerColor,
|
|
49
|
+
accessibilityId: "btn_navigation_back",
|
|
50
|
+
action: goBack
|
|
51
|
+
)
|
|
52
|
+
} else {
|
|
53
|
+
// keep the slot so the toolkit does not jump between root and pushed screens
|
|
54
|
+
Color.clear.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
|
|
55
|
+
}
|
|
56
|
+
if let toolkit {
|
|
57
|
+
if compression.toolbarHidden {
|
|
58
|
+
// an open tab rail takes this slot for a moment
|
|
59
|
+
EmptyView()
|
|
60
|
+
} else if compression.toolbarCompressed {
|
|
61
|
+
// HIG "Toolbar compressed": tools fold into one button, distinct from the useMore icon
|
|
62
|
+
SideBarButton(
|
|
63
|
+
icon: "navigation_menu",
|
|
64
|
+
headerColor: headerColor,
|
|
65
|
+
showBadge: toolkit.data?.showBadge ?? false,
|
|
66
|
+
accessibilityId: "btn_navigation_toolbar",
|
|
67
|
+
action: compression.expandToolbar
|
|
68
|
+
)
|
|
69
|
+
} else {
|
|
70
|
+
Toolkit(
|
|
71
|
+
headerRight: toolkit.data ?? HeaderRightData(),
|
|
72
|
+
tintColor: toolkit.tintColor,
|
|
73
|
+
layout: .sideBarColumn
|
|
74
|
+
)
|
|
75
|
+
.environment(\.headerColor, headerColor)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
.padding(isRight ? .leading : .trailing, Spacing.M)
|
|
80
|
+
.frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
|
|
81
|
+
.background(
|
|
82
|
+
GeometryReader { proxy in
|
|
83
|
+
Color.clear
|
|
84
|
+
.onAppear { columnHeight = proxy.size.height }
|
|
85
|
+
.onChange(of: proxy.size.height) { columnHeight = $0 }
|
|
86
|
+
}
|
|
87
|
+
)
|
|
88
|
+
.padding(.top, top)
|
|
89
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .topTrailing : .topLeading)
|
|
90
|
+
.ignoresSafeArea()
|
|
91
|
+
.onAppear { compression.setToolbarHeight(fullHeight) }
|
|
92
|
+
.onChange(of: fullHeight) { compression.setToolbarHeight($0) }
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Dark glass under light icons (banner headers), light glass otherwise.
|
|
97
|
+
private struct SideBarGlass<S: Shape>: ViewModifier {
|
|
98
|
+
let headerColor: HeaderColor
|
|
99
|
+
let shape: S
|
|
100
|
+
|
|
101
|
+
private var isDark: Bool { headerColor.tintIconColor == Colors.black01 || headerColor.tintIconColor == .white }
|
|
102
|
+
|
|
103
|
+
func body(content: Content) -> some View {
|
|
104
|
+
if isDark {
|
|
105
|
+
content
|
|
106
|
+
.background(shape.fill(Colors.black20.opacity(0.5)))
|
|
107
|
+
.overlay(shape.stroke(Colors.black01.opacity(0.2), lineWidth: 0.5))
|
|
108
|
+
} else {
|
|
109
|
+
content
|
|
110
|
+
.background(
|
|
111
|
+
shape.fill(Colors.black01.opacity(0.85))
|
|
112
|
+
.shadow(color: Color.black.opacity(0.12), radius: 4, x: 0, y: 2)
|
|
113
|
+
)
|
|
114
|
+
.overlay(shape.stroke(Colors.black20.opacity(0.08), lineWidth: 0.5))
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// 48pt glass circle for a single side bar control.
|
|
120
|
+
struct SideBarButton: View {
|
|
121
|
+
let icon: String
|
|
122
|
+
let headerColor: HeaderColor
|
|
123
|
+
var showBadge: Bool = false
|
|
124
|
+
var disabled: Bool = false
|
|
125
|
+
var accessibilityId: String?
|
|
126
|
+
let action: () -> Void
|
|
127
|
+
|
|
128
|
+
var body: some View {
|
|
129
|
+
SwiftUI.Button(action: action) {
|
|
130
|
+
Icon(source: icon, size: sideBarIconSize, color: headerColor.tintIconColor)
|
|
131
|
+
.frame(width: DuoMetrics.controlSize, height: DuoMetrics.controlSize)
|
|
132
|
+
.modifier(SideBarGlass(headerColor: headerColor, shape: Circle()))
|
|
133
|
+
.overlay(alignment: .topTrailing) {
|
|
134
|
+
if showBadge {
|
|
135
|
+
BadgeDot(size: .small).offset(x: -Spacing.XXS, y: Spacing.XXS)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
.buttonStyle(ActiveOpacityButtonStyle())
|
|
140
|
+
.disabled(disabled)
|
|
141
|
+
.accessibilityIdentifier(accessibilityId ?? "")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// 48pt glass capsule grouping related controls, one item per 50pt: vertical in the side bar,
|
|
146
|
+
/// horizontal on a header row.
|
|
147
|
+
struct SideBarGroup<Content: View>: View {
|
|
148
|
+
let headerColor: HeaderColor
|
|
149
|
+
let itemCount: Int
|
|
150
|
+
var horizontal: Bool = false
|
|
151
|
+
@ViewBuilder let content: () -> Content
|
|
152
|
+
|
|
153
|
+
var body: some View {
|
|
154
|
+
let length = itemCount > 1 ? DuoMetrics.itemPitch * CGFloat(itemCount) : DuoMetrics.controlSize
|
|
155
|
+
Group {
|
|
156
|
+
if horizontal {
|
|
157
|
+
HStack(spacing: 0) { content() }
|
|
158
|
+
.frame(width: length, height: DuoMetrics.controlSize)
|
|
159
|
+
} else {
|
|
160
|
+
VStack(spacing: 0) { content() }
|
|
161
|
+
.frame(width: DuoMetrics.controlSize, height: length)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
.modifier(SideBarGlass(headerColor: headerColor, shape: RoundedRectangle(cornerRadius: Radius.XL)))
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
struct SideBarGroupItem: View {
|
|
169
|
+
let icon: String
|
|
170
|
+
let headerColor: HeaderColor
|
|
171
|
+
let accessibilityId: String
|
|
172
|
+
let action: () -> Void
|
|
173
|
+
|
|
174
|
+
var body: some View {
|
|
175
|
+
SwiftUI.Button(action: action) {
|
|
176
|
+
Icon(source: icon, size: sideBarIconSize, color: headerColor.tintIconColor)
|
|
177
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
178
|
+
.contentShape(Rectangle())
|
|
179
|
+
}
|
|
180
|
+
.buttonStyle(ActiveOpacityButtonStyle())
|
|
181
|
+
.accessibilityIdentifier(accessibilityId)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// SnackBar.swift
|
|
3
3
|
// MoMoUIKits
|
|
4
4
|
//
|
|
5
|
-
// SwiftUI mirror of Compose's `
|
|
6
|
-
//
|
|
7
|
-
// indicator), stays for `duration`, then slides back down and is removed.
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/component/SnackBar.kt`: slides up above the screen's
|
|
6
|
+
// footer (or the home indicator), stays for `duration`, then slides back down.
|
|
8
7
|
// `Navigator.showSnackBar(_:)` drives it through the overplay host.
|
|
9
8
|
//
|
|
10
9
|
|
|
@@ -50,11 +49,10 @@ public struct SnackBar {
|
|
|
50
49
|
|
|
51
50
|
/// Self-animating container. Slides the content up on appear, schedules an
|
|
52
51
|
/// auto-dismiss after `duration`, and animates out before removing the overplay.
|
|
53
|
-
@available(iOS 16.0, *)
|
|
54
52
|
struct SnackBarView: View {
|
|
55
53
|
let snackBar: SnackBar
|
|
56
|
-
///
|
|
57
|
-
/// (
|
|
54
|
+
/// Distance from the window bottom to the top of the screen's footer, or the navigation bar
|
|
55
|
+
/// inset without one (Compose's `footerHeight` offset).
|
|
58
56
|
let bottomInset: CGFloat
|
|
59
57
|
|
|
60
58
|
@EnvironmentObject private var navigator: Navigator
|
|
@@ -78,6 +76,7 @@ struct SnackBarView: View {
|
|
|
78
76
|
.padding(.bottom, bottomInset)
|
|
79
77
|
}
|
|
80
78
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
79
|
+
.ignoresSafeArea()
|
|
81
80
|
.onAppear {
|
|
82
81
|
guard !didAppear else { return }
|
|
83
82
|
didAppear = true
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
//
|
|
2
|
+
// HostBackGestureBridge.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/HostBackGestureBridge.kt`: the host owns the edge-pan
|
|
6
|
+
// recogniser and routes a back into the topmost kit stack. NavigationContainer attaches itself.
|
|
7
|
+
// `started` / `progressed` / `completed` / `cancelled` scrub the pop transition with the finger;
|
|
8
|
+
// a host that only calls `pop` gets the regular animated back.
|
|
9
|
+
//
|
|
10
|
+
|
|
11
|
+
import UIKit
|
|
12
|
+
|
|
13
|
+
public final class HostBackGestureBridge {
|
|
14
|
+
public static let shared = HostBackGestureBridge()
|
|
15
|
+
private init() {}
|
|
16
|
+
|
|
17
|
+
// Weak: a container torn down without disappearing must not keep receiving backs.
|
|
18
|
+
private var navigators: [WeakNavigator] = []
|
|
19
|
+
private weak var gestureTarget: Navigator?
|
|
20
|
+
|
|
21
|
+
/// Set while a bottom tab other than the first is selected; the host shows its floating arrow.
|
|
22
|
+
public var shouldUseFloatingArrowBack = false
|
|
23
|
+
|
|
24
|
+
public func attach(_ navigator: Navigator) {
|
|
25
|
+
navigators.removeAll { $0.value == nil }
|
|
26
|
+
guard !navigators.contains(where: { $0.value === navigator }) else { return }
|
|
27
|
+
navigators.append(WeakNavigator(value: navigator))
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
public func detach(_ navigator: Navigator) {
|
|
31
|
+
navigators.removeAll { $0.value == nil || $0.value === navigator }
|
|
32
|
+
if gestureTarget === navigator { gestureTarget = nil }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Whether a kit stack is on screen at all.
|
|
36
|
+
public func checkCanPop() -> Bool { top() != nil }
|
|
37
|
+
|
|
38
|
+
/// Whether the top stack pops a screen itself; false means a back dismisses the mini app.
|
|
39
|
+
public func canPopInner() -> Bool { top()?.canPop() == true }
|
|
40
|
+
|
|
41
|
+
public func pop() { top()?.onBackSafe() }
|
|
42
|
+
|
|
43
|
+
public func started() {
|
|
44
|
+
guard let target = top() else { return }
|
|
45
|
+
gestureTarget = target
|
|
46
|
+
target.backStarted()
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// `progress` is the touch travel over the window width, 0...1.
|
|
50
|
+
public func progressed(_ progress: CGFloat) {
|
|
51
|
+
gestureTarget?.backProgressed(min(max(progress, 0), 1))
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public func completed() {
|
|
55
|
+
(gestureTarget ?? top())?.backCompleted()
|
|
56
|
+
gestureTarget = nil
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
public func cancelled() {
|
|
60
|
+
gestureTarget?.backCancelled()
|
|
61
|
+
gestureTarget = nil
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
@available(*, deprecated, renamed: "attach")
|
|
65
|
+
public func push(_ navigator: Navigator) { attach(navigator) }
|
|
66
|
+
|
|
67
|
+
@available(*, deprecated, renamed: "detach")
|
|
68
|
+
public func remove(_ navigator: Navigator) { detach(navigator) }
|
|
69
|
+
|
|
70
|
+
private func top() -> Navigator? { navigators.last { $0.value != nil }?.value }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@available(*, deprecated, renamed: "HostBackGestureBridge")
|
|
74
|
+
public typealias NavigatorManager = HostBackGestureBridge
|
|
75
|
+
|
|
76
|
+
private struct WeakNavigator {
|
|
77
|
+
weak var value: Navigator?
|
|
78
|
+
}
|
|
@@ -9,13 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import SwiftUI
|
|
11
11
|
|
|
12
|
-
@available(iOS 16.0, *)
|
|
13
12
|
struct ModalScreen<Content: View>: View {
|
|
14
13
|
let barrierDismissible: Bool
|
|
15
14
|
let content: () -> Content
|
|
16
15
|
|
|
17
16
|
@EnvironmentObject private var navigator: Navigator
|
|
18
17
|
@Environment(\.applicationEnvironment) private var appEnv
|
|
18
|
+
@Environment(\.screenLayout) private var layout
|
|
19
19
|
|
|
20
20
|
@State private var scrimOpacity: CGFloat = 0
|
|
21
21
|
@State private var contentScale: CGFloat = 0.8
|
|
@@ -35,8 +35,11 @@ struct ModalScreen<Content: View>: View {
|
|
|
35
35
|
content()
|
|
36
36
|
.scaleEffect(contentScale)
|
|
37
37
|
.opacity(contentOpacity)
|
|
38
|
+
// iPhone Duo: center below the side status bar and above the home indicator
|
|
39
|
+
.padding(.top, layout.isDuoDevice ? layout.duoTopInset : 0)
|
|
40
|
+
.padding(.bottom, layout.isDuoDevice ? layout.insets.bottom : 0)
|
|
38
41
|
}
|
|
39
|
-
//
|
|
42
|
+
// Full screen like RN's Modal, so asymmetric landscape insets don't shift the content sideways.
|
|
40
43
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
|
41
44
|
.ignoresSafeArea(.container)
|
|
42
45
|
.onAppear {
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Navigation.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/Navigation.kt`: a screen's `NavigationOptions` and the
|
|
6
|
+
// per-screen `Navigation` that updates them at runtime.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
/// Per-screen handle (Compose `LocalNavigation`). Read it inside a screen with
|
|
12
|
+
/// `@EnvironmentObject var navigation: Navigation` and call `setOptions` to change the header,
|
|
13
|
+
/// footer or back handling after the screen is shown.
|
|
14
|
+
public final class Navigation: ObservableObject {
|
|
15
|
+
public let id: Int
|
|
16
|
+
public let bottomTabIndex: Int
|
|
17
|
+
@Published public private(set) var options: NavigationOptions
|
|
18
|
+
|
|
19
|
+
private weak var registry: DynamicScreenRegistry?
|
|
20
|
+
/// What the footer (or navigation bar) takes from the window bottom; lifts snack bars.
|
|
21
|
+
var bottomObstruction: CGFloat = 0
|
|
22
|
+
|
|
23
|
+
init(
|
|
24
|
+
id: Int = -1,
|
|
25
|
+
bottomTabIndex: Int = -1,
|
|
26
|
+
options: NavigationOptions?,
|
|
27
|
+
registry: DynamicScreenRegistry? = nil
|
|
28
|
+
) {
|
|
29
|
+
self.id = id
|
|
30
|
+
self.bottomTabIndex = bottomTabIndex
|
|
31
|
+
self.options = options ?? NavigationOptions()
|
|
32
|
+
self.registry = registry
|
|
33
|
+
if id != -1 { registry?.bind(self) }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public func setOptions(_ options: NavigationOptions) {
|
|
37
|
+
update(options)
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Changes only what the closure touches, like Compose's named-argument `setOptions`.
|
|
41
|
+
public func setOptions(_ transform: (inout NavigationOptions) -> Void) {
|
|
42
|
+
var updated = options
|
|
43
|
+
transform(&updated)
|
|
44
|
+
update(updated)
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private func update(_ updated: NavigationOptions) {
|
|
48
|
+
options = updated
|
|
49
|
+
// back handling reads the registry copy
|
|
50
|
+
if id != -1 { registry?.setOptions(id: id, options: updated) }
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public struct NavigationOptions {
|
|
55
|
+
public var screenName: String?
|
|
56
|
+
public var title: String
|
|
57
|
+
/// Rich header title (Compose `headerTitle`); `title` is used when nil.
|
|
58
|
+
public var headerTitle: HeaderTitle?
|
|
59
|
+
public var titlePosition: TitlePosition
|
|
60
|
+
public var headerType: HeaderType
|
|
61
|
+
/// Compose's `HeaderType.Default/Extended(useAnimated:)`: with `inputSearchProps`, the search
|
|
62
|
+
/// rises into the header on scroll.
|
|
63
|
+
public var useAnimated: Bool
|
|
64
|
+
/// Compose's `HeaderType.Transparent`; with `fullScreenContent` the content starts under it.
|
|
65
|
+
public var headerTransparent: Bool
|
|
66
|
+
public var fullScreenContent: Bool
|
|
67
|
+
public var hiddenBack: Bool
|
|
68
|
+
public var backgroundColor: Color?
|
|
69
|
+
// AI-GENERATED START: custom screen background view (Compose backgroundComponent)
|
|
70
|
+
/// Drawn instead of `backgroundColor` over the whole window: under the header and the status bar,
|
|
71
|
+
/// across the iPhone Duo side strips, and not shrunk by the keyboard. Pair with `headerTransparent`.
|
|
72
|
+
public var background: (() -> AnyView)?
|
|
73
|
+
// AI-GENERATED END: custom screen background view (Compose backgroundComponent)
|
|
74
|
+
public var tintColor: Color?
|
|
75
|
+
public var scrollable: Bool
|
|
76
|
+
/// Compose's `ScrollData.scrollToTopCallback`, fired on a status-bar tap.
|
|
77
|
+
public var scrollToTopCallback: (() -> Void)?
|
|
78
|
+
public var useAvoidKeyboard: Bool
|
|
79
|
+
/// Compose's `KeyboardOptions.keyboardShouldPersistTaps`: a tap anywhere hides the keyboard.
|
|
80
|
+
public var keyboardShouldPersistTaps: Bool
|
|
81
|
+
public var headerBackProps: HeaderBackProps?
|
|
82
|
+
/// Toolkit data for the default header right (Compose `HeaderRight.Toolkit`). Use `headerRight`
|
|
83
|
+
/// for a custom view and nil for none.
|
|
84
|
+
public var headerRightData: HeaderRightData?
|
|
85
|
+
public var headerRight: (() -> any View)?
|
|
86
|
+
public var footer: (() -> AnyView)?
|
|
87
|
+
public var footerTransparent: Bool
|
|
88
|
+
public var footerUseMargin: Bool
|
|
89
|
+
public var animatedHeader: AnimatedHeader?
|
|
90
|
+
public var layoutOffset: CGFloat
|
|
91
|
+
public var inputSearchProps: InputSearchProps?
|
|
92
|
+
public var fabProps: FabProps?
|
|
93
|
+
public var onBackHandler: (() -> Void)?
|
|
94
|
+
|
|
95
|
+
public init(
|
|
96
|
+
screenName: String? = nil,
|
|
97
|
+
title: String = "Stack",
|
|
98
|
+
headerTitle: HeaderTitle? = nil,
|
|
99
|
+
titlePosition: TitlePosition = .left,
|
|
100
|
+
headerType: HeaderType = .default,
|
|
101
|
+
useAnimated: Bool = false,
|
|
102
|
+
headerTransparent: Bool = false,
|
|
103
|
+
fullScreenContent: Bool = false,
|
|
104
|
+
hiddenBack: Bool = false,
|
|
105
|
+
backgroundColor: Color? = nil,
|
|
106
|
+
// AI-GENERATED START: custom screen background view (Compose backgroundComponent)
|
|
107
|
+
background: (() -> AnyView)? = nil,
|
|
108
|
+
// AI-GENERATED END: custom screen background view (Compose backgroundComponent)
|
|
109
|
+
tintColor: Color? = Colors.black17,
|
|
110
|
+
scrollable: Bool = true,
|
|
111
|
+
scrollToTopCallback: (() -> Void)? = nil,
|
|
112
|
+
useAvoidKeyboard: Bool = true,
|
|
113
|
+
keyboardShouldPersistTaps: Bool = false,
|
|
114
|
+
headerBackProps: HeaderBackProps? = nil,
|
|
115
|
+
headerRightData: HeaderRightData? = nil,
|
|
116
|
+
headerRight: (() -> any View)? = { HeaderRight() },
|
|
117
|
+
footer: (() -> AnyView)? = nil,
|
|
118
|
+
footerTransparent: Bool = false,
|
|
119
|
+
footerUseMargin: Bool = true,
|
|
120
|
+
animatedHeader: AnimatedHeader? = nil,
|
|
121
|
+
layoutOffset: CGFloat = 56,
|
|
122
|
+
inputSearchProps: InputSearchProps? = nil,
|
|
123
|
+
fabProps: FabProps? = nil,
|
|
124
|
+
onBackHandler: (() -> Void)? = nil
|
|
125
|
+
) {
|
|
126
|
+
self.screenName = screenName
|
|
127
|
+
self.title = title
|
|
128
|
+
self.headerTitle = headerTitle
|
|
129
|
+
self.titlePosition = titlePosition
|
|
130
|
+
self.headerType = headerType
|
|
131
|
+
self.useAnimated = useAnimated
|
|
132
|
+
self.headerTransparent = headerTransparent
|
|
133
|
+
self.fullScreenContent = fullScreenContent
|
|
134
|
+
self.hiddenBack = hiddenBack
|
|
135
|
+
self.backgroundColor = backgroundColor
|
|
136
|
+
// AI-GENERATED START: custom screen background view (Compose backgroundComponent)
|
|
137
|
+
self.background = background
|
|
138
|
+
// AI-GENERATED END: custom screen background view (Compose backgroundComponent)
|
|
139
|
+
self.tintColor = tintColor
|
|
140
|
+
self.scrollable = scrollable
|
|
141
|
+
self.scrollToTopCallback = scrollToTopCallback
|
|
142
|
+
self.useAvoidKeyboard = useAvoidKeyboard
|
|
143
|
+
self.keyboardShouldPersistTaps = keyboardShouldPersistTaps
|
|
144
|
+
self.headerBackProps = headerBackProps
|
|
145
|
+
self.headerRightData = headerRightData
|
|
146
|
+
self.headerRight = headerRight
|
|
147
|
+
self.footer = footer
|
|
148
|
+
self.footerTransparent = footerTransparent
|
|
149
|
+
self.footerUseMargin = footerUseMargin
|
|
150
|
+
self.animatedHeader = animatedHeader
|
|
151
|
+
self.layoutOffset = layoutOffset
|
|
152
|
+
self.inputSearchProps = inputSearchProps
|
|
153
|
+
self.fabProps = fabProps
|
|
154
|
+
self.onBackHandler = onBackHandler
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
extension NavigationOptions {
|
|
159
|
+
var resolvedHeaderRight: (() -> any View)? {
|
|
160
|
+
guard let headerRightData else { return headerRight }
|
|
161
|
+
return { HeaderRight(headerRight: headerRightData, tintColor: tintColor) }
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// `StackScreen` built from options, shared by stack screens and bottom-tab screens.
|
|
166
|
+
struct OptionsStackScreen: View {
|
|
167
|
+
let options: NavigationOptions
|
|
168
|
+
let screenName: String
|
|
169
|
+
let goBack: (() -> Void)?
|
|
170
|
+
let content: () -> AnyView
|
|
171
|
+
|
|
172
|
+
var body: some View {
|
|
173
|
+
StackScreen(
|
|
174
|
+
backgroundColor: options.backgroundColor,
|
|
175
|
+
// AI-GENERATED START: custom screen background view (Compose backgroundComponent)
|
|
176
|
+
background: options.background,
|
|
177
|
+
// AI-GENERATED END: custom screen background view (Compose backgroundComponent)
|
|
178
|
+
headerTransparent: options.headerTransparent,
|
|
179
|
+
fullScreenContent: options.fullScreenContent,
|
|
180
|
+
tintColor: options.tintColor ?? Colors.black17,
|
|
181
|
+
isBack: !options.hiddenBack,
|
|
182
|
+
headerType: options.headerType,
|
|
183
|
+
useAnimated: options.useAnimated,
|
|
184
|
+
title: options.title,
|
|
185
|
+
headerTitle: options.headerTitle,
|
|
186
|
+
titlePosition: options.titlePosition,
|
|
187
|
+
goBack: options.hiddenBack ? nil : goBack,
|
|
188
|
+
scrollable: options.scrollable,
|
|
189
|
+
scrollToTopCallback: options.scrollToTopCallback,
|
|
190
|
+
useAvoidKeyboard: options.useAvoidKeyboard,
|
|
191
|
+
keyboardShouldPersistTaps: options.keyboardShouldPersistTaps,
|
|
192
|
+
footer: options.footer,
|
|
193
|
+
footerTransparent: options.footerTransparent,
|
|
194
|
+
footerUseMargin: options.footerUseMargin,
|
|
195
|
+
headerRight: options.resolvedHeaderRight,
|
|
196
|
+
animatedHeader: options.animatedHeader,
|
|
197
|
+
layoutOffset: options.layoutOffset,
|
|
198
|
+
inputSearchProps: options.inputSearchProps,
|
|
199
|
+
fabProps: options.fabProps,
|
|
200
|
+
screenName: screenName,
|
|
201
|
+
content: content
|
|
202
|
+
)
|
|
203
|
+
}
|
|
204
|
+
}
|