@momo-kits/native-kits 0.300.3-debug → 0.300.4-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 +51 -7
- package/README.md +2 -2
- package/compose/build.gradle.kts +10 -2
- package/compose/build.gradle.kts.backup +8 -0
- package/compose/compose.podspec +1 -3
- package/compose/src/androidMain/kotlin/vn/momo/kits/const/HostFont.android.kt +4 -3
- 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/Context.kt +0 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/DesignSystemKits.kt +62 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/application/LiteScreen.kt +351 -111
- 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 +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 +55 -22
- package/compose/src/commonMain/kotlin/vn/momo/kits/components/PopupPromotion.kt +12 -1
- 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 +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/const/Typography.kt +20 -15
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/Item.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/layout/ScreenLayout.kt +306 -0
- 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/NavigationContainer.kt +6 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/Navigator.kt +6 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/StackScreen.kt +197 -60
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTab.kt +132 -83
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/BottomTabBar.kt +54 -62
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoBarCompression.kt +97 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/bottomtab/DuoTabBar.kt +377 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/Header.kt +122 -24
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderBackground.kt +34 -14
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderJourney.kt +3 -3
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderLocation.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderRight.kt +113 -26
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderTitle.kt +2 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HeaderUser.kt +1 -2
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/HelpCenterRequest.kt +37 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SideBar.kt +192 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/SnackBar.kt +2 -1
- package/compose/src/commonMain/kotlin/vn/momo/kits/navigation/component/ToolkitRequests.kt +8 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/Platform.kt +3 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/platform/ReservedRegions.kt +20 -0
- package/compose/src/commonMain/kotlin/vn/momo/kits/utils/Utils.kt +13 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/HelpCenterRequestTest.kt +44 -0
- package/compose/src/commonTest/kotlin/vn/momo/kits/navigation/component/ToolkitRequestsTest.kt +15 -0
- package/compose/src/iosMain/kotlin/vn/momo/kits/const/HostFont.ios.kt +3 -1
- 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 +138 -0
- package/gradle/libs.versions.toml +3 -14
- package/gradle/wrapper/gradle-wrapper.properties +0 -2
- package/gradle.properties +3 -5
- package/ios/Application/ApplicationEnvironment.swift +24 -6
- package/ios/Application/DesignSystemKits.swift +100 -0
- package/ios/Application/DesignSystemKitsPayload.swift +68 -0
- package/ios/Application/MaxApi.swift +30 -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 +8 -2
- 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 +171 -148
- package/ios/Input/InputPhoneNumber.swift +10 -9
- package/ios/Input/InputSearch.swift +107 -78
- package/ios/Input/InputTextArea.swift +133 -148
- package/ios/InputDropDown/InputDropDown.swift +10 -7
- package/ios/InputMoney/InputMoney.swift +32 -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 +415 -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 +169 -29
- package/ios/Navigation/BottomTab/BottomTab.swift +332 -0
- package/ios/{Application/Navigation → Navigation}/BottomTab/BottomTabBar.swift +18 -7
- package/ios/Navigation/BottomTab/DuoBarCompression.swift +104 -0
- package/ios/Navigation/BottomTab/DuoTabBar.swift +329 -0
- package/ios/{Application → Navigation/Component}/FloatingButton.swift +43 -54
- package/ios/Navigation/Component/Header.swift +545 -0
- package/ios/Navigation/Component/HeaderBackground.swift +154 -0
- package/ios/Navigation/Component/HeaderJourney.swift +73 -0
- package/ios/Navigation/Component/HeaderLocation.swift +48 -0
- package/ios/Navigation/Component/HeaderRight.swift +425 -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 +8 -8
- package/ios/Navigation/HostBackGestureBridge.swift +78 -0
- package/ios/{Application/Navigation/Overplay → Navigation}/ModalScreen.swift +9 -2
- package/ios/Navigation/Navigation.swift +204 -0
- package/ios/Navigation/NavigationContainer.swift +195 -0
- package/ios/{Application/Navigation → Navigation}/Navigator.swift +207 -107
- package/ios/Navigation/StackScreen.swift +930 -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 -135
- package/ios/Popup/PopupInput.swift +10 -13
- package/ios/Popup/PopupLayout.swift +138 -0
- package/ios/Popup/PopupNotify.swift +59 -69
- 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 +7 -3
- 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 +109 -55
- package/ios/Title/Title.swift +6 -17
- package/ios/Tooltip/Tooltip.swift +29 -14
- package/ios/Typography/FontScale.swift +17 -14
- package/ios/Typography/Text.swift +156 -20
- package/ios/Typography/Typography.swift +2 -2
- package/ios/Uploader/Uploader.swift +3 -1
- package/ios/native-kits.podspec +2 -3
- package/ios-demo/MoMoUIKitsDemo.podspec +1 -3
- 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 +2 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/IPhoneDuoDemo.swift +173 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/NavigationDemo.swift +77 -1
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupDisplayDemo.swift +1 -0
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupNotifyDemo.swift +2 -2
- package/ios-demo/Sources/MoMoUIKitsDemo/SampleDemos/PopupPromotionDemo.swift +2 -0
- package/package.json +1 -1
- package/publish.sh +19 -2
- 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/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/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/Typography/FontScaleStore.swift +0 -58
- package/ios/Typography/KitDesignSystem.swift +0 -17
- /package/ios/{Application/Navigation → Navigation}/BottomTab/CurvedContainer.swift +0 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
package vn.momo.kits.navigation.bottomtab
|
|
2
|
+
|
|
3
|
+
import androidx.compose.animation.core.Spring
|
|
4
|
+
import androidx.compose.animation.core.animateDpAsState
|
|
5
|
+
import androidx.compose.animation.core.animateFloatAsState
|
|
6
|
+
import androidx.compose.animation.core.spring
|
|
7
|
+
import androidx.compose.foundation.background
|
|
8
|
+
import androidx.compose.foundation.clickable
|
|
9
|
+
import androidx.compose.foundation.interaction.MutableInteractionSource
|
|
10
|
+
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
|
11
|
+
import androidx.compose.foundation.border
|
|
12
|
+
import androidx.compose.foundation.layout.Arrangement
|
|
13
|
+
import androidx.compose.foundation.layout.Box
|
|
14
|
+
import androidx.compose.foundation.layout.BoxScope
|
|
15
|
+
import androidx.compose.foundation.layout.BoxWithConstraints
|
|
16
|
+
import androidx.compose.foundation.layout.Column
|
|
17
|
+
import androidx.compose.foundation.layout.Row
|
|
18
|
+
import androidx.compose.foundation.layout.RowScope
|
|
19
|
+
import androidx.compose.foundation.layout.absolutePadding
|
|
20
|
+
import androidx.compose.foundation.layout.fillMaxHeight
|
|
21
|
+
import androidx.compose.foundation.layout.fillMaxSize
|
|
22
|
+
import androidx.compose.foundation.layout.fillMaxWidth
|
|
23
|
+
import androidx.compose.foundation.layout.height
|
|
24
|
+
import androidx.compose.foundation.layout.offset
|
|
25
|
+
import androidx.compose.foundation.layout.padding
|
|
26
|
+
import androidx.compose.foundation.layout.size
|
|
27
|
+
import androidx.compose.foundation.layout.width
|
|
28
|
+
import androidx.compose.foundation.shape.CircleShape
|
|
29
|
+
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
30
|
+
import androidx.compose.runtime.Composable
|
|
31
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
32
|
+
import androidx.compose.runtime.LaunchedEffect
|
|
33
|
+
import androidx.compose.runtime.MutableState
|
|
34
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
35
|
+
import androidx.compose.runtime.getValue
|
|
36
|
+
import androidx.compose.runtime.mutableStateOf
|
|
37
|
+
import androidx.compose.runtime.remember
|
|
38
|
+
import androidx.compose.ui.AbsoluteAlignment
|
|
39
|
+
import androidx.compose.ui.Alignment
|
|
40
|
+
import androidx.compose.ui.Modifier
|
|
41
|
+
import androidx.compose.ui.draw.clip
|
|
42
|
+
import androidx.compose.ui.draw.shadow
|
|
43
|
+
import androidx.compose.ui.graphics.graphicsLayer
|
|
44
|
+
import androidx.compose.ui.graphics.Color
|
|
45
|
+
import androidx.compose.ui.graphics.Shape
|
|
46
|
+
import androidx.compose.ui.semantics.Role
|
|
47
|
+
import androidx.compose.ui.text.style.TextOverflow
|
|
48
|
+
import androidx.compose.ui.unit.Dp
|
|
49
|
+
import androidx.compose.ui.unit.dp
|
|
50
|
+
import vn.momo.kits.application.LocalContext
|
|
51
|
+
import vn.momo.kits.application.MiniAppContext
|
|
52
|
+
import vn.momo.kits.components.Badge
|
|
53
|
+
import vn.momo.kits.components.BadgeDot
|
|
54
|
+
import vn.momo.kits.components.DotSize
|
|
55
|
+
import vn.momo.kits.components.Icon
|
|
56
|
+
import vn.momo.kits.components.Text
|
|
57
|
+
import vn.momo.kits.const.AppTheme
|
|
58
|
+
import vn.momo.kits.const.Spacing
|
|
59
|
+
import vn.momo.kits.const.Typography
|
|
60
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
61
|
+
import vn.momo.kits.layout.ScreenLayout
|
|
62
|
+
import vn.momo.kits.layout.SideBarEdge
|
|
63
|
+
import vn.momo.kits.modifier.noFeedbackClickable
|
|
64
|
+
import vn.momo.kits.modifier.setAutomationId
|
|
65
|
+
|
|
66
|
+
// Figma Fold-Design-System MT1/MT2, mirrored from RN DuoTabBar.tsx.
|
|
67
|
+
private val ITEM_SIZE = DuoMetrics.controlSize
|
|
68
|
+
private val ICON_SIZE = 28.dp
|
|
69
|
+
private val QR_ICON_SIZE = 24.dp
|
|
70
|
+
private val RAIL_PADDING = 6.dp
|
|
71
|
+
private val RAIL_GAP = Spacing.XXS
|
|
72
|
+
// selection bubble overflows the 48 rail cell by 5 above and below
|
|
73
|
+
private val RAIL_BUBBLE_WIDTH = 44.dp
|
|
74
|
+
private val RAIL_BUBBLE_HEIGHT = 58.dp
|
|
75
|
+
private val FLOATING_HEIGHT = DuoMetrics.floatingTabBarHeight
|
|
76
|
+
private val FLOATING_PADDING = Spacing.XS
|
|
77
|
+
// the system bar sizes its pill to the tabs instead of stretching it across the display
|
|
78
|
+
private val FLOATING_ITEM_WIDTH = 80.dp
|
|
79
|
+
// while pressed the selection bubble grows into a lens past the pill's edges
|
|
80
|
+
private const val LENS_SCALE_X = 1.2f
|
|
81
|
+
private const val LENS_SCALE_Y = 1.3f
|
|
82
|
+
// Figma Home-2026 tab bar "Selection": brand pink at 8%
|
|
83
|
+
private const val SELECTION_TINT_ALPHA = 0x14 / 255f
|
|
84
|
+
// RN reanimated spring { damping: 18, stiffness: 220, mass: 0.8 }
|
|
85
|
+
private val BUBBLE_SPRING = spring<Dp>(dampingRatio = 0.68f, stiffness = 275f, visibilityThreshold = Dp(0.5f))
|
|
86
|
+
private val LENS_SPRING = spring<Float>(dampingRatio = 0.68f, stiffness = 275f)
|
|
87
|
+
|
|
88
|
+
// Space from the window bottom the rail stack and its gap take, so the header side bar stays above it.
|
|
89
|
+
internal val LocalDuoRailReserve = compositionLocalOf { 0.dp }
|
|
90
|
+
|
|
91
|
+
// Rail plus the QR button under it; compressed, the rail is a single button.
|
|
92
|
+
internal fun duoRailHeight(itemCount: Int, hasQr: Boolean, compressed: Boolean = false): Dp {
|
|
93
|
+
val rail = if (compressed) ITEM_SIZE
|
|
94
|
+
else RAIL_PADDING * 2 + ITEM_SIZE * itemCount + RAIL_GAP * (itemCount - 1).coerceAtLeast(0)
|
|
95
|
+
return rail + if (hasQr) Spacing.M + ITEM_SIZE else 0.dp
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Overlaps the home indicator inset like the system tab bar (21pt above the bottom with a 34pt inset).
|
|
99
|
+
private fun floatingTabBarBottom(insetBottom: Dp): Dp =
|
|
100
|
+
maxOf(insetBottom - DuoMetrics.floatingTabBarInsetOverlap, Spacing.S)
|
|
101
|
+
|
|
102
|
+
// Space tab content leaves under the floating bar; 0 when the rail sits in the side strip or off Duo.
|
|
103
|
+
internal fun duoFloatingTabBarInset(layout: ScreenLayout): Dp =
|
|
104
|
+
if (layout.isIPhoneDuo && layout.sideBar == null) FLOATING_HEIGHT + floatingTabBarBottom(layout.insetBottom) else 0.dp
|
|
105
|
+
|
|
106
|
+
// iPhone Duo tab bar: rail in the side bar, or a floating bar when unfolded portrait.
|
|
107
|
+
@Composable
|
|
108
|
+
internal fun BoxScope.DuoTabBar(
|
|
109
|
+
layout: ScreenLayout,
|
|
110
|
+
items: List<BottomTabItem>,
|
|
111
|
+
floatingButton: BottomTabFloatingButton?,
|
|
112
|
+
selectedIndex: Int,
|
|
113
|
+
onTabSelected: (Int) -> Unit,
|
|
114
|
+
compression: DuoBarCompression = DuoBarCompression(),
|
|
115
|
+
) {
|
|
116
|
+
if (layout.sideBar != null) {
|
|
117
|
+
DuoRailTabBar(layout, items, floatingButton, selectedIndex, onTabSelected, compression)
|
|
118
|
+
} else {
|
|
119
|
+
DuoFloatingTabBar(layout, items, floatingButton, selectedIndex, onTabSelected)
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Vertical bar anchored to the bottom of the side bar, QR button below it (Figma MT1-C/E). When the
|
|
124
|
+
// side bar is short it can shrink to one button showing the current tab (HIG "Tab bar compressed");
|
|
125
|
+
// tapping it expands the rail and compresses the toolbar.
|
|
126
|
+
@Composable
|
|
127
|
+
private fun BoxScope.DuoRailTabBar(
|
|
128
|
+
layout: ScreenLayout,
|
|
129
|
+
items: List<BottomTabItem>,
|
|
130
|
+
floatingButton: BottomTabFloatingButton?,
|
|
131
|
+
selectedIndex: Int,
|
|
132
|
+
onTabSelected: (Int) -> Unit,
|
|
133
|
+
compression: DuoBarCompression,
|
|
134
|
+
) {
|
|
135
|
+
val sideBar = layout.sideBar ?: return
|
|
136
|
+
val isRight = sideBar.edge == SideBarEdge.RIGHT
|
|
137
|
+
|
|
138
|
+
Column(
|
|
139
|
+
modifier = Modifier
|
|
140
|
+
.align(if (isRight) AbsoluteAlignment.BottomRight else AbsoluteAlignment.BottomLeft)
|
|
141
|
+
.width(sideBar.width)
|
|
142
|
+
.padding(bottom = sideBar.bottom)
|
|
143
|
+
.absolutePadding(left = if (isRight) Spacing.M else 0.dp, right = if (isRight) 0.dp else Spacing.M),
|
|
144
|
+
horizontalAlignment = if (isRight) AbsoluteAlignment.Left else AbsoluteAlignment.Right,
|
|
145
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.M),
|
|
146
|
+
) {
|
|
147
|
+
val current = items.getOrNull(selectedIndex)
|
|
148
|
+
if (compression.tabBarCompressed && current != null) {
|
|
149
|
+
Box(
|
|
150
|
+
modifier = Modifier
|
|
151
|
+
.size(ITEM_SIZE)
|
|
152
|
+
.tabBarMaterial(CircleShape)
|
|
153
|
+
.noFeedbackClickable(onClick = compression.expandTabBar)
|
|
154
|
+
.setAutomationId("bottom_tab_bar"),
|
|
155
|
+
contentAlignment = Alignment.Center,
|
|
156
|
+
) {
|
|
157
|
+
DuoTabIcon(current, selected = true)
|
|
158
|
+
}
|
|
159
|
+
} else {
|
|
160
|
+
Rail(items, selectedIndex) {
|
|
161
|
+
onTabSelected(it)
|
|
162
|
+
compression.onTabSelected()
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (floatingButton != null) {
|
|
166
|
+
QrButton(floatingButton, size = ITEM_SIZE)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Column of icon-only tab items with the sliding selection bubble.
|
|
172
|
+
@Composable
|
|
173
|
+
private fun Rail(items: List<BottomTabItem>, selectedIndex: Int, onItemClick: (Int) -> Unit) {
|
|
174
|
+
val pressedIndex = remember { mutableStateOf<Int?>(null) }
|
|
175
|
+
val bubbleOffset by animateDpAsState(
|
|
176
|
+
targetValue = (ITEM_SIZE + RAIL_GAP) * (pressedIndex.value ?: selectedIndex).coerceAtLeast(0),
|
|
177
|
+
animationSpec = BUBBLE_SPRING,
|
|
178
|
+
label = "DuoRailBubble",
|
|
179
|
+
)
|
|
180
|
+
Box(
|
|
181
|
+
Modifier
|
|
182
|
+
.width(ITEM_SIZE)
|
|
183
|
+
.tabBarMaterial(CircleShape)
|
|
184
|
+
.setAutomationId("bottom_tab_bar")
|
|
185
|
+
) {
|
|
186
|
+
Box(
|
|
187
|
+
Modifier
|
|
188
|
+
.offset(
|
|
189
|
+
x = (ITEM_SIZE - RAIL_BUBBLE_WIDTH) / 2,
|
|
190
|
+
y = RAIL_PADDING - (RAIL_BUBBLE_HEIGHT - ITEM_SIZE) / 2 + bubbleOffset,
|
|
191
|
+
)
|
|
192
|
+
.size(RAIL_BUBBLE_WIDTH, RAIL_BUBBLE_HEIGHT)
|
|
193
|
+
.selectionLens(pressedIndex.value != null)
|
|
194
|
+
)
|
|
195
|
+
Column(
|
|
196
|
+
modifier = Modifier.padding(vertical = RAIL_PADDING),
|
|
197
|
+
verticalArrangement = Arrangement.spacedBy(RAIL_GAP),
|
|
198
|
+
) {
|
|
199
|
+
items.forEachIndexed { index, item ->
|
|
200
|
+
Box(
|
|
201
|
+
modifier = Modifier
|
|
202
|
+
.size(ITEM_SIZE)
|
|
203
|
+
.tabClickable(index, pressedIndex) { onItemClick(index) },
|
|
204
|
+
contentAlignment = Alignment.Center,
|
|
205
|
+
) {
|
|
206
|
+
DuoTabIcon(item, selected = index == selectedIndex)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Floating labelled bar for the unfolded portrait display, QR pinned in its own platter beside it.
|
|
214
|
+
@Composable
|
|
215
|
+
private fun BoxScope.DuoFloatingTabBar(
|
|
216
|
+
layout: ScreenLayout,
|
|
217
|
+
items: List<BottomTabItem>,
|
|
218
|
+
floatingButton: BottomTabFloatingButton?,
|
|
219
|
+
selectedIndex: Int,
|
|
220
|
+
onTabSelected: (Int) -> Unit,
|
|
221
|
+
) {
|
|
222
|
+
val pressedIndex = remember { mutableStateOf<Int?>(null) }
|
|
223
|
+
|
|
224
|
+
Row(
|
|
225
|
+
modifier = Modifier
|
|
226
|
+
.align(Alignment.BottomCenter)
|
|
227
|
+
.fillMaxWidth()
|
|
228
|
+
.absolutePadding(
|
|
229
|
+
left = layout.insetLeft + DuoMetrics.floatingTabBarMargin,
|
|
230
|
+
right = layout.insetRight + DuoMetrics.floatingTabBarMargin,
|
|
231
|
+
)
|
|
232
|
+
.padding(bottom = floatingTabBarBottom(layout.insetBottom)),
|
|
233
|
+
horizontalArrangement = Arrangement.spacedBy(Spacing.S, Alignment.CenterHorizontally),
|
|
234
|
+
) {
|
|
235
|
+
Box(
|
|
236
|
+
modifier = Modifier
|
|
237
|
+
.weight(1f, fill = false)
|
|
238
|
+
.width(FLOATING_ITEM_WIDTH * items.size + FLOATING_PADDING * 2)
|
|
239
|
+
.height(FLOATING_HEIGHT)
|
|
240
|
+
.tabBarMaterial(RoundedCornerShape(FLOATING_HEIGHT / 2))
|
|
241
|
+
.padding(FLOATING_PADDING)
|
|
242
|
+
.setAutomationId("bottom_tab_bar")
|
|
243
|
+
) {
|
|
244
|
+
BoxWithConstraints(Modifier.fillMaxSize()) {
|
|
245
|
+
val cellWidth = maxWidth / items.size.coerceAtLeast(1)
|
|
246
|
+
val bubbleOffset by animateDpAsState(
|
|
247
|
+
targetValue = cellWidth * (pressedIndex.value ?: selectedIndex).coerceAtLeast(0),
|
|
248
|
+
animationSpec = BUBBLE_SPRING,
|
|
249
|
+
label = "DuoFloatingBubble",
|
|
250
|
+
)
|
|
251
|
+
Box(
|
|
252
|
+
Modifier
|
|
253
|
+
.offset(x = bubbleOffset)
|
|
254
|
+
.width(cellWidth)
|
|
255
|
+
.fillMaxHeight()
|
|
256
|
+
.selectionLens(pressedIndex.value != null)
|
|
257
|
+
)
|
|
258
|
+
Row(Modifier.fillMaxSize()) {
|
|
259
|
+
items.forEachIndexed { index, item ->
|
|
260
|
+
FloatingCell(item, selected = index == selectedIndex, index, pressedIndex) {
|
|
261
|
+
onTabSelected(index)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
if (floatingButton != null) {
|
|
268
|
+
QrButton(floatingButton, size = FLOATING_HEIGHT, Modifier.shadow(3.dp, CircleShape))
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
@Composable
|
|
274
|
+
private fun RowScope.FloatingCell(
|
|
275
|
+
item: BottomTabItem,
|
|
276
|
+
selected: Boolean,
|
|
277
|
+
index: Int,
|
|
278
|
+
pressedIndex: MutableState<Int?>,
|
|
279
|
+
onClick: () -> Unit,
|
|
280
|
+
) {
|
|
281
|
+
val colors = AppTheme.current.colors
|
|
282
|
+
Column(
|
|
283
|
+
modifier = Modifier
|
|
284
|
+
.weight(1f)
|
|
285
|
+
.fillMaxHeight()
|
|
286
|
+
.tabClickable(index, pressedIndex, onClick),
|
|
287
|
+
horizontalAlignment = Alignment.CenterHorizontally,
|
|
288
|
+
verticalArrangement = Arrangement.spacedBy(Spacing.XXS, Alignment.CenterVertically),
|
|
289
|
+
) {
|
|
290
|
+
DuoTabIcon(item, selected)
|
|
291
|
+
// like the system tab bar the label keeps its design size; Text grows it up to 5pt on the wide Duo displays
|
|
292
|
+
val context = LocalContext.current
|
|
293
|
+
val fixedScale = remember(context) {
|
|
294
|
+
(context ?: MiniAppContext()).copy(userScaleRate = 1f, useOSScaleRate = false)
|
|
295
|
+
}
|
|
296
|
+
CompositionLocalProvider(LocalContext provides fixedScale) {
|
|
297
|
+
Text(
|
|
298
|
+
text = item.label,
|
|
299
|
+
style = Typography.labelXsMedium,
|
|
300
|
+
color = if (selected) colors.primary else colors.text.hint,
|
|
301
|
+
maxLines = 1,
|
|
302
|
+
overflow = TextOverflow.Ellipsis,
|
|
303
|
+
)
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Tracks the finger so the selection lens jumps to the pressed tab before it is selected.
|
|
309
|
+
@Composable
|
|
310
|
+
private fun Modifier.tabClickable(index: Int, pressedIndex: MutableState<Int?>, onClick: () -> Unit): Modifier {
|
|
311
|
+
val interactionSource = remember { MutableInteractionSource() }
|
|
312
|
+
val pressed by interactionSource.collectIsPressedAsState()
|
|
313
|
+
LaunchedEffect(pressed) {
|
|
314
|
+
if (pressed) pressedIndex.value = index
|
|
315
|
+
else if (pressedIndex.value == index) pressedIndex.value = null
|
|
316
|
+
}
|
|
317
|
+
return clickable(interactionSource = interactionSource, indication = null, role = Role.Tab, onClick = onClick)
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Selection bubble; while pressed it grows into a lens (80x54 to about 96x70).
|
|
321
|
+
@Composable
|
|
322
|
+
private fun Modifier.selectionLens(pressed: Boolean): Modifier {
|
|
323
|
+
val lens by animateFloatAsState(if (pressed) 1f else 0f, LENS_SPRING, label = "DuoSelectionLens")
|
|
324
|
+
val tint = AppTheme.current.colors.primary.copy(alpha = SELECTION_TINT_ALPHA)
|
|
325
|
+
return graphicsLayer {
|
|
326
|
+
scaleX = 1 + lens * (LENS_SCALE_X - 1)
|
|
327
|
+
scaleY = 1 + lens * (LENS_SCALE_Y - 1)
|
|
328
|
+
}.background(tint, CircleShape)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
@Composable
|
|
332
|
+
private fun DuoTabIcon(item: BottomTabItem, selected: Boolean) {
|
|
333
|
+
val colors = AppTheme.current.colors
|
|
334
|
+
val badgeLabel = item.badgeLabel?.takeUnless { it.toIntOrNull() == 0 }
|
|
335
|
+
Box {
|
|
336
|
+
Icon(
|
|
337
|
+
source = item.icon,
|
|
338
|
+
size = ICON_SIZE,
|
|
339
|
+
color = if (selected) colors.primary else colors.text.hint,
|
|
340
|
+
)
|
|
341
|
+
when {
|
|
342
|
+
badgeLabel == null && !item.showDot -> Unit
|
|
343
|
+
badgeLabel.isNullOrEmpty() -> BadgeDot(
|
|
344
|
+
size = DotSize.Small,
|
|
345
|
+
modifier = Modifier.align(Alignment.TopEnd),
|
|
346
|
+
)
|
|
347
|
+
else -> Badge(
|
|
348
|
+
badgeLabel,
|
|
349
|
+
modifier = Modifier.align(Alignment.TopStart).offset(x = ICON_SIZE / 2, y = -Spacing.XS),
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
@Composable
|
|
356
|
+
private fun QrButton(data: BottomTabFloatingButton, size: Dp, modifier: Modifier = Modifier) {
|
|
357
|
+
Box(
|
|
358
|
+
modifier = modifier
|
|
359
|
+
.size(size)
|
|
360
|
+
.clip(CircleShape)
|
|
361
|
+
.background(AppTheme.current.colors.primary)
|
|
362
|
+
.noFeedbackClickable(onClick = data.onPress)
|
|
363
|
+
.setAutomationId(data.label),
|
|
364
|
+
contentAlignment = Alignment.Center,
|
|
365
|
+
) {
|
|
366
|
+
Icon(source = data.icon, color = Color.White, size = QR_ICON_SIZE)
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// RN BarMaterial: surface with a 1dp border.
|
|
371
|
+
@Composable
|
|
372
|
+
private fun Modifier.tabBarMaterial(shape: Shape): Modifier {
|
|
373
|
+
val colors = AppTheme.current.colors
|
|
374
|
+
return shadow(3.dp, shape)
|
|
375
|
+
.background(colors.background.surface)
|
|
376
|
+
.border(1.dp, colors.border.default, shape)
|
|
377
|
+
}
|
|
@@ -18,10 +18,14 @@ import androidx.compose.foundation.layout.width
|
|
|
18
18
|
import androidx.compose.foundation.layout.wrapContentHeight
|
|
19
19
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
20
20
|
import androidx.compose.runtime.Composable
|
|
21
|
+
import androidx.compose.runtime.CompositionLocalProvider
|
|
22
|
+
import androidx.compose.runtime.compositionLocalOf
|
|
21
23
|
import androidx.compose.runtime.getValue
|
|
22
24
|
import androidx.compose.ui.Alignment
|
|
23
25
|
import androidx.compose.ui.Modifier
|
|
24
26
|
import androidx.compose.ui.draw.clipToBounds
|
|
27
|
+
import androidx.compose.ui.draw.drawBehind
|
|
28
|
+
import androidx.compose.ui.geometry.Offset
|
|
25
29
|
import androidx.compose.ui.graphics.graphicsLayer
|
|
26
30
|
import androidx.compose.ui.graphics.Brush
|
|
27
31
|
import androidx.compose.ui.graphics.Color
|
|
@@ -33,12 +37,13 @@ import androidx.compose.ui.unit.dp
|
|
|
33
37
|
import vn.momo.kits.components.Icon
|
|
34
38
|
import vn.momo.kits.components.Image
|
|
35
39
|
import vn.momo.kits.components.InputSearchProps
|
|
36
|
-
import vn.momo.kits.const.AppStatusBar
|
|
37
40
|
import vn.momo.kits.const.AppTheme
|
|
38
41
|
import vn.momo.kits.const.Colors
|
|
39
42
|
import vn.momo.kits.components.PopupNotifyProps
|
|
40
43
|
import vn.momo.kits.application.IsShowBaseLineDebug
|
|
41
44
|
import vn.momo.kits.const.Spacing
|
|
45
|
+
import vn.momo.kits.layout.DuoMetrics
|
|
46
|
+
import vn.momo.kits.layout.getScreenLayout
|
|
42
47
|
import vn.momo.kits.modifier.activeOpacityClickable
|
|
43
48
|
import vn.momo.kits.modifier.conditional
|
|
44
49
|
import vn.momo.kits.modifier.setAutomationId
|
|
@@ -47,21 +52,24 @@ import vn.momo.kits.navigation.LocalNavigator
|
|
|
47
52
|
import vn.momo.kits.navigation.LocalOptions
|
|
48
53
|
import vn.momo.kits.navigation.LocalScrollState
|
|
49
54
|
import vn.momo.kits.navigation.getInputSearchType
|
|
55
|
+
import vn.momo.kits.navigation.usesDuoGlassHeader
|
|
56
|
+
import vn.momo.kits.utils.horizontalSafeAreaPadding
|
|
50
57
|
|
|
51
58
|
@InternalApi
|
|
52
59
|
const val HEADER_HEIGHT = 52
|
|
53
60
|
internal enum class InputSearchType { None, Header, Animated }
|
|
54
61
|
enum class TitlePosition { LEFT, CENTER }
|
|
55
62
|
|
|
63
|
+
private val HEADER_ACTIONS_WIDTH = 172.dp
|
|
64
|
+
|
|
65
|
+
// Extra width the 48pt back and toolkit take on the iPhone Duo status bar row.
|
|
66
|
+
private val STATUS_BAR_ROW_ACTIONS_EXTRA = 55.dp
|
|
67
|
+
|
|
56
68
|
@Suppress("FrequentlyChangingValue")
|
|
57
69
|
@Composable
|
|
58
|
-
internal fun
|
|
70
|
+
internal fun headerOpacity(): Float {
|
|
59
71
|
val options = LocalOptions.current
|
|
60
|
-
val navigator = LocalNavigator.current
|
|
61
72
|
val scrollState = LocalScrollState.current
|
|
62
|
-
val headerRightWidthPx = LocalHeaderRightWidthPx.current
|
|
63
|
-
val inputSearchType = getInputSearchType(options)
|
|
64
|
-
|
|
65
73
|
val opacityHeight = when (val header = options.headerType) {
|
|
66
74
|
is HeaderType.Animated -> with(LocalDensity.current) { header.layoutOffSet.roundToPx() }
|
|
67
75
|
else -> HEADER_HEIGHT
|
|
@@ -73,6 +81,54 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
73
81
|
else -> (scrollState.value / opacityHeight.toFloat()).coerceIn(0f, 1f)
|
|
74
82
|
}
|
|
75
83
|
)
|
|
84
|
+
return opacity
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Back button and toolkit share the bar with the title unless the iPhone Duo side bar holds them.
|
|
88
|
+
@Composable
|
|
89
|
+
internal fun headerTitleMaxWidth(): Dp {
|
|
90
|
+
val layout = getScreenLayout()
|
|
91
|
+
val clearance = layout.statusBarClearance
|
|
92
|
+
val clearanceWidth = clearance?.let { it.left + it.right } ?: 0.dp
|
|
93
|
+
if (layout.sideBar != null) return layout.contentWidth - Spacing.M * 2 - clearanceWidth
|
|
94
|
+
return layout.width - HEADER_ACTIONS_WIDTH - (if (clearance != null) STATUS_BAR_ROW_ACTIONS_EXTRA else 0.dp) - clearanceWidth
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Scroll distance over which the Duo glass bar fades in.
|
|
98
|
+
private val DUO_GLASS_FADE_DISTANCE = 16.dp
|
|
99
|
+
|
|
100
|
+
// RN DuoGlassBackground: translucent surface with a hairline once content scrolls under it.
|
|
101
|
+
@Suppress("FrequentlyChangingValue")
|
|
102
|
+
@Composable
|
|
103
|
+
private fun Modifier.duoGlassBar(): Modifier {
|
|
104
|
+
val scrollState = LocalScrollState.current
|
|
105
|
+
val fadePx = with(LocalDensity.current) { DUO_GLASS_FADE_DISTANCE.toPx() }
|
|
106
|
+
val alpha by animateFloatAsState((scrollState.value / fadePx).coerceIn(0f, 1f), label = "DuoGlassAlpha")
|
|
107
|
+
val colors = AppTheme.current.colors
|
|
108
|
+
return background(colors.background.surface.copy(alpha = 0.8f * alpha))
|
|
109
|
+
.drawBehind {
|
|
110
|
+
val stroke = 0.5.dp.toPx()
|
|
111
|
+
drawLine(
|
|
112
|
+
color = colors.border.default.copy(alpha = alpha),
|
|
113
|
+
start = Offset(0f, size.height - stroke / 2),
|
|
114
|
+
end = Offset(size.width, size.height - stroke / 2),
|
|
115
|
+
strokeWidth = stroke,
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@Composable
|
|
121
|
+
internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
122
|
+
val options = LocalOptions.current
|
|
123
|
+
val navigator = LocalNavigator.current
|
|
124
|
+
val headerRightWidthPx = LocalHeaderRightWidthPx.current
|
|
125
|
+
val inputSearchType = getInputSearchType(options)
|
|
126
|
+
val layout = getScreenLayout()
|
|
127
|
+
val useSideBar = layout.sideBar != null
|
|
128
|
+
// iPhone Duo inner portrait: the row sits on the status bar, between its clusters
|
|
129
|
+
val clearance = layout.statusBarClearance
|
|
130
|
+
val statusBarRow = clearance != null && !useSideBar
|
|
131
|
+
val opacity = headerOpacity()
|
|
76
132
|
|
|
77
133
|
val headerColor = getHeaderColor(options.headerType, opacity, options.tintColor, Colors.black_17)
|
|
78
134
|
|
|
@@ -91,13 +147,15 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
91
147
|
// Centered title needs a full-width overlay to stay screen-centered, not boxed between back button and header right.
|
|
92
148
|
val centerTitle = titlePosition == TitlePosition.CENTER && options.headerTitle is HeaderTitle.Default
|
|
93
149
|
|
|
150
|
+
val useDuoGlass = options.usesDuoGlassHeader(layout)
|
|
94
151
|
val headerImage = AppTheme.current.assets.headerBackground
|
|
95
|
-
.takeUnless { options.headerType is HeaderType.Transparent }
|
|
152
|
+
.takeUnless { options.headerType is HeaderType.Transparent || useDuoGlass }
|
|
96
153
|
|
|
97
154
|
Box(
|
|
98
|
-
Modifier.height(
|
|
155
|
+
Modifier.height(layout.headerHeight)
|
|
99
156
|
.fillMaxWidth()
|
|
100
|
-
.
|
|
157
|
+
.then(if (useDuoGlass) Modifier.duoGlassBar() else Modifier)
|
|
158
|
+
.conditional(headerImage.isNullOrEmpty() && !useDuoGlass) {
|
|
101
159
|
background(background.copy(alpha = animatedAlpha))
|
|
102
160
|
}
|
|
103
161
|
.conditional(IsShowBaseLineDebug) {
|
|
@@ -124,21 +182,34 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
124
182
|
}
|
|
125
183
|
}
|
|
126
184
|
Row(
|
|
127
|
-
modifier = Modifier.height(
|
|
185
|
+
modifier = Modifier.height(layout.headerBarHeight)
|
|
128
186
|
.fillMaxWidth()
|
|
129
|
-
.
|
|
187
|
+
.horizontalSafeAreaPadding()
|
|
188
|
+
.padding(
|
|
189
|
+
start = Spacing.M + (clearance?.left ?: 0.dp),
|
|
190
|
+
end = Spacing.M + (clearance?.right ?: 0.dp),
|
|
191
|
+
),
|
|
130
192
|
verticalAlignment = Alignment.CenterVertically,
|
|
131
193
|
horizontalArrangement = Arrangement.SpaceBetween
|
|
132
194
|
) {
|
|
133
|
-
if(!options.hiddenBack) {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
navigator.onBackSafe {
|
|
140
|
-
|
|
141
|
-
|
|
195
|
+
if (!options.hiddenBack && !useSideBar) {
|
|
196
|
+
if (statusBarRow) {
|
|
197
|
+
SideBarButton(
|
|
198
|
+
icon = "arrow-back",
|
|
199
|
+
headerColor = headerColor,
|
|
200
|
+
automationId = "btn_navigation_back",
|
|
201
|
+
onClick = { navigator.onBackSafe { } },
|
|
202
|
+
)
|
|
203
|
+
} else {
|
|
204
|
+
BackButton(
|
|
205
|
+
borderColor = headerColor.borderColor,
|
|
206
|
+
backgroundButton = headerColor.backgroundButton,
|
|
207
|
+
tintIconColor = headerColor.tintIconColor,
|
|
208
|
+
onBackHandler = {
|
|
209
|
+
navigator.onBackSafe { }
|
|
210
|
+
}
|
|
211
|
+
)
|
|
212
|
+
}
|
|
142
213
|
Spacer(Modifier.width(Spacing.S))
|
|
143
214
|
}
|
|
144
215
|
|
|
@@ -155,20 +226,42 @@ internal fun Header(onBackHandler: (() -> Unit)? = null) {
|
|
|
155
226
|
Box(Modifier.onGloballyPositioned {
|
|
156
227
|
if(headerRightWidthPx.intValue != it.size.width) headerRightWidthPx.intValue = it.size.width
|
|
157
228
|
}){
|
|
158
|
-
|
|
229
|
+
val headerRight = options.headerRight
|
|
230
|
+
when {
|
|
231
|
+
useSideBar && headerRight is HeaderRight.Toolkit -> Unit
|
|
232
|
+
statusBarRow && headerRight is HeaderRight.Toolkit -> Toolkit(
|
|
233
|
+
headerRight = headerRight,
|
|
234
|
+
tintColor = options.tintColor,
|
|
235
|
+
headerColor = headerColor,
|
|
236
|
+
layout = ToolkitLayout.SideBarRow,
|
|
237
|
+
)
|
|
238
|
+
else -> CompositionLocalProvider(LocalHeaderColor provides headerColor) {
|
|
239
|
+
HeaderRight(headerRight, options.tintColor, headerColor)
|
|
240
|
+
}
|
|
241
|
+
}
|
|
159
242
|
}
|
|
160
243
|
}
|
|
161
244
|
if (centerTitle) {
|
|
245
|
+
// on the status bar row the clusters are lopsided, so both sides keep the wider one's room
|
|
246
|
+
val centerInset = if (clearance != null) {
|
|
247
|
+
val back = if (options.hiddenBack || useSideBar) 0.dp else DuoMetrics.controlSize + Spacing.S
|
|
248
|
+
val right = with(LocalDensity.current) { headerRightWidthPx.intValue.toDp() }
|
|
249
|
+
maxOf(clearance.left + back, clearance.right + right) + Spacing.S
|
|
250
|
+
} else {
|
|
251
|
+
0.dp
|
|
252
|
+
}
|
|
162
253
|
Box(
|
|
163
|
-
modifier = Modifier.height(
|
|
254
|
+
modifier = Modifier.height(layout.headerBarHeight)
|
|
164
255
|
.fillMaxWidth()
|
|
165
|
-
.
|
|
256
|
+
.horizontalSafeAreaPadding()
|
|
257
|
+
.padding(horizontal = Spacing.M + centerInset),
|
|
166
258
|
contentAlignment = Alignment.Center
|
|
167
259
|
) {
|
|
168
260
|
HeaderContent(
|
|
169
261
|
headerTitle = options.headerTitle,
|
|
170
262
|
tintIconColor = titleColor,
|
|
171
263
|
titlePosition = titlePosition,
|
|
264
|
+
centerWidthFraction = if (clearance != null) 1f else 0.5f,
|
|
172
265
|
modifier = Modifier.fillMaxWidth()
|
|
173
266
|
)
|
|
174
267
|
}
|
|
@@ -202,6 +295,7 @@ internal fun HeaderContent(
|
|
|
202
295
|
headerTitle: HeaderTitle,
|
|
203
296
|
tintIconColor: Color,
|
|
204
297
|
titlePosition: TitlePosition = TitlePosition.LEFT,
|
|
298
|
+
centerWidthFraction: Float = 0.5f,
|
|
205
299
|
modifier: Modifier = Modifier,
|
|
206
300
|
){
|
|
207
301
|
Box(
|
|
@@ -213,7 +307,7 @@ internal fun HeaderContent(
|
|
|
213
307
|
HeaderTitle(
|
|
214
308
|
title = headerTitle.title,
|
|
215
309
|
color = tintIconColor,
|
|
216
|
-
modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER)
|
|
310
|
+
modifier = Modifier.fillMaxWidth(fraction = if (titlePosition == TitlePosition.CENTER) centerWidthFraction else 1f),
|
|
217
311
|
textAlign = if (titlePosition == TitlePosition.CENTER) TextAlign.Center else TextAlign.Start
|
|
218
312
|
)
|
|
219
313
|
}
|
|
@@ -325,6 +419,10 @@ data class HeaderBackProps(
|
|
|
325
419
|
|
|
326
420
|
@InternalApi
|
|
327
421
|
data class HeaderColor(val tintIconColor: Color, val backgroundButton: Color, val borderColor: Color)
|
|
422
|
+
|
|
423
|
+
// The enclosing header's colors, so a custom header right follows the bar (SwiftUI `\.headerColor`).
|
|
424
|
+
internal val LocalHeaderColor = compositionLocalOf<HeaderColor?> { null }
|
|
425
|
+
|
|
328
426
|
internal fun getHeaderColor(headerType: HeaderType, opacity: Float, tintColor: Color?, defaultColor: Color): HeaderColor{
|
|
329
427
|
return if(headerType is HeaderType.Animated) {
|
|
330
428
|
if (opacity == 1f || !headerType.isSurface)
|