@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,304 @@
|
|
|
1
|
+
//
|
|
2
|
+
// DuoTabBar.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// iPhone Duo tab bar, port of RN `BottomTab/DuoTabBar.tsx` (Figma Fold-Design-System MT1/MT2):
|
|
6
|
+
// a glass rail in the side bar, or a floating glass bar when unfolded portrait.
|
|
7
|
+
//
|
|
8
|
+
|
|
9
|
+
import SwiftUI
|
|
10
|
+
|
|
11
|
+
private let itemSize = DuoMetrics.controlSize
|
|
12
|
+
private let iconSize: CGFloat = 28
|
|
13
|
+
private let qrIconSize: CGFloat = 24
|
|
14
|
+
private let railPadding: CGFloat = 6
|
|
15
|
+
private let railGap = Spacing.XXS
|
|
16
|
+
// selection bubble overflows the 48 rail cell by 5 above and below (Figma MT2)
|
|
17
|
+
private let railBubbleWidth: CGFloat = 44
|
|
18
|
+
private let railBubbleHeight: CGFloat = 58
|
|
19
|
+
let duoFloatingTabBarHeight: CGFloat = 62
|
|
20
|
+
private let floatingPadding = Spacing.XS
|
|
21
|
+
private let floatingQrSize: CGFloat = 44
|
|
22
|
+
// floating bar spans 6 of the 8 portrait grid columns, like the content
|
|
23
|
+
private let gridColumns: CGFloat = 8
|
|
24
|
+
private let gridSpan: CGFloat = 6
|
|
25
|
+
private let tabSpring = Animation.interpolatingSpring(mass: 0.8, stiffness: 220, damping: 18)
|
|
26
|
+
|
|
27
|
+
/// Room tab screens leave for the Duo tab bar, provided by BottomTab to its tab StackScreens.
|
|
28
|
+
struct DuoTabBarInsets: Equatable {
|
|
29
|
+
/// Floating bar overlays the content: scroll content, footer and FAB keep clear of it.
|
|
30
|
+
var overlay: CGFloat = 0
|
|
31
|
+
/// Rail sits at the bottom of the side strip: the header side bar stays this far above the window bottom.
|
|
32
|
+
var sideBarReserve: CGFloat = 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private struct DuoTabBarInsetsKey: EnvironmentKey {
|
|
36
|
+
static let defaultValue = DuoTabBarInsets()
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
extension EnvironmentValues {
|
|
40
|
+
var duoTabBarInsets: DuoTabBarInsets {
|
|
41
|
+
get { self[DuoTabBarInsetsKey.self] }
|
|
42
|
+
set { self[DuoTabBarInsetsKey.self] = newValue }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
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
|
+
|
|
53
|
+
extension ScreenLayout {
|
|
54
|
+
func duoTabBarInsets(itemCount: Int, hasFloatingButton: Bool, tabBarCompressed: Bool = false) -> DuoTabBarInsets {
|
|
55
|
+
guard isIPhoneDuo else { return DuoTabBarInsets() }
|
|
56
|
+
guard let sideBar else {
|
|
57
|
+
return DuoTabBarInsets(overlay: duoFloatingTabBarHeight + Spacing.M)
|
|
58
|
+
}
|
|
59
|
+
let rail = duoRailHeight(itemCount: itemCount, hasQr: hasFloatingButton, compressed: tabBarCompressed)
|
|
60
|
+
return DuoTabBarInsets(sideBarReserve: sideBar.bottom + rail + DuoBarCompression.groupGap)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/// Glass on iOS 26, a system blur before it (RN GlassView's fallback).
|
|
65
|
+
struct DuoBarMaterial<S: Shape>: ViewModifier {
|
|
66
|
+
let shape: S
|
|
67
|
+
var visible: Bool = true
|
|
68
|
+
|
|
69
|
+
func body(content: Content) -> some View {
|
|
70
|
+
content.background(material.allowsHitTesting(false))
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@ViewBuilder
|
|
74
|
+
private var material: some View {
|
|
75
|
+
#if compiler(>=6.2)
|
|
76
|
+
if #available(iOS 26.0, *) {
|
|
77
|
+
// glass materializes by toggling its effect, it does not fade with opacity
|
|
78
|
+
Color.clear
|
|
79
|
+
.glassEffect(visible ? .regular : .identity, in: shape)
|
|
80
|
+
.shadow(color: Color.black.opacity(visible ? 0.16 : 0), radius: 16, x: 0, y: 6)
|
|
81
|
+
.animation(.easeInOut(duration: 0.25), value: visible)
|
|
82
|
+
} else {
|
|
83
|
+
blur
|
|
84
|
+
}
|
|
85
|
+
#else
|
|
86
|
+
blur
|
|
87
|
+
#endif
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
private var blur: some View {
|
|
91
|
+
shape.fill(.regularMaterial)
|
|
92
|
+
.shadow(color: Color.black.opacity(0.16), radius: 16, x: 0, y: 6)
|
|
93
|
+
.opacity(visible ? 1 : 0)
|
|
94
|
+
.animation(.easeInOut(duration: 0.25), value: visible)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
struct DuoTabBar: View {
|
|
99
|
+
let items: [BottomTabItem]
|
|
100
|
+
let floatingButton: BottomTabFloatingButton?
|
|
101
|
+
let selectedIndex: Int
|
|
102
|
+
let onTabSelected: (Int) -> Void
|
|
103
|
+
var compression = DuoBarCompression()
|
|
104
|
+
|
|
105
|
+
@Environment(\.screenLayout) private var layout
|
|
106
|
+
|
|
107
|
+
var body: some View {
|
|
108
|
+
if let sideBar = layout.sideBar {
|
|
109
|
+
DuoRailTabBar(sideBar: sideBar, items: items, floatingButton: floatingButton,
|
|
110
|
+
selectedIndex: selectedIndex, onTabSelected: onTabSelected, compression: compression)
|
|
111
|
+
} else {
|
|
112
|
+
DuoFloatingTabBar(items: items, floatingButton: floatingButton,
|
|
113
|
+
selectedIndex: selectedIndex, onTabSelected: onTabSelected)
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
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.
|
|
121
|
+
private struct DuoRailTabBar: View {
|
|
122
|
+
let sideBar: SideBarLayout
|
|
123
|
+
let items: [BottomTabItem]
|
|
124
|
+
let floatingButton: BottomTabFloatingButton?
|
|
125
|
+
let selectedIndex: Int
|
|
126
|
+
let onTabSelected: (Int) -> Void
|
|
127
|
+
let compression: DuoBarCompression
|
|
128
|
+
|
|
129
|
+
@Environment(\.appTheme) private var theme
|
|
130
|
+
|
|
131
|
+
private var isRight: Bool { sideBar.edge == .right }
|
|
132
|
+
|
|
133
|
+
var body: some View {
|
|
134
|
+
VStack(spacing: Spacing.M) {
|
|
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())
|
|
140
|
+
}
|
|
141
|
+
.buttonStyle(.plain)
|
|
142
|
+
.modifier(DuoBarMaterial(shape: Circle()))
|
|
143
|
+
.accessibilityLabel(items[selectedIndex].label)
|
|
144
|
+
.accessibilityIdentifier("bottom_tab_bar")
|
|
145
|
+
} else {
|
|
146
|
+
rail
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if let floatingButton {
|
|
150
|
+
DuoQrButton(data: floatingButton, size: itemSize)
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
.padding(isRight ? .leading : .trailing, Spacing.M)
|
|
154
|
+
.frame(width: sideBar.width, alignment: isRight ? .leading : .trailing)
|
|
155
|
+
.padding(.bottom, sideBar.bottom)
|
|
156
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: isRight ? .bottomTrailing : .bottomLeading)
|
|
157
|
+
.ignoresSafeArea()
|
|
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
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/// Floating labelled bar for the unfolded portrait display, QR in the middle cell (Figma MT1-M).
|
|
193
|
+
private struct DuoFloatingTabBar: View {
|
|
194
|
+
let items: [BottomTabItem]
|
|
195
|
+
let floatingButton: BottomTabFloatingButton?
|
|
196
|
+
let selectedIndex: Int
|
|
197
|
+
let onTabSelected: (Int) -> Void
|
|
198
|
+
|
|
199
|
+
@Environment(\.screenLayout) private var layout
|
|
200
|
+
@Environment(\.appTheme) private var theme
|
|
201
|
+
|
|
202
|
+
private var barWidth: CGFloat {
|
|
203
|
+
let columnWidth = (layout.width - 2 * Spacing.M - (gridColumns - 1) * Spacing.M) / gridColumns
|
|
204
|
+
return columnWidth * gridSpan + (gridSpan - 1) * Spacing.M
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private var useQr: Bool { floatingButton != nil && items.count % 2 == 0 }
|
|
208
|
+
private var middle: Int { items.count / 2 }
|
|
209
|
+
private var cellCount: Int { items.count + (useQr ? 1 : 0) }
|
|
210
|
+
private var cellWidth: CGFloat { (barWidth - floatingPadding * 2) / CGFloat(max(cellCount, 1)) }
|
|
211
|
+
private var visualIndex: Int { useQr && selectedIndex >= middle ? selectedIndex + 1 : selectedIndex }
|
|
212
|
+
|
|
213
|
+
var body: some View {
|
|
214
|
+
ZStack(alignment: .leading) {
|
|
215
|
+
Capsule()
|
|
216
|
+
.fill(theme.colors.background.selected)
|
|
217
|
+
.frame(width: cellWidth)
|
|
218
|
+
.offset(x: CGFloat(visualIndex) * cellWidth)
|
|
219
|
+
.animation(tabSpring, value: visualIndex)
|
|
220
|
+
|
|
221
|
+
HStack(spacing: 0) {
|
|
222
|
+
ForEach(0..<cellCount, id: \.self) { cell in
|
|
223
|
+
if useQr && cell == middle, let floatingButton {
|
|
224
|
+
DuoQrButton(data: floatingButton, size: floatingQrSize)
|
|
225
|
+
.frame(width: cellWidth)
|
|
226
|
+
} else {
|
|
227
|
+
let index = useQr && cell > middle ? cell - 1 : cell
|
|
228
|
+
tabCell(index)
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
.padding(floatingPadding)
|
|
234
|
+
.frame(width: barWidth, height: duoFloatingTabBarHeight)
|
|
235
|
+
.modifier(DuoBarMaterial(shape: Capsule()))
|
|
236
|
+
.accessibilityIdentifier("bottom_tab_bar")
|
|
237
|
+
.padding(.bottom, layout.insets.bottom + Spacing.M)
|
|
238
|
+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom)
|
|
239
|
+
.ignoresSafeArea()
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private func tabCell(_ index: Int) -> some View {
|
|
243
|
+
let selected = index == selectedIndex
|
|
244
|
+
return SwiftUI.Button { onTabSelected(index) } label: {
|
|
245
|
+
VStack(spacing: Spacing.XXS) {
|
|
246
|
+
DuoTabIcon(item: items[index], selected: selected)
|
|
247
|
+
MomoText(
|
|
248
|
+
items[index].label,
|
|
249
|
+
typography: .labelXsMedium,
|
|
250
|
+
color: selected ? theme.colors.primary : theme.colors.text.default,
|
|
251
|
+
textAlign: .center,
|
|
252
|
+
maxLines: 1
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
.frame(width: cellWidth)
|
|
256
|
+
.frame(maxHeight: .infinity)
|
|
257
|
+
.contentShape(Rectangle())
|
|
258
|
+
}
|
|
259
|
+
.buttonStyle(.plain)
|
|
260
|
+
.accessibilityLabel(items[index].label)
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private struct DuoTabIcon: View {
|
|
265
|
+
let item: BottomTabItem
|
|
266
|
+
let selected: Bool
|
|
267
|
+
|
|
268
|
+
@Environment(\.appTheme) private var theme
|
|
269
|
+
|
|
270
|
+
private var badgeLabel: String? {
|
|
271
|
+
guard let label = item.badgeLabel else { return nil }
|
|
272
|
+
if let n = Int(label), n == 0 { return nil }
|
|
273
|
+
return label
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
var body: some View {
|
|
277
|
+
Icon(source: item.icon, size: iconSize, color: selected ? theme.colors.primary : theme.colors.text.default)
|
|
278
|
+
.overlay(alignment: .topTrailing) {
|
|
279
|
+
if let badgeLabel, !badgeLabel.isEmpty {
|
|
280
|
+
Badge(label: badgeLabel).offset(x: Spacing.S, y: -Spacing.XS)
|
|
281
|
+
} else if badgeLabel != nil || item.showDot {
|
|
282
|
+
BadgeDot(size: .small)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private struct DuoQrButton: View {
|
|
289
|
+
let data: BottomTabFloatingButton
|
|
290
|
+
let size: CGFloat
|
|
291
|
+
|
|
292
|
+
@Environment(\.appTheme) private var theme
|
|
293
|
+
|
|
294
|
+
var body: some View {
|
|
295
|
+
SwiftUI.Button(action: data.onPress) {
|
|
296
|
+
Circle()
|
|
297
|
+
.fill(theme.colors.primary)
|
|
298
|
+
.frame(width: size, height: size)
|
|
299
|
+
.overlay(Icon(source: data.icon, size: qrIconSize, color: Colors.black01))
|
|
300
|
+
}
|
|
301
|
+
.buttonStyle(.plain)
|
|
302
|
+
.accessibilityLabel(data.label)
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
//
|
|
2
|
+
// FloatingButton.swift
|
|
3
|
+
// MoMoUIKits
|
|
4
|
+
//
|
|
5
|
+
// SwiftUI mirror of Compose's `navigation/component/FloatingButton.kt`: the screen FAB, whose
|
|
6
|
+
// label collapses while the content scrolls down.
|
|
7
|
+
//
|
|
8
|
+
|
|
1
9
|
import SwiftUI
|
|
2
10
|
import Combine
|
|
3
11
|
|
|
@@ -111,13 +119,16 @@ public struct FloatingButton: View {
|
|
|
111
119
|
content
|
|
112
120
|
}
|
|
113
121
|
}
|
|
114
|
-
.onTapGesture {
|
|
115
|
-
onClick?()
|
|
116
|
-
}
|
|
117
122
|
.padding(Spacing.M)
|
|
118
123
|
.frame(height: height + (Spacing.M * 2))
|
|
119
124
|
.background(containerColor)
|
|
120
125
|
.cornerRadius(100)
|
|
126
|
+
// Compose FloatingActionButton elevation 4dp
|
|
127
|
+
.shadow(color: Color.black.opacity(0.2), radius: 4, x: 0, y: 2)
|
|
128
|
+
.contentShape(Capsule())
|
|
129
|
+
.onTapGesture {
|
|
130
|
+
onClick?()
|
|
131
|
+
}
|
|
121
132
|
.padding(.bottom, bottomPadding)
|
|
122
133
|
.frame(alignment: position == .center ? .center : .trailing)
|
|
123
134
|
.onAppear {
|